This is where things get a lot more complex and you start to see why tools like wit-bindgen
were created.
First, let's assume our guest code (written in C) looks something like this:
typedef struct {
char first_name[16];
uint16_t age;
} Example;
// The host function we want to use
int64_t my_extern_func(Example *e, int64_t *ptr, uint64_t *cptr);
This is perfectly valid for the guest to do. Note that I've chosen integer types with explicit sizes - this is important because WebAssembly is typically compiled as 32-bit, while your host is typically a 64-bit machine, and we don't want to mess things up because some integer type decided to be a different size on one architecture versus the other.
Now the interesting thing to note is that when the guest passes the host a "pointer", it's really just passing you a u32
index into its linear memory. That means the implementation of our host function looks something like this:
linker.func_wrap("env", "my_extern_fun", my_extern_func)?;
fn my_extern_func(caller: Caller<'_>, e: u32, ptr: u32, cptr: u32) -> i64 {
...
}
(the Caller
parameter is what we use to access the internals of a WebAssembly module)
Now, the tricky part is to turn that e
"pointer" back into something that looks like an Example
struct. We do this by asking the caller
to give us the Memory
object exported by this WebAssembly module (typically called "memory"
, I think) that represents the module's linear memory, then asking for mutable/immutable access to the bytes.
Once we've got access to the linear memory as a range of bytes, we can do some unsafe
pointer arithmetic and casting to get the *const Example
back.
#[repr(C)] // Safety: it's our responsiblity make sure this
// matches the C struct's layout *exactly*.
struct Example {
first_name: [u8; 16],
age: u16,
}
fn my_extern_func(caller: Caller<'_>, e: u32, ptr: u32, cptr: u32) -> i64 {
// First, get a reference to the memory object
let memory = caller.get_export("memory")
.unwrap()
.into_memory()
.unwrap(); // TODO: proper error handling
// Next, we need to access the linear memory as a bunch of bytes
let linear_memory: &[u8] = memory.data(&caller);
// Safety: This is safe because
// - We've manually verified that the layout for our two Example structs
// match, and Example doesn't contain any internal pointers which we
// might accidentally interpret as pointers on the host instead of offsets
// into the guest's linear memory
// - The `my_extern_func` function doesn't call back into WebAssembly,
// (which could accidentally lead to it mutating/destroying the values
// while the host has a reference to them, or growing linear memory
// which could leave us with dangling pointers)
// - The guest promises that the pointers are within linear memory
// - The start of linear memory is guaranteed to be aligned, and the
// Example object is guaranteed (by the compiler/guest's allocator) to
// be aligned correctly with respect to the start of linear memory
unsafe {
let e: *const Example = linear_memory.as_ptr().add(e as usize).cast();
// If these are meant to be slices, then use std::slice::from_raw_parts()
let ptr: *const i64 = linear_memory.as_ptr().add(ptr as usize).cast();
let cptr: *const u64 = linear_memory.as_ptr().add(ptr as usize).cast();
// magic goes here
...
}
}
(I haven't actually run this code or checked whether it compiles, but it should work)
You could also parse the byte slice you get from &linear_memory[e as usize..e as usize + 18]
to get a [u8; 16]
followed by a little-endian u16
like you would when implementing the parser for a binary format, but both pointer arithmetic and parsing intimately rely on the layout of Example
so they're pretty much equivalent. I'm okay with taking the unsafe
route because I wrote both sides of the code, they both have the same failure mode, and an unsafe
cast requires 1 line whereas safely parsing would require 5-15 lines with no real benefit.
Before I switched to wit-bindgen
I was maintaining about 4kloc of this sort of binding code at work (although using Wasmer, not Wasmtime), so I can attest to how monotonous and error-prone it is.