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
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,54 @@ 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 (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=-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 }})
needs: basics
Expand Down
8 changes: 7 additions & 1 deletion spellcheck.dic
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
324
330
&
+
<
Expand Down Expand Up @@ -106,6 +106,8 @@ dns
DNS
DoS
dwOpenMode
Emscripten
Emscripten's
endian
enqueue
enqueued
Expand Down Expand Up @@ -160,6 +162,8 @@ IP
IPv4
IPv6
iteratively
JS
JSPI
Kotlin's
latencies
Lauck
Expand Down Expand Up @@ -264,6 +268,7 @@ subfield
suboptimal
subprocess
superset
suspendable
symlink
symlinks
sys
Expand Down Expand Up @@ -320,6 +325,7 @@ Wakers
wakeup
wakeups
WASI
Wasm
watchOS
workstealing
ZST
69 changes: 60 additions & 9 deletions 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,60 @@ fn parse_knobs(mut input: ItemFn, is_test: bool, config: FinalConfig) -> TokenSt

};

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();
// JSPI is enabled for the module, but suspending imports are valid only
// from a promising-wrapped activation. The guard marks libtest's known
// promising activation so its `block_on` may suspend; other entries fail
// at the park leaf instead of trapping in the engine.
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
};
Comment thread
guybedford marked this conversation as resolved.

let body = async #raw_body;
#crate_path::pin!(body);
let body: ::core::pin::Pin<&mut dyn ::core::future::Future<Output = #output_type>> = 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);
}
};

let last_block = match config.flavor {
RuntimeFlavor::Threaded => quote! {
#crate_path::__tokio_unsupported_multi_thread_on_emscripten! {
#native_last_block
}
},
_ if is_test => quote! {
#[cfg(not(target_os = "emscripten"))]
#native_last_block
#[cfg(target_os = "emscripten")]
#emscripten_test_block
},
_ => native_last_block,
};

let body = input.body();

// For test functions pin the body to the stack and use `Pin<&mut dyn
Expand All @@ -532,18 +586,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<Output = #output_type>> = body;
}
} else {
Expand Down
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
26 changes: 26 additions & 0 deletions tokio/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,18 @@
//! 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 supports the single-threaded runtime
//! with the `rt`, `time`, `sync`, `macros`, `fs`, `io-util`, `io-std`, and
//! `test-util` features. The `net`, `process`, `signal`, and `rt-multi-thread`
//! features are not supported.
//!
//! With [JSPI], `#[tokio::test]` can suspend on the host event loop while
//! waiting for timers. Without JSPI, a wait that cannot progress panics.
//!
//! [JSPI]: https://github.com/WebAssembly/js-promise-integration
//!
//! ## Unstable `WASM` support
//!
//! Tokio also has unstable support for some additional `WASM` features. This
Expand All @@ -467,6 +479,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 +491,19 @@ compile_error! {
))]
compile_error!("Only features sync,macros,io-util,rt,time are supported on wasm.");

#[cfg(all(target_os = "emscripten", feature = "process"))]
compile_error!("The `process` feature is not supported on wasm32-unknown-emscripten.");

// 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`.");

Expand Down
13 changes: 10 additions & 3 deletions tokio/src/macros/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ macro_rules! cfg_process {
#[cfg_attr(docsrs, doc(cfg(feature = "process")))]
#[cfg(not(loom))]
#[cfg(not(target_os = "wasi"))]
#[cfg(not(target_os = "emscripten"))]
$item
)*
}
Expand All @@ -414,7 +415,10 @@ macro_rules! cfg_process_driver {
macro_rules! cfg_not_process_driver {
($($item:item)*) => {
$(
#[cfg(not(all(unix, not(loom), feature = "process")))]
#[cfg(any(
target_os = "emscripten",
not(all(unix, not(loom), feature = "process")),
))]
$item
)*
}
Expand All @@ -427,6 +431,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 +443,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 +459,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 +722,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
20 changes: 20 additions & 0 deletions tokio/src/macros/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,23 @@ cfg_macros! {
// Includes re-exports needed to implement macros
#[doc(hidden)]
pub mod support;

#[doc(hidden)]
#[macro_export]
#[cfg(not(target_os = "emscripten"))]
macro_rules! __tokio_unsupported_multi_thread_on_emscripten {
($($body:tt)*) => { $($body)* };
}

#[doc(hidden)]
#[macro_export]
#[cfg(target_os = "emscripten")]
macro_rules! __tokio_unsupported_multi_thread_on_emscripten {
($($body:tt)*) => {
::core::compile_error!(
"the `multi_thread` runtime flavor is not available on \
wasm32-unknown-emscripten (no native threads); use \
`flavor = \"current_thread\"`"
);
};
}
Loading
Loading