Stream: general

Topic: How to pass user-defined types to the host?


view this post on Zulip Ben Little (Jun 12 2023 at 19:44):

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.

view this post on Zulip Lann Martin (Jun 12 2023 at 20:45):

Depends on your needs:

Repository for design and specification of the Component Model - GitHub - WebAssembly/component-model: Repository for design and specification of the Component Model

view this post on Zulip Lann Martin (Jun 12 2023 at 20:47):

externrefs are more "handles" to some resource. They don't really help you pass composite data around.

view this post on Zulip Ben Little (Jun 12 2023 at 22:12):

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.

view this post on Zulip Ben Little (Jun 12 2023 at 22:36):

Memory::read looks promising :thumbs_up:

view this post on Zulip fitzgen (he/him) (Jun 13 2023 at 16:25):

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 an i32.

The module needs to export its memory, and then you'll be able to access it via Caller::get_export.

view this post on Zulip Ben Little (Jun 14 2023 at 19:17):

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?

view this post on Zulip Jamey Sharp (Jun 14 2023 at 19:35):

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.

view this post on Zulip Ben Little (Jun 14 2023 at 19:40):

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?

view this post on Zulip Jamey Sharp (Jun 14 2023 at 19:45):

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.

view this post on Zulip Lann Martin (Jun 14 2023 at 19:48):

and to be clear, most modules do export their single memory

view this post on Zulip Lann Martin (Jun 14 2023 at 20:01):

I'm not sure LLVM even knows how to produce a non-exported memory section... :thinking:

view this post on Zulip bjorn3 (Jun 14 2023 at 20:48):

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.

view this post on Zulip bjorn3 (Jun 14 2023 at 20:48):

(deleted)

view this post on Zulip Ben Little (Jun 15 2023 at 00:01):

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);
    },
)

view this post on Zulip Jamey Sharp (Jun 15 2023 at 00:05):

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.

view this post on Zulip Ben Little (Jun 15 2023 at 00:07):

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?

view this post on Zulip Lann Martin (Jun 15 2023 at 00:44):

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 Stores don't currently clean up resources until the whole thing is dropped.

view this post on Zulip Ben Little (Jun 15 2023 at 01:58):

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?

view this post on Zulip bjorn3 (Jun 15 2023 at 09:31):

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.

view this post on Zulip Ben Little (Jun 15 2023 at 16:15):

Thank you, Jamey, Lann, Bjorn, and fitzgen. This is really helpful for my understanding.


Last updated: Oct 23 2024 at 20:03 UTC