Stream: git-wasmtime

Topic: wasmtime / issue #10262 Question: Any reason to demand Sy...


view this post on Zulip Wasmtime GitHub notifications bot (Feb 20 2025 at 19:23):

xdlin opened issue #10262:

Hi, I got a trouble when calling another async function within WIT async host function because my function depends on a task_local storage which is not Sync, and I found that wit-bindgen generate host function with following signature:

  pub trait Host: Send + ::core::marker::Send {
      type Data;
      fn host_func(
          accessor: &mut wasmtime::component::Accessor<Self::Data>,
          id: wasmtime::component::__internal::String,
      ) -> impl ::core::future::Future<
          Output = wasmtime::Result<
              wasmtime::component::__internal::String,
          >,
      > + Send + Sync + ::core::marker::Send
      where
          Self: Sized;

Is is possible to loose this restriction?

view this post on Zulip Wasmtime GitHub notifications bot (Feb 20 2025 at 20:52):

pchickey commented on issue #10262:

Thanks, this is a great question that I've been wrestling with as well so hopefully this explanation helps you and others who run into this.

The quick answer is: this restriction is not always ideal but its essentially required for Wasmtime to interoperate with the Rust async ecosystem, and lifting it would create considerable difficulty throughout the Wasmtime ecosystem. Its an unfortunate problem I've run into a bunch, especially as I'm currently working on no_std based single-threaded embeddings for wasmtime, but I don't have a satisfactory way to resolve it. If you are using Wasmtime in a single-threaded context, we currently don't have a better answer than to lie to the type system and write unsafe impl Send for MyCtxType {} unsafe impl Sync for MyCtxType {} for the types you impl Host on, in order to work around this.

Here's lots more details, possibly more details than you need:

Wasmtime's async support exists so that Wasmtime can be embedded in async Rust applications. While. not all async Rust applications use an executor that requires Futures which are Send, in practice, many of our production users are using the Tokio ecosystem and have a hard requirement on Tokio's multi-threaded scheduling in order to maximize the capacity of their systems.

Wherever possible, we have kept Wasmtime's interfaces agnostic on requiring Send types. For example, the Store<T> type is careful to not put any Send constraints on T where possible. This lets Rust's type checker determine whether your Store is Send based on whether your T is Send, which is the way things Should Work.

However, if you use Store::limiter_async you end up with a Send constraint on the ResourceLimiterAsync impl - like the Host trait you showed, its an #[async_trait] that sprinkles Send on all of the Futures returned by those methods, which will end up implying Send on everything they capture, which will include Self. This all comes down to what ResourceLimiterAsync is designed to do: it exists to provide a programming hook so that a Wasmtime Store's desire for more memory will await on resources, effectively to give the async runtime the ability to pause a store's execution and resume it, possibly on another thread, when resources are available. Due to the Rust type system we basically had to pick whether to put a Send on ResourceLimiterAsync in order for it to work on multi-threaded Tokio (henceforth I'll refer to this as just Tokio), or else if we left Send off it wouldn't work on Tokio.

Now, you might point out that, like we sometimes see in the various Rust async ecosystems, there could be a limiter_async_local variant of that method which doesn't have the Send constraint so that users could pick whether they are Send or not. This might be possible, but each case we do this for would increase the complexity of Wasmtime's implementation, and there end up being many such cases all throughout Wasmtime - the ResourceLimiterAsync is just one example. You might also notice things like, hey, over in the plain old synchronous Store::limiter there's no Send requirements on the ResourceLimiter itself, but there are Send requirements on the impl FnMut(&mut T) -> &mut dyn ResourceLimiter accessor function - if we were to drop that Send constraint there, we would break Rust's ability to make any <T: Send> Store<T> also be Send, so it would require yet more gymnastics, perhaps culminating in breaking it into distinct Store and StoreLocal that differ only in Send constraints. In my opinion, that would be a total mess - it would be much harder for users to understand Wasmtime, and much harder for maintainers to maintain it.

So, now that I've laid out how Send is infectious in not just the async Wasmtime apis but also in other places, we can generalize that to the futures returned by those Host methods - thats just table stakes for running on an async runtime. When it comes to the constraint put on the type T that impls Host itself (rather than the Futures that methods on T return), that comes down to the the ResourceTable abstraction: any value that the resource table is given ownership of (via push or the push_child variant) must be Send. ResourceTable is a heterogeneous collection, and in order for ResourceTable to be Send, all of the values it owns are Box<dyn Any + Send>. In practice many of the methods in various Host traits are implemented in such a way that the Future captures an item owned by the ResourceTable.

I want to conclude that I sympathize with this Send infection making Wasmtime difficult to program with in situations where you cannot, or don't want to, impl Send on the structures interacting with Wasmtime. I'm working directly in those situations, and I don't like that I end up sprinkling /* SAFETY: this is only executed in a single threaded environment */ unsafe impl Send all over my codebase for the T in Store<T> or for anything I put in the ResourceTable. All I can do is apologize that, when I encountered this problem, I went back and worked through the way that dropping that constraint would require all of Wasmtime to contort to permit it, and decided that it simply wasn't worth it.

At the end of the day, the best answer I have is, this isnt the sort of property that Rust's type system lets us make parametric or truly "zero cost" to the programmer, without costs to the wasmtime project that we as maintainers couldn't stomach. So we had to pick one side and live with the fallout. We picked interoperability with Tokio because some important production uses (including me, years ago) demanded it, and that means other users (including me, now) have to unfortunately live with it.

view this post on Zulip Wasmtime GitHub notifications bot (Feb 20 2025 at 22:10):

xdlin commented on issue #10262:

Hi @pchickey thanks a lot for your explanation in great details, now I totally understand it, that's the trade-off we have to pay to keep a maintable system, which is fair enough.

Then I'll try to find some workaround in my own code to make type checker happy

view this post on Zulip Wasmtime GitHub notifications bot (Feb 20 2025 at 22:38):

pchickey closed issue #10262:

Hi, I got a trouble when calling another async function within WIT async host function because my function depends on a task_local storage which is not Sync, and I found that wit-bindgen generate host function with following signature:

  pub trait Host: Send + ::core::marker::Send {
      type Data;
      fn host_func(
          accessor: &mut wasmtime::component::Accessor<Self::Data>,
          id: wasmtime::component::__internal::String,
      ) -> impl ::core::future::Future<
          Output = wasmtime::Result<
              wasmtime::component::__internal::String,
          >,
      > + Send + Sync + ::core::marker::Send
      where
          Self: Sized;

Is is possible to loose this restriction?

view this post on Zulip Wasmtime GitHub notifications bot (Feb 20 2025 at 22:38):

pchickey commented on issue #10262:

I'll close this but if you or anyone has further questions feel free to continue discussion here.

view this post on Zulip Wasmtime GitHub notifications bot (Feb 21 2025 at 03:58):

PureWhiteWu commented on issue #10262:

Hi @pchickey thanks very much for your detailed explanation! I know why Send is required, because tokio multi thread runtime requires Send, but why is Sync also required in the return future of host_func? This will cause things like task_local unable to compile.

view this post on Zulip Wasmtime GitHub notifications bot (Feb 21 2025 at 17:12):

pchickey commented on issue #10262:

Sync ends up being a constraint for essentially all of the same reasons that Send does, but I'm not sure that I understand the question. What is task_local and how are you using it that is unable to compile?

view this post on Zulip Wasmtime GitHub notifications bot (Feb 21 2025 at 17:48):

PureWhiteWu commented on issue #10262:

For multi-thread runtimes, only Send is required for Future types, so I wonder why is Sync required here for the returned Future of host_func.

As far as I know, the Future will not be accessed by multi threads at the same time, so it doesn't need Sync here.

view this post on Zulip Wasmtime GitHub notifications bot (Feb 21 2025 at 17:57):

PureWhiteWu commented on issue #10262:

And for task_local, this is a mechanism provided by tokio to bind some context to an asynchronous task, similar to synchronous thread_local. Here's the documentation: https://docs.rs/tokio/latest/tokio/macro.task_local.html.

task_local's usage is like thread_local, which needs a RefCell to make it interior mutable. Common examples are like this:

task_local! {
    static CTX: RefCell<Context>;
}

CTX.scope(RefCell::new(Context::new()), async { inner future here }).await;

Because there's a RefCell here, which is not Sync, which will cause the entire Future !Sync.

view this post on Zulip Wasmtime GitHub notifications bot (Feb 21 2025 at 17:59):

PureWhiteWu edited a comment on issue #10262:

For multi-thread runtimes, only Send is required for Future types, so I wonder why is Sync required here for the returned Future of host_func.

As far as I know, the Future will not be accessed by multi threads at the same time, so it doesn't need Sync here. What Futures need is only Send, because it may be send across threads, but will only be executed(polled) on one thread at a moment, since the Future::poll func requires &mut self.

view this post on Zulip Wasmtime GitHub notifications bot (Feb 24 2025 at 20:52):

alexcrichton commented on issue #10262:

The Sync constraint on the future for concurrent functions is a mistake and will get dropped as it's developed. I'll note that the support for concurrent imports is part of the component-model-async work which is not yet complete nor suitable for general-purpose use.

view this post on Zulip Wasmtime GitHub notifications bot (Feb 24 2025 at 20:55):

alexcrichton commented on issue #10262:

I've opened https://github.com/bytecodealliance/wasip3-prototyping/issues/30 in the prototyping repo to track this.

view this post on Zulip Wasmtime GitHub notifications bot (Feb 24 2025 at 23:47):

xdlin commented on issue #10262:

I'll note that the support for concurrent imports is part of the component-model-async work which is not yet complete nor suitable for general-purpose use

@alexcrichton Totally understand, since I'd like to bring the async component model to our own project ASAP which is in early stage too, so I'd like to be deeply invoved in this project and maybe provide some contribution if possible (at least fill some issues)

And thanks so much for you and all other team members' patience and great support~

view this post on Zulip Wasmtime GitHub notifications bot (Feb 25 2025 at 03:21):

alexcrichton commented on issue #10262:

Great! For now it's probably best to file issues in the wasip3-prototyping repo and expect anything ranging from segfaults to crashes to misbehaviors to bugs. If you're ok with that though kire-ticking is appreciated!


Last updated: Feb 28 2025 at 03:10 UTC