Rust & Wasm: Embed Wasmtime in your Rust app

Nikhil Gupta
4 min readJan 7, 2023

--

Till now, we have been entirely focused on running our Rust-based Wasm modules in the browser. In this article, we will take a U-turn and instead, embed Wasmtime in a Rust CLI to dynamically load Wasm modules. This approach can be used to run on-demand WASM modules on the server, thus, providing a way to run user functions with complete security and extensibility.

Create a demo wasm module

Let’s first create a basic wasm module that will expose a simple function to add two numbers, which we later embed in our main application.

As done in previous articles, let’s create a new library like so:

cargo new demo-wasm --lib

and modify the type in Cargo.toml:

[dependencies]

+[lib]
+crate-type = ["cdylib", "rlib"]

Next, let’s modify our lib.rs to expose the function:

#[no_mangle]
pub extern "C" fn add(left: u32, right: u32) -> u32 {
left + right
}

Note that we have added extern "C" and #[no_mangle] so that the APIs are exposed in a C-based interface and the function names are not modified.

Finally, let’s build our wasm:

cargo build --target wasm32-wasi --release

This time, we are targeting wasi, a modular interface for WebAssembly which is implemented by Wasmtime, that provides access to OS-like-features, including files, etc.

Load wasm module

Now, let’s create a Rust application and add Wasmtime dependencies.

cargo new demo

cd demo
cargo add wasmtime wasmtime-wasi

Next, let’s copy our previously built demo_wasm.wasm to our src folder.

We are now all set to load our wasm module and print the available functions. Let’s modify our main.rs like so:

use wasmtime::*;

fn main() {
let engine = Engine::default();

let module = Module::from_file(&engine, "./src/demo_wasm.wasm").unwrap();

let mut exports = module.exports();
while let Some(foo) = exports.next() {
println!("{}", foo.name());
}
}

The code basically involves creating a new engine, loading the wasm module, iterating through the exports and printing their names.

Now, if we run our app using cargo run, we should see the following on the terminal:

memory
add

These are the two functions that have been exposed, that includes add that we had created previously. It also exposes a function memory but we will ignore it for now.

Call the function

In order to call our add function, we need to first create a linker like so:

use wasmtime::*;
+use wasmtime_wasi::{sync::WasiCtxBuilder};

fn main() {
let engine = Engine::default();

let module = Module::from_file(&engine, "./src/demo_wasm.wasm").unwrap();
let mut exports = module.exports();
while let Some(foo) = exports.next() {
println!("{}", foo.name());
}

+ let mut linker = Linker::new(&engine);

+ let wasi = WasiCtxBuilder::new()
+ .inherit_stdio()
+ .inherit_args().unwrap()
+ .build();
+ let mut store = Store::new(&engine, wasi);

+ let link = linker.instantiate(&mut store, &module).unwrap();
}

We imported WasiCtxBuilder and created the following:

  1. A new linker for our engine
  2. A wasi context that can be used to create a store
  3. Link the module with the store

This basically sets up the connections for us to create a handle to our function and call it like so:

let link = linker.instantiate(&mut store, &module).unwrap();

+ let add_fn = link.get_typed_func::<(u32, u32), u32>(&mut store, "add").unwrap();
+ println!("{}", add_fn.call(&mut store, (1, 2)).unwrap())

Now, if you run the app, you should see 3 printed on the terminal.

Ability to print from wasm

Now, let’s try to another function that actually prints from our Wasm module, thus, requiring a WASI implementation.

Let’s modify our lib.rs in demo-wasm, build an updated wasm and copy it into our main app.

#[no_mangle]
pub extern "C" fn add(left: u32, right: u32) -> u32 {
left + right
}

+#[no_mangle]
+pub extern "C" fn print() {
+ println!("Hello World!");
+}

If we try to run our application with this updated wasm, we will see an error:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: unknown import: `wasi_snapshot_preview1::fd_write` has not been defined', src/main.rs:24:56
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

To resolve this, let’s add the wasi implementation like so:

    let mut store = Store::new(&engine, wasi);

+ wasmtime_wasi::add_to_linker(&mut linker, |s| s).unwrap();

let link = linker.instantiate(&mut store, &module).unwrap();

Finally, let’s call our newly exposed print as well:

    println!("{}", add_fn.call(&mut store, (1, 2)).unwrap());

+ let print_fn = link.get_typed_func::<(), ()>(&mut store, "print").unwrap();
+ print_fn.call(&mut store, ()).unwrap();

Now, if you run the updated app, you should see both 3 and Hello World! printed on the terminal :) In this way, we can now load any Wasm module into our app and replace them dynamically as needed!

If you liked this article, subscribe here to get the complete code and updates for the entire collection:

--

--