Skip to content
Open
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
41 changes: 41 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,47 @@ jobs:
RUSTFLAGS: --cfg tokio_unstable
CARGO_TARGET_WASM32_WASIP2_RUNNER: wasmtime run -Sinherit-network

wasm32-unknown-emscripten:
name: test tokio for wasm32-unknown-emscripten
needs: basics
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install Rust ${{ env.rust_stable }}
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.rust_stable }}
targets: wasm32-unknown-emscripten

- name: Install Emscripten
uses: mymindstorm/setup-emsdk@v14
with:
version: 'latest'

- uses: actions/setup-node@v4
with:
node-version: 26

- uses: Swatinem/rust-cache@v2

- name: Install cargo-hack
uses: taiki-e/install-action@v2
with:
tool: cargo-hack

- name: Check tokio feature matrix for emscripten
run: cargo hack check -p tokio --each-feature --exclude-features full,net,process,signal,rt-multi-thread,io-uring,taskdump,schedule-latency --target wasm32-unknown-emscripten
working-directory: tokio
env:
RUSTFLAGS: ""

- name: Test tokio for emscripten
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"

check-external-types:
name: check-external-types (${{ matrix.os }})
needs: basics
Expand Down
5 changes: 4 additions & 1 deletion spellcheck.dic
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
324
327
&
+
<
Expand Down Expand Up @@ -106,6 +106,8 @@ dns
DNS
DoS
dwOpenMode
Emscripten
Emscripten's
endian
enqueue
enqueued
Expand Down Expand Up @@ -320,6 +322,7 @@ Wakers
wakeup
wakeups
WASI
Wasm
watchOS
workstealing
ZST
20 changes: 19 additions & 1 deletion tokio-macros/src/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ fn parse_knobs(mut input: ItemFn, is_test: bool, config: FinalConfig) -> TokenSt

let body_ident = quote! { body };
// This explicit `return` is intentional. See tokio-rs/tokio#4636
let last_block = quote_spanned! {last_stmt_end_span=>
let native_last_block = quote_spanned! {last_stmt_end_span=>

#[allow(clippy::expect_used, clippy::diverging_sub_expression, clippy::needless_return, clippy::unwrap_in_result)]
{
Expand All @@ -521,6 +521,24 @@ 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 last_block = match config.flavor {
RuntimeFlavor::Threaded => quote! {
#[cfg(not(target_os = "emscripten"))]
#native_last_block
#[cfg(target_os = "emscripten")]
::core::compile_error!(
Comment thread
guybedford marked this conversation as resolved.
"the `multi_thread` runtime flavor is not available on \
wasm32-unknown-emscripten (no native threads); use \
`flavor = \"current_thread\"`"
);
},
_ => native_last_block,
};

let body = input.body();

// For test functions pin the body to the stack and use `Pin<&mut dyn
Expand Down
29 changes: 29 additions & 0 deletions tokio-test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub mod task;
/// [`tokio::runtime::Runtime::block_on`][runtime-block-on].
///
/// [runtime-block-on]: https://docs.rs/tokio/1.3.0/tokio/runtime/struct.Runtime.html#method.block_on
#[cfg(not(target_os = "emscripten"))]
pub fn block_on<F: std::future::Future>(future: F) -> F::Output {
use tokio::runtime;

Expand All @@ -34,3 +35,31 @@ pub fn block_on<F: std::future::Future>(future: F) -> F::Output {

rt.block_on(future)
}

/// Emscripten variant: polls once with a no-op waker. Ready futures (mocks,
/// pure computation) complete; ones that must yield (timers, I/O) panic —
/// use `#[tokio::test]` for those.
#[cfg(target_os = "emscripten")]
pub fn block_on<F: std::future::Future>(future: F) -> F::Output {
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};

const VTABLE: RawWakerVTable = RawWakerVTable::new(
|_| RawWaker::new(std::ptr::null(), &VTABLE),
|_| {},
|_| {},
|_| {},
);
// SAFETY: vtable entries are valid no-ops.
let waker = unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) };
let mut cx = Context::from_waker(&waker);

let mut future = Box::pin(future);
match future.as_mut().poll(&mut cx) {
Poll::Ready(output) => output,
Poll::Pending => panic!(
"tokio_test::block_on: future returned Pending on emscripten. \
The main thread cannot block on JS event-loop wakeups; use #[tokio::test] \
for futures that need timers/network I/O."
),
}
}
14 changes: 10 additions & 4 deletions tokio/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ bytes = { version = "1.2.1", optional = true }
mio = { version = "1.2.0", optional = true, default-features = false }
parking_lot = { version = "0.12.0", optional = true }

[target.'cfg(any(not(target_family = "wasm"), all(target_os = "wasi", not(target_env = "p1"))))'.dependencies]
[target.'cfg(any(not(target_family = "wasm"), target_os = "emscripten", all(target_os = "wasi", not(target_env = "p1"))))'.dependencies]
socket2 = { version = "0.6.3", optional = true, features = ["all"] }

# Currently unstable. The API exposed by these features may be broken at any time.
Expand Down Expand Up @@ -130,6 +130,8 @@ libc = { version = "0.2.168", optional = true }

[target.'cfg(unix)'.dev-dependencies]
libc = { version = "0.2.168" }

[target.'cfg(all(unix, not(target_os = "emscripten")))'.dev-dependencies]
nix = { version = "0.31.0", default-features = false, features = ["aio", "fs", "socket"] }

[target.'cfg(windows)'.dependencies.windows-sys]
Expand All @@ -146,22 +148,26 @@ features = [
[dev-dependencies]
tokio-test = "0.4.0"
tokio-stream = "0.1"
tokio-util = { version = "0.7", features = ["rt"] }
futures = { version = "0.3.0", features = ["async-await"] }
futures-test = "0.3.31"
mockall = "0.13.0"
async-stream = "0.3"
futures-concurrency = "7.6.3"

[target.'cfg(not(target_os = "emscripten"))'.dev-dependencies]
tokio-util = { version = "0.7", features = ["rt"] }

[target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
socket2 = "0.6.0"
tempfile = "3.1.0"
proptest = "1"

[target.'cfg(any(not(target_family = "wasm"), target_os = "emscripten"))'.dev-dependencies]
tempfile = "3.1.0"

[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dev-dependencies]
rand = "0.9"

[target.'cfg(all(target_family = "wasm", not(target_os = "wasi")))'.dev-dependencies]
[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dev-dependencies]
wasm-bindgen-test = "0.3.0"

[target.'cfg(target_os = "freebsd")'.dev-dependencies]
Expand Down
53 changes: 53 additions & 0 deletions tokio/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,51 @@
//! immediately instead of blocking forever. On platforms that don't support
//! time, this means that the runtime can never be idle in any way.
//!
//! ### 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 `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.
//!
//! 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"`.
//!
//!
//! #### Linking and running on Emscripten
//!
//! No js-library or other custom file is required, and 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=-sSTACK_SIZE=1048576"
//! ```
//!
//! ## Unstable `WASM` support
//!
//! Tokio also has unstable support for some additional `WASM` features. This
Expand All @@ -467,6 +512,7 @@ compile_error! {
#[cfg(all(
not(tokio_unstable),
target_family = "wasm",
not(target_os = "emscripten"),
any(
feature = "fs",
feature = "io-std",
Expand All @@ -478,6 +524,13 @@ 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
// 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(all(not(tokio_unstable), feature = "io-uring"))]
compile_error!("The `io-uring` feature requires `--cfg tokio_unstable`.");

Expand Down
20 changes: 17 additions & 3 deletions tokio/src/macros/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,11 @@ 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!`.
$item
)*
}
Expand All @@ -407,14 +412,20 @@ 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)* }
}
}

macro_rules! cfg_not_process_driver {
($($item:item)*) => {
$(
#[cfg(not(all(unix, not(loom), feature = "process")))]
#[cfg(any(
not(all(unix, not(loom), feature = "process")),
target_os = "emscripten",
))]
$item
)*
}
Expand All @@ -427,6 +438,8 @@ macro_rules! cfg_signal {
#[cfg_attr(docsrs, doc(cfg(feature = "signal")))]
#[cfg(not(loom))]
#[cfg(not(target_os = "wasi"))]
// No kernel signal delivery on Emscripten; inert there.
#[cfg(not(target_os = "emscripten"))]
$item
)*
}
Expand All @@ -437,6 +450,7 @@ macro_rules! cfg_signal_internal {
$(
#[cfg(any(feature = "signal", all(unix, feature = "process")))]
#[cfg(not(loom))]
#[cfg(not(target_os = "emscripten"))]
$item
)*
}
Expand All @@ -452,7 +466,7 @@ macro_rules! cfg_signal_internal_and_unix {
macro_rules! cfg_not_signal_internal {
($($item:item)*) => {
$(
#[cfg(any(loom, not(unix), not(any(feature = "signal", all(unix, feature = "process")))))]
#[cfg(any(loom, not(unix), target_os = "emscripten", not(any(feature = "signal", all(unix, feature = "process")))))]
$item
)*
}
Expand Down Expand Up @@ -715,7 +729,7 @@ macro_rules! cfg_not_wasip1 {
macro_rules! cfg_is_wasm_not_wasi {
($($item:item)*) => {
$(
#[cfg(all(target_family = "wasm", not(target_os = "wasi")))]
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
$item
)*
}
Expand Down
Loading
Loading