Hi,
The following are perhaps 'basic' questions, but I wasn't able to get clear guidance for all of them from the wasmtime
crate's documentation. I would appreciate your recommendations.
Scenario:
Goal:
Questions:
Linker
or one per invocation of a function in the guest? or, what are the considerations for each option?Module
or one per invocation of a function in the guest? (i've tried caching the module after calling Module::from_file(...)
once, and this has significantly reduced runtime duration. but i want to make sure i fully understand the downsides of doing this, if there are any)wasmtime_wasi::add_to_linker(...)
actually do, and if I do cache the Module
, is it ok to call it just once after i first call Module::from_file(...)
?Store
for each invocation of a function in the guest, or can i reuse Store
s under certain circumstances (i understand there's no built-in GC for Store
s)?Thanks in advance!
Good questions, and thanks for asking!
One important thing you may want to consider is the distinction between a module and an instance. A module is a compiled version of a wasm module, and an instance is a "runnable version" of that module. Modules have no state other than their compilation artifact and can hence be reused amongst many instances. Instances have state, but by default do not share state. The Linker
type is the means by which you go from a Module
to an Instance
by satisfying all the imports of the module with actual runtime values.
I'm explaining these bits because the answer to some of your questions is sort of how the state works out. Do you want one wasm instance for the entire lifetime of the host for example? One instance per function call? The impact of this is what state the wasm instance can share between function calls (e.g. with an instance-per-function-call you'll share no state). With only one instance then you only need one Module
and one Linker
. With instance-per-function (or some other granularity), you only need one Module
and you probably only need one Linker
for the whole lifetime unless you're instantiating with different runtime values for each import.
Given some of that background, here's an attempt to answer some specific questions:
Linker
per lifetime of the process. This may require some refactoring and/or adjusting of the data model of your component, and it may not, depends on the specifics.Module
per wasm blob. If you've only got one wasm blob you should have only one Module
. This is expected and there's no downside to Module
reuse, it's designed to be reused.wasmtime_wasi::add_to_linker
function will populate a Linker
with an implementation of WASI functions that the instance can get access to. This is required if the Module
imports from the wasi_snapshot_preview1
name, for example. The Linker
does not store the Module
so it doesn't matter whether this happens in relation to the creation of the Module
or not.Store
depends on your data sharing module. If you want the wasm instance to persist its linear memory between function invocations you'll want to use the same Store
. If you want no data persisted you should create a second Store
with a second instance. A good rule-of-thumb is one instance per store. You should not repeatedly instantiate into the same Store
for the lifetime of your process since, as you've noted, nothing in a Store
is dropped until the whole Store
is dropped which means that your program would otherwise have a memory leak.If you do end up wanting a single Linker
and multiple Instance
s, take a look at https://docs.rs/wasmtime/6.0.1/wasmtime/struct.InstancePre.html, which is intended to be the fastest way to get multiple instances from the same module/linker config
thanks @Alex Crichton ! this is very helpful information.
one follow up question:
linker.module()
actually do?Store
per Instance
- is it possible to call linker.module()
once (with some Store
), or must i call it foreach Store
that I create? (and is overhead in calling linker.module()
each time i create a new Store
?)The linker.module()
method is intended for what is largely now legacy WASI integration, I'd recommend using .instantiate_pre
as @Lann Martin mentioned and working with that for creating multiple instances (or using .instantiate
if you're only creating one instance)
If you create a new instance-per-function (or similar), I'd go with @Lann Martin's suggestion which is to call linker.instantiate_pre()
and then repeatedly instantiate that instance (since that does type and import checking once and not on each instantiation)
thanks very much @Alex Crichton and @Lann Martin !
i'm not out of the woods just yet...
i define host functions (using linker.func_wrap()
) whose implementation depends on the contents of the current store (i write the input to the guest into them, and have the guest write its output using them)
something like:
let input: &[u8] = ...;
linker
.func_wrap("my_host", "get_input_size", move || -> i32 {
input.len() as i32
})
.unwrap();
linker
.func_wrap(
"my_host",
"get_input",
move |mut caller: Caller<'_, WasiState>, ptr: i32| {
let mem = caller.get_export("memory").unwrap().into_memory().unwrap();
let offset = ptr as u32 as usize;
mem.write(&mut caller, offset, input).unwrap();
},
)
.unwrap();
let output: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(vec![]));
let output_ = Arc::clone(&output);
linker
.func_wrap(
"my_host",
"set_output",
move |mut caller: Caller<'_, WasiState>, ptr: i32, capacity: i32| {
let output = output_.clone();
let mem = caller.get_export("memory").unwrap().into_memory().unwrap();
let offset = ptr as u32 as usize;
let mut buffer: Vec<u8> = vec![0; capacity as usize];
mem.read(&caller, offset, &mut buffer).unwrap();
let mut output = output.lock().unwrap();
*output = buffer;
},
)
.unwrap();
if i call linker.instantiate_pre()
when i instantiate the module (once, at the beginning of the host's lifetime, and before i call the aforementioned linker.func_wrap()
, which differ per function invocation as the input
and output
are different), then the guest doesn't recognize the host's functions and panics.
is there a simple way around this you can think of?
You'll want to place store-specific data in the T
of Store<T>
right now it's WasiState
in your example, but you'd instead want to do something like:
struct MyState {
wasi: WasiState,
input: Vec<u8>,
output: Option<Vec<u8>>,
}
then before you invoke wasm you'd do:
store.data_mut().input = ...;
and after invoking wasm you'd do:
let output = store.data().output.as_ref().ok_or_else(|| ...)?;
great point. thanks again!
If you've got suggestions about how to improve the docs here that'd also be much appreciated as well
be it more examples or more words or less words!
i think an end-to-end example of how to write a guest and a host which pass arguments and returns values through shared memory would be very helpful for people with scenario's like mine
the relationships between Module/Instance/Linker/Store, and best practices in creating/caching them, as mentioned in this thread, can also be helpful if documented in a central place.
@Alex Crichton, as @yonil mentioned, at least for my way of learning, I miss examples and best practice recommendations
Makes sense! I don't know how best to put all that into a document today, but it's good to be aware of nonetheless!
Last updated: Dec 23 2024 at 14:03 UTC