Stream: wasmtime

Topic: How-to: Re-entry


view this post on Zulip QuantumSegfault (Aug 19 2026 at 08:04):

With Wasmtime, what combination of features is needed to allow host -> guest -> host -> (same guest) re-entry for the component model.

We have the host that calls one export of a plugin, which may call a host import, which itself may call a separate export on the same component. Right now we are running into major barriers due to wasm trap: cannot enter component instance

As far as I can tell from reading the latest spec, re-entry SHOULD be possible.

The component model invariant #2 about re-entry is

Components can only be reentered (via component export or thread resumption) when they explicitly block or call a donut wrapped child component. Calls to non-async functions do not count as "blocking" nor do non-blocking (async-lowered) calls to async functions...

With the definition of "block" including stackless coroutine async YIELD and WAIT.

So based on reading this, if marking all the imports and exports async, and awaiting the results of each call in the chain (inside-out), then it should all be fine, as we are blocking. But no dice.

Nothing I've tried works. Yet with jco, I've not run into this issue. Re-entry works exactly as I need.

I have a little experiment repo here. Maybe I'm just missing some simple flag somewhere?
https://github.com/QuantumSegfault/wit-plugin-versioning-experiments/tree/reentry


@Alex Crichton

view this post on Zulip Alex Crichton (Aug 19 2026 at 14:47):

We've gone sort of back-and-forth over time on the spec about what exactly is reentrant and what exactly is allowed in various locations. The test coverage of the spec tests as-is isn't great which is likely why jco/wasmtime diverge in this case. In talking with Luke recently I think the current conclusion is that things are going to be changed to just allow reentrance, but that's a spec change not made yet nor not implemented yet

view this post on Zulip QuantumSegfault (Aug 19 2026 at 16:25):

Oh...so I've been misreading the current spec?

Any guesstimates as too when this will be resolved? This is a pretty significant issue for our uses.

I was wondering what kinds of damage would be done if I just neuter the may_enter check and simply allow re-entry (except when the instance has already trapped).

view this post on Zulip Alex Crichton (Aug 19 2026 at 16:30):

Nah I wouldn't say you've been misreading, but I also wouldn't be surprised if wasmtime and the spec disagree here by accident. This is an area where I haven't paid super close attention myself, and the thinking that we should remove reentrance checks is pretty recent (within the last week or two). Once we're agreed on the spec side it shouldn't be too hard to implement in Wasmtime as it's in theory just deleting code

view this post on Zulip Alex Crichton (Aug 19 2026 at 16:30):

timeline-wise, maybe within a month from now?

view this post on Zulip QuantumSegfault (Aug 19 2026 at 17:00):

Okay. Good to know its at least in the plan.

But maybe you can shed some light on this. Somebody found a solution...

host calls export A (inside Store::run_concurrent), A calls import C, C calls export B. All are marked async in the WIT.

This did not work:

use crate::plugin_host::PluginHost;

wasmtime::component::bindgen!({
    path: "../../wit",
    imports: { default: store | async }
});

impl PluginImports for PluginHost {}

impl<T: Send> PluginImportsWithStore<T> for PluginHost {
    async fn c(accessor: &wasmtime::component::Accessor<T, Self>) -> String {
        let p_arc = accessor.with(|mut a| {
            let host = a.get();
            host.plugin.as_ref().unwrap().upgrade().unwrap()
        });

        // even explicit .func_b().call_concurrent did not
        let result = p_arc.plugin_instance.call_b(accessor).await;

        format!("C: {}", result.unwrap())
    }
}

But this does! It is maybe not the prettiest thing, but it worked.

use crate::plugin_host::PluginHost;

wasmtime::component::bindgen!({
    path: "../../wit",
    imports: { default: store | async }
});

impl PluginImports for PluginHost {}

struct CallB {
    tx: tokio::sync::oneshot::Sender<wasmtime::Result<String>>,
}

impl<T: Send + 'static> wasmtime::component::AccessorTask<T, PluginHost> for CallB {
    async fn run(
        self,
        accessor: &wasmtime::component::Accessor<T, PluginHost>,
    ) -> wasmtime::Result<()> {
        let p_arc = accessor.with(|mut a| {
            let host = a.get();
            host.plugin.as_ref().unwrap().upgrade().unwrap()
        });

        let result = p_arc.plugin_instance.call_b(accessor).await;
        let _ = self.tx.send(result);
        Ok(())
    }
}

impl<T: Send + 'static> PluginImportsWithStore<T> for PluginHost {
    async fn c(accessor: &wasmtime::component::Accessor<T, Self>) -> String {
        let (tx, rx) = tokio::sync::oneshot::channel();
        let _ = accessor.spawn(CallB { tx });
        let result = rx.await.unwrap();

        format!("C: {}", result.unwrap())
    }
}

So I was just doing it a little wrong the whole time??? :man_facepalming:

view this post on Zulip Alex Crichton (Aug 19 2026 at 19:15):

hm yeah with one of those working and one not working that sounds to me like it's a bug in Wasmtime as those should more-or-less be the same or similar

view this post on Zulip Alex Crichton (Aug 19 2026 at 19:16):

as a workaround for now though that seems reasonable

view this post on Zulip iKranium (Aug 19 2026 at 20:03):

QuantumSegfault said:

Okay. Good to know its at least in the plan.

But maybe you can shed some light on this. Somebody found a solution...


Clever solution, it satisfies the Rust borrow rules in the wasmtime engine by converting them to futures and leveraging the native OS context switching. The one doesn't work because it did not serialize the entire roundtrip as a complex function and called into itself, violating the borrow rules by duplicating &mut references

You could also use Asyncify that re-writes the recursions as a brute form of stack-switching, which adds bloat and execution overhead. The new Context-Switching Proposal will provide stack-switchining in the WASM environment, but mainly for threads I think: https://github.com/WebAssembly/stack-switching

view this post on Zulip iKranium (Aug 20 2026 at 03:36):

iKranium said:


Here's how Holochain handled the issue in a Wasmer runtime for a wasm32-unknown-unknown target:
https://docs.rs/holochain_wasmer_guest/latest/holochain_wasmer_guest/

view this post on Zulip Till Schneidereit (Aug 20 2026 at 11:42):

(removed two entirely off-topic messages)

view this post on Zulip Till Schneidereit (Aug 20 2026 at 17:52):

in discussions in DMs, @iKranium clarified that their messages were very much intended to be on topic, including the link to https://docs.rs/holochain_wasmer_guest/latest/holochain_wasmer_guest/. I'll leave it to them to explain how exactly that page relates to this topic or could help with the original question raised

view this post on Zulip QuantumSegfault (Aug 25 2026 at 20:37):

So, more on this.

I was under the impression that the CM was supposed to avoid the "function coloring" problem. But right now, if I want to make my plugin export async (purely to solve the re-entry problem), all the host imports that may transitively call such exports must ALSO be async.

Is this intended? The WIT for the prior examples was:

package test:plugin;

world plugin {
    import c: async func() -> string;

    export a: async func();
    export b: async func() -> string;
}

If I try to make c sync (remove async), I end up just deadlocking awaiting on rx.

view this post on Zulip Joel Dice (Aug 25 2026 at 20:50):

I'm actually working on a patch right now to make Wasmtime match https://github.com/WebAssembly/component-model/pull/705, which allows reentrance in all cases, and should avoid the need for the oneshot::channel workaround. And as Alex said, the fact that the oneshot::channel workaround even worked was probably a bug.

I'd recommend re-testing with https://github.com/bytecodealliance/wasmtime/pull/14146 once I've pushed an update.

The general rule is that sync-typed functions are not allowed to block before returning a value, but the host has "super-powers" to do things that wouldn't be allowed in guest-to-guest interactions, so you might be able to get away with a sync-typed host import that reenters the guest via an async-typed export. However, it seems more honest and flexible to make the host import async-typed, giving the guest the opportunity to call it asynchronously and concurrently with other calls.

view this post on Zulip QuantumSegfault (Aug 25 2026 at 21:01):

Looking forward to it. :slight_smile:

view this post on Zulip QuantumSegfault (Aug 27 2026 at 04:49):

Tried the PR after you made that last push, seems to work! Tried some different things.

The minimal amount of async I found is:

world plugin {
    import c: func() -> string;

    export a: async func();
    export b: func() -> string;
}

As long as the inital export is async (to help satisfy other constraints due to Tokio and WASI), I was able to call a sync import, which calls a sync export.

I imagine I'd probably make the whole chain async for the reason you described, but interesting to see it works!

view this post on Zulip Joel Dice (Aug 27 2026 at 13:27):

Cool, glad it works. That PR is still a work-in-progress as Luke and I work through a few more details, but I'll switch it out of draft mode once it's ready.


Last updated: Aug 30 2026 at 09:07 UTC