This demo shows WebAssembly (Wasm) calling host application functions, where both Wasm and application are in Rust. In particular, Wasm is passing references to objects that have either static or dynamic size.
Wasmtime is an embedded WebAssembly virtual machine runtime. The Rust application uses Wasmtime to load and run a Rust WebAssembly module. This demo shows the WebAssembly module calling functions in the host application.
Wasmtime is new and evolving. Features to import and export functions between WebAssembly and host will almost certainly be enhanced. Here are some production uses:
- Fastly provides a http_guest API. Customers use this to deploy Rust web apps on edge compute.
- Cloudflare provides a Wasm Wireshark firewall API, Wirefilter, to run customer Wasm firewall at the edge. Cloudflare is expanding this to Web Application Firewall.
This demo is intended to show how to work within certain interim limitations on argument types.
One limitation is, WebAssembly (Wasm) is 32-bit while the application is 64-bit. Wasm pointers are a 32-bit offset in Virtual Machine (VM) memory. To obtain a 64-bit address on the host side, Wasm pointers must be indexed into VM memory's byte array. Fat pointers such as &[u8] or &str are handled transparently on the WebAssembly side; however, on the host side, they are received as two separate arguments, the 32-bit offset and size.
An additional limitation is pointers to structs. Passing a pointer to a struct (e.g. &struct) requires additional code in both WebAssembly and the host application. This demo shows examples for two kinds of structs:
- Structs that have the Serialize trait. We serialize it and pass the offset and length of the serialized copy instead. Fields can be String, Vec and other dynamic sized types.
- Structs that have the Copy trait — a fixed size and no pointer fields. We pass the the offset and size of the struct itself.
There are certain trade-offs.
- Serialization verifies the struct's field types.
- Directly passing 'Copy' structs does not, and is faster.
In both examples here, the size of the struct is verified.
To build this demo, first install rust, then add features:
rustup target add wasm32-wasi
cargo install wasm-pack
After the above, clone this project:
git clone https://github.com/rich-murphey/wasm-hostcall-example.git
cd wasm-hostcall-example
Then build the WebAssembly module:
wasm-pack build wasm
Then build and run the application:
cargo run
Rust WebAssembly imports these functions from the host application to demonstrate passing various argument types:
fn log_int(s: i32) // passes an integer
fn log_str(s: &str) // passes pointer and size, zero-copy.
fn log_ab(ab: &AB) // passes pointer and size of a serialized copy
fn log_cd(cd: &CD) // passes pointer and size of a struct, zero-copy.
#[derive(Debug, Serialize, Deserialize)]
pub struct AB {
pub a: u32,
pub b: String,
}
#[derive(Debug, Copy, Clone)]
pub struct CD {
pub c: i32,
pub d: ArrayString::<[u8; CD_N]>,
}
The WebAssembly (Wasm) function hello() in wasm/src/lib.rs calls the above functions.
pub fn hello() -> Result<i32,JsValue> {
log_int(1234);
log_str("Hello World!");
log_ab(&AB{a: 1234, b: "abcd".to_string()});
log_cd(&CD::from(1234, "hello world"));
Ok(4567)
}
The WebAssembly side of the API is defined in wasm/src/imports.rs. Note that log_int() and log_str() do not need any additional conversion on the WebAssembly side.
The host (application) side of the API is defined in src/exports.rs:
// Given a rust &str at an offset and size in caller's Wasm memory, log it to stdout.
fn log_str(caller: Caller<'_>, offset: i32, size: i32) -> Result<(), Trap> {
let mem :Memory = mem_from(&caller)?; // caller's VM memory
let slice :&[u8] = slice_from(&mem, offset, size)?; // string's byte slice
let string :&str = std::str::from_utf8(slice) // convert to utf-8
.or_else(|_|Err(Trap::new("invalid utf-8")))?;
println!("str: {}", string); // print the string
Ok(())
}
See exports.rs and imports.rs for the corresponding code for the other functions in the API.
- The Bytecode Alliance hosts WebAssembly, Wasmtime, Cargo Wasi, Wasi, Lucet and others.
- Wasmtime implements a WebAssembly run-time (virtual machine).
- Cargo Wasi is a tool for compiling rust modules to WebAssembly.
- WebAssembly System Interface is analogous to parts of libc.
- Fastly's http_guest API. Fastly's Terrarium runs customer's WebAssembly in a edge web server. Customer's WebAssembly module handles specified http requests.
- Cloudflare's Wirefilter, runs customers Wireshark-like filters in WebAssembly on edge compute.*
- The Deno plugin API is an interface between host native Rust and either Wasm or Javascript: Rust host plugin example, client javascript example, blog post.
Suggestions and comments are welcome. Please feel free to open an issue if you can suggest improvements, or find parts that are unclear.