├── .rustfmt.toml ├── Cargo.toml ├── example ├── src │ ├── main.rs │ ├── measure.rs │ ├── lib.rs │ ├── output.rs │ └── entry.rs ├── Cargo.toml ├── pkg │ └── index.html └── README.md ├── .cargo └── config.toml ├── package ├── src │ ├── glue │ │ ├── common │ │ │ ├── polling.rs │ │ │ ├── thread_check.rs │ │ │ ├── select_future.rs │ │ │ ├── mod.rs │ │ │ ├── once_channel.rs │ │ │ └── local_channel.rs │ │ ├── mod.rs │ │ ├── time │ │ │ └── mod.rs │ │ └── task │ │ │ ├── pool.rs │ │ │ ├── join_set.rs │ │ │ └── mod.rs │ └── lib.rs ├── Cargo.toml └── README.md ├── .gitignore ├── package_proc ├── Cargo.toml └── src │ └── lib.rs ├── LICENSE └── README.md /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | style_edition = "2024" 2 | max_width = 80 3 | tab_spaces = 2 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["package", "package_proc", "example"] 3 | resolver = "3" 4 | 5 | [patch.crates-io] 6 | tokio_with_wasm = { path = "./package" } 7 | tokio_with_wasm_proc = { path = "./package_proc" } 8 | -------------------------------------------------------------------------------- /example/src/main.rs: -------------------------------------------------------------------------------- 1 | mod entry; 2 | mod measure; 3 | mod output; 4 | 5 | use entry::*; 6 | use measure::*; 7 | 8 | // On native platforms, the `main` function 9 | // is called by the executable automatically. 10 | fn main() { 11 | async_main(); 12 | } 13 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | # Tell Rust-analyzer to perform 3 | # type checking and linting in webassembly mode. 4 | # If you make any changes to this field, 5 | # you might have to restart Rust-analyzer for the change to take effect. 6 | target = "wasm32-unknown-unknown" 7 | -------------------------------------------------------------------------------- /example/src/measure.rs: -------------------------------------------------------------------------------- 1 | use crate::print_fit; 2 | 3 | /// Prints right away when an instance is dropped. 4 | pub struct Dropper { 5 | pub name: String, 6 | } 7 | 8 | impl Drop for Dropper { 9 | fn drop(&mut self) { 10 | print_fit!("Dropper \"{}\" has been dropped", self.name); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod entry; 2 | mod measure; 3 | mod output; 4 | 5 | use entry::*; 6 | use measure::*; 7 | 8 | use wasm_bindgen::prelude::wasm_bindgen; 9 | 10 | // On the web, this macro tells `wasm_bindgen` 11 | // to run the function on JavaScript module initialization. 12 | #[wasm_bindgen(start)] 13 | fn main() { 14 | async_main(); 15 | } 16 | -------------------------------------------------------------------------------- /package/src/glue/common/polling.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::task::{Wake, Waker}; 3 | 4 | pub struct NoopWaker; 5 | 6 | impl Wake for NoopWaker { 7 | fn wake(self: Arc) {} 8 | } 9 | 10 | /// Creates a no-op `Waker` for polling. 11 | /// "noop" stands for "no operation", 12 | /// meaning it does nothing when executed. 13 | pub fn noop_waker() -> Waker { 14 | Waker::from(Arc::new(NoopWaker)) 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | -------------------------------------------------------------------------------- /package_proc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tokio_with_wasm_proc" 3 | version = "0.8.7" 4 | edition = "2021" 5 | license = "MIT" 6 | description = "Mimicking tokio functionalities on web browsers" 7 | repository = "https://github.com/cunarist/tokio-with-wasm" 8 | 9 | [package.metadata.docs.rs] 10 | default-target = "wasm32-unknown-unknown" 11 | all-features = true 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | syn = { version = "2.0.77", features = ["full"] } 18 | quote = "1.0.37" 19 | -------------------------------------------------------------------------------- /example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT" 6 | description = "Example usage of `tokio_with_wasm` crate" 7 | repository = "https://github.com/cunarist/tokio-with-wasm" 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [dependencies] 13 | tokio = { version = "1.x.x", features = ["macros", "sync", "time", "rt"] } 14 | tokio_with_wasm = { version = "*", features = ["macros", "sync", "time", "rt"] } 15 | wasm-bindgen = "0.2.95" 16 | chrono = "0.4.39" 17 | -------------------------------------------------------------------------------- /example/pkg/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | WASM Example 8 | 16 | 17 | 18 |

WASM Example

19 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | ## Commands 2 | 3 | To install dependencies: 4 | 5 | ```shell 6 | cargo install wasm-pack 7 | cargo install miniserve 8 | ``` 9 | 10 | To compile: 11 | 12 | ```shell 13 | export RUSTFLAGS="-C target-feature=+atomics,+bulk-memory,+mutable-globals" 14 | export RUSTUP_TOOLCHAIN="nightly" 15 | wasm-pack build . --target web -- -Z build-std=std,panic_abort 16 | ``` 17 | 18 | To run on a web browser: 19 | 20 | ```shell 21 | miniserve pkg --index index.html --header "Cross-Origin-Opener-Policy:same-origin" --header "Cross-Origin-Embedder-Policy:require-corp" 22 | ``` 23 | 24 | > You need to temporarily modify `.cargo/config.toml` to make `cargo run` use the native platform. 25 | -------------------------------------------------------------------------------- /package/src/glue/common/thread_check.rs: -------------------------------------------------------------------------------- 1 | use js_sys::global; 2 | use std::cell::LazyCell; 3 | use wasm_bindgen::JsValue; 4 | 5 | /// The name of a JS object 6 | /// that is only present in the blocking thread. 7 | pub static BLOCKING_KEY: &str = "isBlockingTokioThread"; 8 | 9 | thread_local! { 10 | pub static IS_MAIN_THREAD: LazyCell = LazyCell::new(|| { 11 | let global_obj = global(); 12 | let is_blocking_thread = 13 | js_sys::Reflect::has(&global_obj, &JsValue::from_str(BLOCKING_KEY)) 14 | .unwrap_or(false); 15 | !is_blocking_thread 16 | }); 17 | } 18 | 19 | pub fn is_main_thread() -> bool { 20 | let mut is_main: bool = false; 21 | IS_MAIN_THREAD.with(|cell| { 22 | is_main = **cell; 23 | }); 24 | is_main 25 | } 26 | -------------------------------------------------------------------------------- /package/src/glue/mod.rs: -------------------------------------------------------------------------------- 1 | //! JavaScript glue module that mimics `tokio`. 2 | 3 | mod common; 4 | 5 | #[cfg(feature = "macros")] 6 | pub use tokio::{join, pin, select, try_join}; 7 | 8 | #[cfg(feature = "sync")] 9 | pub use tokio::sync; 10 | 11 | #[cfg(feature = "time")] 12 | pub mod time; 13 | 14 | #[cfg(feature = "rt")] 15 | pub mod task; 16 | #[cfg(feature = "rt")] 17 | pub use task::spawn; 18 | #[cfg(feature = "rt")] 19 | pub(crate) use task::*; 20 | 21 | #[cfg(all( 22 | any(feature = "rt", feature = "rt-multi-thread"), 23 | feature = "macros" 24 | ))] 25 | pub use tokio_with_wasm_proc::main; 26 | 27 | #[doc(hidden)] 28 | #[cfg(all( 29 | any(feature = "rt", feature = "rt-multi-thread"), 30 | feature = "macros" 31 | ))] 32 | // This export is needed for the `main` macro. 33 | pub use wasm_bindgen_futures::spawn_local; 34 | 35 | #[allow(unused_imports)] 36 | pub(crate) use common::*; 37 | -------------------------------------------------------------------------------- /example/src/output.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all( 2 | target_arch = "wasm32", 3 | target_vendor = "unknown", 4 | target_os = "unknown" 5 | ))] 6 | pub mod printing { 7 | use wasm_bindgen::prelude::wasm_bindgen; 8 | 9 | #[wasm_bindgen] 10 | extern "C" { 11 | #[wasm_bindgen(js_namespace = globalThis, js_name = eval)] 12 | fn eval(script: &str); 13 | } 14 | 15 | pub fn do_printing(s: &str) { 16 | let script = format!("document.body.innerHTML += '

{s}

';",); 17 | eval(&script); 18 | } 19 | } 20 | 21 | #[cfg(not(all( 22 | target_arch = "wasm32", 23 | target_vendor = "unknown", 24 | target_os = "unknown" 25 | )))] 26 | pub mod printing { 27 | pub fn do_printing(s: &str) { 28 | println!("{s}"); 29 | } 30 | } 31 | 32 | #[macro_export] 33 | /// Prints to the HTML document when compiled to WASM. 34 | /// Otherwise, it prints to `stdout`. 35 | macro_rules! print_fit { 36 | ($($t:tt)*) => { 37 | $crate::output::printing::do_printing(&format!($($t)*)) 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /package/src/glue/common/select_future.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | 5 | pub struct SelectFuture { 6 | future_a: Pin>>, 7 | future_b: Pin>>, 8 | } 9 | 10 | impl SelectFuture { 11 | pub fn new( 12 | future_a: impl Future + 'static, 13 | future_b: impl Future + 'static, 14 | ) -> Self { 15 | SelectFuture { 16 | future_a: Box::pin(future_a), 17 | future_b: Box::pin(future_b), 18 | } 19 | } 20 | } 21 | 22 | impl Future for SelectFuture { 23 | type Output = T; 24 | fn poll( 25 | mut self: Pin<&mut Self>, 26 | cx: &mut Context<'_>, 27 | ) -> Poll { 28 | if let Poll::Ready(output) = self.future_a.as_mut().poll(cx) { 29 | return Poll::Ready(output); 30 | } 31 | if let Poll::Ready(output) = self.future_b.as_mut().poll(cx) { 32 | return Poll::Ready(output); 33 | } 34 | Poll::Pending 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /package/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tokio_with_wasm" 3 | version = "0.8.7" 4 | edition = "2021" 5 | license = "MIT" 6 | description = "Mimicking tokio functionalities on web browsers" 7 | repository = "https://github.com/cunarist/tokio-with-wasm" 8 | 9 | [package.metadata.docs.rs] 10 | default-target = "wasm32-unknown-unknown" 11 | all-features = true 12 | 13 | [features] 14 | # https://github.com/tokio-rs/tokio/blob/master/tokio/Cargo.toml 15 | default = [] 16 | full = ["macros", "sync", "time", "rt", "rt-multi-thread"] 17 | macros = ["tokio/macros"] 18 | sync = ["tokio/sync"] 19 | time = [] 20 | rt = [] 21 | rt-multi-thread = [] 22 | 23 | [dependencies] 24 | tokio_with_wasm_proc = "0.8.7" 25 | tokio = "1.x.x" 26 | wasm-bindgen = "0.2.95" 27 | wasm-bindgen-futures = "0.4.45" 28 | js-sys = "0.3.70" 29 | web-sys = { version = "0.3.70", features = [ 30 | 'Worker', 31 | 'WorkerOptions', 32 | 'WorkerType', 33 | 'DedicatedWorkerGlobalScope', 34 | 'MessageEvent', 35 | 'ErrorEvent', 36 | 'Blob', 37 | "BlobPropertyBag", 38 | 'Url', 39 | ] } 40 | -------------------------------------------------------------------------------- /package/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A library that adapts the popular async runtime `tokio` for web browsers. 2 | //! 3 | //! It provides a similar set of features specifically for web applications 4 | //! by leveraging the JavaScript web API. 5 | //! 6 | //! This library includes JavaScript glue code 7 | //! to mimic the behavior of real `tokio`, 8 | //! making it possible to run asynchronous Rust code in the browser. 9 | //! Since `tokio_with_wasm` adapts to the JavaScript event loop 10 | //! and does not include its own runtime, 11 | //! some advanced features of `tokio` might not be fully supported. 12 | 13 | #[cfg(not(all( 14 | target_arch = "wasm32", 15 | target_vendor = "unknown", 16 | target_os = "unknown" 17 | )))] 18 | pub use tokio as alias; 19 | 20 | #[cfg(all( 21 | target_arch = "wasm32", 22 | target_vendor = "unknown", 23 | target_os = "unknown" 24 | ))] 25 | pub use crate as alias; 26 | 27 | #[cfg(all( 28 | target_arch = "wasm32", 29 | target_vendor = "unknown", 30 | target_os = "unknown" 31 | ))] 32 | mod glue; 33 | 34 | #[allow(unused_imports)] 35 | #[cfg(all( 36 | target_arch = "wasm32", 37 | target_vendor = "unknown", 38 | target_os = "unknown" 39 | ))] 40 | pub use glue::*; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2014 Alex Crichton 4 | Copyright (c) Tokio Contributors 5 | Copyright (c) 2025 Cunarist 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /package_proc/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::{ItemFn, parse_macro_input}; 4 | 5 | /// Attribute macro that mimics `tokio::main`. 6 | /// This macro writes a function that simply spawns the given future 7 | /// inside the JavaScript environment. 8 | /// To execute the function, you might need to use 9 | /// `#[wasm_bindgen(start)]` in addition to this macro. 10 | #[proc_macro_attribute] 11 | pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream { 12 | // Parse the input tokens as a function 13 | let input_fn = parse_macro_input!(item as ItemFn); 14 | 15 | // Extract function components 16 | let vis = &input_fn.vis; 17 | let fn_name = &input_fn.sig.ident; 18 | let fn_args = &input_fn.sig.inputs; 19 | let fn_block = &input_fn.block; 20 | let return_type = &input_fn.sig.output; 21 | 22 | // Generate a non-async function 23 | // that calls the original function with `spawn_local` 24 | let expanded = quote! { 25 | #vis fn #fn_name() { 26 | async fn original(#fn_args) #return_type #fn_block 27 | 28 | // Spawn the async function in a local task 29 | tokio_with_wasm::spawn_local(async { 30 | let _ = original().await; 31 | }); 32 | } 33 | }; 34 | 35 | TokenStream::from(expanded) 36 | } 37 | -------------------------------------------------------------------------------- /package/src/glue/common/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![allow(unused_imports)] 3 | 4 | mod local_channel; 5 | mod once_channel; 6 | mod polling; 7 | mod select_future; 8 | mod thread_check; 9 | 10 | pub use local_channel::*; 11 | pub use once_channel::*; 12 | pub use polling::*; 13 | pub use select_future::*; 14 | pub use thread_check::*; 15 | 16 | use js_sys::Function; 17 | use wasm_bindgen::prelude::{JsValue, wasm_bindgen}; 18 | 19 | #[wasm_bindgen] 20 | extern "C" { 21 | #[wasm_bindgen(js_namespace = console, js_name = error)] 22 | pub fn error(s: &str); 23 | #[wasm_bindgen(js_namespace = Date, js_name = now)] 24 | pub fn now() -> f64; 25 | #[wasm_bindgen(js_namespace = globalThis, js_name = setTimeout)] 26 | pub fn set_timeout(callback: &Function, milliseconds: f64); 27 | #[wasm_bindgen(js_namespace = globalThis, js_name = setInterval)] 28 | pub fn set_interval(callback: &Function, milliseconds: f64) -> i32; 29 | #[wasm_bindgen(js_namespace = globalThis, js_name = clearInterval)] 30 | pub fn clear_interval(id: i32); 31 | } 32 | 33 | pub trait LogError { 34 | fn log_error(&self, code: &str); 35 | } 36 | 37 | impl LogError for JsValue { 38 | fn log_error(&self, code: &str) { 39 | error(&format!("Error `{code}` in `tokio_with_wasm`:\n{self:?}")); 40 | } 41 | } 42 | 43 | impl LogError for Result { 44 | fn log_error(&self, code: &str) { 45 | if let Err(js_value) = self { 46 | error(&format!( 47 | "Error `{code}` in `tokio_with_wasm`:\n{js_value:?}" 48 | )); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /package/src/glue/common/once_channel.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | use std::sync::atomic::{AtomicBool, Ordering}; 4 | use std::sync::{Arc, Mutex}; 5 | use std::task::{Context, Poll, Waker}; 6 | 7 | pub fn once_channel() -> (OnceSender, OnceReceiver) { 8 | let notified = Arc::new(AtomicBool::new(false)); 9 | let value = Arc::new(Mutex::new(None)); 10 | let waker = Arc::new(Mutex::new(None)); 11 | 12 | let sender = OnceSender { 13 | notified: notified.clone(), 14 | value: value.clone(), 15 | waker: waker.clone(), 16 | }; 17 | let receiver = OnceReceiver { 18 | notified, 19 | value, 20 | waker, 21 | }; 22 | 23 | (sender, receiver) 24 | } 25 | 26 | #[derive(Clone)] 27 | pub struct OnceSender { 28 | notified: Arc, 29 | value: Arc>>, 30 | waker: Arc>>, 31 | } 32 | 33 | impl OnceSender { 34 | pub fn send(&self, value: T) { 35 | if let Ok(mut guard) = self.value.lock() { 36 | guard.replace(value); 37 | self.notified.store(true, Ordering::SeqCst); 38 | } 39 | if let Ok(mut guard) = self.waker.lock() { 40 | if let Some(waker) = guard.take() { 41 | waker.wake(); 42 | } 43 | } 44 | } 45 | } 46 | 47 | pub struct OnceReceiver { 48 | notified: Arc, 49 | value: Arc>>, 50 | waker: Arc>>, 51 | } 52 | 53 | impl OnceReceiver { 54 | pub fn is_done(&self) -> bool { 55 | self.notified.load(Ordering::SeqCst) 56 | } 57 | } 58 | 59 | impl Future for OnceReceiver { 60 | type Output = T; 61 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 62 | if self.notified.load(Ordering::SeqCst) { 63 | if let Ok(mut guard) = self.value.lock() { 64 | if let Some(value) = guard.take() { 65 | return Poll::Ready(value); 66 | } 67 | } 68 | } 69 | if let Ok(mut guard) = self.waker.lock() { 70 | guard.replace(cx.waker().clone()); 71 | } 72 | Poll::Pending 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /example/src/entry.rs: -------------------------------------------------------------------------------- 1 | use crate::{Dropper, print_fit}; 2 | use chrono::Utc; 3 | use std::time::Duration; 4 | use tokio::task::{JoinSet, spawn, spawn_blocking, yield_now}; 5 | use tokio::time::{interval, sleep}; 6 | use tokio_with_wasm::alias as tokio; 7 | 8 | #[tokio::main(flavor = "current_thread")] 9 | pub async fn async_main() { 10 | test_join_handles().await; 11 | test_yield().await; 12 | test_interval().await; 13 | test_join_set().await; 14 | } 15 | 16 | async fn test_join_handles() { 17 | print_fit!("Tasks spawned"); 18 | let async_join_handle = spawn(async { 19 | // Simulate a 2-second async task. 20 | sleep(Duration::from_secs(2)).await; 21 | }); 22 | let blocking_join_handle = spawn_blocking(|| { 23 | // Simulate a 3-second blocking task. 24 | std::thread::sleep(Duration::from_secs(3)); 25 | }); 26 | 27 | let _async_result = async_join_handle.await; 28 | print_fit!("Async task joined"); 29 | let _blocking_result = blocking_join_handle.await; 30 | print_fit!("Blocking task joined"); 31 | } 32 | 33 | async fn test_yield() { 34 | for i in 1..=500 { 35 | yield_now().await; 36 | // Run some code that blocks for a few milliseconds. 37 | calculate_cpu_bound(); 38 | if i % 100 == 0 { 39 | print_fit!("Repeating task, iteration: {}", i); 40 | } 41 | } 42 | } 43 | 44 | async fn test_interval() { 45 | let mut ticker = interval(Duration::from_secs(1)); 46 | for i in 1..=5 { 47 | ticker.tick().await; 48 | print_fit!("Interval task, iteration: {}", i); 49 | } 50 | } 51 | 52 | fn calculate_cpu_bound() { 53 | let start = Utc::now().timestamp_millis(); 54 | let mut _sum = 0.0; 55 | while Utc::now().timestamp_millis() - start < 10 { 56 | for i in 0..10_000 { 57 | _sum += (i as f64).sqrt().sin().cos(); 58 | } 59 | } 60 | } 61 | 62 | async fn test_join_set() { 63 | print_fit!("Creating JoinSet"); 64 | let mut join_set: JoinSet<_> = JoinSet::new(); 65 | // Spawn many async tasks and add them to the JoinSet. 66 | for i in 1..=4 { 67 | join_set.spawn(async move { 68 | let _dropper = Dropper { 69 | name: format!("FROM_JOIN_SET_{i}"), 70 | }; 71 | sleep(Duration::from_secs(i)).await; 72 | }); 73 | } 74 | // Await only some of the tasks in the JoinSet. 75 | // Unfinished tasks should be aborted when the JoinSet is dropped. 76 | for _ in 1..=2 { 77 | if let Some(result) = join_set.join_next().await { 78 | if result.is_ok() { 79 | print_fit!("A task in the JoinSet finished successfully"); 80 | } else { 81 | print_fit!("A task in the JoinSet encountered an error") 82 | } 83 | } 84 | } 85 | print_fit!("Dropping JoinSet"); 86 | } 87 | -------------------------------------------------------------------------------- /package/src/glue/common/local_channel.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::VecDeque; 3 | use std::future::Future; 4 | use std::pin::Pin; 5 | use std::rc::Rc; 6 | use std::task::{Context, Poll, Waker}; 7 | 8 | /// Creates an unbounded channel, returning the sender and receiver. 9 | /// This channel is not `Send`, which means it cannot be sent across threads. 10 | /// The sender and receiver are not cloneable. 11 | pub fn local_channel() -> (LocalSender, LocalReceiver) { 12 | let shared = Rc::new(RefCell::new(ChannelCore { 13 | queue: VecDeque::new(), 14 | waker: None, 15 | closed: false, 16 | })); 17 | let sender = LocalSender { 18 | shared: shared.clone(), 19 | }; 20 | let receiver = LocalReceiver { shared }; 21 | (sender, receiver) 22 | } 23 | 24 | struct ChannelCore { 25 | queue: VecDeque, 26 | /// Waker for the task currently waiting on a message. 27 | waker: Option, 28 | /// Indicates that the channel is closed. 29 | closed: bool, 30 | } 31 | 32 | pub struct LocalSender { 33 | shared: Rc>>, 34 | } 35 | 36 | /// The sender side of an unbounded channel. 37 | impl LocalSender { 38 | /// Attempts to send an item into the channel. 39 | pub fn send(&self, item: T) { 40 | let mut shared = self.shared.borrow_mut(); 41 | if shared.closed { 42 | return; 43 | } 44 | shared.queue.push_back(item); 45 | if let Some(waker) = shared.waker.take() { 46 | waker.wake(); 47 | } 48 | } 49 | } 50 | 51 | impl Drop for LocalSender { 52 | fn drop(&mut self) { 53 | let mut shared = self.shared.borrow_mut(); 54 | // When the sender is dropped, 55 | // mark the channel as closed and wake the receiver. 56 | shared.closed = true; 57 | if let Some(waker) = shared.waker.take() { 58 | waker.wake(); 59 | } 60 | } 61 | } 62 | 63 | /// The receiver side of an unbounded channel. 64 | pub struct LocalReceiver { 65 | shared: Rc>>, 66 | } 67 | 68 | impl LocalReceiver { 69 | /// Polls the channel for the next available message. 70 | fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll> { 71 | let mut shared = self.shared.borrow_mut(); 72 | if let Some(item) = shared.queue.pop_front() { 73 | Poll::Ready(Some(item)) 74 | } else if shared.closed { 75 | // No more messages will ever arrive. 76 | Poll::Ready(None) 77 | } else { 78 | // No item available; store the waker to be notified. 79 | shared.waker = Some(cx.waker().clone()); 80 | Poll::Pending 81 | } 82 | } 83 | 84 | /// Returns a future that resolves to the next available message. 85 | pub fn next(&mut self) -> ChannelNext<'_, T> { 86 | ChannelNext { receiver: self } 87 | } 88 | } 89 | 90 | /// A future that resolves to the next item received. 91 | pub struct ChannelNext<'a, T> { 92 | receiver: &'a mut LocalReceiver, 93 | } 94 | 95 | impl Future for ChannelNext<'_, T> { 96 | type Output = Option; 97 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 98 | // Delegate to the receiver’s poll_next method. 99 | self.get_mut().receiver.poll_next(cx) 100 | } 101 | } 102 | 103 | impl Drop for LocalReceiver { 104 | fn drop(&mut self) { 105 | let mut shared = self.shared.borrow_mut(); 106 | // Mark the channel as closed when the receiver is dropped. 107 | shared.closed = true; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /package/src/glue/time/mod.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for tracking time. 2 | //! 3 | //! This module provides a number of types for executing code after a set period 4 | //! of time. 5 | 6 | use crate::{ 7 | LocalReceiver, LogError, clear_interval, local_channel, set_interval, 8 | set_timeout, 9 | }; 10 | use js_sys::Promise; 11 | use std::error::Error; 12 | use std::fmt::{Display, Formatter}; 13 | use std::future::Future; 14 | use std::io; 15 | use std::pin::Pin; 16 | use std::task::{Context, Poll}; 17 | use std::time::Duration; 18 | use wasm_bindgen::prelude::{Closure, JsCast}; 19 | use wasm_bindgen_futures::JsFuture; 20 | 21 | async fn time_future(duration: Duration) { 22 | let milliseconds = duration.as_millis() as f64; 23 | let promise = Promise::new(&mut |resolve, _reject| { 24 | set_timeout(&resolve, milliseconds); 25 | }); 26 | JsFuture::from(promise).await.log_error("TIME_FUTURE"); 27 | } 28 | 29 | /// Waits until `duration` has elapsed. 30 | pub fn sleep(duration: Duration) -> Sleep { 31 | let time_future = time_future(duration); 32 | Sleep { 33 | time_future: Box::pin(time_future), 34 | } 35 | } 36 | 37 | /// Future returned by `sleep`. 38 | pub struct Sleep { 39 | time_future: Pin>>, 40 | } 41 | 42 | impl Future for Sleep { 43 | type Output = (); 44 | fn poll( 45 | mut self: Pin<&mut Self>, 46 | cx: &mut Context<'_>, 47 | ) -> Poll { 48 | self.time_future.as_mut().poll(cx) 49 | } 50 | } 51 | 52 | /// Poll a future with a timeout. 53 | /// If the future is ready, return the output. 54 | /// If the future is pending, poll the sleep future. 55 | pub fn timeout(duration: Duration, future: F) -> Timeout 56 | where 57 | F: Future, 58 | { 59 | let time_future = time_future(duration); 60 | Timeout { 61 | future: Box::pin(future), 62 | time_future: Box::pin(time_future), 63 | } 64 | } 65 | 66 | /// Future returned by `timeout`. 67 | pub struct Timeout { 68 | future: Pin>, 69 | time_future: Pin>>, 70 | } 71 | 72 | impl Future for Timeout { 73 | type Output = Result; 74 | fn poll( 75 | mut self: Pin<&mut Self>, 76 | cx: &mut Context<'_>, 77 | ) -> Poll { 78 | // Poll the future first. 79 | // If it's ready, return the output. 80 | // If it's pending, poll the sleep future. 81 | match self.future.as_mut().poll(cx) { 82 | Poll::Ready(output) => Poll::Ready(Ok(output)), 83 | Poll::Pending => match self.time_future.as_mut().poll(cx) { 84 | Poll::Ready(()) => Poll::Ready(Err(Elapsed(()))), 85 | Poll::Pending => Poll::Pending, 86 | }, 87 | } 88 | } 89 | } 90 | 91 | /// Errors returned by `Timeout`. 92 | /// 93 | /// This error is returned when a timeout expires before the function was able 94 | /// to finish. 95 | #[derive(Debug, PartialEq, Eq)] 96 | pub struct Elapsed(()); 97 | 98 | impl Display for Elapsed { 99 | fn fmt(&self, fmt: &mut Formatter<'_>) -> std::fmt::Result { 100 | "deadline has elapsed".fmt(fmt) 101 | } 102 | } 103 | 104 | impl Error for Elapsed {} 105 | 106 | impl From for io::Error { 107 | fn from(_err: Elapsed) -> io::Error { 108 | io::ErrorKind::TimedOut.into() 109 | } 110 | } 111 | 112 | /// Creates a new interval that ticks every `period` duration. 113 | pub fn interval(period: Duration) -> Interval { 114 | let (tx, rx) = local_channel::<()>(); 115 | let period_ms = period.as_millis() as f64; 116 | // Create a closure that sends a tick via the channel. 117 | let closure = Closure::wrap(Box::new(move || { 118 | tx.send(()); 119 | }) as Box); 120 | // Register an interval with the closure. 121 | let interval_id = set_interval(closure.as_ref().unchecked_ref(), period_ms); 122 | // Release memory management of this closure from Rust to the JS GC. 123 | closure.forget(); 124 | Interval { 125 | period, 126 | rx, 127 | interval_id, 128 | } 129 | } 130 | 131 | /// A structure that represents an interval that ticks at a specified period. 132 | /// It provides methods to wait for the next tick, reset the interval, 133 | /// and ensure the interval is cleaned up when it is dropped. 134 | pub struct Interval { 135 | period: Duration, 136 | rx: LocalReceiver<()>, 137 | interval_id: i32, 138 | } 139 | 140 | impl Interval { 141 | /// Waits until the next tick. 142 | pub async fn tick(&mut self) { 143 | self.rx.next().await; 144 | } 145 | 146 | /// Resets the interval, making the next tick occur 147 | /// after the original period. 148 | /// This clears the existing interval and establishes a new one. 149 | pub fn reset(&mut self) { 150 | // Clear the existing interval. 151 | clear_interval(self.interval_id); 152 | // Create a new channel to receive ticks. 153 | let (tx, rx) = local_channel::<()>(); 154 | self.rx = rx; 155 | let period_ms = self.period.as_millis() as f64; 156 | // Set up a new interval. 157 | let closure = Closure::wrap(Box::new(move || { 158 | tx.send(()); 159 | }) as Box); 160 | self.interval_id = 161 | set_interval(closure.as_ref().unchecked_ref(), period_ms); 162 | // Release memory management of this closure from Rust to the JS GC. 163 | closure.forget(); 164 | } 165 | } 166 | 167 | impl Drop for Interval { 168 | fn drop(&mut self) { 169 | clear_interval(self.interval_id); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `tokio_with_wasm` 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/tokio_with_wasm.svg)](https://crates.io/crates/tokio_with_wasm) 4 | [![Documentation](https://docs.rs/tokio_with_wasm/badge.svg)](https://docs.rs/tokio_with_wasm) 5 | [![License](https://img.shields.io/crates/l/tokio_with_wasm.svg)](https://github.com/cunarist/tokio-with-wasm/blob/main/LICENSE) 6 | 7 | ![Recording](https://github.com/cunarist/tokio-with-wasm/assets/66480156/77fa5838-23c7-4e3b-b1ba-61146972c2aa) 8 | 9 | > Tested with [Rinf](https://github.com/cunarist/rinf) 10 | 11 | `tokio_with_wasm` is a Rust library that provides `tokio` specifically designed for web browsers. It aims to provide the exact same `tokio` features for web applications, leveraging JavaScript web API. 12 | 13 | This library is made up of JavaScript glue code that mimics the behavior of real `tokio`. Because `tokio_with_wasm` doesn't have its own runtime and adapts to the JavaScript event loop, advanced features of `tokio` might not work. 14 | 15 | When using `spawn_blocking()`, the number of web workers are automatically adjusted adapting to the number of parallel tasks. Refer to the docs for additional details. 16 | 17 | This library assumes that you're compilng your Rust project with `wasm-pack` and `wasm-bindgen`, which currently uses `wasm32-unknown-unknown` Rust target. Note that this library currently only supports the `web` target of `wasm-bindgen`, not [others](https://rustwasm.github.io/wasm-bindgen/reference/deployment.html) such as `no-modules`. 18 | 19 | ## Features 20 | 21 | - **Familiar API**: If you're familiar with `tokio`, you'll feel right at home with `tokio_with_wasm`. It provides similar functionality and follows the same patterns for spawning and managing asynchronous tasks. 22 | 23 | - **Web Worker Integration**: `tokio_with_wasm` adapts to the JavaScript environment by utilizing web API under the hood. This means you can write Rust code that runs concurrently and efficiently in web applications. 24 | 25 | - **Spawn Async and Blocking Tasks**: You can spawn both asynchronous and blocking tasks. Asynchronous tasks allow you to perform non-blocking operations, while blocking tasks are suitable for compute-heavy or synchronous tasks. 26 | 27 | > Though various IO functionalities can be added in the future, they're not included yet. 28 | 29 | ## Usage 30 | 31 | Add this library to your `Cargo.toml` alongside `tokio`: 32 | 33 | ```toml 34 | [dependencies] 35 | tokio = { version = "0.0.0", features = ["rt"] } 36 | tokio_with_wasm = { version = "0.0.0", features = ["rt"] } 37 | ``` 38 | 39 | Here's a simple example of using `tokio_with_wasm` that works on both native platforms and web browsers: 40 | 41 | ```rust 42 | use tokio::task::{spawn, spawn_blocking, yield_now, JoinSet}; 43 | use tokio::time::{interval, sleep}; 44 | use tokio_with_wasm::alias as tokio; 45 | 46 | #[tokio::main(flavor = "current_thread")] 47 | async fn main() { 48 | let async_join_handle = spawn(async { 49 | // Asynchronous code here. 50 | // This will run concurrently 51 | // in the same web worker(thread). 52 | }); 53 | let blocking_join_handle = spawn_blocking(|| { 54 | // Blocking code here. 55 | // This will run parallelly 56 | // in the external pool of web workers. 57 | }); 58 | let async_result = async_join_handle.await; 59 | let blocking_result = blocking_join_handle.await; 60 | for i in 1..=1000 { 61 | // Some repeating task here 62 | // that shouldn't block the JavaScript runtime. 63 | yield_now().await; 64 | } 65 | } 66 | ``` 67 | 68 | The `use tokio_with_wasm::alias as tokio;` statement is functionally equivalent to the code below. This import is provided for convenience and to allow for shorter code. 69 | 70 | ```rust 71 | #[cfg(all( 72 | target_arch = "wasm32", 73 | target_vendor = "unknown", 74 | target_os = "unknown" 75 | ))] 76 | use tokio_with_wasm as tokio; 77 | 78 | #[cfg(not(all( 79 | target_arch = "wasm32", 80 | target_vendor = "unknown", 81 | target_os = "unknown" 82 | )))] 83 | use tokio; 84 | ``` 85 | 86 | ## Documentation 87 | 88 | API documentation can be found on [docs.rs](https://docs.rs/tokio_with_wasm). 89 | 90 | ## Caution 91 | 92 | Keep in mind that you should NEVER write panicking code. 93 | 94 | On `wasm32-unknown-unknown`, there's currently [no way](https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen_futures/fn.future_to_promise.html#panics) to catch and unwind panics like on native platforms. Panics will eventually lead to leaked JavaScript `Promise`s. 95 | 96 | Stick to the `Result` enum whenever possible. 97 | 98 | ## Building and Deploying 99 | 100 | If you're using Web Workers (threads) by calling `spawn_blocking`, you need to set specific Rust compiler flags. Also, you must use the `nightly` toolchain and include certain Rust standard library components in the compilation. 101 | 102 | - `target-feature` flags 103 | - `+atomics` 104 | - `+bulk-memory` 105 | - `+mutable-globals` 106 | - `build-std` components 107 | - `std` 108 | - `panic_abort` 109 | 110 | Here's a full example command: 111 | 112 | ```shell 113 | export RUSTFLAGS="-C target-feature=+atomics,+bulk-memory,+mutable-globals" 114 | export RUSTUP_TOOLCHAIN="nightly" 115 | wasm-pack build --target web -- -Z build-std=std,panic_abort 116 | ``` 117 | 118 | After building your webassembly module and preparing it for deployment, ensure that your web server is configured to include cross-origin-related HTTP headers in its responses. These headers enable clients using your website to gain access to `SharedArrayBuffer` web API, which is something similar to shared memory on the web. 119 | 120 | - [`Cross-Origin-Opener-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy): `same-origin` 121 | - [`Cross-Origin-Embedder-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy): `require-corp` 122 | 123 | Additionally, don't forget to specify the MIME type `application/wasm` for `.wasm` files within the server configurations to ensure optimal performance. 124 | 125 | ## Why This is Needed 126 | 127 | The web has many restrictions due to its sandboxed environment which prevents the use of threads, time, file IO, network IO, and many other native functionalities. Consequently, certain features are missing from Rust's `std` due to these limitations. That's why `tokio` doesn't really work well on web browsers. 128 | 129 | To address this issue, this crate offers `tokio` modules with the **same names** as the original native ones, providing workarounds for these constraints. 130 | 131 | ## Future Vision 132 | 133 | Because a large portion of Rust's web ecosystem is based on `wasm32-unknown-unknown` right now, we had to make an alias crate of `tokio` to use its functionalities directly on the web. 134 | 135 | Hopefully, when `wasm32-wasi` becomes the mainstream Rust target for the web, [`jco`](https://github.com/bytecodealliance/jco) might be an alternative to `wasm-bindgen` as it can provide full `std` functionalities with browser shims (polyfills). However, this will take time because the [`wasi-threads`](https://github.com/WebAssembly/wasi-threads) proposal still has a long way to go. 136 | 137 | Until that time, there's `tokio_with_wasm`! 138 | 139 | ## Contribution Guide 140 | 141 | Contributions are always welcome! If you have any suggestions, bug reports, or want to contribute to the development of `tokio_with_wasm`, please open an issue or submit a pull request. 142 | 143 | There are situations where you cannot use native Rust code directly on the web. This is because `wasm32-unknown-unknown` Rust target used by `wasm-bindgen` doesn't have a full `std` module. Refer to the links below to understand how to interact with JavaScript with `wasm-bindgen`. 144 | 145 | - https://rustwasm.github.io/wasm-bindgen/reference/attributes/on-js-imports/js_name.html 146 | - https://rustwasm.github.io/wasm-bindgen/reference/attributes/on-js-imports/js_namespace.html 147 | 148 | It is possible for rust code to be called in a **web worker**. Therefore, we cannot access the global `window` JavaScript object 149 | just like when you work in the main thread of JavaScript. Refer to the link below to check which web APIs are available in a web worker. 150 | You'll be surprised by various capabilities that modern JavaScript has. 151 | 152 | - https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Functions_and_classes_available_to_workers 153 | 154 | Please note that this library uses a quite hacky and naive approach to mimic native `tokio` functionalities. That's because this library is regarded as a temporary solution for the period before `wasm32-wasi`. Any kind of PR is possible, as long as it makes things just work on the web. 155 | -------------------------------------------------------------------------------- /package/README.md: -------------------------------------------------------------------------------- 1 | # `tokio_with_wasm` 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/tokio_with_wasm.svg)](https://crates.io/crates/tokio_with_wasm) 4 | [![Documentation](https://docs.rs/tokio_with_wasm/badge.svg)](https://docs.rs/tokio_with_wasm) 5 | [![License](https://img.shields.io/crates/l/tokio_with_wasm.svg)](https://github.com/cunarist/tokio-with-wasm/blob/main/LICENSE) 6 | 7 | ![Recording](https://github.com/cunarist/tokio-with-wasm/assets/66480156/77fa5838-23c7-4e3b-b1ba-61146972c2aa) 8 | 9 | > Tested with [Rinf](https://github.com/cunarist/rinf) 10 | 11 | `tokio_with_wasm` is a Rust library that provides `tokio` specifically designed for web browsers. It aims to provide the exact same `tokio` features for web applications, leveraging JavaScript web API. 12 | 13 | This library is made up of JavaScript glue code that mimics the behavior of real `tokio`. Because `tokio_with_wasm` doesn't have its own runtime and adapts to the JavaScript event loop, advanced features of `tokio` might not work. 14 | 15 | When using `spawn_blocking()`, the number of web workers are automatically adjusted adapting to the number of parallel tasks. Refer to the docs for additional details. 16 | 17 | This library assumes that you're compilng your Rust project with `wasm-pack` and `wasm-bindgen`, which currently uses `wasm32-unknown-unknown` Rust target. Note that this library currently only supports the `web` target of `wasm-bindgen`, not [others](https://rustwasm.github.io/wasm-bindgen/reference/deployment.html) such as `no-modules`. 18 | 19 | ## Features 20 | 21 | - **Familiar API**: If you're familiar with `tokio`, you'll feel right at home with `tokio_with_wasm`. It provides similar functionality and follows the same patterns for spawning and managing asynchronous tasks. 22 | 23 | - **Web Worker Integration**: `tokio_with_wasm` adapts to the JavaScript environment by utilizing web API under the hood. This means you can write Rust code that runs concurrently and efficiently in web applications. 24 | 25 | - **Spawn Async and Blocking Tasks**: You can spawn both asynchronous and blocking tasks. Asynchronous tasks allow you to perform non-blocking operations, while blocking tasks are suitable for compute-heavy or synchronous tasks. 26 | 27 | > Though various IO functionalities can be added in the future, they're not included yet. 28 | 29 | ## Usage 30 | 31 | Add this library to your `Cargo.toml` alongside `tokio`: 32 | 33 | ```toml 34 | [dependencies] 35 | tokio = { version = "0.0.0", features = ["rt"] } 36 | tokio_with_wasm = { version = "0.0.0", features = ["rt"] } 37 | ``` 38 | 39 | Here's a simple example of using `tokio_with_wasm` that works on both native platforms and web browsers: 40 | 41 | ```rust 42 | use tokio::task::{spawn, spawn_blocking, yield_now, JoinSet}; 43 | use tokio::time::{interval, sleep}; 44 | use tokio_with_wasm::alias as tokio; 45 | 46 | #[tokio::main(flavor = "current_thread")] 47 | async fn main() { 48 | let async_join_handle = spawn(async { 49 | // Asynchronous code here. 50 | // This will run concurrently 51 | // in the same web worker(thread). 52 | }); 53 | let blocking_join_handle = spawn_blocking(|| { 54 | // Blocking code here. 55 | // This will run parallelly 56 | // in the external pool of web workers. 57 | }); 58 | let async_result = async_join_handle.await; 59 | let blocking_result = blocking_join_handle.await; 60 | for i in 1..=1000 { 61 | // Some repeating task here 62 | // that shouldn't block the JavaScript runtime. 63 | yield_now().await; 64 | } 65 | } 66 | ``` 67 | 68 | The `use tokio_with_wasm::alias as tokio;` statement is functionally equivalent to the code below. This import is provided for convenience and to allow for shorter code. 69 | 70 | ```rust 71 | #[cfg(all( 72 | target_arch = "wasm32", 73 | target_vendor = "unknown", 74 | target_os = "unknown" 75 | ))] 76 | use tokio_with_wasm as tokio; 77 | 78 | #[cfg(not(all( 79 | target_arch = "wasm32", 80 | target_vendor = "unknown", 81 | target_os = "unknown" 82 | )))] 83 | use tokio; 84 | ``` 85 | 86 | ## Documentation 87 | 88 | API documentation can be found on [docs.rs](https://docs.rs/tokio_with_wasm). 89 | 90 | ## Caution 91 | 92 | Keep in mind that you should NEVER write panicking code. 93 | 94 | On `wasm32-unknown-unknown`, there's currently [no way](https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen_futures/fn.future_to_promise.html#panics) to catch and unwind panics like on native platforms. Panics will eventually lead to leaked JavaScript `Promise`s. 95 | 96 | Stick to the `Result` enum whenever possible. 97 | 98 | ## Building and Deploying 99 | 100 | If you're using Web Workers (threads) by calling `spawn_blocking`, you need to set specific Rust compiler flags. Also, you must use the `nightly` toolchain and include certain Rust standard library components in the compilation. 101 | 102 | - `target-feature` flags 103 | - `+atomics` 104 | - `+bulk-memory` 105 | - `+mutable-globals` 106 | - `build-std` components 107 | - `std` 108 | - `panic_abort` 109 | 110 | Here's a full example command: 111 | 112 | ```shell 113 | export RUSTFLAGS="-C target-feature=+atomics,+bulk-memory,+mutable-globals" 114 | export RUSTUP_TOOLCHAIN="nightly" 115 | wasm-pack build --target web -- -Z build-std=std,panic_abort 116 | ``` 117 | 118 | After building your webassembly module and preparing it for deployment, ensure that your web server is configured to include cross-origin-related HTTP headers in its responses. These headers enable clients using your website to gain access to `SharedArrayBuffer` web API, which is something similar to shared memory on the web. 119 | 120 | - [`Cross-Origin-Opener-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy): `same-origin` 121 | - [`Cross-Origin-Embedder-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy): `require-corp` 122 | 123 | Additionally, don't forget to specify the MIME type `application/wasm` for `.wasm` files within the server configurations to ensure optimal performance. 124 | 125 | ## Why This is Needed 126 | 127 | The web has many restrictions due to its sandboxed environment which prevents the use of threads, time, file IO, network IO, and many other native functionalities. Consequently, certain features are missing from Rust's `std` due to these limitations. That's why `tokio` doesn't really work well on web browsers. 128 | 129 | To address this issue, this crate offers `tokio` modules with the **same names** as the original native ones, providing workarounds for these constraints. 130 | 131 | ## Future Vision 132 | 133 | Because a large portion of Rust's web ecosystem is based on `wasm32-unknown-unknown` right now, we had to make an alias crate of `tokio` to use its functionalities directly on the web. 134 | 135 | Hopefully, when `wasm32-wasi` becomes the mainstream Rust target for the web, [`jco`](https://github.com/bytecodealliance/jco) might be an alternative to `wasm-bindgen` as it can provide full `std` functionalities with browser shims (polyfills). However, this will take time because the [`wasi-threads`](https://github.com/WebAssembly/wasi-threads) proposal still has a long way to go. 136 | 137 | Until that time, there's `tokio_with_wasm`! 138 | 139 | ## Contribution Guide 140 | 141 | Contributions are always welcome! If you have any suggestions, bug reports, or want to contribute to the development of `tokio_with_wasm`, please open an issue or submit a pull request. 142 | 143 | There are situations where you cannot use native Rust code directly on the web. This is because `wasm32-unknown-unknown` Rust target used by `wasm-bindgen` doesn't have a full `std` module. Refer to the links below to understand how to interact with JavaScript with `wasm-bindgen`. 144 | 145 | - https://rustwasm.github.io/wasm-bindgen/reference/attributes/on-js-imports/js_name.html 146 | - https://rustwasm.github.io/wasm-bindgen/reference/attributes/on-js-imports/js_namespace.html 147 | 148 | It is possible for rust code to be called in a **web worker**. Therefore, we cannot access the global `window` JavaScript object 149 | just like when you work in the main thread of JavaScript. Refer to the link below to check which web APIs are available in a web worker. 150 | You'll be surprised by various capabilities that modern JavaScript has. 151 | 152 | - https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Functions_and_classes_available_to_workers 153 | 154 | Please note that this library uses a quite hacky and naive approach to mimic native `tokio` functionalities. That's because this library is regarded as a temporary solution for the period before `wasm32-wasi`. Any kind of PR is possible, as long as it makes things just work on the web. 155 | -------------------------------------------------------------------------------- /package/src/glue/task/pool.rs: -------------------------------------------------------------------------------- 1 | use crate::{BLOCKING_KEY, LogError, now}; 2 | use js_sys::{Array, JsString, Object, Reflect, eval, global}; 3 | use std::cell::RefCell; 4 | use std::collections::VecDeque; 5 | use std::rc::Rc; 6 | use wasm_bindgen::prelude::{Closure, JsCast, JsValue, wasm_bindgen}; 7 | use wasm_bindgen::{memory, module}; 8 | use web_sys::{ 9 | Blob, BlobPropertyBag, DedicatedWorkerGlobalScope, ErrorEvent, Event, 10 | MessageEvent, Url, Worker, WorkerOptions, WorkerType, 11 | }; 12 | 13 | pub static MAX_WORKERS: usize = 512; 14 | 15 | pub struct WorkerPool { 16 | pool_state: Rc, 17 | } 18 | 19 | struct PoolState { 20 | total_workers_count: RefCell, 21 | idle_workers: RefCell>, 22 | queued_tasks: RefCell>, 23 | callback: Closure, 24 | } 25 | 26 | struct ManagedWorker { 27 | deactivated_time: RefCell, // Timestamp in milliseconds 28 | worker: Worker, 29 | } 30 | 31 | struct Task { 32 | callable: Box, 33 | } 34 | 35 | impl Default for WorkerPool { 36 | fn default() -> Self { 37 | WorkerPool { 38 | pool_state: Rc::new(PoolState { 39 | total_workers_count: RefCell::new(0), 40 | idle_workers: RefCell::new(Vec::with_capacity(MAX_WORKERS)), 41 | queued_tasks: RefCell::new(VecDeque::new()), 42 | callback: Closure::new(|event: Event| { 43 | JsValue::from_str(&format!("{event:?}")).log_error("POOL_CALLBACK"); 44 | }), 45 | }), 46 | } 47 | } 48 | } 49 | 50 | impl WorkerPool { 51 | /// Creates a new `WorkerPool` which immediately creates `initial` workers. 52 | /// 53 | /// The pool created here can be used over a long period of time, and it 54 | /// will be initially primed with `initial` workers. Currently workers are 55 | /// never released or gc'd until the whole pool is destroyed. 56 | /// 57 | /// # Errors 58 | /// 59 | /// Returns any error that may happen while a JS web worker is created and a 60 | /// message is sent to it. 61 | pub fn new() -> WorkerPool { 62 | WorkerPool::default() 63 | } 64 | 65 | /// Unconditionally spawns a new worker 66 | /// 67 | /// The worker isn't registered with this `WorkerPool` but is capable of 68 | /// executing work for this wasm module. 69 | /// 70 | /// # Errors 71 | /// 72 | /// Returns any error that may happen while a JS web worker is created and a 73 | /// message is sent to it. 74 | fn create_worker(&self) -> Result { 75 | *self.pool_state.total_workers_count.borrow_mut() += 1; 76 | let script = format!( 77 | " 78 | import init, * as wasmBindings from '{}'; 79 | globalThis.wasmBindings = wasmBindings; 80 | globalThis.{BLOCKING_KEY} = true; 81 | self.onmessage = event => {{ 82 | let initialised = init(event.data).catch(err => {{ 83 | // Propagate to main `onerror`: 84 | setTimeout(() => {{ 85 | throw err; 86 | }}); 87 | // Rethrow to keep promise rejected 88 | // and prevent execution of further commands: 89 | throw err; 90 | }}); 91 | 92 | self.onmessage = async event => {{ 93 | // This will queue further commands up 94 | // until the module is fully initialised: 95 | await initialised; 96 | wasmBindings.task_worker_entry_point(event.data); 97 | }}; 98 | }}; 99 | ", 100 | get_script_path()? 101 | ); 102 | let blob_property_bag = BlobPropertyBag::new(); 103 | blob_property_bag.set_type("text/javascript"); 104 | let blob = Blob::new_with_blob_sequence_and_options( 105 | &Array::from_iter([JsValue::from(script)]).into(), 106 | &blob_property_bag, 107 | )?; 108 | let url = Url::create_object_url_with_blob(&blob)?; 109 | let options = WorkerOptions::new(); 110 | options.set_type(WorkerType::Module); 111 | let worker = Worker::new_with_options(&url, &options)?; 112 | 113 | // With a worker spun up send it the module/memory so it can start 114 | // instantiating the wasm module. Later it might receive further 115 | // messages about code to run on the wasm module. 116 | let worker_init = Object::new(); 117 | Reflect::set(&worker_init, &JsString::from("module_or_path"), &module())?; 118 | Reflect::set(&worker_init, &JsString::from("memory"), &memory())?; 119 | worker.post_message(&worker_init)?; 120 | 121 | Ok(worker) 122 | } 123 | 124 | /// Fetches a worker from this pool, creating one if necessary. 125 | /// 126 | /// This will attempt to pull an already-spawned web worker from our cache 127 | /// if one is available, otherwise it will spawn a new worker and return the 128 | /// newly spawned worker. 129 | /// 130 | /// # Errors 131 | /// 132 | /// Returns any error that may happen while a JS web worker is created and a 133 | /// message is sent to it. 134 | fn get_worker(&self) -> Result { 135 | match self.pool_state.idle_workers.borrow_mut().pop() { 136 | Some(managed_worker) => Ok(managed_worker.worker), 137 | None => self.create_worker(), 138 | } 139 | } 140 | 141 | /// Executes the work `f` in a web worker, spawning a web worker if 142 | /// necessary. 143 | /// 144 | /// This will acquire a web worker and then send the closure `f` to the 145 | /// worker to execute. The worker won't be usable for anything else while 146 | /// `f` is executing, and no callbacks are registered for when the worker 147 | /// finishes. 148 | /// 149 | /// # Errors 150 | /// 151 | /// Returns any error that may happen while a JS web worker is created and a 152 | /// message is sent to it. 153 | fn execute(&self, task: Task) -> Result { 154 | let worker = self.get_worker()?; 155 | let work = Box::new(task); 156 | let ptr = Box::into_raw(work); 157 | match worker.post_message(&JsValue::from(ptr as u32)) { 158 | Ok(()) => Ok(worker), 159 | Err(error) => { 160 | unsafe { 161 | drop(Box::from_raw(ptr)); 162 | } 163 | Err(error) 164 | } 165 | } 166 | } 167 | 168 | /// Configures an `onmessage` callback for the `worker` specified for the 169 | /// web worker to be reclaimed and re-inserted into this pool when a message 170 | /// is received. 171 | /// 172 | /// Currently this `WorkerPool` abstraction is intended to execute one-off 173 | /// style work where the work itself doesn't send any notifications and 174 | /// whatn it's done the worker is ready to execute more work. This method is 175 | /// used for all spawned workers to ensure that when the work is finished 176 | /// the worker is reclaimed back into this pool. 177 | fn reclaim_on_message(&self, worker: Worker) { 178 | let pool_state = Rc::downgrade(&self.pool_state); 179 | let worker2 = worker.clone(); 180 | let reclaim_slot = Rc::new(RefCell::new(None)); 181 | let slot2 = reclaim_slot.clone(); 182 | let reclaim = Closure::::new(move |event: Event| { 183 | if let Some(error) = event.dyn_ref::() { 184 | JsValue::from_str(&error.message()).log_error("RECLAIM_EVENT"); 185 | // TODO: this probably leaks memory somehow? It's sort of 186 | // unclear what to do about errors in workers right now. 187 | return; 188 | } 189 | 190 | // If this is a completion event then can deallocate our own 191 | // callback by clearing out `slot2` which contains our own closure. 192 | if let Some(_msg) = event.dyn_ref::() { 193 | if let Some(pool_state) = pool_state.upgrade() { 194 | pool_state.push_worker(worker2.clone()); 195 | } 196 | *slot2.borrow_mut() = None; 197 | return; 198 | } 199 | 200 | // Unhandled worker event exists. 201 | JsValue::from_str(&format!("{event:?}")).log_error("UNHANDLED_RECLAIM"); 202 | }); 203 | worker.set_onmessage(Some(reclaim.as_ref().unchecked_ref())); 204 | *reclaim_slot.borrow_mut() = Some(reclaim); 205 | } 206 | } 207 | 208 | impl WorkerPool { 209 | /// Executes `f` in a web worker. 210 | /// 211 | /// This pool manages a set of web workers to draw from, and `f` will be 212 | /// spawned quickly into one if the worker is idle. If no idle workers are 213 | /// available then a new web worker will be spawned. 214 | /// 215 | /// Once `f` returns the worker assigned to `f` is automatically reclaimed 216 | /// by this `WorkerPool`. This method provides no method of learning when 217 | /// `f` completes, and for that you'll need to use `run_notify`. 218 | /// 219 | /// # Errors 220 | /// 221 | /// If an error happens while spawning a web worker or sending a message to 222 | /// a web worker, that error is returned. 223 | fn run(&self, task: Task) -> Result<(), JsValue> { 224 | let worker = self.execute(task)?; 225 | self.reclaim_on_message(worker); 226 | Ok(()) 227 | } 228 | 229 | pub fn remove_inactive_workers(&self) { 230 | let mut idle_workers = self.pool_state.idle_workers.borrow_mut(); 231 | let current_timestamp = now(); 232 | idle_workers.retain(|managed_worker| { 233 | let deactivated_time = *managed_worker.deactivated_time.borrow(); 234 | let passed_time = current_timestamp - deactivated_time; 235 | let is_active = passed_time < 10000.0; // 10 seconds 236 | if !is_active { 237 | managed_worker.worker.terminate(); 238 | *self.pool_state.total_workers_count.borrow_mut() -= 1; 239 | } 240 | is_active 241 | }); 242 | } 243 | 244 | pub fn flush_queued_tasks(&self) { 245 | while *self.pool_state.total_workers_count.borrow() < MAX_WORKERS { 246 | let mut queued_tasks = self.pool_state.queued_tasks.borrow_mut(); 247 | let queued_task = match queued_tasks.pop_front() { 248 | Some(inner) => inner, 249 | None => break, 250 | }; 251 | self.run(queued_task).log_error("FLUSH_QUEUED_TASKS"); 252 | } 253 | } 254 | 255 | pub fn queue_task(&self, callable: impl FnOnce() + Send + 'static) { 256 | let mut queued_tasks = self.pool_state.queued_tasks.borrow_mut(); 257 | queued_tasks.push_back(Task { 258 | callable: Box::new(callable), 259 | }); 260 | drop(queued_tasks); 261 | self.flush_queued_tasks(); 262 | } 263 | } 264 | 265 | impl PoolState { 266 | fn push_worker(&self, worker: Worker) { 267 | worker.set_onmessage(Some(self.callback.as_ref().unchecked_ref())); 268 | worker.set_onerror(Some(self.callback.as_ref().unchecked_ref())); 269 | let mut workers = self.idle_workers.borrow_mut(); 270 | for prev in workers.iter() { 271 | let prev: &JsValue = &prev.worker; 272 | let worker: &JsValue = &worker; 273 | assert!(prev != worker); 274 | } 275 | workers.push(ManagedWorker { 276 | deactivated_time: RefCell::new(now()), 277 | worker, 278 | }); 279 | } 280 | } 281 | 282 | /// Entry point invoked by JavaScript in a worker. 283 | #[wasm_bindgen] 284 | pub fn task_worker_entry_point(ptr: u32) -> Result<(), JsValue> { 285 | let ptr = unsafe { Box::from_raw(ptr as *mut Task) }; 286 | let global = global().unchecked_into::(); 287 | (ptr.callable)(); 288 | global.post_message(&JsValue::undefined())?; 289 | Ok(()) 290 | } 291 | 292 | pub fn get_script_path() -> Result { 293 | let string = eval( 294 | r" 295 | (() => { 296 | try { 297 | throw new Error(); 298 | } catch (e) { 299 | let parts = e.stack.match(/(?:\(|@)(\S+):\d+:\d+/); 300 | return parts[1]; 301 | } 302 | })() 303 | ", 304 | )? 305 | .as_string() 306 | .ok_or(JsValue::from( 307 | "Could not convert JS string path to native string", 308 | ))?; 309 | Ok(string) 310 | } 311 | -------------------------------------------------------------------------------- /package/src/glue/task/join_set.rs: -------------------------------------------------------------------------------- 1 | //! A collection of tasks spawned in JavaScript runtime. 2 | //! 3 | //! This module provides the [`JoinSet`] type, a collection which stores a set 4 | //! of spawned tasks and allows asynchronously awaiting the output of those 5 | //! tasks as they complete. See the documentation for the [`JoinSet`] type for 6 | //! details. 7 | use crate::{ 8 | AbortHandle, JoinError, JoinHandle, noop_waker, spawn, spawn_blocking, 9 | }; 10 | use std::collections::VecDeque; 11 | use std::fmt::{Debug, Formatter}; 12 | use std::future::Future; 13 | use std::pin::Pin; 14 | use std::task::{Context, Poll}; 15 | 16 | /// A collection of tasks spawned in JavaScript. 17 | /// 18 | /// A `JoinSet` can be used to await the completion of some or all of the tasks 19 | /// in the set. The set is not guaranteed to be ordered, 20 | /// and the tasks will be returned in the order they complete. 21 | /// 22 | /// All of the tasks must have the same return type `T`. 23 | /// 24 | /// When the `JoinSet` is dropped, all tasks in the `JoinSet` are immediately aborted. 25 | /// 26 | /// # Examples 27 | /// 28 | /// Spawn multiple tasks and wait for them. 29 | /// 30 | /// ``` 31 | /// use tokio::task::JoinSet; 32 | /// 33 | /// #[tokio::main] 34 | /// async fn main() { 35 | /// let mut set = JoinSet::new(); 36 | /// 37 | /// for i in 0..10 { 38 | /// set.spawn(async move { i }); 39 | /// } 40 | /// 41 | /// let mut seen = [false; 10]; 42 | /// while let Some(res) = set.join_next().await { 43 | /// let idx = res.unwrap(); 44 | /// seen[idx] = true; 45 | /// } 46 | /// 47 | /// for i in 0..10 { 48 | /// assert!(seen[i]); 49 | /// } 50 | /// } 51 | /// ``` 52 | pub struct JoinSet { 53 | inner: VecDeque>, 54 | } 55 | 56 | impl JoinSet { 57 | /// Create a new `JoinSet`. 58 | pub fn new() -> Self { 59 | Self { 60 | inner: VecDeque::new(), 61 | } 62 | } 63 | 64 | /// Returns the number of tasks currently in the `JoinSet`. 65 | pub fn len(&self) -> usize { 66 | self.inner.len() 67 | } 68 | 69 | /// Returns whether the `JoinSet` is empty. 70 | pub fn is_empty(&self) -> bool { 71 | self.inner.is_empty() 72 | } 73 | } 74 | 75 | impl JoinSet { 76 | /// Spawn the provided task on the `JoinSet`, returning an [`AbortHandle`] 77 | /// that can be used to remotely cancel the task. 78 | /// 79 | /// The provided future will start running in the background immediately 80 | /// when this method is called, even if you don't await anything on this 81 | /// `JoinSet`. 82 | /// 83 | /// [`AbortHandle`]: crate::task::AbortHandle 84 | #[track_caller] 85 | pub fn spawn(&mut self, task: F) -> AbortHandle 86 | where 87 | F: Future, 88 | F: 'static, 89 | { 90 | let join_handle = spawn(task); 91 | let abort_handle = join_handle.abort_handle(); 92 | self.inner.push_back(join_handle); 93 | abort_handle 94 | } 95 | 96 | /// Spawn the blocking code on the blocking threadpool and store 97 | /// it in this `JoinSet`, returning an [`AbortHandle`] that can be 98 | /// used to remotely cancel the task. 99 | /// 100 | /// # Examples 101 | /// 102 | /// Spawn multiple blocking tasks and wait for them. 103 | /// 104 | /// ``` 105 | /// use tokio::task::JoinSet; 106 | /// 107 | /// #[tokio::main] 108 | /// async fn main() { 109 | /// let mut set = JoinSet::new(); 110 | /// 111 | /// for i in 0..10 { 112 | /// set.spawn_blocking(move || { i }); 113 | /// } 114 | /// 115 | /// let mut seen = [false; 10]; 116 | /// while let Some(res) = set.join_next().await { 117 | /// let idx = res.unwrap(); 118 | /// seen[idx] = true; 119 | /// } 120 | /// 121 | /// for i in 0..10 { 122 | /// assert!(seen[i]); 123 | /// } 124 | /// } 125 | /// ``` 126 | /// 127 | /// [`AbortHandle`]: crate::task::AbortHandle 128 | #[track_caller] 129 | pub fn spawn_blocking(&mut self, f: F) -> AbortHandle 130 | where 131 | F: FnOnce() -> T, 132 | F: Send + 'static, 133 | T: Send, 134 | { 135 | let join_handle = spawn_blocking(f); 136 | let abort_handle = join_handle.abort_handle(); 137 | self.inner.push_back(join_handle); 138 | abort_handle 139 | } 140 | 141 | /// Waits until one of the tasks in the set completes and returns its output. 142 | /// 143 | /// Returns `None` if the set is empty. 144 | /// 145 | /// # Cancel Safety 146 | /// 147 | /// This method is cancel safe. If `join_next` is used as the event in a `tokio::select!` 148 | /// statement and some other branch completes first, it is guaranteed that no tasks were 149 | /// removed from this `JoinSet`. 150 | pub async fn join_next(&mut self) -> Option> { 151 | std::future::poll_fn(|cx| self.poll_join_next(cx)).await 152 | } 153 | 154 | /// Tries to join one of the tasks in the set that has completed and return its output. 155 | /// 156 | /// Returns `None` if there are no completed tasks, or if the set is empty. 157 | pub fn try_join_next(&mut self) -> Option> { 158 | // Get the number of `JoinHandle`s. 159 | let handle_count = self.inner.len(); 160 | if handle_count == 0 { 161 | return None; 162 | } 163 | 164 | // Create an async context with a waker that does nothing. 165 | let waker = noop_waker(); 166 | let mut cx = Context::from_waker(&waker); 167 | 168 | // Loop over all `JoinHandle`s to find one that's ready. 169 | for _ in 0..handle_count { 170 | let mut handle = match self.inner.pop_front() { 171 | Some(inner) => inner, 172 | None => continue, // Logically never none 173 | }; 174 | let polled = Pin::new(&mut handle).poll(&mut cx); 175 | if let Poll::Ready(result) = polled { 176 | return Some(result); 177 | } 178 | self.inner.push_back(handle); 179 | } 180 | 181 | None 182 | } 183 | 184 | /// Aborts all tasks and waits for them to finish shutting down. 185 | /// 186 | /// Calling this method is equivalent to calling [`abort_all`] and then calling [`join_next`] in 187 | /// a loop until it returns `None`. 188 | /// 189 | /// This method ignores any panics in the tasks shutting down. When this call returns, the 190 | /// `JoinSet` will be empty. 191 | /// 192 | /// [`abort_all`]: fn@Self::abort_all 193 | /// [`join_next`]: fn@Self::join_next 194 | pub async fn shutdown(&mut self) { 195 | self.abort_all(); 196 | while self.join_next().await.is_some() {} 197 | } 198 | 199 | /// Awaits the completion of all tasks in this `JoinSet`, returning a vector of their results. 200 | /// 201 | /// The results will be stored in the order they completed not the order they were spawned. 202 | /// This is a convenience method that is equivalent to calling [`join_next`] in 203 | /// a loop. If any tasks on the `JoinSet` fail with an [`JoinError`], then this call 204 | /// to `join_all` will panic and all remaining tasks on the `JoinSet` are 205 | /// cancelled. To handle errors in any other way, manually call [`join_next`] 206 | /// in a loop. 207 | /// 208 | /// # Examples 209 | /// 210 | /// Spawn multiple tasks and `join_all` them. 211 | /// 212 | /// ``` 213 | /// use tokio::task::JoinSet; 214 | /// use std::time::Duration; 215 | /// 216 | /// #[tokio::main] 217 | /// async fn main() { 218 | /// let mut set = JoinSet::new(); 219 | /// 220 | /// for i in 0..3 { 221 | /// set.spawn(async move { 222 | /// tokio::time::sleep(Duration::from_secs(3 - i)).await; 223 | /// i 224 | /// }); 225 | /// } 226 | /// 227 | /// let output = set.join_all().await; 228 | /// assert_eq!(output, vec![2, 1, 0]); 229 | /// } 230 | /// ``` 231 | /// 232 | /// Equivalent implementation of `join_all`, using [`join_next`] and loop. 233 | /// 234 | /// ``` 235 | /// use tokio::task::JoinSet; 236 | /// use std::panic; 237 | /// 238 | /// #[tokio::main] 239 | /// async fn main() { 240 | /// let mut set = JoinSet::new(); 241 | /// 242 | /// for i in 0..3 { 243 | /// set.spawn(async move {i}); 244 | /// } 245 | /// 246 | /// let mut output = Vec::new(); 247 | /// while let Some(res) = set.join_next().await{ 248 | /// match res { 249 | /// Ok(t) => output.push(t), 250 | /// Err(_) => (), 251 | /// } 252 | /// } 253 | /// assert_eq!(output.len(),3); 254 | /// } 255 | /// ``` 256 | /// [`join_next`]: fn@Self::join_next 257 | /// [`JoinError::id`]: fn@crate::task::JoinError::id 258 | pub async fn join_all(mut self) -> Vec { 259 | let mut output = Vec::with_capacity(self.len()); 260 | 261 | while let Some(res) = self.join_next().await { 262 | if let Ok(t) = res { 263 | output.push(t) 264 | } 265 | } 266 | output 267 | } 268 | 269 | /// Aborts all tasks on this `JoinSet`. 270 | /// 271 | /// This does not remove the tasks from the `JoinSet`. To wait for the tasks to complete 272 | /// cancellation, you should call `join_next` in a loop until the `JoinSet` is empty. 273 | pub fn abort_all(&mut self) { 274 | self.inner.iter().for_each(|jh| jh.abort()); 275 | } 276 | 277 | /// Removes all tasks from this `JoinSet` without aborting them. 278 | /// 279 | /// The tasks removed by this call will continue to run in the background even if the `JoinSet` 280 | /// is dropped. 281 | pub fn detach_all(&mut self) { 282 | self.inner.clear(); 283 | } 284 | 285 | /// Polls for one of the tasks in the set to complete. 286 | /// 287 | /// If this returns `Poll::Ready(Some(_))`, then the task that completed is removed from the set. 288 | /// 289 | /// When the method returns `Poll::Pending`, the `Waker` in the provided `Context` is scheduled 290 | /// to receive a wakeup when a task in the `JoinSet` completes. Note that on multiple calls to 291 | /// `poll_join_next`, only the `Waker` from the `Context` passed to the most recent call is 292 | /// scheduled to receive a wakeup. 293 | /// 294 | /// # Returns 295 | /// 296 | /// This function returns: 297 | /// 298 | /// * `Poll::Pending` if the `JoinSet` is not empty but there is no task whose output is 299 | /// available right now. 300 | /// * `Poll::Ready(Some(Ok(value)))` if one of the tasks in this `JoinSet` has completed. 301 | /// The `value` is the return value of one of the tasks that completed. 302 | /// * `Poll::Ready(Some(Err(err)))` if one of the tasks in this `JoinSet` has panicked or been 303 | /// aborted. The `err` is the `JoinError` from the panicked/aborted task. 304 | /// * `Poll::Ready(None)` if the `JoinSet` is empty. 305 | /// 306 | /// Note that this method may return `Poll::Pending` even if one of the tasks has completed. 307 | /// This can happen if the [coop budget] is reached. 308 | /// 309 | /// [coop budget]: crate::task#cooperative-scheduling 310 | pub fn poll_join_next( 311 | &mut self, 312 | cx: &mut Context<'_>, 313 | ) -> Poll>> { 314 | // Get the number of `JoinHandle`s. 315 | let handle_count = self.inner.len(); 316 | if handle_count == 0 { 317 | return Poll::Ready(None); 318 | } 319 | 320 | // Loop over all `JoinHandle`s to find one that's ready. 321 | for _ in 0..handle_count { 322 | let mut handle = match self.inner.pop_front() { 323 | Some(inner) => inner, 324 | None => continue, // Logically never none 325 | }; 326 | let polled = Pin::new(&mut handle).poll(cx); 327 | if let Poll::Ready(result) = polled { 328 | return Poll::Ready(Some(result)); 329 | } 330 | self.inner.push_back(handle); 331 | } 332 | 333 | Poll::Pending 334 | } 335 | } 336 | 337 | impl Drop for JoinSet { 338 | fn drop(&mut self) { 339 | self 340 | .inner 341 | .iter() 342 | .for_each(|join_handle| join_handle.abort()); 343 | } 344 | } 345 | 346 | impl Debug for JoinSet { 347 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 348 | f.debug_struct("JoinSet").field("len", &self.len()).finish() 349 | } 350 | } 351 | 352 | impl Default for JoinSet { 353 | fn default() -> Self { 354 | Self::new() 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /package/src/glue/task/mod.rs: -------------------------------------------------------------------------------- 1 | //! Asynchronous green-threads. 2 | //! 3 | //! Resembling the familiar `tokio::task` patterns. 4 | //! this module leverages web workers to execute tasks in parallel, 5 | //! making it ideal for high-performance web applications. 6 | 7 | mod join_set; 8 | mod pool; 9 | 10 | pub use join_set::*; 11 | use wasm_bindgen::prelude::JsValue; 12 | 13 | use crate::{ 14 | LogError, OnceReceiver, OnceSender, SelectFuture, is_main_thread, 15 | once_channel, set_timeout, 16 | }; 17 | use js_sys::Promise; 18 | use pool::WorkerPool; 19 | use std::error::Error; 20 | use std::fmt::{Debug, Display, Formatter}; 21 | use std::future::Future; 22 | use std::pin::Pin; 23 | use std::task::{Context, Poll}; 24 | use wasm_bindgen_futures::{JsFuture, spawn_local}; 25 | 26 | thread_local! { 27 | static WORKER_POOL: WorkerPool = { 28 | let worker_pool = WorkerPool::new(); 29 | spawn_local(manage_pool()); 30 | worker_pool 31 | } 32 | } 33 | 34 | /// Manages the worker pool by periodically checking for 35 | /// inactive web workers and queued tasks. 36 | async fn manage_pool() { 37 | loop { 38 | WORKER_POOL.with(|worker_pool| { 39 | worker_pool.remove_inactive_workers(); 40 | worker_pool.flush_queued_tasks(); 41 | }); 42 | let promise = Promise::new(&mut |resolve, _reject| { 43 | set_timeout(&resolve, 100.0); 44 | }); 45 | JsFuture::from(promise).await.log_error("MANAGE_POOL"); 46 | } 47 | } 48 | 49 | /// Spawns a new asynchronous task, returning a 50 | /// [`JoinHandle`] for it. 51 | /// 52 | /// The provided future will start running in the JavaScript event loop 53 | /// when `spawn` is called, even if you don't await the returned 54 | /// `JoinHandle`. 55 | /// 56 | /// Spawning a task enables the task to execute concurrently to other tasks. The 57 | /// spawned task will always execute on the current web worker(thread), 58 | /// as that's how JavaScript's `Promise` basically works. 59 | /// 60 | /// # Examples 61 | /// 62 | /// In this example, a server is started and `spawn` is used to start a new task 63 | /// that processes each received connection. 64 | /// 65 | /// ```no_run 66 | /// use std::io; 67 | /// use tokio_with_wasm as tokio; 68 | /// 69 | /// async fn process() -> io::Result<()> { 70 | /// // Some process... 71 | /// } 72 | /// 73 | /// async fn work() -> io::Result<()> { 74 | /// let result = tokio::spawn(async move { 75 | /// // Process this job concurrently. 76 | /// process(socket).await 77 | /// }).await?;; 78 | /// } 79 | /// ``` 80 | /// 81 | /// To run multiple tasks in parallel and receive their results, join 82 | /// handles can be stored in a vector. 83 | /// ``` 84 | /// use tokio_with_wasm as tokio; 85 | /// 86 | /// async fn my_background_op(id: i32) -> String { 87 | /// let s = format!("Starting background task {}.", id); 88 | /// println!("{}", s); 89 | /// s 90 | /// 91 | /// let ops = vec![1, 2, 3]; 92 | /// let mut tasks = Vec::with_capacity(ops.len()); 93 | /// for op in ops { 94 | /// // This call will make them start running in the background 95 | /// // immediately. 96 | /// tasks.push(tokio::spawn(my_background_op(op))); 97 | /// } 98 | /// 99 | /// let mut outputs = Vec::with_capacity(tasks.len()); 100 | /// for task in tasks { 101 | /// match task.await { 102 | /// Ok(output) => outputs.push(output), 103 | /// Err(err) => { 104 | /// println!("An error occurred: {}", err); 105 | /// } 106 | /// } 107 | /// } 108 | /// println!("{:?}", outputs); 109 | /// # } 110 | /// ``` 111 | /// This example pushes the tasks to `outputs` in the order they were 112 | /// started in. 113 | /// 114 | /// # Using `!Send` values from a task 115 | /// 116 | /// The task supplied to `spawn` is not required to implement `Send`. 117 | /// This is different from multi-threaded native async runtimes, 118 | /// because JavaScript environment is inherently single-threaded. 119 | /// 120 | /// For example, this will work: 121 | /// 122 | /// ``` 123 | /// use std::rc::Rc; 124 | /// use tokio_with_wasm as tokio; 125 | /// 126 | /// fn use_rc(rc: Rc<()>) { 127 | /// // Do stuff w/ rc 128 | /// # drop(rc); 129 | /// } 130 | /// 131 | /// async fn work() { 132 | /// tokio::spawn(async { 133 | /// // Force the `Rc` to stay in a scope with no `.await` 134 | /// { 135 | /// let rc = Rc::new(()); 136 | /// use_rc(rc.clone()); 137 | /// } 138 | /// 139 | /// tokio::task::yield_now().await; 140 | /// }).await; 141 | /// } 142 | /// ``` 143 | /// 144 | /// This will work too, unlike multi-threaded native runtimes 145 | /// where `!Send` values cannot live across `.await`: 146 | /// 147 | /// ``` 148 | /// use std::rc::Rc; 149 | /// use tokio_with_wasm as tokio; 150 | /// 151 | /// fn use_rc(rc: Rc<()>) { 152 | /// // Do stuff w/ rc 153 | /// # drop(rc); 154 | /// } 155 | /// 156 | /// async fn work() { 157 | /// tokio::spawn(async { 158 | /// let rc = Rc::new(()); 159 | /// 160 | /// tokio::task::yield_now().await; 161 | /// 162 | /// use_rc(rc.clone()); 163 | /// }).await; 164 | /// } 165 | /// ``` 166 | pub fn spawn(future: F) -> JoinHandle 167 | where 168 | F: std::future::Future + 'static, 169 | T: 'static, 170 | { 171 | if !is_main_thread() { 172 | JsValue::from_str(concat!( 173 | "Calling `spawn` in a blocking thread is not allowed. ", 174 | "While this is possible in real `tokio`, ", 175 | "it may cause undefined behavior in the JavaScript environment. ", 176 | "Instead, use `tokio::sync::mpsc::channel` ", 177 | "to listen for messages from the main thread ", 178 | "and spawn a task there." 179 | )) 180 | .log_error("SPAWN"); 181 | panic!(); 182 | } 183 | let (join_sender, join_receiver) = once_channel(); 184 | let (cancel_sender, cancel_receiver) = once_channel::<()>(); 185 | spawn_local(async move { 186 | let result = SelectFuture::new( 187 | async move { 188 | let output = future.await; 189 | Ok(output) 190 | }, 191 | async move { 192 | cancel_receiver.await; 193 | Err(JoinError { cancelled: true }) 194 | }, 195 | ) 196 | .await; 197 | join_sender.send(result); 198 | }); 199 | JoinHandle { 200 | join_receiver, 201 | cancel_sender, 202 | } 203 | } 204 | 205 | /// Runs the provided closure on a web worker(thread) where blocking is acceptable. 206 | /// 207 | /// In general, issuing a blocking call or performing a lot of compute in a 208 | /// future without yielding is problematic, as it may prevent the JavaScript runtime from 209 | /// driving other futures forward. This function runs the provided closure on a 210 | /// web worker dedicated to blocking operations. 211 | /// 212 | /// More and more web workers will be spawned when they are requested through this 213 | /// function until the upper limit of 512 is reached. 214 | /// After reaching the upper limit, the tasks will wait for 215 | /// any of the web workers to become idle. 216 | /// When a web worker remains idle for 10 seconds, it will be terminated 217 | /// and get removed from the worker pool, which is a similiar behavior to that of `tokio`. 218 | /// The web worker limit is very large by default, because `spawn_blocking` is often 219 | /// used for various kinds of IO operations that cannot be performed 220 | /// asynchronously. When you run CPU-bound code using `spawn_blocking`, you 221 | /// should keep this large upper limit in mind. 222 | /// 223 | /// # Examples 224 | /// 225 | /// Pass an input value and receive result of computation: 226 | /// 227 | /// ``` 228 | /// use tokio_with_wasm as tokio; 229 | /// 230 | /// // Initial input 231 | /// let mut data = "Hello, ".to_string(); 232 | /// let output = tokio::task::spawn_blocking(move || { 233 | /// // Stand-in for compute-heavy work or using synchronous APIs 234 | /// data.push_str("world"); 235 | /// // Pass ownership of the value back to the asynchronous context 236 | /// data 237 | /// }).await?; 238 | /// 239 | /// // `output` is the value returned from the thread 240 | /// assert_eq!(output.as_str(), "Hello, world"); 241 | /// Ok(()) 242 | /// ``` 243 | pub fn spawn_blocking(callable: C) -> JoinHandle 244 | where 245 | C: FnOnce() -> T + Send + 'static, 246 | T: Send + 'static, 247 | { 248 | if !is_main_thread() { 249 | JsValue::from_str(concat!( 250 | "Calling `spawn_blocking` in a blocking thread is not allowed. ", 251 | "While this is possible in real `tokio`, ", 252 | "it may cause undefined behavior in the JavaScript environment. ", 253 | "Instead, use `tokio::sync::mpsc::channel` ", 254 | "to listen for messages from the main thread ", 255 | "and spawn a task there." 256 | )) 257 | .log_error("SPAWN_BLOCKING"); 258 | panic!(); 259 | } 260 | let (join_sender, join_receiver) = once_channel(); 261 | let (cancel_sender, cancel_receiver) = once_channel::<()>(); 262 | WORKER_POOL.with(move |worker_pool| { 263 | worker_pool.queue_task(move || { 264 | if cancel_receiver.is_done() { 265 | join_sender.send(Err(JoinError { cancelled: true })); 266 | return; 267 | } 268 | let returned = callable(); 269 | join_sender.send(Ok(returned)); 270 | }) 271 | }); 272 | JoinHandle { 273 | join_receiver, 274 | cancel_sender, 275 | } 276 | } 277 | 278 | /// Yields execution back to the JavaScript event loop. 279 | /// 280 | /// To avoid blocking inside a long-running function, 281 | /// you have to yield to the async event loop regularly. 282 | /// 283 | /// The async task may resume when it has its turn back. 284 | /// Meanwhile, any other pending tasks will be scheduled 285 | /// by the JavaScript runtime. 286 | pub async fn yield_now() { 287 | let promise = Promise::new(&mut |resolve, _reject| { 288 | set_timeout(&resolve, 0.0); 289 | }); 290 | JsFuture::from(promise).await.log_error("YIELD_NOW"); 291 | } 292 | 293 | /// An owned permission to join on a task (awaiting its termination). 294 | /// 295 | /// This can be thought of as the equivalent of 296 | /// [`std::thread::JoinHandle`] or `tokio::task::JoinHandle` for 297 | /// a task that is executed concurrently. 298 | /// 299 | /// A `JoinHandle` *detaches* the associated task when it is dropped, which 300 | /// means that there is no longer any handle to the task, and no way to `join` 301 | /// on it. 302 | /// 303 | /// This struct is created by the [`spawn`] and [`spawn_blocking`] 304 | /// functions. 305 | /// 306 | /// # Examples 307 | /// 308 | /// Creation from [`spawn`]: 309 | /// 310 | /// ``` 311 | /// use tokio_with_wasm as tokio; 312 | /// use tokio::spawn; 313 | /// 314 | /// let join_handle: tokio::task::JoinHandle<_> = spawn(async { 315 | /// // some work here 316 | /// }); 317 | /// ``` 318 | /// 319 | /// Creation from [`spawn_blocking`]: 320 | /// 321 | /// ``` 322 | /// use tokio_with_wasm as tokio; 323 | /// use tokio::task::spawn_blocking; 324 | /// 325 | /// let join_handle: tokio::task::JoinHandle<_> = spawn_blocking(|| { 326 | /// // some blocking work here 327 | /// }); 328 | /// ``` 329 | /// 330 | /// Child being detached and outliving its parent: 331 | /// 332 | /// ```no_run 333 | /// use tokio_with_wasm as tokio; 334 | /// use tokio::spawn; 335 | /// 336 | /// let original_task = spawn(async { 337 | /// let _detached_task = spawn(async { 338 | /// // Here we sleep to make sure that the first task returns before. 339 | /// // Assume that code takes a few seconds to execute here. 340 | /// // This will be called, even though the JoinHandle is dropped. 341 | /// println!("♫ Still alive ♫"); 342 | /// }); 343 | /// }); 344 | /// 345 | /// original_task.await; 346 | /// println!("Original task is joined."); 347 | /// ``` 348 | pub struct JoinHandle { 349 | join_receiver: OnceReceiver>, 350 | cancel_sender: OnceSender<()>, 351 | } 352 | 353 | impl Future for JoinHandle { 354 | type Output = Result; 355 | fn poll( 356 | mut self: Pin<&mut Self>, 357 | cx: &mut Context<'_>, 358 | ) -> Poll { 359 | let pinned_receiver = Pin::new(&mut self.join_receiver); 360 | pinned_receiver.poll(cx) 361 | } 362 | } 363 | 364 | impl Debug for JoinHandle 365 | where 366 | T: Debug, 367 | { 368 | fn fmt(&self, fmt: &mut Formatter<'_>) -> std::fmt::Result { 369 | fmt.debug_struct("JoinHandle").finish() 370 | } 371 | } 372 | 373 | impl JoinHandle { 374 | /// Abort the task associated with the handle. 375 | /// 376 | /// Awaiting a cancelled task might complete as usual if the task was 377 | /// already completed at the time it was cancelled, but most likely it 378 | /// will fail with a cancelled `JoinError`. 379 | /// 380 | /// Be aware that tasks spawned using [`spawn_blocking`] cannot be aborted 381 | /// because they are not async. If you call `abort` on a `spawn_blocking` 382 | /// task, then this *will not have any effect*, and the task will continue 383 | /// running normally. The exception is if the task has not started running 384 | /// yet; in that case, calling `abort` may prevent the task from starting. 385 | /// 386 | /// ```rust 387 | /// use tokio_with_wasm as tokio; 388 | /// use tokio::time; 389 | /// 390 | /// # #[tokio::main(flavor = "current_thread", start_paused = true)] 391 | /// # async fn main() { 392 | /// let mut handles = Vec::new(); 393 | /// 394 | /// handles.push(tokio::spawn(async { 395 | /// time::sleep(time::Duration::from_secs(10)).await; 396 | /// true 397 | /// })); 398 | /// 399 | /// handles.push(tokio::spawn(async { 400 | /// time::sleep(time::Duration::from_secs(10)).await; 401 | /// false 402 | /// })); 403 | /// 404 | /// for handle in &handles { 405 | /// handle.abort(); 406 | /// } 407 | /// 408 | /// for handle in handles { 409 | /// assert!(handle.await.unwrap_err().is_cancelled()); 410 | /// } 411 | /// # } 412 | /// ``` 413 | pub fn abort(&self) { 414 | self.cancel_sender.send(()); 415 | } 416 | 417 | /// Checks if the task associated with this `JoinHandle` has finished. 418 | /// 419 | /// Please note that this method can return `false` even if [`abort`] has been 420 | /// called on the task. This is because the cancellation process may take 421 | /// some time, and this method does not return `true` until it has 422 | /// completed. 423 | pub fn is_finished(&self) -> bool { 424 | self.join_receiver.is_done() 425 | } 426 | 427 | /// Returns a new `AbortHandle` that can be used to remotely abort this task. 428 | pub fn abort_handle(&self) -> AbortHandle { 429 | AbortHandle { 430 | cancel_sender: self.cancel_sender.clone(), 431 | } 432 | } 433 | } 434 | 435 | /// Returned when a task failed to execute to completion. 436 | #[derive(Debug)] 437 | pub struct JoinError { 438 | cancelled: bool, 439 | } 440 | 441 | impl Display for JoinError { 442 | fn fmt(&self, fmt: &mut Formatter<'_>) -> std::fmt::Result { 443 | fmt.write_str("task failed to execute to completion") 444 | } 445 | } 446 | 447 | impl Error for JoinError {} 448 | 449 | impl JoinError { 450 | pub fn is_cancelled(&self) -> bool { 451 | self.cancelled 452 | } 453 | } 454 | 455 | /// An owned permission to abort a spawned task, without awaiting its completion. 456 | /// 457 | /// Unlike a [`JoinHandle`], an `AbortHandle` does *not* represent the 458 | /// permission to await the task's completion, only to terminate it. 459 | /// 460 | /// The task may be aborted by calling the [`AbortHandle::abort`] method. 461 | /// Dropping an `AbortHandle` releases the permission to terminate the task 462 | /// --- it does *not* abort the task. 463 | /// 464 | /// Be aware that tasks spawned using [`spawn_blocking`] cannot be aborted 465 | /// because they are not async. If you call `abort` on a `spawn_blocking` task, 466 | /// then this *will not have any effect*, and the task will continue running 467 | /// normally. The exception is if the task has not started running yet; in that 468 | /// case, calling `abort` may prevent the task from starting. 469 | /// 470 | /// [`JoinHandle`]: crate::task::JoinHandle 471 | /// [`spawn_blocking`]: crate::task::spawn_blocking 472 | #[derive(Clone)] 473 | pub struct AbortHandle { 474 | cancel_sender: OnceSender<()>, 475 | } 476 | 477 | impl AbortHandle { 478 | /// Abort the task associated with the handle. 479 | /// 480 | /// Awaiting a cancelled task might complete as usual if the task was 481 | /// already completed at the time it was cancelled, but most likely it 482 | /// will fail with a [cancelled] `JoinError`. 483 | /// 484 | /// If the task was already cancelled, such as by [`JoinHandle::abort`], 485 | /// this method will do nothing. 486 | /// 487 | /// Be aware that tasks spawned using [`spawn_blocking`] cannot be aborted 488 | /// because they are not async. If you call `abort` on a `spawn_blocking` 489 | /// task, then this *will not have any effect*, and the task will continue 490 | /// running normally. The exception is if the task has not started running 491 | /// yet; in that case, calling `abort` may prevent the task from starting. 492 | pub fn abort(&self) { 493 | self.cancel_sender.send(()); 494 | } 495 | } 496 | 497 | impl Debug for AbortHandle { 498 | fn fmt(&self, fmt: &mut Formatter<'_>) -> std::fmt::Result { 499 | fmt.debug_struct("AbortHandle").finish() 500 | } 501 | } 502 | --------------------------------------------------------------------------------