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 Stores under certain circumstances (i understand there's no built-in GC for Stores)?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 Instances, 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 06 2025 at 06:05 UTC