From 9f2563689893aeee7af1f2ed6faee0a96290ba8d Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Fri, 17 Jul 2026 12:24:19 -0700 Subject: [PATCH 1/3] feat: JSPI suspension for wasm32-unknown-emscripten Integrates the #[tokio::test] suite for wasm32-unknown-emscripten using the canonical current_thread scheduler, drivers, and task system on top of JSPI, including the native block_on drive loop. The only platform-specific code is the JSPI park, where inside a JSPI promising wrapper (entered by the #[tokio::test] expansion via the jspi crate's enter_promising) a timed park suspends the whole stack on a host timer while the host event loop runs, so that waits are real. This JSPI suspension path is guarded by an activation wrapper which is only enabled by the test suite currently. Outside an activation, or for a wait nothing could ever wake (no reactor here, and host timers are the only mid-park wake source), the leaf panics with a targeted error instead of deadlocking the host loop. spawn_blocking remains unsupported (no threads). CI runs the contract tests in two lanes: -sJSPI (waits suspend) and without (waits panic). --- .github/workflows/ci.yml | 11 ++- spellcheck.dic | 4 +- tokio-macros/src/entry.rs | 66 ++++++++++--- tokio/src/lib.rs | 67 ++++++++------ tokio/src/macros/cfg.rs | 11 +-- tokio/src/process/emscripten.rs | 127 -------------------------- tokio/src/process/mod.rs | 7 +- tokio/src/runtime/jspi.rs | 96 +++++++++++++++++++ tokio/src/runtime/mod.rs | 12 +++ tokio/src/runtime/park.rs | 89 +++++++++++++++++- tokio/tests/join_handle_panic.rs | 12 ++- tokio/tests/rt_basic.rs | 31 ++++++- tokio/tests/rt_common_before_park.rs | 3 + tokio/tests/rt_emscripten_block_on.rs | 73 +++++++++++++++ tokio/tests/rt_emscripten_jspi.rs | 121 ++++++++++++++++++++++++ tokio/tests/rt_handle.rs | 12 ++- tokio/tests/rt_panic.rs | 21 ++++- tokio/tests/rt_poll_callbacks.rs | 2 +- tokio/tests/rt_shutdown_err.rs | 14 ++- tokio/tests/rt_time_start_paused.rs | 12 ++- tokio/tests/sync_once_cell.rs | 12 ++- tokio/tests/sync_panic.rs | 12 ++- tokio/tests/sync_set_once.rs | 12 ++- tokio/tests/task_abort.rs | 29 ++++-- tokio/tests/task_emscripten.rs | 60 ++++++++++++ tokio/tests/task_id.rs | 30 ++++-- tokio/tests/task_join_set.rs | 13 ++- tokio/tests/task_local.rs | 15 ++- tokio/tests/task_panic.rs | 17 +++- tokio/tests/task_yield_now.rs | 10 +- tokio/tests/test_clock.rs | 12 ++- tokio/tests/time_interval.rs | 12 ++- tokio/tests/time_panic.rs | 41 ++++++--- tokio/tests/time_pause.rs | 21 ++++- tokio/tests/time_sleep.rs | 12 ++- tokio/tests/time_timeout.rs | 12 ++- tokio/tests/unwindsafe.rs | 17 +++- 37 files changed, 888 insertions(+), 240 deletions(-) delete mode 100644 tokio/src/process/emscripten.rs create mode 100644 tokio/src/runtime/jspi.rs create mode 100644 tokio/tests/rt_emscripten_block_on.rs create mode 100644 tokio/tests/rt_emscripten_jspi.rs create mode 100644 tokio/tests/task_emscripten.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97af268d19f..0417cf3994d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1202,12 +1202,19 @@ jobs: env: RUSTFLAGS: "" - - name: Test tokio for emscripten + - name: Test tokio for emscripten (JSPI) run: cargo test -p tokio --target wasm32-unknown-emscripten --features "rt,time,sync,macros,io-util,test-util" --tests working-directory: tokio env: CARGO_TARGET_WASM32_UNKNOWN_EMSCRIPTEN_RUNNER: node - RUSTFLAGS: "-C link-args=-sALLOW_MEMORY_GROWTH=1 -C link-args=-sEXIT_RUNTIME=1 -C link-args=-sSTACK_SIZE=1048576" + RUSTFLAGS: "-C link-args=-sALLOW_MEMORY_GROWTH=1 -C link-args=-sEXIT_RUNTIME=1 -C link-args=-sJSPI -C link-args=-sSTACK_SIZE=1048576" + + - name: Test tokio for emscripten (non-JSPI) + run: cargo test -p tokio --target wasm32-unknown-emscripten --features "rt,time,sync,macros,io-util,test-util" --test rt_emscripten_block_on + working-directory: tokio + env: + CARGO_TARGET_WASM32_UNKNOWN_EMSCRIPTEN_RUNNER: node + RUSTFLAGS: "-C link-args=-sALLOW_MEMORY_GROWTH=1 -C link-args=-sEXIT_RUNTIME=1" check-external-types: name: check-external-types (${{ matrix.os }}) diff --git a/spellcheck.dic b/spellcheck.dic index 2e01dad8402..60de12c6912 100644 --- a/spellcheck.dic +++ b/spellcheck.dic @@ -1,4 +1,4 @@ -327 +329 & + < @@ -162,6 +162,8 @@ IP IPv4 IPv6 iteratively +JS +JSPI Kotlin's latencies Lauck diff --git a/tokio-macros/src/entry.rs b/tokio-macros/src/entry.rs index 9c4f7f9c3b2..00432400359 100644 --- a/tokio-macros/src/entry.rs +++ b/tokio-macros/src/entry.rs @@ -521,10 +521,49 @@ fn parse_knobs(mut input: ItemFn, is_test: bool, config: FinalConfig) -> TokenSt }; - // Emscripten runs the native expansion; only the `multi_thread` flavor - // diverges — it has no native threads there, so it's rejected with a - // targeted error rather than the opaque failure of the native - // multi-thread `block_on`. + let output_type = match &input.sig.output { + // For functions with no return value syn doesn't print anything, + // but that doesn't work as `Output` for our boxed `Future`, so + // default to `()` (the same type as the function output). + syn::ReturnType::Default => quote! { () }, + syn::ReturnType::Type(_, ret_type) => quote! { #ret_type }, + }; + let raw_body = input.body(); + // On Emscripten `#[tokio::test]` claims exclusive JSPI suspension for the + // promising activation libtest runs it under (Emscripten auto-promising- + // wraps `main` under `-sJSPI`), so the body's `block_on` can suspend on + // the host event loop; without `-sJSPI` no guard is created (waits + // panic). + let emscripten_test_block = quote_spanned! {last_stmt_end_span=> + #[allow(clippy::expect_used, clippy::diverging_sub_expression, clippy::needless_return, clippy::unwrap_in_result)] + { + let _suspend_guard = if #crate_path::runtime::jspi_enabled() { + ::core::option::Option::Some(#crate_path::runtime::SuspendGuard::new()) + } else { + ::core::option::Option::None + }; + + let body = async #raw_body; + #crate_path::pin!(body); + let body: ::core::pin::Pin<&mut dyn ::core::future::Future> = body; + + // Inner block: keep the `use` from shadowing names in the + // user body, which shares this scope. + let rt = { + #use_builder + + #rt + .enable_all() + .#build + .expect("Failed building the Runtime") + }; + return rt.block_on(body); + } + }; + + // The `multi_thread` flavor has no native threads on Emscripten, so it's + // rejected with a targeted error (pending a `PROXY_TO_PTHREAD` runtime) + // rather than the opaque failure of the native multi-thread `block_on`. let last_block = match config.flavor { RuntimeFlavor::Threaded => quote! { #[cfg(not(target_os = "emscripten"))] @@ -536,6 +575,12 @@ fn parse_knobs(mut input: ItemFn, is_test: bool, config: FinalConfig) -> TokenSt `flavor = \"current_thread\"`" ); }, + _ if is_test => quote! { + #[cfg(not(target_os = "emscripten"))] + #native_last_block + #[cfg(target_os = "emscripten")] + #emscripten_test_block + }, _ => native_last_block, }; @@ -550,18 +595,15 @@ fn parse_knobs(mut input: ItemFn, is_test: bool, config: FinalConfig) -> TokenSt // // We don't do this for the main function as it should only be used once so // there will be no benefit. - let output_type = match &input.sig.output { - // For functions with no return value syn doesn't print anything, - // but that doesn't work as `Output` for our boxed `Future`, so - // default to `()` (the same type as the function output). - syn::ReturnType::Default => quote! { () }, - syn::ReturnType::Type(_, ret_type) => quote! { #ret_type }, - }; - let body = if is_test { + // On Emscripten the body construction lives inside the capture-free + // closure (see `emscripten_test_block`). quote! { + #[cfg(not(target_os = "emscripten"))] let body = async #body; + #[cfg(not(target_os = "emscripten"))] #crate_path::pin!(body); + #[cfg(not(target_os = "emscripten"))] let body: ::core::pin::Pin<&mut dyn ::core::future::Future> = body; } } else { diff --git a/tokio/src/lib.rs b/tokio/src/lib.rs index d98898cb6c9..9294ca4560d 100644 --- a/tokio/src/lib.rs +++ b/tokio/src/lib.rs @@ -446,46 +446,52 @@ //! //! ### Emscripten support //! -//! The `wasm32-unknown-emscripten` target is supported at parity with the -//! other Wasm targets. A host-event-loop execution model with a parking -//! `block_on` is a planned follow-up; until it lands, a `block_on` whose -//! future cannot resolve synchronously behaves as on other single-threaded -//! Wasm targets. -//! -//! Supported features: `rt`, `time`, `sync`, `macros`, `fs`, `io-util`, -//! `io-std`, and `test-util` — the same surface as the other single-threaded -//! Wasm targets. The `net` reactor (epoll-backed, over Emscripten's socket -//! support) is planned as a follow-up; until then the `net` feature fails to -//! build for this target (rejected by `mio`). +//! The `wasm32-unknown-emscripten` target runs Tokio's canonical +//! single-threaded (`current_thread`) scheduler, time driver, and task system +//! unchanged. `Runtime::block_on` drives the scheduler to a synchronous fixed +//! point; a future that would have to suspend (await a timer or an external +//! wake) panics with a targeted error, since the host event loop cannot run +//! while Wasm blocks the stack. +//! +//! [JSPI] (-sJSPI in Emscripten) implements an exception to this behaviour: +//! a drive that would block instead suspends the whole calling stack on a +//! host timer — the host loop keeps running and wakes it. Emscripten +//! auto-promising-wraps `main` under `-sJSPI`, so `#[tokio::main]` and +//! `#[tokio::test]` suspend as-is; calling `block_on` from a host callback +//! outside a promising activation traps at the engine. The drive is +//! non-reentrant, like `block_on` everywhere else. +//! +//! [JSPI]: https://github.com/WebAssembly/js-promise-integration +//! +//! Supported features: `rt`, `time`, `sync`, `macros`, `io-util`, and +//! `test-util`. The `net` reactor (epoll-backed, over Emscripten's socket +//! support) and the inline `fs`/`io-std` shims are planned as follow-ups. //! //! The `process`, `signal`, and `rt-multi-thread` features are rejected at //! compile time: `process`/`signal` have no underlying primitives (`fork`/`exec`, //! kernel signal delivery) and `rt-multi-thread` has no native threads. //! -//! `spawn_blocking` dispatches to the blocking thread pool and so behaves as on -//! the other single-threaded Wasm targets. Running `spawn_blocking` closures -//! and `fs`/stdio `std::*` calls inline over Emscripten's synchronous syscalls -//! is part of the planned host-event-loop follow-up. +//! `spawn_blocking` is unchanged: with no threadpool to dispatch to it fails +//! at runtime, as on the other single-threaded Wasm targets. //! //! Panics behave as on native: `wasm32-unknown-emscripten` defaults to //! `panic = "unwind"`, so panic recovery works, a panicking task yields //! `Err(JoinError)`, and `JoinError::is_panic` / `JoinError::into_panic` //! report the payload. //! -//! `#[tokio::test]` / `#[tokio::main]` use the native macro expansion; the -//! `multi_thread` flavor is rejected (no native threads) — use -//! `flavor = "current_thread"`. -//! +//! `#[tokio::main]` still defaults to the unsupported `multi_thread` flavor, +//! `#[tokio::main(flavor = "current_thread")]` is currently required. //! //! #### Linking and running on Emscripten //! -//! No js-library or other custom file is required, and plain `node` runs the -//! test binaries directly: +//! No js-library or custom file is required (Tokio embeds its JS glue in +//! the objects); plain `node` runs the test binaries directly: //! //! ```text //! CARGO_TARGET_WASM32_UNKNOWN_EMSCRIPTEN_RUNNER="node" //! RUSTFLAGS="-C link-args=-sALLOW_MEMORY_GROWTH=1 \ //! -C link-args=-sEXIT_RUNTIME=1 \ +//! -C link-args=-sJSPI \ //! -C link-args=-sSTACK_SIZE=1048576" //! ``` //! @@ -524,12 +530,21 @@ compile_error! { ))] compile_error!("Only features sync,macros,io-util,rt,time are supported on wasm."); -// On Emscripten, `process`, `signal`, and `rt-multi-thread` compile but are -// inert, so `full` (and any dependency that enables these features) still -// builds. `process` and `signal` have no `fork`/`exec` or kernel signal +// On Emscripten, `process` and `signal` have no `fork`/`exec` or kernel signal // delivery, so their modules are compiled out (see `cfg_process!` / -// `cfg_signal!`). The multi-threaded runtime compiles but only runs under a -// `PROXY_TO_PTHREAD` build; `#[tokio::main]` steers to `current_thread`. +// `cfg_signal!`) exactly as on wasi; naming those APIs is a compile error. +// `rt-multi-thread` compiles but only runs under a `PROXY_TO_PTHREAD` build; +// `#[tokio::main]` steers to `current_thread`. + +// We currently only support the single-threaded Emscripten runtime, with +// support for JSPI. +// A `-pthread` (atomics) build breaks its assumptions, until the kernel is made +// thread-aware. +#[cfg(all(target_os = "emscripten", feature = "rt", target_feature = "atomics"))] +compile_error!( + "Tokio's `wasm32-unknown-emscripten` runtime is single-threaded and does \ +not support `-pthread` (atomics) builds." +); #[cfg(all(not(tokio_unstable), feature = "io-uring"))] compile_error!("The `io-uring` feature requires `--cfg tokio_unstable`."); diff --git a/tokio/src/macros/cfg.rs b/tokio/src/macros/cfg.rs index 86c1f199386..f8b1f8b2201 100644 --- a/tokio/src/macros/cfg.rs +++ b/tokio/src/macros/cfg.rs @@ -398,11 +398,9 @@ macro_rules! cfg_process { #[cfg_attr(docsrs, doc(cfg(feature = "process")))] #[cfg(not(loom))] #[cfg(not(target_os = "wasi"))] - // Emscripten has no `fork`/`exec`, so the `process` module is a - // throwing stub there (see `process/emscripten.rs`); it still - // compiles so dependents that name the types build. The orphan - // reaper / signal driver it would otherwise need stays off via - // `cfg_process_driver!`. + // Emscripten has no `fork`/`exec`, so the `process` module is not + // available there. + #[cfg(not(target_os = "emscripten"))] $item )* } @@ -412,9 +410,6 @@ macro_rules! cfg_process_driver { ($($item:item)*) => { #[cfg(unix)] #[cfg(not(loom))] - // The driver (orphan reaper backed by the signal handler) doesn't exist - // on Emscripten; the process module there is a throwing stub. - #[cfg(not(target_os = "emscripten"))] cfg_process! { $($item)* } } } diff --git a/tokio/src/process/emscripten.rs b/tokio/src/process/emscripten.rs deleted file mode 100644 index 093b3eb8aa1..00000000000 --- a/tokio/src/process/emscripten.rs +++ /dev/null @@ -1,127 +0,0 @@ -//! Throwing process stub for `wasm32-unknown-emscripten`. -//! -//! emscripten has no `fork`/`exec`, so there is nothing to spawn or reap: the -//! orphan reaper / signal driver the unix backend relies on is compiled out -//! (see `cfg_process_driver!`). We still provide the `imp` surface that -//! `process::mod` is generic over so dependents that merely name these types -//! keep compiling. Spawning fails before it reaches here (`std`'s own -//! `Command::spawn` returns `Unsupported` on emscripten), and the `Child` / -//! `ChildStdio` types are uninhabited — every method is unreachable. - -use crate::io::{AsyncRead, AsyncWrite, ReadBuf}; -use crate::process::kill::Kill; -use crate::process::SpawnedChild; - -use std::fmt; -use std::future::Future; -use std::io; -use std::os::fd::{AsFd, AsRawFd, BorrowedFd, IntoRawFd, OwnedFd, RawFd}; -use std::pin::Pin; -use std::process::{Child as StdChild, ExitStatus, Stdio}; -use std::task::{Context, Poll}; - -fn unsupported() -> io::Result { - Err(io::Error::new( - io::ErrorKind::Unsupported, - "spawning processes is not supported on wasm32-unknown-emscripten", - )) -} - -pub(crate) enum Child {} - -impl Child { - pub(crate) fn id(&self) -> u32 { - match *self {} - } - - pub(crate) fn try_wait(&mut self) -> io::Result> { - match *self {} - } -} - -impl fmt::Debug for Child { - fn fmt(&self, _fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self {} - } -} - -impl Kill for Child { - fn kill(&mut self) -> io::Result<()> { - match *self {} - } -} - -impl Future for Child { - type Output = io::Result; - - fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { - match *self.get_mut() {} - } -} - -pub(crate) fn build_child(_child: StdChild) -> io::Result { - unsupported() -} - -pub(crate) enum ChildStdio {} - -impl ChildStdio { - pub(crate) fn into_owned_fd(self) -> io::Result { - match self {} - } -} - -impl fmt::Debug for ChildStdio { - fn fmt(&self, _fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self {} - } -} - -impl AsRawFd for ChildStdio { - fn as_raw_fd(&self) -> RawFd { - match *self {} - } -} - -impl AsFd for ChildStdio { - fn as_fd(&self) -> BorrowedFd<'_> { - match *self {} - } -} - -impl AsyncWrite for ChildStdio { - fn poll_write( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - _buf: &[u8], - ) -> Poll> { - match *self.get_mut() {} - } - - fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - match *self.get_mut() {} - } - - fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - match *self.get_mut() {} - } -} - -impl AsyncRead for ChildStdio { - fn poll_read( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - _buf: &mut ReadBuf<'_>, - ) -> Poll> { - match *self.get_mut() {} - } -} - -pub(crate) fn convert_to_stdio(io: ChildStdio) -> io::Result { - match io {} -} - -pub(crate) fn stdio(io: T) -> io::Result { - let _ = io.into_raw_fd(); - unsupported() -} diff --git a/tokio/src/process/mod.rs b/tokio/src/process/mod.rs index 6ca2e2b8f82..fb233cb4eb2 100644 --- a/tokio/src/process/mod.rs +++ b/tokio/src/process/mod.rs @@ -227,12 +227,7 @@ //! [`Child`]: crate::process::Child #[path = "unix/mod.rs"] -#[cfg(all(unix, not(target_os = "emscripten")))] -mod imp; - -// Emscripten has no `fork`/`exec`: a throwing stub keeps the API present. -#[path = "emscripten.rs"] -#[cfg(target_os = "emscripten")] +#[cfg(unix)] mod imp; #[cfg(unix)] diff --git a/tokio/src/runtime/jspi.rs b/tokio/src/runtime/jspi.rs new file mode 100644 index 00000000000..9ce1494457d --- /dev/null +++ b/tokio/src/runtime/jspi.rs @@ -0,0 +1,96 @@ +//! Minimal JSPI primitives for `wasm32-unknown-emscripten`. +//! +//! Tokio's suspension model is strictly non-reentrant: `#[tokio::test]` claims +//! suspension for its whole promising activation with a [`SuspendGuard`], +//! and [`sleep`] — the one suspending import the runtime issues — parks +//! that activation on a host timer. With never more than one suspension in +//! flight, no spill-stack save/restore is needed: any wasm re-entered +//! while the activation is parked completes before the resume, leaving the +//! spill stack above the suspended frame untouched. + +use std::cell::Cell; +use std::time::Duration; + +thread_local! { + static SUSPENDABLE: Cell = const { Cell::new(false) }; +} + +/// Marks the `#[tokio::test]` promising activation as suspendable for the +/// body's extent; `Drop` clears the flag across panic unwinds. Internal to +/// the test expansion, not a user convention. +#[derive(Debug)] +pub struct SuspendGuard(()); + +impl SuspendGuard { + /// Marks the current activation suspendable until drop. + #[allow(clippy::new_without_default)] + pub fn new() -> SuspendGuard { + SUSPENDABLE.set(true); + SuspendGuard(()) + } +} + +impl Drop for SuspendGuard { + fn drop(&mut self) { + SUSPENDABLE.set(false); + } +} + +/// Whether the park leaf may suspend: a [`SuspendGuard`] is live. +pub(crate) fn can_suspend() -> bool { + SUSPENDABLE.get() +} + +// Emscripten EM_JS convention: `__em_js__` data exports carry JS +// bodies into the objects, and `__asyncjs__` names get +// `WebAssembly.Suspending` treatment under `-sJSPI`. The static must be +// referenced from linked code (`anchor`) so its archive member is pulled +// in. +const TOKIO_JSPI_SLEEP: &str = "(ms)<::>{ return Asyncify.handleAsync(async () => { await new Promise((r) => setTimeout(r, ms)); }); }"; + +const fn em_js(s: &str) -> [u8; N] { + // NUL-terminated: N == s.len() + 1 + let mut a = [0u8; N]; + let b = s.as_bytes(); + let mut i = 0; + while i < b.len() { + a[i] = b[i]; + i += 1; + } + a +} + +#[allow(non_upper_case_globals)] +#[no_mangle] +#[used] +static __em_js____asyncjs__tokio_jspi_sleep: [u8; TOKIO_JSPI_SLEEP.len() + 1] = + em_js(TOKIO_JSPI_SLEEP); + +unsafe extern "C" { + /// Reports the ASYNCIFY build mode: 0 = none, 1 = asyncify, 2 = JSPI. + safe fn emscripten_has_asyncify() -> i32; +} + +// Suspending import: parks on a host timeout. Unit return, never rejects, +// `Asyncify.handleAsync` keeps the runtime alive across the suspension. +#[link(wasm_import_module = "env")] +unsafe extern "C-unwind" { + #[link_name = "__asyncjs__tokio_jspi_sleep"] + safe fn tokio_jspi_sleep_import(ms: f64); +} + +#[inline(never)] +fn anchor() { + std::hint::black_box(__em_js____asyncjs__tokio_jspi_sleep.as_ptr()); +} + +/// Whether JSPI suspension is available: linked with `-sJSPI`. +pub fn jspi_enabled() -> bool { + emscripten_has_asyncify() == 2 +} + +/// Suspend the owning activation for `dur` on a host timer. +pub(crate) fn sleep(dur: Duration) { + anchor(); + tokio_jspi_sleep_import(dur.as_secs_f64() * 1000.0); +} diff --git a/tokio/src/runtime/mod.rs b/tokio/src/runtime/mod.rs index 05bb5fa6ee1..84af32599f6 100644 --- a/tokio/src/runtime/mod.rs +++ b/tokio/src/runtime/mod.rs @@ -416,6 +416,16 @@ mod tests; pub(crate) mod context; +#[cfg(all(target_os = "emscripten", feature = "rt"))] +pub(crate) mod jspi; + +// Used by the `#[tokio::test]` expansion on Emscripten: the guard claims +// JSPI suspension for the test's promising activation so the body's +// `block_on` can suspend on the host loop. Not public API. +#[cfg(all(target_os = "emscripten", feature = "rt", feature = "test-util"))] +#[doc(hidden)] +pub use jspi::{jspi_enabled, SuspendGuard}; + pub(crate) mod park; pub(crate) mod driver; @@ -550,6 +560,8 @@ cfg_rt! { } cfg_fs! { + // Emscripten's `fs` uses the inline blocking shim, not the pool. + #[cfg_attr(target_os = "emscripten", allow(unused_imports))] pub(crate) use blocking::spawn_mandatory_blocking; } diff --git a/tokio/src/runtime/park.rs b/tokio/src/runtime/park.rs index cf502c40408..7a638e813eb 100644 --- a/tokio/src/runtime/park.rs +++ b/tokio/src/runtime/park.rs @@ -28,6 +28,7 @@ const EMPTY: usize = 0; const PARKED: usize = 1; const NOTIFIED: usize = 2; +#[cfg(not(all(target_os = "emscripten", feature = "rt")))] tokio_thread_local! { static CURRENT_PARKER: ParkThread = ParkThread::new(); } @@ -76,6 +77,7 @@ impl ParkThread { // ==== impl Inner ==== impl Inner { + #[cfg(not(all(target_os = "emscripten", feature = "rt")))] fn park(&self) { // If we were previously notified then we consume this notification and // return quickly. @@ -123,7 +125,41 @@ impl Inner { } } - /// Parks the current thread for at most `dur`. + #[cfg(all(target_os = "emscripten", feature = "rt"))] + fn park(&self) { + // If we were previously notified then we consume this notification and + // return quickly. + if self + .state + .compare_exchange(NOTIFIED, EMPTY, SeqCst, SeqCst) + .is_ok() + { + return; + } + + // On Emscripten a would-block wait with no deadline can never be + // woken: host timers are the only mid-park wake source (tokio has + // no reactor here, and every tokio-internal waker fires during the + // drive, before the park). Fail fast instead of deadlocking, + // inside or outside a JSPI root alike. + panic!( + "cannot block to wait for an external wake on \ + wasm32-unknown-emscripten: no source exists that could deliver \ + the wake (host timers are the only mid-park wake source, and \ + this wait has no deadline)" + ); + } + + /// Consume a pending notification token, if any. + #[cfg(all(target_os = "emscripten", feature = "rt"))] + fn consume_notified(&self) -> bool { + self.state + .compare_exchange(NOTIFIED, EMPTY, SeqCst, SeqCst) + .is_ok() + } + + /// Parks the current thread for at most `dur` (native condvar path). + #[cfg(not(all(target_os = "emscripten", feature = "rt")))] fn park_timeout(&self, dur: Duration) { // Like `park` above we have a fast path for an already-notified thread, // and afterwards we start coordinating for a sleep. Return quickly. @@ -174,6 +210,42 @@ impl Inner { } } + /// Parks the current thread for at most `dur`. + #[cfg(all(target_os = "emscripten", feature = "rt"))] + fn park_timeout(&self, dur: Duration) { + // Like `park` above we have a fast path for an already-notified thread, + // and afterwards we start coordinating for a sleep. Return quickly. + if self + .state + .compare_exchange(NOTIFIED, EMPTY, SeqCst, SeqCst) + .is_ok() + { + return; + } + + // Emscripten: with a live suspend guard, waiting is real — + // suspend on a host timer (consume a notification delivered during + // the sleep so the token doesn't leak into the next park). Otherwise + // a zero-duration park is a no-op, and any real wait is impossible + // (suspending would trap, busy-waiting would starve the host loop + // every wake depends on) so it panics. + if crate::runtime::jspi::can_suspend() { + // Includes `dur == 0`: a genuine host turn, letting timers + // and microtasks run mid-drive (the scheduler's maintenance + // yield). + crate::runtime::jspi::sleep(dur); + let _ = self.consume_notified(); + } else if dur == Duration::from_millis(0) { + // Native semantics: a zero-duration park returns immediately. + } else { + panic!( + "cannot block for a timed wait on wasm32-unknown-emscripten \ + outside a JSPI activation: link with `-sJSPI` and run inside \ + `#[tokio::test]` to suspend on the host event loop" + ); + } + } + fn unpark(&self) { // To ensure the unparked thread will observe any writes we made before // this call, we must perform a release operation that `park` can @@ -231,6 +303,12 @@ use std::task::{RawWaker, RawWakerVTable, Waker}; /// Blocks the current thread using a condition variable. #[derive(Debug)] pub(crate) struct CachedParkThread { + // While the sole JSPI activation is parked, host callbacks can still + // run tokio code on this thread; a shared thread-local parker could + // hand this stack's notification token to that code. Each blocking + // call gets its own parker instead. + #[cfg(all(target_os = "emscripten", feature = "rt"))] + park: ParkThread, _anchor: PhantomData>, } @@ -241,6 +319,8 @@ impl CachedParkThread { /// the thread that the caller intends to park. pub(crate) fn new() -> CachedParkThread { CachedParkThread { + #[cfg(all(target_os = "emscripten", feature = "rt"))] + park: ParkThread::new(), _anchor: PhantomData, } } @@ -268,7 +348,12 @@ impl CachedParkThread { where F: FnOnce(&ParkThread) -> R, { - CURRENT_PARKER.try_with(|inner| f(inner)) + #[cfg(not(all(target_os = "emscripten", feature = "rt")))] + return CURRENT_PARKER.try_with(|inner| f(inner)); + + // See the field comment for Emscripten on `park` + #[cfg(all(target_os = "emscripten", feature = "rt"))] + return Ok(f(&self.park)); } pub(crate) fn block_on(&mut self, f: F) -> Result { diff --git a/tokio/tests/join_handle_panic.rs b/tokio/tests/join_handle_panic.rs index 248d5702f68..0fa50d81d61 100644 --- a/tokio/tests/join_handle_panic.rs +++ b/tokio/tests/join_handle_panic.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(all(feature = "full", not(target_os = "wasi")))] // Wasi doesn't support panic recovery +#![cfg(any( + all(feature = "full", not(target_os = "wasi")), + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] #![cfg(panic = "unwind")] struct PanicsOnDrop; diff --git a/tokio/tests/rt_basic.rs b/tokio/tests/rt_basic.rs index 3fab2649a12..ca951676faf 100644 --- a/tokio/tests/rt_basic.rs +++ b/tokio/tests/rt_basic.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use tokio::runtime::{self, Runtime}; use tokio::sync::oneshot; @@ -249,7 +259,10 @@ fn spawn_two() { } } -#[cfg_attr(target_os = "wasi", ignore = "WASI: std::thread::spawn not supported")] +#[cfg_attr( + target_family = "wasm", + ignore = "wasm targets do not support std::thread::spawn" +)] #[test] fn spawn_remote() { let rt = rt(); @@ -346,7 +359,10 @@ mod unstable { } #[test] - #[cfg_attr(target_os = "wasi", ignore = "Wasi does not support panic recovery")] + #[cfg_attr( + target_family = "wasm", + ignore = "wasm targets do not support std::thread::spawn" + )] fn spawns_do_nothing() { use std::sync::Arc; @@ -375,7 +391,10 @@ mod unstable { } #[test] - #[cfg_attr(target_os = "wasi", ignore = "Wasi does not support panic recovery")] + #[cfg_attr( + target_family = "wasm", + ignore = "wasm targets do not support std::thread::spawn" + )] fn shutdown_all_concurrent_block_on() { const N: usize = 2; use std::sync::{mpsc, Arc}; @@ -479,6 +498,10 @@ fn rt() -> Runtime { } #[test] +#[cfg_attr( + target_os = "emscripten", + ignore = "on_thread_park never fires on emscripten (kernel suspends to the JS event loop instead of parking)" +)] fn before_park_yields() { use futures::task::ArcWake; use std::sync::Arc; diff --git a/tokio/tests/rt_common_before_park.rs b/tokio/tests/rt_common_before_park.rs index 74a28163225..5d37e31fd65 100644 --- a/tokio/tests/rt_common_before_park.rs +++ b/tokio/tests/rt_common_before_park.rs @@ -1,6 +1,9 @@ #![warn(rust_2018_idioms)] #![cfg(feature = "full")] #![cfg(not(target_os = "wasi"))] // Wasi doesn't support threads + // Not widened to emscripten: every test here exercises the `on_thread_park` + // Builder hook, which never fires on emscripten (the kernel suspends to the + // JS event loop instead of parking), plus a cross-thread wake. use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; diff --git a/tokio/tests/rt_emscripten_block_on.rs b/tokio/tests/rt_emscripten_block_on.rs new file mode 100644 index 00000000000..02c6518f872 --- /dev/null +++ b/tokio/tests/rt_emscripten_block_on.rs @@ -0,0 +1,73 @@ +//! `Runtime::block_on` outside a JSPI promising activation drives the +//! scheduler synchronously to a fixed point: immediate futures return their +//! value; any wait panics at the park leaf, in both CI lanes (JSPI linked +//! and not), since only the `#[tokio::test]` activation can suspend (see +//! `rt_emscripten_jspi`). + +#![cfg(all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "macros" +))] + +use std::panic::{catch_unwind, AssertUnwindSafe}; +use std::time::Duration; + +use tokio::runtime::Builder; + +fn rt() -> tokio::runtime::Runtime { + Builder::new_current_thread().enable_all().build().unwrap() +} + +/// Assert `f` panics with the targeted would-suspend message. +fn assert_panics_cannot_block_on(f: impl FnOnce()) { + let err = catch_unwind(AssertUnwindSafe(f)).expect_err("expected a would-suspend panic"); + let msg = err + .downcast_ref::() + .map(String::as_str) + .or_else(|| err.downcast_ref::<&str>().copied()) + .unwrap_or(""); + assert!( + msg.contains("cannot block"), + "unexpected panic message: {msg}" + ); +} + +#[test] +fn block_on_returns_immediate_value() { + let out = rt().block_on(async { 1 + 2 }); + assert_eq!(out, 3); +} + +#[test] +fn block_on_drives_ready_spawned_tasks() { + // Spawned tasks that complete synchronously must be driven to + // completion within the same fixed-point pump. + let out = rt().block_on(async { + let a = tokio::spawn(async { 20 }); + let b = tokio::spawn(async { 22 }); + a.await.unwrap() + b.await.unwrap() + }); + assert_eq!(out, 42); +} + +#[test] +fn timer_wait_panics_outside_activation() { + assert_panics_cannot_block_on(|| { + rt().block_on(async { + tokio::time::sleep(Duration::from_millis(10)).await; + }); + }); +} + +#[test] +fn external_wait_panics_outside_activation() { + // A oneshot whose sender never fires: pending with no wake source. + assert_panics_cannot_block_on(|| { + rt().block_on(async { + let (_tx, rx) = tokio::sync::oneshot::channel::<()>(); + let _ = rx.await; + }); + }); +} diff --git a/tokio/tests/rt_emscripten_jspi.rs b/tokio/tests/rt_emscripten_jspi.rs new file mode 100644 index 00000000000..d51ea7060d7 --- /dev/null +++ b/tokio/tests/rt_emscripten_jspi.rs @@ -0,0 +1,121 @@ +//! JSPI suspension contracts. With `-sJSPI` a would-block wait in a +//! `#[tokio::test]` body suspends on a host timer while the host loop +//! delivers wakes. Without it a wait throws (see `rt_emscripten_block_on`). +//! +//! NOTE: This is the only Emscripten test file with real timer tests. + +#![cfg(all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros" +))] + +use std::sync::Arc; +use std::time::Duration; + +use tokio::runtime::Builder; +use tokio::sync::Notify; +use tokio::time::{sleep, Instant}; + +fn rt() -> tokio::runtime::Runtime { + Builder::new_current_thread().enable_all().build().unwrap() +} + +#[test] +fn block_on_yield_now_takes_a_host_turn() { + let out = rt().block_on(async { + tokio::task::yield_now().await; + 7 + }); + assert_eq!(out, 7); +} + +#[tokio::test] +async fn root_sleep_parks_and_resumes() { + let start = tokio::time::Instant::now(); + tokio::time::sleep(Duration::from_millis(20)).await; + assert!( + start.elapsed() >= Duration::from_millis(15), + "the park must actually wait out the timer deadline" + ); +} + +#[tokio::test] +async fn root_spawned_tasks_with_timers() { + let out = async { + let a = tokio::spawn(async { + tokio::time::sleep(Duration::from_millis(5)).await; + 20 + }); + let b = tokio::spawn(async { + tokio::time::sleep(Duration::from_millis(10)).await; + 22 + }); + a.await.unwrap() + b.await.unwrap() + } + .await; + assert_eq!(out, 42); +} + +#[tokio::test] +async fn sequential_parks_inside_one_root() { + // Each park must suspend and resume independently; leaf bookkeeping + // must balance across them. + for i in 0..3u32 { + let start = tokio::time::Instant::now(); + tokio::time::sleep(Duration::from_millis(2)).await; + assert!(start.elapsed() >= Duration::from_millis(1), "park {i}"); + } +} + +#[tokio::test] +async fn root_park_resumes_on_timer_driven_wake() { + // The spawned task's timer bounds the driver park; on resume it sends + // and wakes the root future. + let (tx, rx) = tokio::sync::oneshot::channel::(); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_millis(5)).await; + tx.send(11).unwrap(); + }); + assert_eq!(rx.await.unwrap(), 11); +} + +// A self-rewaking task must not starve the real host timer: the +// event-interval park yields a 0ms host turn so the timer still fires. +#[tokio::test] +async fn greedy_task_does_not_starve_host_timer() { + tokio::spawn(async { + loop { + tokio::task::yield_now().await; + } + }); + sleep(Duration::from_millis(5)).await; +} + +// When a nearer timer fires, the next park must re-arm for a still-pending +// farther timer rather than dropping it. +#[tokio::test] +async fn farther_timer_survives_nearer_timer_firing() { + let start = Instant::now(); + + let notify = Arc::new(Notify::new()); + let n = notify.clone(); + let near = tokio::spawn(async move { + sleep(Duration::from_millis(5)).await; + n.notify_one(); + }); + let waiter = tokio::spawn(async move { + notify.notified().await; + }); + + sleep(Duration::from_millis(25)).await; + assert!( + start.elapsed() >= Duration::from_millis(25), + "farther timer did not hold its deadline" + ); + + near.await.unwrap(); + waiter.await.unwrap(); +} diff --git a/tokio/tests/rt_handle.rs b/tokio/tests/rt_handle.rs index 8feb7207d0b..b391e929da5 100644 --- a/tokio/tests/rt_handle.rs +++ b/tokio/tests/rt_handle.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use std::sync::Arc; use tokio::runtime::Runtime; diff --git a/tokio/tests/rt_panic.rs b/tokio/tests/rt_panic.rs index 1bf05580ab6..a1650e92941 100644 --- a/tokio/tests/rt_panic.rs +++ b/tokio/tests/rt_panic.rs @@ -1,11 +1,23 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] #![cfg(not(target_os = "wasi"))] // Wasi doesn't support panic recovery #![cfg(panic = "unwind")] use futures::future; use std::error::Error; -use tokio::runtime::{Builder, Handle, Runtime}; +#[cfg(not(target_os = "emscripten"))] +use tokio::runtime::Builder; +use tokio::runtime::{Handle, Runtime}; mod support { pub mod panic; @@ -47,6 +59,7 @@ fn into_panic_panic_caller() -> Result<(), Box> { } #[test] +#[cfg(not(target_os = "emscripten"))] // no rt-multi-thread fn builder_worker_threads_panic_caller() -> Result<(), Box> { let panic_location_file = test_panic(|| { let _ = Builder::new_multi_thread().worker_threads(0).build(); @@ -59,6 +72,7 @@ fn builder_worker_threads_panic_caller() -> Result<(), Box> { } #[test] +#[cfg(not(target_os = "emscripten"))] // no rt-multi-thread fn builder_max_blocking_threads_panic_caller() -> Result<(), Box> { let panic_location_file = test_panic(|| { let _ = Builder::new_multi_thread().max_blocking_threads(0).build(); @@ -71,6 +85,7 @@ fn builder_max_blocking_threads_panic_caller() -> Result<(), Box> { } #[test] +#[cfg(not(target_os = "emscripten"))] // no rt-multi-thread fn builder_global_queue_interval_panic_caller() -> Result<(), Box> { let panic_location_file = test_panic(|| { let _ = Builder::new_multi_thread().global_queue_interval(0).build(); @@ -83,6 +98,7 @@ fn builder_global_queue_interval_panic_caller() -> Result<(), Box> { } #[test] +#[cfg(not(target_os = "emscripten"))] // no rt-multi-thread fn builder_event_interval_interval_panic_caller() -> Result<(), Box> { let panic_location_file = test_panic(|| { let _ = Builder::new_multi_thread().event_interval(0).build(); @@ -95,6 +111,7 @@ fn builder_event_interval_interval_panic_caller() -> Result<(), Box> } #[test] +#[cfg(not(target_os = "emscripten"))] // no rt-multi-thread fn builder_name_panic_caller() -> Result<(), Box> { let panic_location_file = test_panic(|| { let _ = Builder::new_multi_thread().name(" ").build(); diff --git a/tokio/tests/rt_poll_callbacks.rs b/tokio/tests/rt_poll_callbacks.rs index 8ccff385772..f0996ff1b73 100644 --- a/tokio/tests/rt_poll_callbacks.rs +++ b/tokio/tests/rt_poll_callbacks.rs @@ -5,7 +5,7 @@ use std::sync::{atomic::AtomicUsize, Arc, Mutex}; use tokio::task::yield_now; -#[cfg(not(target_os = "wasi"))] +#[cfg(not(target_family = "wasm"))] #[test] fn callbacks_fire_multi_thread() { let poll_start_counter = Arc::new(AtomicUsize::new(0)); diff --git a/tokio/tests/rt_shutdown_err.rs b/tokio/tests/rt_shutdown_err.rs index b92d8eb24f1..b0203b18787 100644 --- a/tokio/tests/rt_shutdown_err.rs +++ b/tokio/tests/rt_shutdown_err.rs @@ -1,8 +1,19 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] #![cfg(not(miri))] // No socket in miri. use std::io; +#[cfg(not(target_family = "wasm"))] use tokio::net::TcpListener; use tokio::runtime::Builder; @@ -10,6 +21,7 @@ fn rt() -> tokio::runtime::Runtime { Builder::new_current_thread().enable_all().build().unwrap() } +#[cfg(not(target_family = "wasm"))] // needs net (TcpListener) #[test] fn test_is_rt_shutdown_err() { let rt1 = rt(); diff --git a/tokio/tests/rt_time_start_paused.rs b/tokio/tests/rt_time_start_paused.rs index 1765d625e19..8bd66005bed 100644 --- a/tokio/tests/rt_time_start_paused.rs +++ b/tokio/tests/rt_time_start_paused.rs @@ -1,4 +1,14 @@ -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use tokio::time::{Duration, Instant}; diff --git a/tokio/tests/sync_once_cell.rs b/tokio/tests/sync_once_cell.rs index a05438f24c8..a4a9bfb584d 100644 --- a/tokio/tests/sync_once_cell.rs +++ b/tokio/tests/sync_once_cell.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use std::mem; use std::sync::atomic::{AtomicU32, Ordering}; diff --git a/tokio/tests/sync_panic.rs b/tokio/tests/sync_panic.rs index c781c846bf5..9bb5a5afce6 100644 --- a/tokio/tests/sync_panic.rs +++ b/tokio/tests/sync_panic.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(all(feature = "full", not(target_os = "wasi")))] +#![cfg(any( + all(feature = "full", not(target_os = "wasi")), + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] #![cfg(panic = "unwind")] use std::{error::Error, sync::Arc}; diff --git a/tokio/tests/sync_set_once.rs b/tokio/tests/sync_set_once.rs index 5b6d88a9f51..3ffdfd1882e 100644 --- a/tokio/tests/sync_set_once.rs +++ b/tokio/tests/sync_set_once.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use std::sync::{ atomic::{AtomicU32, Ordering}, diff --git a/tokio/tests/task_abort.rs b/tokio/tests/task_abort.rs index 8de366454e0..d0877a20a65 100644 --- a/tokio/tests/task_abort.rs +++ b/tokio/tests/task_abort.rs @@ -1,16 +1,29 @@ #![warn(rust_2018_idioms)] -#![cfg(all(feature = "full", not(target_os = "wasi")))] // Wasi doesn't support panic recovery - +#![cfg(any( + all(feature = "full", not(target_os = "wasi")), + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] + +#[cfg(not(target_family = "wasm"))] use std::sync::Arc; +#[cfg(not(target_family = "wasm"))] use std::thread::sleep; +#[cfg(not(target_family = "wasm"))] use tokio::time::Duration; use tokio::runtime::Builder; -#[cfg(panic = "unwind")] +#[cfg(all(panic = "unwind", not(target_family = "wasm")))] struct PanicOnDrop; -#[cfg(panic = "unwind")] +#[cfg(all(panic = "unwind", not(target_family = "wasm")))] impl Drop for PanicOnDrop { fn drop(&mut self) { panic!("Well what did you expect would happen..."); @@ -19,6 +32,7 @@ impl Drop for PanicOnDrop { /// Checks that a suspended task can be aborted without panicking as reported in /// issue #3157: . +#[cfg(not(target_family = "wasm"))] #[test] fn test_abort_without_panic_3157() { let rt = Builder::new_multi_thread() @@ -41,6 +55,7 @@ fn test_abort_without_panic_3157() { /// Checks that a suspended task can be aborted inside of a current_thread /// executor without panicking as reported in issue #3662: /// . +#[cfg(not(target_family = "wasm"))] #[test] fn test_abort_without_panic_3662() { use std::sync::atomic::{AtomicBool, Ordering}; @@ -104,6 +119,7 @@ fn test_abort_without_panic_3662() { /// Checks that a suspended LocalSet task can be aborted from a remote thread /// without panicking and without running the tasks destructor on the wrong thread. /// +#[cfg(not(target_family = "wasm"))] #[test] fn remote_abort_local_set_3929() { struct DropCheck { @@ -147,6 +163,7 @@ fn remote_abort_local_set_3929() { /// Checks that a suspended task can be aborted even if the `JoinHandle` is immediately dropped. /// issue #3964: . +#[cfg(not(target_family = "wasm"))] #[test] fn test_abort_wakes_task_3964() { let rt = Builder::new_current_thread().enable_time().build().unwrap(); @@ -177,8 +194,8 @@ fn test_abort_wakes_task_3964() { /// Checks that aborting a task whose destructor panics does not allow the /// panic to escape the task. +#[cfg(all(panic = "unwind", not(target_family = "wasm")))] #[test] -#[cfg(panic = "unwind")] fn test_abort_task_that_panics_on_drop_contained() { let rt = Builder::new_current_thread().enable_time().build().unwrap(); @@ -201,8 +218,8 @@ fn test_abort_task_that_panics_on_drop_contained() { } /// Checks that aborting a task whose destructor panics has the expected result. +#[cfg(all(panic = "unwind", not(target_family = "wasm")))] #[test] -#[cfg(panic = "unwind")] fn test_abort_task_that_panics_on_drop_returned() { let rt = Builder::new_current_thread().enable_time().build().unwrap(); diff --git a/tokio/tests/task_emscripten.rs b/tokio/tests/task_emscripten.rs new file mode 100644 index 00000000000..3ff2b542355 --- /dev/null +++ b/tokio/tests/task_emscripten.rs @@ -0,0 +1,60 @@ +//! Regression tests for emscripten-specific behavior: the `u64`-microseconds +//! `Instant` (whose f64-milliseconds predecessor broke exact arithmetic) and +//! the unsupported-`spawn_blocking` contract. Broader async coverage comes +//! from the standard suite, which runs on emscripten. + +#![cfg(target_os = "emscripten")] + +use std::time::Duration; + +use tokio::time::Instant; + +#[test] +fn instant_now_works() { + let now = Instant::now(); + let _ = now.elapsed(); +} + +#[test] +fn instant_comparison() { + let a = Instant::now(); + for _ in 0..1000 { + std::hint::black_box(()); + } + let b = Instant::now(); + assert!(b >= a, "later instant should be >= earlier instant"); +} + +#[test] +fn instant_arithmetic() { + // Pins the round-trip property `(t + d) - d == t` that the original + // f64-milliseconds Instant impl broke. The u64-microseconds storage + // makes this exact. + let now = Instant::now(); + let duration = Duration::from_millis(100); + let later = now + duration; + assert!(later > now); + let diff = later - now; + assert_eq!(diff, duration); + let back = later - duration; + assert_eq!(back, now); +} + +#[test] +fn instant_with_seconds() { + let now = Instant::now(); + let duration = Duration::from_secs(1); + let later = now + duration; + let elapsed = later.duration_since(now); + assert_eq!(elapsed.as_secs(), 1); +} + +/// There is no threadpool on a single-threaded JS worker: `spawn_blocking` +/// is unsupported on emscripten, as on the other single-threaded wasm +/// targets. `tokio::fs` and `tokio::io::{stdin, stdout, stderr}` do not rely +/// on it there — their syscalls complete synchronously. +#[tokio::test] +#[should_panic = "OS can't spawn worker thread"] +async fn spawn_blocking_is_unsupported() { + let _ = tokio::task::spawn_blocking(|| 42).await; +} diff --git a/tokio/tests/task_id.rs b/tokio/tests/task_id.rs index 0cbf80d5ace..b54dda7b42b 100644 --- a/tokio/tests/task_id.rs +++ b/tokio/tests/task_id.rs @@ -1,10 +1,21 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use std::error::Error; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; +#[cfg(not(target_family = "wasm"))] use tokio::runtime::Runtime; use tokio::sync::oneshot; use tokio::task::{self, Id, LocalSet}; @@ -21,7 +32,7 @@ async fn task_id_spawn() { .unwrap(); } -#[cfg(not(target_os = "wasi"))] +#[cfg(not(any(target_os = "wasi", target_os = "emscripten")))] #[tokio::test(flavor = "current_thread")] async fn task_id_spawn_blocking() { task::spawn_blocking(|| println!("task id: {}", task::id())) @@ -38,7 +49,7 @@ async fn task_id_collision_current_thread() { assert_ne!(id1.unwrap(), id2.unwrap()); } -#[cfg(not(target_os = "wasi"))] +#[cfg(not(target_family = "wasm"))] #[tokio::test(flavor = "multi_thread")] async fn task_id_collision_multi_thread() { let handle1 = tokio::spawn(async { task::id() }); @@ -59,7 +70,7 @@ async fn task_ids_match_current_thread() { handle.await.unwrap(); } -#[cfg(not(target_os = "wasi"))] +#[cfg(not(target_family = "wasm"))] #[tokio::test(flavor = "multi_thread")] async fn task_ids_match_multi_thread() { let (tx, rx) = oneshot::channel(); @@ -72,7 +83,8 @@ async fn task_ids_match_multi_thread() { } #[cfg(not(target_os = "wasi"))] -#[tokio::test(flavor = "multi_thread")] +#[cfg_attr(target_os = "emscripten", tokio::test(flavor = "current_thread"))] +#[cfg_attr(not(target_os = "emscripten"), tokio::test(flavor = "multi_thread"))] async fn task_id_future_destructor_completion() { struct MyFuture { tx: Option>, @@ -100,7 +112,8 @@ async fn task_id_future_destructor_completion() { } #[cfg(not(target_os = "wasi"))] -#[tokio::test(flavor = "multi_thread")] +#[cfg_attr(target_os = "emscripten", tokio::test(flavor = "current_thread"))] +#[cfg_attr(not(target_os = "emscripten"), tokio::test(flavor = "multi_thread"))] async fn task_id_future_destructor_abort() { struct MyFuture { tx: Option>, @@ -205,7 +218,7 @@ fn task_try_id_outside_task() { assert_eq!(None, task::try_id()); } -#[cfg(not(target_os = "wasi"))] +#[cfg(not(target_family = "wasm"))] #[test] fn task_try_id_inside_block_on() { let rt = Runtime::new().unwrap(); @@ -248,7 +261,7 @@ async fn task_id_nested_spawn_local() { .await; } -#[cfg(not(target_os = "wasi"))] +#[cfg(not(target_family = "wasm"))] #[tokio::test(flavor = "multi_thread")] async fn task_id_block_in_place_block_on_spawn() { use tokio::runtime::Builder; @@ -283,6 +296,7 @@ fn task_id_outside_task_panic_caller() -> Result<(), Box> { Ok(()) } +#[cfg(not(target_family = "wasm"))] #[test] #[cfg_attr(not(panic = "unwind"), ignore)] fn task_id_inside_block_on_panic_caller() -> Result<(), Box> { diff --git a/tokio/tests/task_join_set.rs b/tokio/tests/task_join_set.rs index 38534d1b734..203c2e2510c 100644 --- a/tokio/tests/task_join_set.rs +++ b/tokio/tests/task_join_set.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use futures::future::{pending, FutureExt}; use std::panic; @@ -433,6 +443,7 @@ mod spawn_local { set.spawn_local(async {}); } + #[cfg(not(target_family = "wasm"))] #[tokio::test(flavor = "multi_thread")] #[should_panic( expected = "`spawn_local` called from outside of a `task::LocalSet` or `runtime::LocalRuntime`" diff --git a/tokio/tests/task_local.rs b/tokio/tests/task_local.rs index be9de724163..e39ad862a81 100644 --- a/tokio/tests/task_local.rs +++ b/tokio/tests/task_local.rs @@ -1,11 +1,22 @@ -#![cfg(all(feature = "full", not(target_os = "wasi")))] // Wasi doesn't support threads +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; use tokio::sync::oneshot; -#[tokio::test(flavor = "multi_thread")] +#[cfg_attr(not(target_family = "wasm"), tokio::test(flavor = "multi_thread"))] +#[cfg_attr(target_family = "wasm", tokio::test)] async fn local() { tokio::task_local! { static REQ_ID: u32; diff --git a/tokio/tests/task_panic.rs b/tokio/tests/task_panic.rs index 8b4de2ada54..5747f69add1 100644 --- a/tokio/tests/task_panic.rs +++ b/tokio/tests/task_panic.rs @@ -1,11 +1,23 @@ #![warn(rust_2018_idioms)] -#![cfg(all(feature = "full", not(target_os = "wasi")))] +#![cfg(any( + all(feature = "full", not(target_os = "wasi")), + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] #![cfg(panic = "unwind")] use futures::future; use std::error::Error; use tokio::runtime::Builder; -use tokio::task::{self, block_in_place}; +use tokio::task; +#[cfg(not(target_os = "emscripten"))] +use tokio::task::block_in_place; mod support { pub mod panic; @@ -13,6 +25,7 @@ mod support { use support::panic::test_panic; #[test] +#[cfg(not(target_os = "emscripten"))] // no rt-multi-thread fn block_in_place_panic_caller() -> Result<(), Box> { let panic_location_file = test_panic(|| { let rt = Builder::new_current_thread().enable_all().build().unwrap(); diff --git a/tokio/tests/task_yield_now.rs b/tokio/tests/task_yield_now.rs index e6fe5d2009a..343b9418125 100644 --- a/tokio/tests/task_yield_now.rs +++ b/tokio/tests/task_yield_now.rs @@ -1,4 +1,11 @@ -#![cfg(all(feature = "full", not(target_os = "wasi"), tokio_unstable))] +#![cfg(all( + any( + feature = "full", + all(target_os = "emscripten", feature = "rt", feature = "macros") + ), + not(target_os = "wasi"), + tokio_unstable +))] use tokio::task; use tokio_test::task::spawn; @@ -15,6 +22,7 @@ fn yield_now_outside_of_runtime() { assert!(task.poll().is_ready()); } +#[cfg(not(target_family = "wasm"))] #[tokio::test(flavor = "multi_thread")] async fn yield_now_external_executor_and_block_in_place() { let j = tokio::spawn(async { diff --git a/tokio/tests/test_clock.rs b/tokio/tests/test_clock.rs index 891636fdb28..79c69d019e3 100644 --- a/tokio/tests/test_clock.rs +++ b/tokio/tests/test_clock.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use tokio::time::{self, Duration, Instant}; diff --git a/tokio/tests/time_interval.rs b/tokio/tests/time_interval.rs index 7472a37123c..8138b954ba1 100644 --- a/tokio/tests/time_interval.rs +++ b/tokio/tests/time_interval.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use std::pin::Pin; use std::task::{Context, Poll}; diff --git a/tokio/tests/time_panic.rs b/tokio/tests/time_panic.rs index 918d02a416e..8ac21e7247d 100644 --- a/tokio/tests/time_panic.rs +++ b/tokio/tests/time_panic.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(all(feature = "full", not(target_os = "wasi")))] // Wasi doesn't support panic recovery +#![cfg(any( + all(feature = "full", not(target_os = "wasi")), + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] #![cfg(panic = "unwind")] use futures::future; @@ -22,21 +32,24 @@ fn rt_combinations() -> Vec { .unwrap(); rts.push(rt); - let rt = tokio::runtime::Builder::new_multi_thread() - .worker_threads(1) - .enable_all() - .build() - .unwrap(); - rts.push(rt); + #[cfg(not(target_os = "emscripten"))] + { + let rt = tokio::runtime::Builder::new_multi_thread() + .worker_threads(1) + .enable_all() + .build() + .unwrap(); + rts.push(rt); - let rt = tokio::runtime::Builder::new_multi_thread() - .worker_threads(4) - .enable_all() - .build() - .unwrap(); - rts.push(rt); + let rt = tokio::runtime::Builder::new_multi_thread() + .worker_threads(4) + .enable_all() + .build() + .unwrap(); + rts.push(rt); + } - #[cfg(tokio_unstable)] + #[cfg(all(tokio_unstable, not(target_os = "emscripten")))] { let rt = tokio::runtime::Builder::new_multi_thread() .worker_threads(1) diff --git a/tokio/tests/time_pause.rs b/tokio/tests/time_pause.rs index be993b4c8dd..3ca5be6ea20 100644 --- a/tokio/tests/time_pause.rs +++ b/tokio/tests/time_pause.rs @@ -1,13 +1,25 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] #![cfg(not(miri))] // Too slow on miri. +#[cfg(not(target_os = "emscripten"))] use rand::SeedableRng; +#[cfg(not(target_os = "emscripten"))] use rand::{rngs::StdRng, Rng}; use tokio::time::{self, Duration, Instant, Sleep}; use tokio_test::{assert_elapsed, assert_pending, assert_ready, assert_ready_eq, task}; -#[cfg(not(target_os = "wasi"))] +#[cfg(all(feature = "full", not(target_os = "wasi")))] use tokio_test::assert_err; use std::{ @@ -48,6 +60,10 @@ async fn pause_time_in_spawn_threads() { assert_err!(t.await); } +// `#[tokio::main]` returning a value isn't supported on emscripten: the +// worker entry can only signal completion, not marshal a return value back +// across the worker boundary. +#[cfg(not(target_os = "emscripten"))] #[test] fn paused_time_is_deterministic() { let run_1 = paused_time_stress_run(); @@ -56,6 +72,7 @@ fn paused_time_is_deterministic() { assert_eq!(run_1, run_2); } +#[cfg(not(target_os = "emscripten"))] #[tokio::main(flavor = "current_thread", start_paused = true)] async fn paused_time_stress_run() -> Vec { let mut rng = StdRng::seed_from_u64(1); diff --git a/tokio/tests/time_sleep.rs b/tokio/tests/time_sleep.rs index b82b1cc6ae4..3fe6c48d236 100644 --- a/tokio/tests/time_sleep.rs +++ b/tokio/tests/time_sleep.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] #![cfg(not(miri))] // Too slow on Miri. use std::future::Future; diff --git a/tokio/tests/time_timeout.rs b/tokio/tests/time_timeout.rs index ec871cf62fe..0959392ca1f 100644 --- a/tokio/tests/time_timeout.rs +++ b/tokio/tests/time_timeout.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(feature = "full")] +#![cfg(any( + feature = "full", + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use tokio::sync::oneshot; use tokio::time::{self, timeout, timeout_at, Instant}; diff --git a/tokio/tests/unwindsafe.rs b/tokio/tests/unwindsafe.rs index 8ab6654295b..5f1db57a4f0 100644 --- a/tokio/tests/unwindsafe.rs +++ b/tokio/tests/unwindsafe.rs @@ -1,5 +1,15 @@ #![warn(rust_2018_idioms)] -#![cfg(all(feature = "full", not(target_os = "wasi")))] // Wasi does not support panic recovery +#![cfg(any( + all(feature = "full", not(target_os = "wasi")), + all( + target_os = "emscripten", + feature = "rt", + feature = "time", + feature = "sync", + feature = "macros", + feature = "test-util" + ) +))] use std::panic::{RefUnwindSafe, UnwindSafe}; @@ -14,6 +24,7 @@ fn join_handle_is_unwind_safe() { } #[test] +#[cfg(any(not(target_os = "emscripten"), feature = "net"))] fn net_types_are_unwind_safe() { is_unwind_safe::(); is_unwind_safe::(); @@ -22,8 +33,10 @@ fn net_types_are_unwind_safe() { } #[test] -#[cfg(unix)] +#[cfg(all(unix, any(not(target_os = "emscripten"), feature = "net")))] fn unix_net_types_are_unwind_safe() { + // No datagram `AF_UNIX` on emscripten's node backend. + #[cfg(not(target_os = "emscripten"))] is_unwind_safe::(); is_unwind_safe::(); is_unwind_safe::(); From d8129a519b7e1f5ce04b06adc08f25a9a3335565 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Mon, 20 Jul 2026 19:17:45 -0700 Subject: [PATCH 2/3] chore: fix spellcheck --- spellcheck.dic | 3 ++- tokio/src/runtime/jspi.rs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/spellcheck.dic b/spellcheck.dic index 60de12c6912..073dfd98227 100644 --- a/spellcheck.dic +++ b/spellcheck.dic @@ -1,4 +1,4 @@ -329 +330 & + < @@ -268,6 +268,7 @@ subfield suboptimal subprocess superset +suspendable symlink symlinks sys diff --git a/tokio/src/runtime/jspi.rs b/tokio/src/runtime/jspi.rs index 9ce1494457d..5f1ca488ca4 100644 --- a/tokio/src/runtime/jspi.rs +++ b/tokio/src/runtime/jspi.rs @@ -4,7 +4,7 @@ //! suspension for its whole promising activation with a [`SuspendGuard`], //! and [`sleep`] — the one suspending import the runtime issues — parks //! that activation on a host timer. With never more than one suspension in -//! flight, no spill-stack save/restore is needed: any wasm re-entered +//! flight, no spill-stack save/restore is needed: any Wasm re-entered //! while the activation is parked completes before the resume, leaving the //! spill stack above the suspended frame untouched. @@ -67,7 +67,7 @@ static __em_js____asyncjs__tokio_jspi_sleep: [u8; TOKIO_JSPI_SLEEP.len() + 1] = em_js(TOKIO_JSPI_SLEEP); unsafe extern "C" { - /// Reports the ASYNCIFY build mode: 0 = none, 1 = asyncify, 2 = JSPI. + /// Reports the `ASYNCIFY` build mode: 0 = none, 1 = `asyncify`, 2 = JSPI. safe fn emscripten_has_asyncify() -> i32; } From 54a711987184af158e793a419dcca96e04a05ce0 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Mon, 27 Jul 2026 18:52:54 -0700 Subject: [PATCH 3/3] chore: fix spellcheck --- tokio/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tokio/src/lib.rs b/tokio/src/lib.rs index 9294ca4560d..471ac30c6a7 100644 --- a/tokio/src/lib.rs +++ b/tokio/src/lib.rs @@ -453,7 +453,7 @@ //! wake) panics with a targeted error, since the host event loop cannot run //! while Wasm blocks the stack. //! -//! [JSPI] (-sJSPI in Emscripten) implements an exception to this behaviour: +//! [JSPI] (-sJSPI in Emscripten) implements an exception to this behavior: //! a drive that would block instead suspends the whole calling stack on a //! host timer — the host loop keeps running and wakes it. Emscripten //! auto-promising-wraps `main` under `-sJSPI`, so `#[tokio::main]` and