Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 23 additions & 16 deletions crates/guest-rust/src/rt/async_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
};
Expand All @@ -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::<cabi::wasip3_task>()) };
let _reset = ResetTask(prev);

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<dyn Future<Output = ()>>) {
spawn::push(Pin::from(task))
}
}

/// Status for "this task is actively being polled"
Expand Down
36 changes: 36 additions & 0 deletions crates/guest-rust/src/rt/async_support/cabi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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! {
Expand All @@ -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
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -174,3 +193,20 @@ 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,

/// Optionally-specified hook to spawn a rust future as a task.
///
/// 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<unsafe fn(ptr: *mut c_void, Box<dyn Future<Output = ()>>)>,
}
72 changes: 68 additions & 4 deletions crates/guest-rust/src/rt/async_support/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -122,10 +130,66 @@ impl<'a> Tasks<'a> {
pub fn spawn_local<T: 'static>(future: impl Future<Output = T> + 'static) -> JoinHandle<T> {
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::<cabi::wasip3_task_v3>();
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 }
}
Expand Down
Loading