I want to be able to construct some data MyType
in a WASM module and then pass that data to the host. I can imagine a few ways to do this:
One is defining WasmTy
for MyType
. However it looks like this trait cannot be defined outside of the core wasmtime
crate because that trait's method signatures use StoreOpaque
which is not an exported type.
Another way would be to convert MyType
to a Vec<u8>
or less conveniently a[u8; MYTYPE_SIZE]
. However again neither Vec<u8>
nor [u8]
implement WasmTy
.
The last way is using ExternRef
, which seems like the way things are "meant" to be done for passing opaque data that the runtime would then validate and cast into the appropriate type. However the docs aren't clear about the best practices for producing ExternRef
in the WASM module and consuming it from the host, so I'm not sure if I'm misunderstanding how this should be used.
Depends on your needs:
bindgen
above does.externref
s are more "handles" to some resource. They don't really help you pass composite data around.
Okay thank you, that makes some sense. I actually just noticed that in the docs for Caller
, it suggests passing a pointer and then accessing the module's memory. However I don't see any way to access the module's memory using the Caller
interface. I can access the underlying state and exports, but I don't think either of those will let me resolve the pointer, presumably passed as an i32
.
Memory::read
looks promising :thumbs_up:
Ben Little said:
However I don't see any way to access the module's memory using the
Caller
interface. I can access the underlying state and exports, but I don't think either of those will let me resolve the pointer, presumably passed as ani32
.
The module needs to export its memory, and then you'll be able to access it via Caller::get_export
.
Hmm, I don't think I understand. Is memory not a property of the module that is visible from the runtime? It seems strange to need to export it.
My current mental model is that the runtime controls the memory of the module. To access a value in the module's memory segment, I would allocate that value in the module and pass a pointer to runtime via an extern fn
called by the module. The runtime would then access the value by adding the pointer to the offset of the module's memory in the runtime's memory.
For passing a value to the module, the runtime doesn't know about the module's allocator. Therefore the module needs to allocate memory for the value, pass a pointer to the runtime as above, and have the runtime put that value in the module's memory, again using the start of the module's memory + pointer.
Am I thinking about this at too low of a level? Or am I misunderstanding the memory management, perhaps?
It's important to understand that within a wasm guest, a "pointer" is just a 32-bit integer that's relative to the starting address of linear memory. If you pass a pointer from the guest to the host, then to access that memory location the host needs to add that integer to the base address of that guest's linear memory.
And yes, the runtime knows where the module's linear memory is with respect to the host's address space. But the Wasmtime API (as well as the APIs of other runtimes, I assume) doesn't give you a way to access resources of a module unless the module explicitly exports those resources.
Thanks, still a little confused.
But the Wasmtime API (as well as the APIs of other runtimes, I assume) doesn't give you a way to access resources of a module unless the module explicitly exports those resources.
Will Memory::read
and Memory::write
not do essentially this? Or is that not considered part of the wasmtime API?
The issue is where you get the Memory
from in order to call that method on it. I believe the only option here is https://docs.rs/wasmtime/latest/wasmtime/struct.Instance.html#method.get_memory which only operates on exported memories.
and to be clear, most modules do export their single memory
I'm not sure LLVM even knows how to produce a non-exported memory section... :thinking:
You can tell wasm-ld (lld) to import a memory without re-exporting it and in fact that is the default when importing a memory afaik.
(deleted)
Would that look like caller.get_export("memory")
or
let engine = Engine::default();
let module = Module::from_binary(&engine, &binary)?;
let mut store = Store::new(&engine, MyData::default());
let memory = Memory::new(&mut store, MemoryType::new(1, None))?;
let get = Func::wrap(
&mut store,
move |mut caller: Caller<'_, MyData>, ptr: i32| {
let data = caller.data_mut().get().serialize_bytes();
memory.write(caller, ptr as usize, &data).unwrap();
},
);
let set = Func::wrap(
&mut store,
move |mut caller: Caller<'_, MyData>, ptr: i32| {
let mut buffer = [0; 1024];
memory
.read(&mut caller, ptr as usize, &mut buffer)
.unwrap();
let data = MyData::deserialize_bytes(&buffer).unwrap();
caller.data_mut().set(data);
},
)
The code sample you've pasted there creates a new Memory
, independent from the one your instance is actually using. So those read and write calls would run successfully but access an entirely different chunk of memory than your guest is using. No, I think your other suggestion of Caller::get_export
is the right choice here.
Thanks! The docs for Memory::new
say
The
store
argument will be the owner of the returned [Memory
].
Does this mean that the supplied store would have two distinct linear memory segments?
Yes. A Store
can hold resources for many instances if you want. You still have to explicitly provide those resources to the instances they belong to. I believe Instance::new
can take a Extern::Memory
or something for memory imports. The main limitation is that Store
s don't currently clean up resources until the whole thing is dropped.
Okay I think that makes sense. So I can get the export with Caller::get_export
and cast it to Memory
with Extern::into_memory
. Is there a special name that LLVM gives to the memory export? Is it just "memory"
? Or do I need to define the export manually in some language-dependent manner?
Wasi requires the linear memory to be called memory
and wasm-ld exports it with this name if told to export it. Another language that doesn't use wasi may export it with another name, but you could require that any wasm module that will use your interface has to use memory
as name for the export.
Thank you, Jamey, Lann, Bjorn, and fitzgen. This is really helpful for my understanding.
Last updated: Dec 23 2024 at 13:07 UTC