From e1685a1e7e7089f2d52b48245813a685bd01d428 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Thu, 24 Sep 2026 07:37:47 -0700 Subject: [PATCH 1/2] Spawn through the `wasip3_task_vtable` This commit adds hooks in the C ABI portion of this crate to spawn Rust tasks. This fixes the issue where spawned tasks by any particular version of `wit-bindgen` weren't guaranteed to get picked up by whatever `wit-bindgen` happened to be running exports. This still isn't a great solution because it'll take time to roll out and it'll additionally require that both imports and exports have the `async-spawn` cargo feature enabled, but it's about the best that can be done for now and at least now there's a panic indicating that a spawned task won't get polled because it's not looked at on the exported side. Closes #1305 --- crates/guest-rust/src/rt/async_support.rs | 39 +++++----- .../guest-rust/src/rt/async_support/cabi.rs | 34 +++++++++ .../guest-rust/src/rt/async_support/spawn.rs | 72 +++++++++++++++++-- 3 files changed, 125 insertions(+), 20 deletions(-) diff --git a/crates/guest-rust/src/rt/async_support.rs b/crates/guest-rust/src/rt/async_support.rs index a33157a40..1de25d29e 100644 --- a/crates/guest-rust/src/rt/async_support.rs +++ b/crates/guest-rust/src/rt/async_support.rs @@ -123,6 +123,7 @@ struct TaskState<'a> { inter_task_wakeup: inter_task_wakeup::State, } +#[derive(Default)] struct SharedTaskState { /// One of `SLEEP_STATE_*` indicating the current status. sleep_state: AtomicU32, @@ -159,12 +160,7 @@ unsafe impl Send for CabiWaitable {} impl TaskState<'_> { fn new(future: BoxFuture<'_>) -> TaskState<'_> { - let shared = Arc::new(SharedTaskState { - sleep_state: AtomicU32::new(0), - inter_task_stream: Default::default(), - waitables: Default::default(), - waitable_set: Default::default(), - }); + let shared = Arc::new(SharedTaskState::default()); TaskState { waker: shared.clone().into(), shared, @@ -325,12 +321,12 @@ impl TaskState<'_> { // The `ptr` field of `wasip3_task` is to `SharedTaskState` which is // what's cloned/handed out/etc. let shared_raw: *const SharedTaskState = &*self.shared; - let mut wasip3_task = cabi::wasip3_task_v2 { + let mut wasip3_task = cabi::wasip3_task_v3 { v1: cabi::wasip3_task { ptr: shared_raw.cast_mut().cast(), - version: cabi::WASIP3_TASK_V2, - waitable_register: SharedTaskState::CABI_VTABLE.waitable_register, - waitable_unregister: SharedTaskState::CABI_VTABLE.waitable_unregister, + version: cabi::WASIP3_TASK_V3, + waitable_register: SharedTaskState::CABI_VTABLE.v2.waitable_register, + waitable_unregister: SharedTaskState::CABI_VTABLE.v2.waitable_unregister, }, vtable: &SharedTaskState::CABI_VTABLE, }; @@ -339,7 +335,7 @@ impl TaskState<'_> { // structure, and then cast its raw pointer to the "smaller" historical // version, ensuring the final pointer has provenace over the entire // structure. - let wasip3_task: *mut cabi::wasip3_task_v2 = &mut wasip3_task; + let wasip3_task: *mut cabi::wasip3_task_v3 = &mut wasip3_task; let prev = unsafe { cabi::wasip3_task_set(wasip3_task.cast::()) }; let _reset = ResetTask(prev); @@ -367,11 +363,17 @@ impl Drop for TaskState<'_> { } impl SharedTaskState { - const CABI_VTABLE: cabi::wasip3_task_vtable = cabi::wasip3_task_vtable { - waitable_register: Self::cabi_waitable_register, - waitable_unregister: Self::cabi_waitable_unregister, - drop: Self::cabi_drop, - clone: Self::cabi_clone, + const CABI_VTABLE: cabi::wasip3_task_vtable_v3 = cabi::wasip3_task_vtable_v3 { + v2: cabi::wasip3_task_vtable { + waitable_register: Self::cabi_waitable_register, + waitable_unregister: Self::cabi_waitable_unregister, + drop: Self::cabi_drop, + clone: Self::cabi_clone, + }, + #[cfg(feature = "async-spawn")] + rust_spawn: Some(Self::cabi_rust_spawn), + #[cfg(not(feature = "async-spawn"))] + rust_spawn: None, }; /// Adds the `waitable` provided to this task's waitable set. @@ -439,6 +441,11 @@ impl SharedTaskState { let mut me = unsafe { Self::cabi_to_self(ptr) }; unsafe { ManuallyDrop::drop(&mut me) } } + + #[cfg(feature = "async-spawn")] + unsafe fn cabi_rust_spawn(_ptr: *mut c_void, task: Box>) { + spawn::push(Pin::from(task)) + } } /// Status for "this task is actively being polled" diff --git a/crates/guest-rust/src/rt/async_support/cabi.rs b/crates/guest-rust/src/rt/async_support/cabi.rs index b03f5da92..674636c9f 100644 --- a/crates/guest-rust/src/rt/async_support/cabi.rs +++ b/crates/guest-rust/src/rt/async_support/cabi.rs @@ -66,7 +66,14 @@ //! While doing this everything was moved into a vtable structure instead of //! inline in `wasip3_task` to make it easier to add more function pointers //! in the future if necessary. +//! +//! ### V3 +//! +//! This was added 2026-09-25 in response to #1305 to get `spawn_local` working +//! across versions of `wit-bindgen`. This added a new entry to the vtable below +//! with a new v3 version of the structure. +use alloc::boxed::Box; use core::ffi::c_void; extern_wasm! { @@ -90,6 +97,7 @@ extern_wasm! { /// fields `ptr`, `waitable_register`, and `waitable_unregister`. pub const WASIP3_TASK_V1: u32 = 1; pub const WASIP3_TASK_V2: u32 = 2; +pub const WASIP3_TASK_V3: u32 = 3; /// Indirect "vtable" used to connect imported functions and exported tasks. /// Executors (e.g. exported functions) define and manage this while imports @@ -130,6 +138,17 @@ pub struct wasip3_task_v2 { pub vtable: &'static wasip3_task_vtable, } +/// Representation when `wasip3_task::version` is `WASIP3_TASK_V3`. +#[repr(C)] +pub struct wasip3_task_v3 { + /// The original task structure. + pub v1: wasip3_task, + + /// An always-valid pointer to a list of function pointers, described + /// below. + pub vtable: &'static wasip3_task_vtable_v3, +} + /// Function pointer operations that can operate on `wasip3_task::ptr`. /// /// This was introduced in the "v2" ABI and is a member of `wasip3_task_v2`. @@ -174,3 +193,18 @@ pub struct wasip3_task_vtable { /// that's not managed with this lifetime. pub drop: unsafe extern "C" fn(ptr: *mut c_void), } + +/// Function pointer operations that can operate on `wasip3_task::ptr`. +/// +/// This was introduced in the "v3" ABI and is a member of `wasip3_task_v3`. +#[repr(C)] +pub struct wasip3_task_vtable_v3 { + pub v2: wasip3_task_vtable, + + /// Drops and deallocates the provided pointer previously created by a + /// call to the `clone` callback above. + /// + /// This must not be called on the `ptr` value within `wasip3_task::ptr` as + /// that's not managed with this lifetime. + pub rust_spawn: Option>)>, +} diff --git a/crates/guest-rust/src/rt/async_support/spawn.rs b/crates/guest-rust/src/rt/async_support/spawn.rs index 8443832e1..031281f0e 100644 --- a/crates/guest-rust/src/rt/async_support/spawn.rs +++ b/crates/guest-rust/src/rt/async_support/spawn.rs @@ -3,11 +3,13 @@ // references can be a hazard due to recursive access. #![allow(static_mut_refs)] +use super::cabi; use crate::rt::async_support::BoxFuture; use alloc::boxed::Box; use alloc::vec::Vec; use core::future::Future; use core::pin::Pin; +use core::ptr; use core::task::{Context, Poll}; use futures::channel::oneshot; use futures::future::{AbortHandle, Abortable, Aborted}; @@ -77,6 +79,12 @@ impl<'a> Tasks<'a> { } } +pub(super) fn push(task: BoxFuture<'static>) { + unsafe { + SPAWNED.push(task); + } +} + /// Spawn the provided `future` to get executed concurrently with the /// currently-running async computation. /// @@ -122,10 +130,66 @@ impl<'a> Tasks<'a> { pub fn spawn_local(future: impl Future + 'static) -> JoinHandle { let (sender, receiver) = oneshot::channel(); let (abort, registration) = AbortHandle::new_pair(); - unsafe { - SPAWNED.push(Box::pin(async move { - let _ = sender.send(Abortable::new(future, registration).await); - })); + let future = Box::new(async move { + let _ = sender.send(Abortable::new(future, registration).await); + }); + + // This isn't quite as easy as just pushing onto the `SPAWNED` static within + // this file because the request for spawning a task can come from any + // version of the `wit-bindgen` crate but only the version that's running + // the exported task is actually capable of handling the request to spawn + // something. This means that our `SPAWNED` static may not actually be read + // by the export running because it might be a different version of + // `wit-bindgen`. To arbitrate this the `cabi` module has a hook, which is + // configured by exports, to receive Rust futures to spawn. + // + // This itself is a bit thorny to handle a few cases: + // + // * The hook for spawn was added after `cabi` was created, so a revision + // was necessary with a bumped number. Historical versions don't have a + // spawn hook at all. + // + // * The `async-spawn` feature is a conditional feature of this crate that + // may not be enabled at compile time. If it's not enabled then the + // `rust_spawn` hook won't be specified. + // + // For now this just panics if something can't be spawned. In both cases it + // means that there's some other version of `wit-bindgen` running the export + // that this request to spawn can't be satisfied with, and there's really + // not much that can be done. + // + // TODO: this is a pretty awful error to run into as a developer and a + // pretty awful experience. There's nothing that can be done to solve this + // other than "update wit-bindgen somewhere else" which is not always easy + // to do. Not that I've got a better idea of what to do here. + let ok = unsafe { + let task = cabi::wasip3_task_set(ptr::null_mut()); + assert!(!task.is_null()); + assert!((*task).version >= cabi::WASIP3_TASK_V1); + + let ok = if (*task).version >= cabi::WASIP3_TASK_V3 { + let task = task.cast::(); + match (*task).vtable.rust_spawn { + Some(f) => { + f((*task).v1.ptr, future); + true + } + None => false, + } + } else { + false + }; + cabi::wasip3_task_set(task); + ok + }; + + if !ok { + panic!( + "failed to `spawn_local` because there's a different version of \ + the `wit-bindgen` crate running the export than this \ + `wit-bindgen` crate and that one is either too old \ + or has the `async-spawn` feature disabled" + ); } JoinHandle { receiver, abort } } From 2c6b4f43167fd580d9b155a7a880d1252c2eac4e Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 25 Sep 2026 08:01:50 -0700 Subject: [PATCH 2/2] Fix docs --- crates/guest-rust/src/rt/async_support/cabi.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/guest-rust/src/rt/async_support/cabi.rs b/crates/guest-rust/src/rt/async_support/cabi.rs index 674636c9f..df001c5f6 100644 --- a/crates/guest-rust/src/rt/async_support/cabi.rs +++ b/crates/guest-rust/src/rt/async_support/cabi.rs @@ -201,10 +201,12 @@ pub struct wasip3_task_vtable { pub struct wasip3_task_vtable_v3 { pub v2: wasip3_task_vtable, - /// Drops and deallocates the provided pointer previously created by a - /// call to the `clone` callback above. + /// Optionally-specified hook to spawn a rust future as a task. /// - /// This must not be called on the `ptr` value within `wasip3_task::ptr` as - /// that's not managed with this lifetime. + /// This takes the `task.ptr` field as the first argument and the + /// task-to-spawn as the second argument. This is Rust-specific and hence + /// uses the "Rust" ABI. This additionally is optionally specified because + /// not all versions of `wit-bindgen` have support for spawning (it's a + /// crate feature). pub rust_spawn: Option>)>, }