From e246f427603751190d2f8e9fb39c2a088cad7ff3 Mon Sep 17 00:00:00 2001 From: chemicstry Date: Thu, 9 Mar 2023 21:24:39 +0200 Subject: [PATCH 01/24] Add required dependencies to README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 03df47a..d651758 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,12 @@ For a higher-level threading solution, see [wasm-bindgen-rayon](https://github.c #### wasm-bindgen +- Install nightly toolchain and dependencies: +```bash +rustup toolchain install nightly +rustup component add rust-src --toolchain nightly +cargo install wasm-bindgen-cli +``` - Build with `./build_wasm.sh` (bash) or `./build_wasm.ps1` (PowerShell). This custom build step is required because prebuilt standard library does not have support for atomics yet. Read more about this [here](https://rustwasm.github.io/2018/10/24/multithreading-rust-and-wasm.html). - Serve `examples` directory over HTTP with cross-origin isolation enabled and open `simple.html` in the browser. Inspect console output. You can use `cargo install sfz` as a basic HTTP server and serve with `sfz examples --coi`. From a4b0153f4d6281503d1eea8fb22ffd9673464057 Mon Sep 17 00:00:00 2001 From: chemicstry Date: Thu, 9 Mar 2023 21:26:22 +0200 Subject: [PATCH 02/24] Move javascript files into a subdir --- src/{ => js}/module_workers_polyfill.min.js | 0 src/{ => js}/script_path.js | 0 src/{ => js}/web_worker.js | 0 src/{ => js}/web_worker_module.js | 0 src/lib.rs | 8 ++++---- 5 files changed, 4 insertions(+), 4 deletions(-) rename src/{ => js}/module_workers_polyfill.min.js (100%) rename src/{ => js}/script_path.js (100%) rename src/{ => js}/web_worker.js (100%) rename src/{ => js}/web_worker_module.js (100%) diff --git a/src/module_workers_polyfill.min.js b/src/js/module_workers_polyfill.min.js similarity index 100% rename from src/module_workers_polyfill.min.js rename to src/js/module_workers_polyfill.min.js diff --git a/src/script_path.js b/src/js/script_path.js similarity index 100% rename from src/script_path.js rename to src/js/script_path.js diff --git a/src/web_worker.js b/src/js/web_worker.js similarity index 100% rename from src/web_worker.js rename to src/js/web_worker.js diff --git a/src/web_worker_module.js b/src/js/web_worker_module.js similarity index 100% rename from src/web_worker_module.js rename to src/js/web_worker_module.js diff --git a/src/lib.rs b/src/lib.rs index 527a7af..28ab395 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,7 @@ struct WebWorkerContext { } #[cfg(feature = "es_modules")] -#[wasm_bindgen(module = "/src/module_workers_polyfill.min.js")] +#[wasm_bindgen(module = "/src/js/module_workers_polyfill.min.js")] extern "C" { fn load_module_workers_polyfill(); } @@ -35,7 +35,7 @@ static DEFAULT_BUILDER: DefaultBuilder = Mutex::new(None); /// Extracts path of the `wasm_bindgen` generated .js shim script pub fn get_wasm_bindgen_shim_script_path() -> String { - js_sys::eval(include_str!("script_path.js")) + js_sys::eval(include_str!("js/script_path.js")) .unwrap() .as_string() .unwrap() @@ -56,11 +56,11 @@ pub fn get_worker_script(wasm_bindgen_shim_url: Option) -> String { let template; #[cfg(feature = "es_modules")] { - template = include_str!("web_worker_module.js"); + template = include_str!("js/web_worker_module.js"); } #[cfg(not(feature = "es_modules"))] { - template = include_str!("web_worker.js"); + template = include_str!("js/web_worker.js"); } let script = template.replace("WASM_BINDGEN_SHIM_URL", &wasm_bindgen_shim_url); From 35ea30d0f27b98303a7a110eaa057e14485ef8a6 Mon Sep 17 00:00:00 2001 From: chemicstry Date: Thu, 9 Mar 2023 21:32:44 +0200 Subject: [PATCH 03/24] Move utilities into a separate module --- src/lib.rs | 53 ++++------------------------------------------------ src/utils.rs | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 49 deletions(-) create mode 100644 src/utils.rs diff --git a/src/lib.rs b/src/lib.rs index 28ab395..ea3240a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ use async_channel::Receiver; use futures::executor::block_on; +use utils::get_worker_script; use std::any::Any; use std::fmt; use std::mem; @@ -17,8 +18,9 @@ use std::{ }; use wasm_bindgen::prelude::*; -use wasm_bindgen::*; -use web_sys::{Blob, DedicatedWorkerGlobalScope, Url, Worker, WorkerOptions, WorkerType}; +use web_sys::{DedicatedWorkerGlobalScope, Worker, WorkerOptions, WorkerType}; + +mod utils; struct WebWorkerContext { func: Box, @@ -33,53 +35,6 @@ extern "C" { type DefaultBuilder = Mutex>; static DEFAULT_BUILDER: DefaultBuilder = Mutex::new(None); -/// Extracts path of the `wasm_bindgen` generated .js shim script -pub fn get_wasm_bindgen_shim_script_path() -> String { - js_sys::eval(include_str!("js/script_path.js")) - .unwrap() - .as_string() - .unwrap() -} - -/// Generates worker entry script as URL encoded blob -pub fn get_worker_script(wasm_bindgen_shim_url: Option) -> String { - static mut SCRIPT_URL: Option = None; - - if let Some(url) = unsafe { SCRIPT_URL.as_ref() } { - url.clone() - } else { - // If wasm bindgen shim url is not provided, try to obtain one automatically - let wasm_bindgen_shim_url = - wasm_bindgen_shim_url.unwrap_or_else(get_wasm_bindgen_shim_script_path); - - // Generate script from template - let template; - #[cfg(feature = "es_modules")] - { - template = include_str!("js/web_worker_module.js"); - } - #[cfg(not(feature = "es_modules"))] - { - template = include_str!("js/web_worker.js"); - } - let script = template.replace("WASM_BINDGEN_SHIM_URL", &wasm_bindgen_shim_url); - - // Create url encoded blob - let arr = js_sys::Array::new(); - arr.set(0, JsValue::from_str(&script)); - let blob = Blob::new_with_str_sequence(&arr).unwrap(); - let url = Url::create_object_url_with_blob( - &blob - .slice_with_f64_and_f64_and_content_type(0.0, blob.size(), "text/javascript") - .unwrap(), - ) - .unwrap(); - unsafe { SCRIPT_URL = Some(url.clone()) }; - - url - } -} - /// Entry point for web workers #[wasm_bindgen] pub fn wasm_thread_entry_point(ptr: u32) { diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..8968b43 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,51 @@ +use std::sync::MutexGuard; + +use wasm_bindgen::prelude::*; +use web_sys::{Blob, Url}; + +/// Extracts path of the `wasm_bindgen` generated .js shim script +pub fn get_wasm_bindgen_shim_script_path() -> String { + js_sys::eval(include_str!("js/script_path.js")) + .unwrap() + .as_string() + .unwrap() +} + +/// Generates worker entry script as URL encoded blob +pub fn get_worker_script(wasm_bindgen_shim_url: Option) -> String { + static mut SCRIPT_URL: Option = None; + + if let Some(url) = unsafe { SCRIPT_URL.as_ref() } { + url.clone() + } else { + // If wasm bindgen shim url is not provided, try to obtain one automatically + let wasm_bindgen_shim_url = + wasm_bindgen_shim_url.unwrap_or_else(get_wasm_bindgen_shim_script_path); + + // Generate script from template + let template; + #[cfg(feature = "es_modules")] + { + template = include_str!("js/web_worker_module.js"); + } + #[cfg(not(feature = "es_modules"))] + { + template = include_str!("js/web_worker.js"); + } + let script = template.replace("WASM_BINDGEN_SHIM_URL", &wasm_bindgen_shim_url); + + // Create url encoded blob + let arr = js_sys::Array::new(); + arr.set(0, JsValue::from_str(&script)); + let blob = Blob::new_with_str_sequence(&arr).unwrap(); + let url = Url::create_object_url_with_blob( + &blob + .slice_with_f64_and_f64_and_content_type(0.0, blob.size(), "text/javascript") + .unwrap(), + ) + .unwrap(); + unsafe { SCRIPT_URL = Some(url.clone()) }; + + url + } +} From dac94ea72aa143af4f7c89af761142a679bfb59a Mon Sep 17 00:00:00 2001 From: chemicstry Date: Thu, 9 Mar 2023 22:06:26 +0200 Subject: [PATCH 04/24] Use spin locks to prevent main thread panics --- src/lib.rs | 59 ++++++++++++++++++++++++++++++++++------------------ src/utils.rs | 57 ++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 83 insertions(+), 33 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ea3240a..9c7c2d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,10 @@ use async_channel::Receiver; use futures::executor::block_on; -use utils::get_worker_script; +use utils::SpinLockMutex; use std::any::Any; use std::fmt; use std::mem; +use utils::get_worker_script; use std::sync::Mutex; pub use std::thread::{current, sleep, Result, Thread, ThreadId}; @@ -26,15 +27,6 @@ struct WebWorkerContext { func: Box, } -#[cfg(feature = "es_modules")] -#[wasm_bindgen(module = "/src/js/module_workers_polyfill.min.js")] -extern "C" { - fn load_module_workers_polyfill(); -} - -type DefaultBuilder = Mutex>; -static DEFAULT_BUILDER: DefaultBuilder = Mutex::new(None); - /// Entry point for web workers #[wasm_bindgen] pub fn wasm_thread_entry_point(ptr: u32) { @@ -75,8 +67,10 @@ impl WorkerMessage { } } +static DEFAULT_BUILDER: Mutex> = Mutex::new(None); + /// Thread factory, which can be used in order to configure the properties of a new thread. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone)] pub struct Builder { // A name for the thread-to-be, for identification in panic messages name: Option, @@ -88,31 +82,52 @@ pub struct Builder { wasm_bindgen_shim_url: Option, } +impl Default for Builder { + fn default() -> Self { + DEFAULT_BUILDER.lock_spin().unwrap().clone().unwrap_or(Self::empty()) + } +} + impl Builder { - /// Generates the base configuration for spawning a thread, from which - /// configuration methods can be chained. + /// Creates a builder inheriting global configuration options set by [Self::set_default]. pub fn new() -> Builder { - let default_builder = DEFAULT_BUILDER.lock().unwrap().clone(); - default_builder.unwrap_or(Builder::default()) + Builder::default() + } + + /// Creates a builder without inheriting global options set by [Self::set_default]. + pub fn empty() -> Builder { + Self { + name: None, + prefix: None, + stack_size: None, + wasm_bindgen_shim_url: None, + } } + /// Sets current values as global default for all new builders created with [Builder::new] or [Builder::default]. pub fn set_default(self) { - *DEFAULT_BUILDER.lock().unwrap() = Some(self); + *DEFAULT_BUILDER.lock_spin().unwrap() = Some(self); } - /// Sets the prefix of the thread-to-be. + /// Sets the prefix of the thread. pub fn prefix(mut self, prefix: String) -> Builder { self.prefix = Some(prefix); self } - /// Names the thread-to-be. + /// Sets the name of the thread. + /// + /// If not set, the default name is autogenerated. pub fn name(mut self, name: String) -> Builder { self.name = Some(name); self } /// Sets the size of the stack (in bytes) for the new thread. + /// + /// # Warning + /// + /// This is currently not supported by wasm, but provided for API consistency with [std::thread]. pub fn stack_size(mut self, size: usize) -> Builder { self.stack_size = Some(size); self @@ -135,9 +150,13 @@ impl Builder { unsafe { self.spawn_unchecked(f) } } + /// Spawns a new scoped thread using the settings set through this `Builder`. + /// + /// Unlike [Scope::spawn], this method yields an [`io::Result`] to + /// capture any failure to create the thread at the OS level. pub fn spawn_scoped<'scope, 'env, F, T>( self, - scope: &'scope Scope<'scope, 'env>, + _scope: &'scope Scope<'scope, 'env>, f: F, ) -> std::io::Result> where @@ -228,7 +247,7 @@ impl Builder { #[cfg(feature = "es_modules")] { - load_module_workers_polyfill(); + utils::load_module_workers_polyfill(); options.type_(WorkerType::Module); } #[cfg(not(feature = "es_modules"))] diff --git a/src/utils.rs b/src/utils.rs index 8968b43..ca10b6e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,9 +1,17 @@ -use std::sync::MutexGuard; +use std::sync::{Mutex, MutexGuard, LockResult, TryLockError}; use wasm_bindgen::prelude::*; use web_sys::{Blob, Url}; -/// Extracts path of the `wasm_bindgen` generated .js shim script +#[cfg(feature = "es_modules")] +#[wasm_bindgen(module = "/src/js/module_workers_polyfill.min.js")] +extern "C" { + fn load_module_workers_polyfill(); +} + +/// Extracts path of the `wasm_bindgen` generated .js shim script. +/// +/// Internally, this intentionally generates a javascript exception to obtain a stacktrace containing the current script URL. pub fn get_wasm_bindgen_shim_script_path() -> String { js_sys::eval(include_str!("js/script_path.js")) .unwrap() @@ -13,25 +21,22 @@ pub fn get_wasm_bindgen_shim_script_path() -> String { /// Generates worker entry script as URL encoded blob pub fn get_worker_script(wasm_bindgen_shim_url: Option) -> String { - static mut SCRIPT_URL: Option = None; + // Cache URL so that subsequent calls are less expensive + static CACHED_URL: Mutex> = Mutex::new(None); - if let Some(url) = unsafe { SCRIPT_URL.as_ref() } { - url.clone() + if let Some(url) = CACHED_URL.lock_spin().unwrap().clone() { + url } else { // If wasm bindgen shim url is not provided, try to obtain one automatically let wasm_bindgen_shim_url = wasm_bindgen_shim_url.unwrap_or_else(get_wasm_bindgen_shim_script_path); // Generate script from template - let template; #[cfg(feature = "es_modules")] - { - template = include_str!("js/web_worker_module.js"); - } + let template = include_str!("js/web_worker_module.js"); #[cfg(not(feature = "es_modules"))] - { - template = include_str!("js/web_worker.js"); - } + let template = include_str!("js/web_worker.js"); + let script = template.replace("WASM_BINDGEN_SHIM_URL", &wasm_bindgen_shim_url); // Create url encoded blob @@ -44,8 +49,34 @@ pub fn get_worker_script(wasm_bindgen_shim_url: Option) -> String { .unwrap(), ) .unwrap(); - unsafe { SCRIPT_URL = Some(url.clone()) }; + + *CACHED_URL.lock_spin().unwrap() = Some(url.clone()); url } } + +/// A spin lock mutex extension. +/// +/// Atomic wait panics in wasm main thread so we can't use `Mutex::lock()`. +/// This is a helper, which implement spinlock by calling `Mutex::try_lock()` in a loop. +/// Care must be taken not to introduce deadlocks when using this trait. +pub trait SpinLockMutex { + type Inner; + + fn lock_spin<'a>(&'a self) -> LockResult>; +} + +impl SpinLockMutex for Mutex { + type Inner = T; + + fn lock_spin<'a>(&'a self) -> LockResult> { + loop { + match self.try_lock() { + Ok(guard) => break Ok(guard), + Err(TryLockError::WouldBlock) => {}, + Err(TryLockError::Poisoned(e)) => break Err(e) + } + } + } +} From c7c5c880607eb68c01780cb4aa7fda0949152f4c Mon Sep 17 00:00:00 2001 From: chemicstry Date: Thu, 9 Mar 2023 22:11:45 +0200 Subject: [PATCH 05/24] Move scoped into a module --- src/lib.rs | 138 +++++--------------------------------------------- src/scoped.rs | 122 ++++++++++++++++++++++++++++++++++++++++++++ src/utils.rs | 15 ++++-- 3 files changed, 147 insertions(+), 128 deletions(-) create mode 100644 src/scoped.rs diff --git a/src/lib.rs b/src/lib.rs index 9c7c2d0..191c6e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,26 +1,20 @@ use async_channel::Receiver; use futures::executor::block_on; -use utils::SpinLockMutex; use std::any::Any; use std::fmt; use std::mem; use utils::get_worker_script; +use utils::SpinLockMutex; +pub use scoped::*; +pub use utils::*; use std::sync::Mutex; pub use std::thread::{current, sleep, Result, Thread, ThreadId}; -use std::{ - marker::PhantomData, - panic::{catch_unwind, resume_unwind, AssertUnwindSafe}, - sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering}, - Arc, - }, - time::Duration, -}; use wasm_bindgen::prelude::*; use web_sys::{DedicatedWorkerGlobalScope, Worker, WorkerOptions, WorkerType}; +mod scoped; mod utils; struct WebWorkerContext { @@ -84,7 +78,11 @@ pub struct Builder { impl Default for Builder { fn default() -> Self { - DEFAULT_BUILDER.lock_spin().unwrap().clone().unwrap_or(Self::empty()) + DEFAULT_BUILDER + .lock_spin() + .unwrap() + .clone() + .unwrap_or(Self::empty()) } } @@ -116,7 +114,7 @@ impl Builder { } /// Sets the name of the thread. - /// + /// /// If not set, the default name is autogenerated. pub fn name(mut self, name: String) -> Builder { self.name = Some(name); @@ -124,9 +122,9 @@ impl Builder { } /// Sets the size of the stack (in bytes) for the new thread. - /// + /// /// # Warning - /// + /// /// This is currently not supported by wasm, but provided for API consistency with [std::thread]. pub fn stack_size(mut self, size: usize) -> Builder { self.stack_size = Some(size); @@ -150,25 +148,6 @@ impl Builder { unsafe { self.spawn_unchecked(f) } } - /// Spawns a new scoped thread using the settings set through this `Builder`. - /// - /// Unlike [Scope::spawn], this method yields an [`io::Result`] to - /// capture any failure to create the thread at the OS level. - pub fn spawn_scoped<'scope, 'env, F, T>( - self, - _scope: &'scope Scope<'scope, 'env>, - f: F, - ) -> std::io::Result> - where - F: FnOnce() -> T + Send + 'scope, - T: Send + 'scope, - { - Ok(ScopedJoinHandle( - unsafe { self.spawn_unchecked(f) }?, - PhantomData, - )) - } - /// Spawns a new thread without any lifetime restrictions by taking ownership /// of the `Builder`, and returns an [`io::Result`] to its [`JoinHandle`]. /// @@ -247,7 +226,7 @@ impl Builder { #[cfg(feature = "es_modules")] { - utils::load_module_workers_polyfill(); + load_module_workers_polyfill(); options.type_(WorkerType::Module); } #[cfg(not(feature = "es_modules"))] @@ -351,94 +330,3 @@ where { Builder::new().spawn(f).expect("failed to spawn thread") } - -use core::num::NonZeroUsize; -pub fn available_parallelism() -> std::io::Result { - // TODO: Use [Navigator::hardware_concurrency](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Navigator.html#method.hardware_concurrency) - Ok(NonZeroUsize::new(8).unwrap()) -} - -pub struct ScopeData { - num_running_threads: AtomicUsize, - a_thread_panicked: AtomicBool, - main_thread: Thread, -} - -pub struct Scope<'scope, 'env: 'scope> { - data: Arc, - /// Invariance over 'scope, to make sure 'scope cannot shrink, - /// which is necessary for soundness. - /// - /// Without invariance, this would compile fine but be unsound: - /// - /// ```compile_fail,E0373 - /// std::thread::scope(|s| { - /// s.spawn(|| { - /// let a = String::from("abcd"); - /// s.spawn(|| println!("{a:?}")); // might run after `a` is dropped - /// }); - /// }); - /// ``` - scope: PhantomData<&'scope mut &'scope ()>, - env: PhantomData<&'env mut &'env ()>, -} - -pub fn scope<'env, F, T>(f: F) -> T -where - F: for<'scope> FnOnce(&'scope Scope<'scope, 'env>) -> T, -{ - // We put the `ScopeData` into an `Arc` so that other threads can finish their - // `decrement_num_running_threads` even after this function returns. - let scope = Scope { - data: Arc::new(ScopeData { - num_running_threads: AtomicUsize::new(0), - main_thread: current(), - a_thread_panicked: AtomicBool::new(false), - }), - env: PhantomData, - scope: PhantomData, - }; - - // Run `f`, but catch panics so we can make sure to wait for all the threads to join. - let result = catch_unwind(AssertUnwindSafe(|| f(&scope))); - - // Wait until all the threads are finished. - while scope.data.num_running_threads.load(Ordering::Acquire) != 0 { - // park(); - // TODO: Replaced by a wasm-friendly version of park() - sleep(Duration::from_millis(1)); - } - - // Throw any panic from `f`, or the return value of `f` if no thread panicked. - match result { - Err(e) => resume_unwind(e), - Ok(_) if scope.data.a_thread_panicked.load(Ordering::Relaxed) => { - panic!("a scoped thread panicked") - } - Ok(result) => result, - } -} - -pub struct ScopedJoinHandle<'scope, T>(crate::JoinHandle, PhantomData<&'scope ()>); -impl<'scope, T> ScopedJoinHandle<'scope, T> { - pub fn join(self) -> std::io::Result { - self.0 - .join() - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, "")) - } -} - -pub fn spawn_scoped<'scope, 'env, F, T>( - builder: crate::Builder, - scope: &'scope Scope<'scope, 'env>, - f: F, -) -> std::io::Result> -where - F: FnOnce() -> T + Send + 'scope, - T: Send + 'scope, -{ - Ok(ScopedJoinHandle( - unsafe { builder.spawn_unchecked(f) }?, - PhantomData, - )) -} diff --git a/src/scoped.rs b/src/scoped.rs new file mode 100644 index 0000000..c727fcc --- /dev/null +++ b/src/scoped.rs @@ -0,0 +1,122 @@ +use std::{ + marker::PhantomData, + panic::{catch_unwind, resume_unwind, AssertUnwindSafe}, + sync::{ + atomic::{AtomicBool, AtomicUsize, Ordering}, + Arc, + }, + thread::{current, sleep, Thread}, + time::Duration, +}; + +use crate::Builder; + +pub struct ScopeData { + num_running_threads: AtomicUsize, + a_thread_panicked: AtomicBool, + main_thread: Thread, +} + +pub struct Scope<'scope, 'env: 'scope> { + data: Arc, + /// Invariance over 'scope, to make sure 'scope cannot shrink, + /// which is necessary for soundness. + /// + /// Without invariance, this would compile fine but be unsound: + /// + /// ```compile_fail,E0373 + /// std::thread::scope(|s| { + /// s.spawn(|| { + /// let a = String::from("abcd"); + /// s.spawn(|| println!("{a:?}")); // might run after `a` is dropped + /// }); + /// }); + /// ``` + scope: PhantomData<&'scope mut &'scope ()>, + env: PhantomData<&'env mut &'env ()>, +} + +impl Builder { + /// Spawns a new scoped thread using the settings set through this `Builder`. + /// + /// Unlike [Scope::spawn], this method yields an [`io::Result`] to + /// capture any failure to create the thread at the OS level. + pub fn spawn_scoped<'scope, 'env, F, T>( + self, + _scope: &'scope Scope<'scope, 'env>, + f: F, + ) -> std::io::Result> + where + F: FnOnce() -> T + Send + 'scope, + T: Send + 'scope, + { + Ok(ScopedJoinHandle( + unsafe { self.spawn_unchecked(f) }?, + PhantomData, + )) + } +} + +pub fn scope<'env, F, T>(f: F) -> T +where + F: for<'scope> FnOnce(&'scope Scope<'scope, 'env>) -> T, +{ + // We put the `ScopeData` into an `Arc` so that other threads can finish their + // `decrement_num_running_threads` even after this function returns. + let scope = Scope { + data: Arc::new(ScopeData { + num_running_threads: AtomicUsize::new(0), + main_thread: current(), + a_thread_panicked: AtomicBool::new(false), + }), + env: PhantomData, + scope: PhantomData, + }; + + // Run `f`, but catch panics so we can make sure to wait for all the threads to join. + let result = catch_unwind(AssertUnwindSafe(|| f(&scope))); + + // Wait until all the threads are finished. + while scope.data.num_running_threads.load(Ordering::Acquire) != 0 { + // park(); + // TODO: Replaced by a wasm-friendly version of park() + sleep(Duration::from_millis(1)); + } + + // Throw any panic from `f`, or the return value of `f` if no thread panicked. + match result { + Err(e) => resume_unwind(e), + Ok(_) if scope.data.a_thread_panicked.load(Ordering::Relaxed) => { + panic!("a scoped thread panicked") + } + Ok(result) => result, + } +} + +pub struct ScopedJoinHandle<'scope, T>( + pub(crate) crate::JoinHandle, + pub(crate) PhantomData<&'scope ()>, +); + +impl<'scope, T> ScopedJoinHandle<'scope, T> { + pub fn join(self) -> std::io::Result { + self.0 + .join() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, "")) + } +} + +pub fn spawn_scoped<'scope, 'env, F, T>( + builder: crate::Builder, + scope: &'scope Scope<'scope, 'env>, + f: F, +) -> std::io::Result> +where + F: FnOnce() -> T + Send + 'scope, + T: Send + 'scope, +{ + Ok(ScopedJoinHandle( + unsafe { builder.spawn_unchecked(f) }?, + PhantomData, + )) +} diff --git a/src/utils.rs b/src/utils.rs index ca10b6e..74a04c7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,8 +1,17 @@ -use std::sync::{Mutex, MutexGuard, LockResult, TryLockError}; +use std::{ + io, + num::NonZeroUsize, + sync::{LockResult, Mutex, MutexGuard, TryLockError}, +}; use wasm_bindgen::prelude::*; use web_sys::{Blob, Url}; +pub fn available_parallelism() -> io::Result { + // TODO: Use [Navigator::hardware_concurrency](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Navigator.html#method.hardware_concurrency) + Ok(NonZeroUsize::new(8).unwrap()) +} + #[cfg(feature = "es_modules")] #[wasm_bindgen(module = "/src/js/module_workers_polyfill.min.js")] extern "C" { @@ -74,8 +83,8 @@ impl SpinLockMutex for Mutex { loop { match self.try_lock() { Ok(guard) => break Ok(guard), - Err(TryLockError::WouldBlock) => {}, - Err(TryLockError::Poisoned(e)) => break Err(e) + Err(TryLockError::WouldBlock) => {} + Err(TryLockError::Poisoned(e)) => break Err(e), } } } From b64d53176ec1b760453baae3a2330a396e2708ac Mon Sep 17 00:00:00 2001 From: chemicstry Date: Thu, 9 Mar 2023 22:13:50 +0200 Subject: [PATCH 06/24] Add better formatting rules --- examples/simple.rs | 23 +++++++---------------- rustfmt.toml | 7 +++++++ src/lib.rs | 33 ++++++++------------------------- src/scoped.rs | 15 +++------------ src/utils.rs | 6 +++--- 5 files changed, 28 insertions(+), 56 deletions(-) create mode 100644 rustfmt.toml diff --git a/examples/simple.rs b/examples/simple.rs index b738159..fc9de8e 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,15 +1,16 @@ -use std::time::Duration; - #[cfg(not(target_arch = "wasm32"))] use std::thread; +use std::time::Duration; + #[cfg(target_arch = "wasm32")] use wasm_thread as thread; #[cfg(target_arch = "wasm32")] mod wasm { - use crate::main; use wasm_bindgen::prelude::*; + use crate::main; + // Prevent `wasm_bindgen` from autostarting main on all spawned threads #[wasm_bindgen(start)] pub fn dummy_main() {} @@ -25,28 +26,18 @@ mod wasm { fn main() { #[cfg(not(target_arch = "wasm32"))] - env_logger::init_from_env( - env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), - ); + env_logger::init_from_env(env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info")); for _ in 0..2 { thread::spawn(|| { for i in 1..3 { - log::info!( - "hi number {} from the spawned thread {:?}!", - i, - thread::current().id() - ); + log::info!("hi number {} from the spawned thread {:?}!", i, thread::current().id()); thread::sleep(Duration::from_millis(1)); } }); } for i in 1..3 { - log::info!( - "hi number {} from the main thread {:?}!", - i, - thread::current().id() - ); + log::info!("hi number {} from the main thread {:?}!", i, thread::current().id()); } } diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..7412f2d --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,7 @@ +unstable_features = true +max_width = 120 +comment_width = 120 +wrap_comments = true +format_code_in_doc_comments = true +group_imports = "StdExternalCrate" +imports_granularity = "Crate" diff --git a/src/lib.rs b/src/lib.rs index 191c6e6..1eea9e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,16 +1,11 @@ +pub use std::thread::{current, sleep, Result, Thread, ThreadId}; +use std::{any::Any, fmt, mem, sync::Mutex}; + use async_channel::Receiver; use futures::executor::block_on; -use std::any::Any; -use std::fmt; -use std::mem; -use utils::get_worker_script; -use utils::SpinLockMutex; - pub use scoped::*; pub use utils::*; -use std::sync::Mutex; -pub use std::thread::{current, sleep, Result, Thread, ThreadId}; - +use utils::{get_worker_script, SpinLockMutex}; use wasm_bindgen::prelude::*; use web_sys::{DedicatedWorkerGlobalScope, Worker, WorkerOptions, WorkerType}; @@ -78,11 +73,7 @@ pub struct Builder { impl Default for Builder { fn default() -> Self { - DEFAULT_BUILDER - .lock_spin() - .unwrap() - .clone() - .unwrap_or(Self::empty()) + DEFAULT_BUILDER.lock_spin().unwrap().clone().unwrap_or(Self::empty()) } } @@ -175,9 +166,7 @@ impl Builder { sender.try_send(res).ok(); }); let context = WebWorkerContext { - func: mem::transmute::, Box>( - main, - ), + func: mem::transmute::, Box>(main), }; if MAIN_THREAD_ID.is_none() { @@ -187,11 +176,7 @@ impl Builder { if MAIN_THREAD_ID.unwrap_unchecked() == current().id() { self.spawn_for_context(context); } else { - WorkerMessage::SpawnThread(BuilderRequest { - builder: self, - context, - }) - .post(); + WorkerMessage::SpawnThread(BuilderRequest { builder: self, context }).post(); } Ok(JoinHandle(JoinInner { receiver })) @@ -239,9 +224,7 @@ impl Builder { let worker_reference = std::rc::Rc::new(std::cell::Cell::new(None)); let worker_reference_callback = worker_reference.clone(); let callback = Closure::wrap(Box::new(move |x: &web_sys::MessageEvent| { - let req = Box::from_raw( - std::mem::transmute::<_, u64>(x.data().as_f64().unwrap()) as *mut WorkerMessage - ); + let req = Box::from_raw(std::mem::transmute::<_, u64>(x.data().as_f64().unwrap()) as *mut WorkerMessage); match *req { WorkerMessage::SpawnThread(builder) => { builder.spawn(); diff --git a/src/scoped.rs b/src/scoped.rs index c727fcc..4f8fd1e 100644 --- a/src/scoped.rs +++ b/src/scoped.rs @@ -50,10 +50,7 @@ impl Builder { F: FnOnce() -> T + Send + 'scope, T: Send + 'scope, { - Ok(ScopedJoinHandle( - unsafe { self.spawn_unchecked(f) }?, - PhantomData, - )) + Ok(ScopedJoinHandle(unsafe { self.spawn_unchecked(f) }?, PhantomData)) } } @@ -93,10 +90,7 @@ where } } -pub struct ScopedJoinHandle<'scope, T>( - pub(crate) crate::JoinHandle, - pub(crate) PhantomData<&'scope ()>, -); +pub struct ScopedJoinHandle<'scope, T>(pub(crate) crate::JoinHandle, pub(crate) PhantomData<&'scope ()>); impl<'scope, T> ScopedJoinHandle<'scope, T> { pub fn join(self) -> std::io::Result { @@ -115,8 +109,5 @@ where F: FnOnce() -> T + Send + 'scope, T: Send + 'scope, { - Ok(ScopedJoinHandle( - unsafe { builder.spawn_unchecked(f) }?, - PhantomData, - )) + Ok(ScopedJoinHandle(unsafe { builder.spawn_unchecked(f) }?, PhantomData)) } diff --git a/src/utils.rs b/src/utils.rs index 74a04c7..2a48515 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -20,7 +20,8 @@ extern "C" { /// Extracts path of the `wasm_bindgen` generated .js shim script. /// -/// Internally, this intentionally generates a javascript exception to obtain a stacktrace containing the current script URL. +/// Internally, this intentionally generates a javascript exception to obtain a stacktrace containing the current script +/// URL. pub fn get_wasm_bindgen_shim_script_path() -> String { js_sys::eval(include_str!("js/script_path.js")) .unwrap() @@ -37,8 +38,7 @@ pub fn get_worker_script(wasm_bindgen_shim_url: Option) -> String { url } else { // If wasm bindgen shim url is not provided, try to obtain one automatically - let wasm_bindgen_shim_url = - wasm_bindgen_shim_url.unwrap_or_else(get_wasm_bindgen_shim_script_path); + let wasm_bindgen_shim_url = wasm_bindgen_shim_url.unwrap_or_else(get_wasm_bindgen_shim_script_path); // Generate script from template #[cfg(feature = "es_modules")] From dc0bb62f520ca5af9e3d69e4e8f9a260e36ac777 Mon Sep 17 00:00:00 2001 From: chemicstry Date: Thu, 9 Mar 2023 23:49:48 +0200 Subject: [PATCH 07/24] Properly implement scoped thread join --- Cargo.toml | 10 ++- examples/simple.html | 27 +++---- examples/simple.rs | 33 +++++++++ src/lib.rs | 49 ++++++++++--- src/scoped.rs | 166 +++++++++++++++++++++++++++++++------------ src/utils.rs | 60 ++++++++-------- 6 files changed, 248 insertions(+), 97 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 207fc3e..638667d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,15 @@ es_modules = [] [dependencies] wasm-bindgen = "0.2" -web-sys = { version = "0.3", features = ["Blob", "DedicatedWorkerGlobalScope", "MessageEvent", "Url", "Worker", "WorkerType", "WorkerOptions"]} +web-sys = { version = "0.3", features = [ + "Blob", + "DedicatedWorkerGlobalScope", + "MessageEvent", + "Url", + "Worker", + "WorkerType", + "WorkerOptions", +] } js-sys = "0.3" futures = "0.3" async-channel = "1.4" diff --git a/examples/simple.html b/examples/simple.html index 6c17347..08969a7 100644 --- a/examples/simple.html +++ b/examples/simple.html @@ -1,13 +1,16 @@ - - - - - - - - + + + + + + + + + + + \ No newline at end of file diff --git a/examples/simple.rs b/examples/simple.rs index fc9de8e..59fe9f4 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -40,4 +40,37 @@ fn main() { for i in 1..3 { log::info!("hi number {} from the main thread {:?}!", i, thread::current().id()); } + + // It's not possible to do a scope on the main thread, because blocking waits are not supported, but we can use + // scope inside web workers. + thread::spawn(|| { + log::info!("Start scope test on thread {:?}", thread::current().id()); + + let mut a = vec![1, 2, 3]; + let mut x = 0; + + thread::scope(|s| { + s.spawn(|| { + log::info!("hello from the first scoped thread {:?}", thread::current().id()); + // We can borrow `a` here. + log::debug!("a = {:?}", &a); + }); + s.spawn(|| { + log::info!("hello from the second scoped thread {:?}", thread::current().id()); + // We can even mutably borrow `x` here, + // because no other threads are using it. + x += a[0] + a[2]; + }); + + log::info!( + "Hello from scope \"main\" thread {:?} inside scope.", + thread::current().id() + ); + }); + + // After the scope, we can modify and access our variables again: + a.push(4); + assert_eq!(x, a.len()); + log::info!("Scope done x={} a.len()={}", x, a.len()); + }); } diff --git a/src/lib.rs b/src/lib.rs index 1eea9e3..cd95064 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,9 @@ pub use std::thread::{current, sleep, Result, Thread, ThreadId}; -use std::{any::Any, fmt, mem, sync::Mutex}; +use std::{ + fmt, mem, + panic::{catch_unwind, AssertUnwindSafe}, + sync::{Arc, Mutex}, +}; use async_channel::Receiver; use futures::executor::block_on; @@ -154,6 +158,15 @@ impl Builder { /// `'static` references (both [`Builder::spawn`] /// and [`spawn`] enforce this property statically) pub unsafe fn spawn_unchecked<'a, F, T>(self, f: F) -> std::io::Result> + where + F: FnOnce() -> T, + F: Send + 'a, + T: Send + 'a, + { + Ok(JoinHandle(unsafe { self.spawn_unchecked_(f, None) }?)) + } + + unsafe fn spawn_unchecked_<'a, F, T>(self, f: F, scope: Option>) -> std::io::Result> where F: FnOnce() -> T, F: Send + 'a, @@ -162,7 +175,7 @@ impl Builder { let (sender, receiver) = async_channel::bounded(1); let main = Box::new(move || { - let res = f(); + let res = catch_unwind(AssertUnwindSafe(|| f())); sender.try_send(res).ok(); }); let context = WebWorkerContext { @@ -179,7 +192,11 @@ impl Builder { WorkerMessage::SpawnThread(BuilderRequest { builder: self, context }).post(); } - Ok(JoinHandle(JoinInner { receiver })) + if let Some(scope) = &scope { + scope.increment_num_running_threads(); + } + + Ok(JoinInner { receiver, scope }) } unsafe fn spawn_for_context(self, ctx: WebWorkerContext) { @@ -261,19 +278,29 @@ impl Builder { /// Inner representation for JoinHandle struct JoinInner { - // thread: Thread, - receiver: Receiver, + receiver: Receiver>, + scope: Option>, } impl JoinInner { - fn join(&mut self) -> Result { + fn join(self) -> Result { let res = block_on(self.receiver.recv()); - res.map_err(|e| Box::new(e) as Box<(dyn Any + Send + 'static)>) + + if let Some(scope) = &self.scope { + scope.decrement_num_running_threads(false); + } + + res.unwrap() } - async fn join_async(&mut self) -> Result { + async fn join_async(self) -> Result { let res = self.receiver.recv().await; - res.map_err(|e| Box::new(e) as Box<(dyn Any + Send + 'static)>) + + if let Some(scope) = &self.scope { + scope.decrement_num_running_threads(false); + } + + res.unwrap() } } @@ -288,12 +315,12 @@ impl JoinHandle { } /// Waits for the associated thread to finish. - pub fn join(mut self) -> Result { + pub fn join(self) -> Result { self.0.join() } /// Waits for the associated thread to finish asynchronously. - pub async fn join_async(mut self) -> Result { + pub async fn join_async(self) -> Result { self.0.join_async().await } } diff --git a/src/scoped.rs b/src/scoped.rs index 4f8fd1e..0b9468b 100644 --- a/src/scoped.rs +++ b/src/scoped.rs @@ -3,20 +3,15 @@ use std::{ panic::{catch_unwind, resume_unwind, AssertUnwindSafe}, sync::{ atomic::{AtomicBool, AtomicUsize, Ordering}, - Arc, + Arc, Barrier, }, - thread::{current, sleep, Thread}, - time::Duration, }; -use crate::Builder; - -pub struct ScopeData { - num_running_threads: AtomicUsize, - a_thread_panicked: AtomicBool, - main_thread: Thread, -} +use crate::{is_web_worker_thread, Builder, JoinInner}; +/// A scope to spawn scoped threads in. +/// +/// See [`scope`] for details. pub struct Scope<'scope, 'env: 'scope> { data: Arc, /// Invariance over 'scope, to make sure 'scope cannot shrink, @@ -36,35 +31,79 @@ pub struct Scope<'scope, 'env: 'scope> { env: PhantomData<&'env mut &'env ()>, } -impl Builder { - /// Spawns a new scoped thread using the settings set through this `Builder`. - /// - /// Unlike [Scope::spawn], this method yields an [`io::Result`] to - /// capture any failure to create the thread at the OS level. - pub fn spawn_scoped<'scope, 'env, F, T>( - self, - _scope: &'scope Scope<'scope, 'env>, - f: F, - ) -> std::io::Result> - where - F: FnOnce() -> T + Send + 'scope, - T: Send + 'scope, - { - Ok(ScopedJoinHandle(unsafe { self.spawn_unchecked(f) }?, PhantomData)) +/// An owned permission to join on a scoped thread (block on its termination). +/// +/// See [`Scope::spawn`] for details. +pub struct ScopedJoinHandle<'scope, T>(pub(crate) JoinInner, pub(crate) PhantomData<&'scope ()>); + +pub(super) struct ScopeData { + num_running_threads: AtomicUsize, + a_thread_panicked: AtomicBool, + /// Blocks main thread until all other threads finish execution. + /// Rust no longer has a simple semaphore so we use barrier instead. + main_thread_barrier: Barrier, +} + +impl ScopeData { + pub(super) fn increment_num_running_threads(&self) { + // We check for 'overflow' with usize::MAX / 2, to make sure there's no + // chance it overflows to 0, which would result in unsoundness. + if self.num_running_threads.fetch_add(1, Ordering::Relaxed) > usize::MAX / 2 { + // This can only reasonably happen by mem::forget()'ing a lot of ScopedJoinHandles. + self.decrement_num_running_threads(false); + panic!("too many running threads in thread scope"); + } + } + + pub(super) fn decrement_num_running_threads(&self, panic: bool) { + if panic { + self.a_thread_panicked.store(true, Ordering::Relaxed); + } + + if self.num_running_threads.fetch_sub(1, Ordering::Release) == 1 { + // Barrier is initialized with 2 and first wait is consumed by the main thread so this will decrease counter + // to 0 and wake it. + self.main_thread_barrier.wait(); + } } } +/// Create a scope for spawning scoped threads. +/// +/// The function passed to `scope` will be provided a [`Scope`] object, +/// through which scoped threads can be [spawned][`Scope::spawn`]. +/// +/// Unlike non-scoped threads, scoped threads can borrow non-`'static` data, +/// as the scope guarantees all threads will be joined at the end of the scope. +/// +/// All threads spawned within the scope that haven't been manually joined +/// will be automatically joined before this function returns. +/// +/// # Panics +/// +/// If any of the automatically joined threads panicked, this function will panic. +/// +/// If you want to handle panics from spawned threads, +/// [`join`][ScopedJoinHandle::join] them before the end of the scope. +/// +/// On wasm, this will panic on main thread because blocking join is not allowed. pub fn scope<'env, F, T>(f: F) -> T where F: for<'scope> FnOnce(&'scope Scope<'scope, 'env>) -> T, { + // Fail early to avoid flaky panics that depend on execution time + if !is_web_worker_thread() { + panic!("scope is not allowed on the main thread"); + } + // We put the `ScopeData` into an `Arc` so that other threads can finish their // `decrement_num_running_threads` even after this function returns. let scope = Scope { data: Arc::new(ScopeData { num_running_threads: AtomicUsize::new(0), - main_thread: current(), a_thread_panicked: AtomicBool::new(false), + // Initialize barrier with 2 slots: one for main thread and second for the waker. + main_thread_barrier: Barrier::new(2), }), env: PhantomData, scope: PhantomData, @@ -75,9 +114,7 @@ where // Wait until all the threads are finished. while scope.data.num_running_threads.load(Ordering::Acquire) != 0 { - // park(); - // TODO: Replaced by a wasm-friendly version of park() - sleep(Duration::from_millis(1)); + scope.data.main_thread_barrier.wait(); } // Throw any panic from `f`, or the return value of `f` if no thread panicked. @@ -90,24 +127,63 @@ where } } -pub struct ScopedJoinHandle<'scope, T>(pub(crate) crate::JoinHandle, pub(crate) PhantomData<&'scope ()>); +impl<'scope, 'env> Scope<'scope, 'env> { + /// Spawns a new thread within a scope, returning a [`ScopedJoinHandle`] for it. + /// + /// Unlike non-scoped threads, threads spawned with this function may + /// borrow non-`'static` data from the outside the scope. See [`scope`] for + /// details. + /// + /// The join handle provides a [`join`] method that can be used to join the spawned + /// thread. If the spawned thread panics, [`join`] will return an [`Err`] containing + /// the panic payload. + /// + /// If the join handle is dropped, the spawned thread will implicitly joined at the + /// end of the scope. In that case, if the spawned thread panics, [`scope`] will + /// panic after all threads are joined. + /// + /// This call will create a thread using default parameters of [`Builder`]. + /// If you want to specify the stack size or the name of the thread, use + /// [`Builder::spawn_scoped`] instead. + /// + /// # Panics + /// + /// Panics if the OS fails to create a thread; use [`Builder::spawn_scoped`] + /// to recover from such errors. + /// + /// [`join`]: ScopedJoinHandle::join + pub fn spawn(&'scope self, f: F) -> ScopedJoinHandle<'scope, T> + where + F: FnOnce() -> T + Send + 'scope, + T: Send + 'scope, + { + Builder::new().spawn_scoped(self, f).expect("failed to spawn thread") + } +} -impl<'scope, T> ScopedJoinHandle<'scope, T> { - pub fn join(self) -> std::io::Result { - self.0 - .join() - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, "")) +impl Builder { + /// Spawns a new scoped thread using the settings set through this `Builder`. + /// + /// Unlike [Scope::spawn], this method yields an [`io::Result`] to + /// capture any failure to create the thread at the OS level. + pub fn spawn_scoped<'scope, 'env, F, T>( + self, + scope: &'scope Scope<'scope, 'env>, + f: F, + ) -> std::io::Result> + where + F: FnOnce() -> T + Send + 'scope, + T: Send + 'scope, + { + Ok(ScopedJoinHandle( + unsafe { self.spawn_unchecked_(f, Some(scope.data.clone())) }?, + PhantomData, + )) } } -pub fn spawn_scoped<'scope, 'env, F, T>( - builder: crate::Builder, - scope: &'scope Scope<'scope, 'env>, - f: F, -) -> std::io::Result> -where - F: FnOnce() -> T + Send + 'scope, - T: Send + 'scope, -{ - Ok(ScopedJoinHandle(unsafe { builder.spawn_unchecked(f) }?, PhantomData)) +impl<'scope, T> ScopedJoinHandle<'scope, T> { + pub fn join(self) -> crate::Result { + self.0.join() + } } diff --git a/src/utils.rs b/src/utils.rs index 2a48515..919f122 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -5,13 +5,17 @@ use std::{ }; use wasm_bindgen::prelude::*; -use web_sys::{Blob, Url}; +use web_sys::{Blob, Url, WorkerGlobalScope}; pub fn available_parallelism() -> io::Result { // TODO: Use [Navigator::hardware_concurrency](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Navigator.html#method.hardware_concurrency) Ok(NonZeroUsize::new(8).unwrap()) } +pub fn is_web_worker_thread() -> bool { + js_sys::eval("self").unwrap().dyn_into::().is_ok() +} + #[cfg(feature = "es_modules")] #[wasm_bindgen(module = "/src/js/module_workers_polyfill.min.js")] extern "C" { @@ -35,34 +39,34 @@ pub fn get_worker_script(wasm_bindgen_shim_url: Option) -> String { static CACHED_URL: Mutex> = Mutex::new(None); if let Some(url) = CACHED_URL.lock_spin().unwrap().clone() { - url - } else { - // If wasm bindgen shim url is not provided, try to obtain one automatically - let wasm_bindgen_shim_url = wasm_bindgen_shim_url.unwrap_or_else(get_wasm_bindgen_shim_script_path); - - // Generate script from template - #[cfg(feature = "es_modules")] - let template = include_str!("js/web_worker_module.js"); - #[cfg(not(feature = "es_modules"))] - let template = include_str!("js/web_worker.js"); - - let script = template.replace("WASM_BINDGEN_SHIM_URL", &wasm_bindgen_shim_url); - - // Create url encoded blob - let arr = js_sys::Array::new(); - arr.set(0, JsValue::from_str(&script)); - let blob = Blob::new_with_str_sequence(&arr).unwrap(); - let url = Url::create_object_url_with_blob( - &blob - .slice_with_f64_and_f64_and_content_type(0.0, blob.size(), "text/javascript") - .unwrap(), - ) - .unwrap(); - - *CACHED_URL.lock_spin().unwrap() = Some(url.clone()); - - url + return url; } + + // If wasm bindgen shim url is not provided, try to obtain one automatically + let wasm_bindgen_shim_url = wasm_bindgen_shim_url.unwrap_or_else(get_wasm_bindgen_shim_script_path); + + // Generate script from template + #[cfg(feature = "es_modules")] + let template = include_str!("js/web_worker_module.js"); + #[cfg(not(feature = "es_modules"))] + let template = include_str!("js/web_worker.js"); + + let script = template.replace("WASM_BINDGEN_SHIM_URL", &wasm_bindgen_shim_url); + + // Create url encoded blob + let arr = js_sys::Array::new(); + arr.set(0, JsValue::from_str(&script)); + let blob = Blob::new_with_str_sequence(&arr).unwrap(); + let url = Url::create_object_url_with_blob( + &blob + .slice_with_f64_and_f64_and_content_type(0.0, blob.size(), "text/javascript") + .unwrap(), + ) + .unwrap(); + + *CACHED_URL.lock_spin().unwrap() = Some(url.clone()); + + url } /// A spin lock mutex extension. From 1097fe73fc1775d9ada326dae7021825104293d2 Mon Sep 17 00:00:00 2001 From: chemicstry Date: Fri, 10 Mar 2023 02:44:47 +0200 Subject: [PATCH 08/24] Major refactor to properly implement join guards --- .gitignore | 1 + Cargo.toml | 4 +- examples/simple.rs | 34 +- rust-toolchain.toml | 6 + src/lib.rs | 347 +------------- .../js/module_workers_polyfill.min.js | 0 src/{ => wasm32}/js/script_path.js | 0 src/{ => wasm32}/js/web_worker.js | 0 src/{ => wasm32}/js/web_worker_module.js | 0 src/wasm32/mod.rs | 448 ++++++++++++++++++ src/{ => wasm32}/scoped.rs | 39 +- src/wasm32/signal.rs | 65 +++ src/{ => wasm32}/utils.rs | 14 +- 13 files changed, 587 insertions(+), 371 deletions(-) create mode 100644 rust-toolchain.toml rename src/{ => wasm32}/js/module_workers_polyfill.min.js (100%) rename src/{ => wasm32}/js/script_path.js (100%) rename src/{ => wasm32}/js/web_worker.js (100%) rename src/{ => wasm32}/js/web_worker_module.js (100%) create mode 100644 src/wasm32/mod.rs rename src/{ => wasm32}/scoped.rs (84%) create mode 100644 src/wasm32/signal.rs rename src/{ => wasm32}/utils.rs (86%) diff --git a/.gitignore b/.gitignore index 3aadd9d..e43f6e2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ Cargo.lock /examples/target /examples-wasm-pack/target .vscode/ +!.vscode/settings.json diff --git a/Cargo.toml b/Cargo.toml index 638667d..9465fd9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,10 +25,12 @@ web-sys = { version = "0.3", features = [ "Worker", "WorkerType", "WorkerOptions", + "Window", + "Navigator", + "WorkerNavigator", ] } js-sys = "0.3" futures = "0.3" -async-channel = "1.4" [dev-dependencies] log = "0.4" diff --git a/examples/simple.rs b/examples/simple.rs index 59fe9f4..464b430 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,8 +1,5 @@ -#[cfg(not(target_arch = "wasm32"))] -use std::thread; use std::time::Duration; -#[cfg(target_arch = "wasm32")] use wasm_thread as thread; #[cfg(target_arch = "wasm32")] @@ -28,13 +25,17 @@ fn main() { #[cfg(not(target_arch = "wasm32"))] env_logger::init_from_env(env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info")); + log::info!("Available parallelism: {:?}", thread::available_parallelism()); + + let mut threads = vec![]; + for _ in 0..2 { - thread::spawn(|| { + threads.push(thread::spawn(|| { for i in 1..3 { log::info!("hi number {} from the spawned thread {:?}!", i, thread::current().id()); thread::sleep(Duration::from_millis(1)); } - }); + })); } for i in 1..3 { @@ -43,18 +44,24 @@ fn main() { // It's not possible to do a scope on the main thread, because blocking waits are not supported, but we can use // scope inside web workers. - thread::spawn(|| { + threads.push(thread::spawn(|| { log::info!("Start scope test on thread {:?}", thread::current().id()); let mut a = vec![1, 2, 3]; let mut x = 0; thread::scope(|s| { - s.spawn(|| { + let handle = s.spawn(|| { log::info!("hello from the first scoped thread {:?}", thread::current().id()); // We can borrow `a` here. - log::debug!("a = {:?}", &a); + log::info!("a = {:?}", &a); + // Return a subslice of borrowed `a` + &a[0..2] }); + + // Wait for the returned value from first thread + log::info!("a[0..2] = {:?}", handle.join().unwrap()); + s.spawn(|| { log::info!("hello from the second scoped thread {:?}", thread::current().id()); // We can even mutably borrow `x` here, @@ -71,6 +78,13 @@ fn main() { // After the scope, we can modify and access our variables again: a.push(4); assert_eq!(x, a.len()); - log::info!("Scope done x={} a.len()={}", x, a.len()); - }); + log::info!("Scope done x = {}, a.len() = {}", x, a.len()); + })); + + // Wait for all threads, otherwise program exits before threads finish execution. + // We can't do blocking join on wasm main thread though. + #[cfg(not(target_arch = "wasm32"))] + for handle in threads { + handle.join().unwrap(); + } } diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..f81cd8b --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,6 @@ +# Before upgrading check that everything is available on all tier1 targets here: +# https://rust-lang.github.io/rustup-components-history +[toolchain] +channel = "nightly-2023-02-07" +components = ["rust-src", "rustfmt"] +targets = ["wasm32-unknown-unknown"] diff --git a/src/lib.rs b/src/lib.rs index cd95064..fead769 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,342 +1,13 @@ -pub use std::thread::{current, sleep, Result, Thread, ThreadId}; -use std::{ - fmt, mem, - panic::{catch_unwind, AssertUnwindSafe}, - sync::{Arc, Mutex}, -}; - -use async_channel::Receiver; -use futures::executor::block_on; -pub use scoped::*; -pub use utils::*; -use utils::{get_worker_script, SpinLockMutex}; -use wasm_bindgen::prelude::*; -use web_sys::{DedicatedWorkerGlobalScope, Worker, WorkerOptions, WorkerType}; - -mod scoped; -mod utils; - -struct WebWorkerContext { - func: Box, -} - -/// Entry point for web workers -#[wasm_bindgen] -pub fn wasm_thread_entry_point(ptr: u32) { - let ctx = unsafe { Box::from_raw(ptr as *mut WebWorkerContext) }; - (ctx.func)(); - WorkerMessage::ThreadComplete.post(); -} - -static mut MAIN_THREAD_ID: Option = None; - -struct BuilderRequest { - builder: Builder, - context: WebWorkerContext, -} - -impl BuilderRequest { - pub unsafe fn spawn(self) { - self.builder.spawn_for_context(self.context); - } -} - -enum WorkerMessage { - SpawnThread(BuilderRequest), - ThreadComplete, -} - -impl WorkerMessage { - pub fn post(self) { - let req = Box::new(self); - let req = unsafe { std::mem::transmute::<_, f64>(Box::into_raw(req) as u64) }; - - js_sys::eval("self") - .unwrap() - .dyn_into::() - .unwrap() - .post_message(&JsValue::from(req)) - .unwrap(); - } -} - -static DEFAULT_BUILDER: Mutex> = Mutex::new(None); - -/// Thread factory, which can be used in order to configure the properties of a new thread. -#[derive(Debug, Clone)] -pub struct Builder { - // A name for the thread-to-be, for identification in panic messages - name: Option, - // A prefix for the thread-to-be, for identification in panic messages - prefix: Option, - // The size of the stack for the spawned thread in bytes - stack_size: Option, - // Url of the `wasm_bindgen` generated shim `.js` script to use as web worker entry point - wasm_bindgen_shim_url: Option, -} - -impl Default for Builder { - fn default() -> Self { - DEFAULT_BUILDER.lock_spin().unwrap().clone().unwrap_or(Self::empty()) - } -} - -impl Builder { - /// Creates a builder inheriting global configuration options set by [Self::set_default]. - pub fn new() -> Builder { - Builder::default() - } - - /// Creates a builder without inheriting global options set by [Self::set_default]. - pub fn empty() -> Builder { - Self { - name: None, - prefix: None, - stack_size: None, - wasm_bindgen_shim_url: None, - } - } - - /// Sets current values as global default for all new builders created with [Builder::new] or [Builder::default]. - pub fn set_default(self) { - *DEFAULT_BUILDER.lock_spin().unwrap() = Some(self); - } - - /// Sets the prefix of the thread. - pub fn prefix(mut self, prefix: String) -> Builder { - self.prefix = Some(prefix); - self - } - - /// Sets the name of the thread. - /// - /// If not set, the default name is autogenerated. - pub fn name(mut self, name: String) -> Builder { - self.name = Some(name); - self - } - - /// Sets the size of the stack (in bytes) for the new thread. - /// - /// # Warning - /// - /// This is currently not supported by wasm, but provided for API consistency with [std::thread]. - pub fn stack_size(mut self, size: usize) -> Builder { - self.stack_size = Some(size); - self - } - - /// Sets the URL of wasm_bindgen generated shim script. - pub fn wasm_bindgen_shim_url(mut self, url: String) -> Builder { - self.wasm_bindgen_shim_url = Some(url); - self - } - - /// Spawns a new thread by taking ownership of the `Builder`, and returns an - /// [`io::Result`] to its [`JoinHandle`]. - pub fn spawn(self, f: F) -> std::io::Result> - where - F: FnOnce() -> T, - F: Send + 'static, - T: Send + 'static, - { - unsafe { self.spawn_unchecked(f) } - } - - /// Spawns a new thread without any lifetime restrictions by taking ownership - /// of the `Builder`, and returns an [`io::Result`] to its [`JoinHandle`]. - /// - /// # Safety - /// - /// The caller has to ensure that no references in the supplied thread closure - /// or its return type can outlive the spawned thread's lifetime. This can be - /// guaranteed in two ways: - /// - /// - ensure that [`join`][`JoinHandle::join`] is called before any referenced - /// data is dropped - /// - use only types with `'static` lifetime bounds, i.e., those with no or only - /// `'static` references (both [`Builder::spawn`] - /// and [`spawn`] enforce this property statically) - pub unsafe fn spawn_unchecked<'a, F, T>(self, f: F) -> std::io::Result> - where - F: FnOnce() -> T, - F: Send + 'a, - T: Send + 'a, - { - Ok(JoinHandle(unsafe { self.spawn_unchecked_(f, None) }?)) - } - - unsafe fn spawn_unchecked_<'a, F, T>(self, f: F, scope: Option>) -> std::io::Result> - where - F: FnOnce() -> T, - F: Send + 'a, - T: Send + 'a, - { - let (sender, receiver) = async_channel::bounded(1); +#![cfg_attr(target_arch = "wasm32", feature(stdsimd))] - let main = Box::new(move || { - let res = catch_unwind(AssertUnwindSafe(|| f())); - sender.try_send(res).ok(); - }); - let context = WebWorkerContext { - func: mem::transmute::, Box>(main), - }; - - if MAIN_THREAD_ID.is_none() { - MAIN_THREAD_ID = Some(current().id()); - } - - if MAIN_THREAD_ID.unwrap_unchecked() == current().id() { - self.spawn_for_context(context); - } else { - WorkerMessage::SpawnThread(BuilderRequest { builder: self, context }).post(); - } - - if let Some(scope) = &scope { - scope.increment_num_running_threads(); - } - - Ok(JoinInner { receiver, scope }) - } - - unsafe fn spawn_for_context(self, ctx: WebWorkerContext) { - let Builder { - name, - prefix, - wasm_bindgen_shim_url, - .. - } = self; - - // Get worker script as URL encoded blob - let script = get_worker_script(wasm_bindgen_shim_url); - - // Todo: figure out how to set stack size - let mut options = WorkerOptions::new(); - match (name, prefix) { - (Some(name), Some(prefix)) => { - options.name(&format!("{}:{}", prefix, name)); - } - (Some(name), None) => { - options.name(&name); - } - (None, Some(prefix)) => { - let random = (js_sys::Math::random() * 10e10) as u64; - options.name(&format!("{}:{}", prefix, random)); - } - (None, None) => {} - }; - - #[cfg(feature = "es_modules")] - { - load_module_workers_polyfill(); - options.type_(WorkerType::Module); - } - #[cfg(not(feature = "es_modules"))] - { - options.type_(WorkerType::Classic); - } - - // Spawn the worker - let worker = Worker::new_with_options(script.as_str(), &options).unwrap(); - let worker_reference = std::rc::Rc::new(std::cell::Cell::new(None)); - let worker_reference_callback = worker_reference.clone(); - let callback = Closure::wrap(Box::new(move |x: &web_sys::MessageEvent| { - let req = Box::from_raw(std::mem::transmute::<_, u64>(x.data().as_f64().unwrap()) as *mut WorkerMessage); - match *req { - WorkerMessage::SpawnThread(builder) => { - builder.spawn(); - } - WorkerMessage::ThreadComplete => { - worker_reference.replace(None); - } - }; - }) as Box); - worker.set_onmessage(Some(callback.as_ref().unchecked_ref())); - callback.forget(); - - let ctx_ptr = Box::into_raw(Box::new(ctx)); - - // Pack shared wasm (module and memory) and work as a single JS array - let init = js_sys::Array::new(); - init.push(&wasm_bindgen::module()); - init.push(&wasm_bindgen::memory()); - init.push(&JsValue::from(ctx_ptr as u32)); - - // Send initialization message - worker_reference_callback.set(Some( - match worker.post_message(&init) { - Ok(()) => Ok(worker), - Err(e) => { - drop(Box::from_raw(ctx_ptr)); - Err(e) - } - } - .unwrap(), - )); - } -} - -/// Inner representation for JoinHandle -struct JoinInner { - receiver: Receiver>, - scope: Option>, -} - -impl JoinInner { - fn join(self) -> Result { - let res = block_on(self.receiver.recv()); - - if let Some(scope) = &self.scope { - scope.decrement_num_running_threads(false); - } - - res.unwrap() - } - - async fn join_async(self) -> Result { - let res = self.receiver.recv().await; - - if let Some(scope) = &self.scope { - scope.decrement_num_running_threads(false); - } - - res.unwrap() - } -} - -/// An owned permission to join on a thread (block on its termination). -pub struct JoinHandle(JoinInner); - -impl JoinHandle { - /// Extracts a handle to the underlying thread. - pub fn thread(&self) -> &Thread { - unimplemented!(); - //&self.0.thread - } - - /// Waits for the associated thread to finish. - pub fn join(self) -> Result { - self.0.join() - } +// Import reusable APIs from std +pub use std::thread::{current, sleep, Result, Thread, ThreadId}; - /// Waits for the associated thread to finish asynchronously. - pub async fn join_async(self) -> Result { - self.0.join_async().await - } -} +#[cfg(target_arch = "wasm32")] +mod wasm32; -impl fmt::Debug for JoinHandle { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.pad("JoinHandle { .. }") - } -} +#[cfg(not(target_arch = "wasm32"))] +pub use std::thread::*; -/// Spawns a new thread, returning a JoinHandle for it. -pub fn spawn(f: F) -> JoinHandle -where - F: FnOnce() -> T, - F: Send + 'static, - T: Send + 'static, -{ - Builder::new().spawn(f).expect("failed to spawn thread") -} +#[cfg(target_arch = "wasm32")] +pub use wasm32::*; diff --git a/src/js/module_workers_polyfill.min.js b/src/wasm32/js/module_workers_polyfill.min.js similarity index 100% rename from src/js/module_workers_polyfill.min.js rename to src/wasm32/js/module_workers_polyfill.min.js diff --git a/src/js/script_path.js b/src/wasm32/js/script_path.js similarity index 100% rename from src/js/script_path.js rename to src/wasm32/js/script_path.js diff --git a/src/js/web_worker.js b/src/wasm32/js/web_worker.js similarity index 100% rename from src/js/web_worker.js rename to src/wasm32/js/web_worker.js diff --git a/src/js/web_worker_module.js b/src/wasm32/js/web_worker_module.js similarity index 100% rename from src/js/web_worker_module.js rename to src/wasm32/js/web_worker_module.js diff --git a/src/wasm32/mod.rs b/src/wasm32/mod.rs new file mode 100644 index 0000000..1170c4b --- /dev/null +++ b/src/wasm32/mod.rs @@ -0,0 +1,448 @@ +pub use std::thread::{current, sleep, Result, Thread, ThreadId}; +use std::{ + cell::UnsafeCell, + fmt, + marker::PhantomData, + mem, + panic::{catch_unwind, AssertUnwindSafe}, + rc::Rc, + sync::{Arc, Mutex}, +}; + +use scoped::ScopeData; +pub use scoped::{scope, Scope, ScopedJoinHandle}; +use signal::Signal; +use utils::SpinLockMutex; +pub use utils::{available_parallelism, get_wasm_bindgen_shim_script_path, get_worker_script, is_web_worker_thread}; +use wasm_bindgen::prelude::*; +use web_sys::{DedicatedWorkerGlobalScope, Worker, WorkerOptions, WorkerType}; + +mod scoped; +mod signal; +mod utils; + +struct WebWorkerContext { + func: Box, +} + +/// Entry point for web workers +#[wasm_bindgen] +pub fn wasm_thread_entry_point(ptr: u32) { + let ctx = unsafe { Box::from_raw(ptr as *mut WebWorkerContext) }; + (ctx.func)(); + WorkerMessage::ThreadComplete.post(); +} + +/// Used to relay spawn requests from web workers to main thread +struct BuilderRequest { + builder: Builder, + context: WebWorkerContext, +} + +impl BuilderRequest { + pub unsafe fn spawn(self) { + self.builder.spawn_for_context(self.context); + } +} + +/// Web worker to main thread messages +enum WorkerMessage { + /// Request to spawn thread + SpawnThread(BuilderRequest), + /// Thread has completed execution + ThreadComplete, +} + +impl WorkerMessage { + pub fn post(self) { + let req = Box::new(self); + + js_sys::eval("self") + .unwrap() + .dyn_into::() + .unwrap() + .post_message(&JsValue::from(Box::into_raw(req) as u32)) + .unwrap(); + } +} + +static DEFAULT_BUILDER: Mutex> = Mutex::new(None); + +/// Thread factory, which can be used in order to configure the properties of a new thread. +#[derive(Debug, Clone)] +pub struct Builder { + // A name for the thread-to-be, for identification in panic messages + name: Option, + // A prefix for the thread-to-be, for identification in panic messages + prefix: Option, + // The size of the stack for the spawned thread in bytes + stack_size: Option, + // Url of the `wasm_bindgen` generated shim `.js` script to use as web worker entry point + wasm_bindgen_shim_url: Option, +} + +impl Default for Builder { + fn default() -> Self { + DEFAULT_BUILDER.lock_spin().unwrap().clone().unwrap_or(Self::empty()) + } +} + +impl Builder { + /// Creates a builder inheriting global configuration options set by [Self::set_default]. + pub fn new() -> Builder { + Builder::default() + } + + /// Creates a builder without inheriting global options set by [Self::set_default]. + pub fn empty() -> Builder { + Self { + name: None, + prefix: None, + stack_size: None, + wasm_bindgen_shim_url: None, + } + } + + /// Sets current values as global default for all new builders created with [Builder::new] or [Builder::default]. + pub fn set_default(self) { + *DEFAULT_BUILDER.lock_spin().unwrap() = Some(self); + } + + /// Sets the prefix of the thread. + pub fn prefix(mut self, prefix: String) -> Builder { + self.prefix = Some(prefix); + self + } + + /// Sets the name of the thread. + /// + /// If not set, the default name is autogenerated. + pub fn name(mut self, name: String) -> Builder { + self.name = Some(name); + self + } + + /// Sets the size of the stack (in bytes) for the new thread. + /// + /// # Warning + /// + /// This is currently not supported by wasm, but provided for API consistency with [std::thread]. + pub fn stack_size(mut self, size: usize) -> Builder { + self.stack_size = Some(size); + self + } + + /// Sets the URL of wasm_bindgen generated shim script. + pub fn wasm_bindgen_shim_url(mut self, url: String) -> Builder { + self.wasm_bindgen_shim_url = Some(url); + self + } + + /// Spawns a new thread by taking ownership of the `Builder`, and returns an + /// [`io::Result`] to its [`JoinHandle`]. + pub fn spawn(self, f: F) -> std::io::Result> + where + F: FnOnce() -> T, + F: Send + 'static, + T: Send + 'static, + { + unsafe { self.spawn_unchecked(f) } + } + + /// Spawns a new thread without any lifetime restrictions by taking ownership + /// of the `Builder`, and returns an [`io::Result`] to its [`JoinHandle`]. + /// + /// # Safety + /// + /// The caller has to ensure that no references in the supplied thread closure + /// or its return type can outlive the spawned thread's lifetime. This can be + /// guaranteed in two ways: + /// + /// - ensure that [`join`][`JoinHandle::join`] is called before any referenced + /// data is dropped + /// - use only types with `'static` lifetime bounds, i.e., those with no or only + /// `'static` references (both [`Builder::spawn`] + /// and [`spawn`] enforce this property statically) + pub unsafe fn spawn_unchecked<'a, F, T>(self, f: F) -> std::io::Result> + where + F: FnOnce() -> T, + F: Send + 'a, + T: Send + 'a, + { + Ok(JoinHandle(unsafe { self.spawn_unchecked_(f, None) }?)) + } + + pub(crate) unsafe fn spawn_unchecked_<'a, 'scope, F, T>( + self, + f: F, + scope_data: Option>, + ) -> std::io::Result> + where + F: FnOnce() -> T, + F: Send + 'a, + T: Send + 'a, + 'scope: 'a, + { + let my_signal = Arc::new(Signal::new()); + let their_signal = my_signal.clone(); + + let my_packet: Arc> = Arc::new(Packet { + scope: scope_data, + result: UnsafeCell::new(None), + _marker: PhantomData, + }); + let their_packet = my_packet.clone(); + + // Pass `f` in `MaybeUninit` because actually that closure might *run longer than the lifetime of `F`*. + // See for more details. + // To prevent leaks we use a wrapper that drops its contents. + #[repr(transparent)] + struct MaybeDangling(mem::MaybeUninit); + impl MaybeDangling { + fn new(x: T) -> Self { + MaybeDangling(mem::MaybeUninit::new(x)) + } + fn into_inner(self) -> T { + // SAFETY: we are always initiailized. + let ret = unsafe { self.0.assume_init_read() }; + // Make sure we don't drop. + mem::forget(self); + ret + } + } + impl Drop for MaybeDangling { + fn drop(&mut self) { + // SAFETY: we are always initiailized. + unsafe { self.0.assume_init_drop() }; + } + } + + let f = MaybeDangling::new(f); + let main = Box::new(move || { + // SAFETY: we constructed `f` initialized. + let f = f.into_inner(); + // Execute the closure and catch any panics + let try_result = catch_unwind(AssertUnwindSafe(|| f())); + // SAFETY: `their_packet` as been built just above and moved by the + // closure (it is an Arc<...>) and `my_packet` will be stored in the + // same `JoinInner` as this closure meaning the mutation will be + // safe (not modify it and affect a value far away). + unsafe { *their_packet.result.get() = Some(try_result) }; + // Here `their_packet` gets dropped, and if this is the last `Arc` for that packet that + // will call `decrement_num_running_threads` and therefore signal that this thread is + // done. + drop(their_packet); + // Notify waiting handles + their_signal.signal(); + // Here, the lifetime `'a` and even `'scope` can end. `main` keeps running for a bit + // after that before returning itself. + }); + + // Erase lifetime + let context = WebWorkerContext { + func: mem::transmute::, Box>(main), + }; + + if is_web_worker_thread() { + WorkerMessage::SpawnThread(BuilderRequest { builder: self, context }).post(); + } else { + self.spawn_for_context(context); + } + + if let Some(scope) = &my_packet.scope { + scope.increment_num_running_threads(); + } + + Ok(JoinInner { + signal: my_signal, + packet: my_packet, + }) + } + + unsafe fn spawn_for_context(self, ctx: WebWorkerContext) { + let Builder { + name, + prefix, + wasm_bindgen_shim_url, + .. + } = self; + + // Get worker script as URL encoded blob + let script = get_worker_script(wasm_bindgen_shim_url); + + // Todo: figure out how to set stack size + let mut options = WorkerOptions::new(); + match (name, prefix) { + (Some(name), Some(prefix)) => { + options.name(&format!("{}:{}", prefix, name)); + } + (Some(name), None) => { + options.name(&name); + } + (None, Some(prefix)) => { + let random = (js_sys::Math::random() * 10e10) as u64; + options.name(&format!("{}:{}", prefix, random)); + } + (None, None) => {} + }; + + #[cfg(feature = "es_modules")] + { + load_module_workers_polyfill(); + options.type_(WorkerType::Module); + } + #[cfg(not(feature = "es_modules"))] + { + options.type_(WorkerType::Classic); + } + + // Spawn the worker + let worker = Rc::new(Worker::new_with_options(script.as_str(), &options).unwrap()); + + // Make copy and keep a reference in callback handler so that GC does not despawn worker + let mut their_worker = Some(worker.clone()); + + let callback = Closure::wrap(Box::new(move |x: &web_sys::MessageEvent| { + // All u32 bits map to f64 mantisa so it's safe to cast like that + let req = Box::from_raw(x.data().as_f64().unwrap() as u32 as *mut WorkerMessage); + + match *req { + WorkerMessage::SpawnThread(builder) => { + builder.spawn(); + } + WorkerMessage::ThreadComplete => { + // Drop worker reference so it can be cleaned up by GC + their_worker.take(); + } + }; + }) as Box); + worker.set_onmessage(Some(callback.as_ref().unchecked_ref())); + + // TODO: cleanup this leak somehow + callback.forget(); + + let ctx_ptr = Box::into_raw(Box::new(ctx)); + + // Pack shared wasm (module and memory) and work as a single JS array + let init = js_sys::Array::new(); + init.push(&wasm_bindgen::module()); + init.push(&wasm_bindgen::memory()); + init.push(&JsValue::from(ctx_ptr as u32)); + + // Send initialization message + match worker.post_message(&init) { + Ok(()) => Ok(worker), + Err(e) => { + drop(Box::from_raw(ctx_ptr)); + Err(e) + } + } + .unwrap(); + } +} + +// This packet is used to communicate the return value between the spawned +// thread and the rest of the program. It is shared through an `Arc` and +// there's no need for a mutex here because synchronization happens with `join()` +// (the caller will never read this packet until the thread has exited). +// +// An Arc to the packet is stored into a `JoinInner` which in turns is placed +// in `JoinHandle`. +struct Packet<'scope, T> { + scope: Option>, + result: UnsafeCell>>, + _marker: PhantomData>, +} + +// Due to the usage of `UnsafeCell` we need to manually implement Sync. +// The type `T` should already always be Send (otherwise the thread could not +// have been created) and the Packet is Sync because all access to the +// `UnsafeCell` synchronized (by the `join()` boundary), and `ScopeData` is Sync. +unsafe impl<'scope, T: Send> Sync for Packet<'scope, T> {} + +impl<'scope, T> Drop for Packet<'scope, T> { + fn drop(&mut self) { + // If this packet was for a thread that ran in a scope, the thread + // panicked, and nobody consumed the panic payload, we make sure + // the scope function will panic. + let unhandled_panic = matches!(self.result.get_mut(), Some(Err(_))); + // Drop the result without causing unwinding. + // This is only relevant for threads that aren't join()ed, as + // join() will take the `result` and set it to None, such that + // there is nothing left to drop here. + // If this panics, we should handle that, because we're outside the + // outermost `catch_unwind` of our thread. + // We just abort in that case, since there's nothing else we can do. + // (And even if we tried to handle it somehow, we'd also need to handle + // the case where the panic payload we get out of it also panics on + // drop, and so on. See issue #86027.) + if let Err(_) = catch_unwind(AssertUnwindSafe(|| { + *self.result.get_mut() = None; + })) { + panic!("thread result panicked on drop"); + } + // Book-keeping so the scope knows when it's done. + if let Some(scope) = &self.scope { + // Now that there will be no more user code running on this thread + // that can use 'scope, mark the thread as 'finished'. + // It's important we only do this after the `result` has been dropped, + // since dropping it might still use things it borrowed from 'scope. + scope.decrement_num_running_threads(unhandled_panic); + } + } +} + +/// Inner representation for JoinHandle +pub(crate) struct JoinInner<'scope, T> { + packet: Arc>, + signal: Arc, +} + +impl<'scope, T> JoinInner<'scope, T> { + pub fn join(mut self) -> Result { + self.signal.wait(); + Arc::get_mut(&mut self.packet).unwrap().result.get_mut().take().unwrap() + } + + pub async fn join_async(mut self) -> Result { + self.signal.wait_async().await; + Arc::get_mut(&mut self.packet).unwrap().result.get_mut().take().unwrap() + } +} + +/// An owned permission to join on a thread (block on its termination). +pub struct JoinHandle(JoinInner<'static, T>); + +impl JoinHandle { + /// Extracts a handle to the underlying thread. + pub fn thread(&self) -> &Thread { + unimplemented!(); + //&self.0.thread + } + + /// Waits for the associated thread to finish. + pub fn join(self) -> Result { + self.0.join() + } + + /// Waits for the associated thread to finish asynchronously. + pub async fn join_async(self) -> Result { + self.0.join_async().await + } +} + +impl fmt::Debug for JoinHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad("JoinHandle { .. }") + } +} + +/// Spawns a new thread, returning a JoinHandle for it. +pub fn spawn(f: F) -> JoinHandle +where + F: FnOnce() -> T, + F: Send + 'static, + T: Send + 'static, +{ + Builder::new().spawn(f).expect("failed to spawn thread") +} diff --git a/src/scoped.rs b/src/wasm32/scoped.rs similarity index 84% rename from src/scoped.rs rename to src/wasm32/scoped.rs index 0b9468b..5396ebf 100644 --- a/src/scoped.rs +++ b/src/wasm32/scoped.rs @@ -3,11 +3,11 @@ use std::{ panic::{catch_unwind, resume_unwind, AssertUnwindSafe}, sync::{ atomic::{AtomicBool, AtomicUsize, Ordering}, - Arc, Barrier, + Arc, }, }; -use crate::{is_web_worker_thread, Builder, JoinInner}; +use super::{signal::Signal, utils::is_web_worker_thread, Builder, JoinInner}; /// A scope to spawn scoped threads in. /// @@ -34,18 +34,16 @@ pub struct Scope<'scope, 'env: 'scope> { /// An owned permission to join on a scoped thread (block on its termination). /// /// See [`Scope::spawn`] for details. -pub struct ScopedJoinHandle<'scope, T>(pub(crate) JoinInner, pub(crate) PhantomData<&'scope ()>); +pub struct ScopedJoinHandle<'scope, T>(JoinInner<'scope, T>); -pub(super) struct ScopeData { +pub(crate) struct ScopeData { num_running_threads: AtomicUsize, a_thread_panicked: AtomicBool, - /// Blocks main thread until all other threads finish execution. - /// Rust no longer has a simple semaphore so we use barrier instead. - main_thread_barrier: Barrier, + signal: Signal, } impl ScopeData { - pub(super) fn increment_num_running_threads(&self) { + pub(crate) fn increment_num_running_threads(&self) { // We check for 'overflow' with usize::MAX / 2, to make sure there's no // chance it overflows to 0, which would result in unsoundness. if self.num_running_threads.fetch_add(1, Ordering::Relaxed) > usize::MAX / 2 { @@ -55,15 +53,14 @@ impl ScopeData { } } - pub(super) fn decrement_num_running_threads(&self, panic: bool) { + pub(crate) fn decrement_num_running_threads(&self, panic: bool) { if panic { self.a_thread_panicked.store(true, Ordering::Relaxed); } if self.num_running_threads.fetch_sub(1, Ordering::Release) == 1 { - // Barrier is initialized with 2 and first wait is consumed by the main thread so this will decrease counter - // to 0 and wake it. - self.main_thread_barrier.wait(); + // All threads have terminated + self.signal.signal(); } } } @@ -102,8 +99,7 @@ where data: Arc::new(ScopeData { num_running_threads: AtomicUsize::new(0), a_thread_panicked: AtomicBool::new(false), - // Initialize barrier with 2 slots: one for main thread and second for the waker. - main_thread_barrier: Barrier::new(2), + signal: Signal::new(), }), env: PhantomData, scope: PhantomData, @@ -114,7 +110,7 @@ where // Wait until all the threads are finished. while scope.data.num_running_threads.load(Ordering::Acquire) != 0 { - scope.data.main_thread_barrier.wait(); + scope.data.signal.wait(); } // Throw any panic from `f`, or the return value of `f` if no thread panicked. @@ -175,15 +171,18 @@ impl Builder { F: FnOnce() -> T + Send + 'scope, T: Send + 'scope, { - Ok(ScopedJoinHandle( - unsafe { self.spawn_unchecked_(f, Some(scope.data.clone())) }?, - PhantomData, - )) + Ok(ScopedJoinHandle(unsafe { + self.spawn_unchecked_(f, Some(scope.data.clone())) + }?)) } } impl<'scope, T> ScopedJoinHandle<'scope, T> { - pub fn join(self) -> crate::Result { + pub fn join(self) -> super::Result { self.0.join() } + + pub async fn join_async(self) -> super::Result { + self.0.join_async().await + } } diff --git a/src/wasm32/signal.rs b/src/wasm32/signal.rs new file mode 100644 index 0000000..0d7c27f --- /dev/null +++ b/src/wasm32/signal.rs @@ -0,0 +1,65 @@ +use std::{ + arch::wasm32, + sync::{ + atomic::{AtomicU32, Ordering}, + Mutex, + }, + task::{Poll, Waker}, +}; + +use futures::future::poll_fn; + +use super::utils::SpinLockMutex; + +/// A combined sync/async synchronization primitive that allows waiting for a condition. +pub struct Signal { + waiters: Mutex>, + // Starts with 0 and changes to 1 when signaled + value: AtomicU32, +} + +impl Signal { + pub fn new() -> Self { + Self { + waiters: Mutex::new(Default::default()), + value: AtomicU32::new(0), + } + } + + /// Sends a signal and unlocks all waiters. + pub fn signal(&self) { + self.value.store(1, Ordering::SeqCst); + + // Wake all blocking waiters + unsafe { + wasm32::memory_atomic_notify(&self.value as *const AtomicU32 as *mut i32, i32::MAX as u32); + } + + // Wake all async waiters + for waiter in self.waiters.lock_spin().unwrap().drain(..) { + waiter.wake(); + } + } + + /// Synchronously waits until [Self::signal] is called. + pub fn wait(&self) { + while self.value.load(Ordering::Relaxed) == 0 { + unsafe { + wasm32::memory_atomic_wait32(&self.value as *const AtomicU32 as *mut i32, 0, -1); + } + } + } + + /// Asynchronously waits until [Self::signal] is called. + pub async fn wait_async(&self) { + poll_fn(|cx| { + if self.value.load(Ordering::Relaxed) == 1 { + Poll::Ready(()) + } else { + self.waiters.lock_spin().unwrap().push(cx.waker().clone()); + Poll::Pending + } + }) + .await + } +} diff --git a/src/utils.rs b/src/wasm32/utils.rs similarity index 86% rename from src/utils.rs rename to src/wasm32/utils.rs index 919f122..0228079 100644 --- a/src/utils.rs +++ b/src/wasm32/utils.rs @@ -8,8 +8,18 @@ use wasm_bindgen::prelude::*; use web_sys::{Blob, Url, WorkerGlobalScope}; pub fn available_parallelism() -> io::Result { - // TODO: Use [Navigator::hardware_concurrency](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Navigator.html#method.hardware_concurrency) - Ok(NonZeroUsize::new(8).unwrap()) + if let Some(window) = web_sys::window() { + return Ok(NonZeroUsize::new(window.navigator().hardware_concurrency() as usize).unwrap()); + } + + if let Ok(worker) = js_sys::eval("self").unwrap().dyn_into::() { + return Ok(NonZeroUsize::new(worker.navigator().hardware_concurrency() as usize).unwrap()); + } + + Err(io::Error::new( + io::ErrorKind::Unsupported, + "hardware_concurrency unsupported", + )) } pub fn is_web_worker_thread() -> bool { From 3e032fb565dc52210e4df801b1bec5cdd45b74e5 Mon Sep 17 00:00:00 2001 From: chemicstry Date: Fri, 10 Mar 2023 14:24:59 +0200 Subject: [PATCH 09/24] Simplify examples without dummy main wasm-bindgen no longer autostarts main on worker threads: https://github.com/rustwasm/wasm-bindgen/pull/3236 --- Cargo.toml | 2 +- README.md | 6 +++- examples-wasm-pack/Cargo.toml | 9 ++---- examples-wasm-pack/module/simple.html | 26 +++++++-------- examples-wasm-pack/no-module/simple.html | 25 ++++++++------- examples-wasm-pack/src/lib.rs | 41 ++++-------------------- examples/simple.html | 4 +-- examples/simple.rs | 21 +++--------- src/wasm32/mod.rs | 2 +- src/wasm32/utils.rs | 4 +-- 10 files changed, 48 insertions(+), 92 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9465fd9..4dd1ec4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,5 +37,5 @@ log = "0.4" env_logger = "0.7" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] -console_log = { version = "0.2", features = ["color"] } +console_log = { version = "1.0", features = ["color"] } console_error_panic_hook = "0.1" diff --git a/README.md b/README.md index d651758..93ad3ef 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,11 @@ cargo install wasm-bindgen-cli #### wasm-pack -- Build with `./examples-wasm-pack/web-build.sh` for an example targeting `web`, and `./examples/wasm-pack/web-build-no-module.sh` for an example targeting `no-modules`. +- Install `wasm-pack`: +```bash +cargo install wasm-pack +``` +- Build with `./examples-wasm-pack/web-build.sh` for an example targeting `web`, and `./examples-wasm-pack/web-build-no-module.sh` for an example targeting `no-modules`. - Serve `./examples-wasm-pack/module` or `./examples-wasm-pack/no-module`, respectively, over HTTP and open `simple.html` in browser. Inspect console output. ### Example output diff --git a/examples-wasm-pack/Cargo.toml b/examples-wasm-pack/Cargo.toml index 385c8fa..4f0d600 100644 --- a/examples-wasm-pack/Cargo.toml +++ b/examples-wasm-pack/Cargo.toml @@ -9,13 +9,8 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -wasm_thread = { path = "../." } +wasm_thread = { path = "../" } log = "0.4" -env_logger = "0.7" wasm-bindgen = "0.2" -web-sys = { version = "0.3", features = ["Blob", "DedicatedWorkerGlobalScope", "MessageEvent", "Url", "Worker", "WorkerType", "WorkerOptions"]} -js-sys = "0.3" -futures = "0.3" -async-channel = "1.4" -console_log = { version = "0.2", features = ["color"] } +console_log = { version = "1.0", features = ["color"] } console_error_panic_hook = "0.1" diff --git a/examples-wasm-pack/module/simple.html b/examples-wasm-pack/module/simple.html index 827da21..67626a7 100644 --- a/examples-wasm-pack/module/simple.html +++ b/examples-wasm-pack/module/simple.html @@ -1,14 +1,14 @@ - - - - - - - + + + + + + + + + + \ No newline at end of file diff --git a/examples-wasm-pack/no-module/simple.html b/examples-wasm-pack/no-module/simple.html index 6c17347..c278964 100644 --- a/examples-wasm-pack/no-module/simple.html +++ b/examples-wasm-pack/no-module/simple.html @@ -1,13 +1,14 @@ - - - - - - - - + + + + + + + + + + + \ No newline at end of file diff --git a/examples-wasm-pack/src/lib.rs b/examples-wasm-pack/src/lib.rs index b738159..332d132 100644 --- a/examples-wasm-pack/src/lib.rs +++ b/examples-wasm-pack/src/lib.rs @@ -1,52 +1,23 @@ use std::time::Duration; -#[cfg(not(target_arch = "wasm32"))] -use std::thread; -#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; use wasm_thread as thread; -#[cfg(target_arch = "wasm32")] -mod wasm { - use crate::main; - use wasm_bindgen::prelude::*; - - // Prevent `wasm_bindgen` from autostarting main on all spawned threads - #[wasm_bindgen(start)] - pub fn dummy_main() {} - - // Export explicit run function to start main - #[wasm_bindgen] - pub fn run() { - console_log::init().unwrap(); - console_error_panic_hook::set_once(); - main(); - } -} - +#[wasm_bindgen(start)] fn main() { - #[cfg(not(target_arch = "wasm32"))] - env_logger::init_from_env( - env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"), - ); + console_log::init().unwrap(); + console_error_panic_hook::set_once(); for _ in 0..2 { thread::spawn(|| { for i in 1..3 { - log::info!( - "hi number {} from the spawned thread {:?}!", - i, - thread::current().id() - ); + log::info!("hi number {} from the spawned thread {:?}!", i, thread::current().id()); thread::sleep(Duration::from_millis(1)); } }); } for i in 1..3 { - log::info!( - "hi number {} from the main thread {:?}!", - i, - thread::current().id() - ); + log::info!("hi number {} from the main thread {:?}!", i, thread::current().id()); } } diff --git a/examples/simple.html b/examples/simple.html index 08969a7..c278964 100644 --- a/examples/simple.html +++ b/examples/simple.html @@ -7,9 +7,7 @@ diff --git a/examples/simple.rs b/examples/simple.rs index 464b430..600d6d1 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -2,26 +2,13 @@ use std::time::Duration; use wasm_thread as thread; -#[cfg(target_arch = "wasm32")] -mod wasm { - use wasm_bindgen::prelude::*; - - use crate::main; - - // Prevent `wasm_bindgen` from autostarting main on all spawned threads - #[wasm_bindgen(start)] - pub fn dummy_main() {} - - // Export explicit run function to start main - #[wasm_bindgen] - pub fn run() { +fn main() { + #[cfg(target_arch = "wasm32")] + { console_log::init().unwrap(); console_error_panic_hook::set_once(); - main(); } -} -fn main() { #[cfg(not(target_arch = "wasm32"))] env_logger::init_from_env(env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info")); @@ -82,7 +69,7 @@ fn main() { })); // Wait for all threads, otherwise program exits before threads finish execution. - // We can't do blocking join on wasm main thread though. + // We can't do blocking join on wasm main thread though, but the browser window will continue running. #[cfg(not(target_arch = "wasm32"))] for handle in threads { handle.join().unwrap(); diff --git a/src/wasm32/mod.rs b/src/wasm32/mod.rs index 1170c4b..48e3fca 100644 --- a/src/wasm32/mod.rs +++ b/src/wasm32/mod.rs @@ -288,7 +288,7 @@ impl Builder { #[cfg(feature = "es_modules")] { - load_module_workers_polyfill(); + utils::load_module_workers_polyfill(); options.type_(WorkerType::Module); } #[cfg(not(feature = "es_modules"))] diff --git a/src/wasm32/utils.rs b/src/wasm32/utils.rs index 0228079..11da5e2 100644 --- a/src/wasm32/utils.rs +++ b/src/wasm32/utils.rs @@ -27,9 +27,9 @@ pub fn is_web_worker_thread() -> bool { } #[cfg(feature = "es_modules")] -#[wasm_bindgen(module = "/src/js/module_workers_polyfill.min.js")] +#[wasm_bindgen(module = "/src/wasm32/js/module_workers_polyfill.min.js")] extern "C" { - fn load_module_workers_polyfill(); + pub fn load_module_workers_polyfill(); } /// Extracts path of the `wasm_bindgen` generated .js shim script. From 9d50823f9dd5327081b1fa2ed8a274590cdb3592 Mon Sep 17 00:00:00 2001 From: chemicstry Date: Fri, 10 Mar 2023 14:55:46 +0200 Subject: [PATCH 10/24] Remove unneeded flags in scripts --- examples-wasm-pack/web-build-no-module.ps1 | 1 - examples-wasm-pack/web-build-no-module.sh | 3 +-- examples-wasm-pack/web-build.ps1 | 1 - examples-wasm-pack/web-build.sh | 3 +-- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/examples-wasm-pack/web-build-no-module.ps1 b/examples-wasm-pack/web-build-no-module.ps1 index cf5917d..8cbfd89 100644 --- a/examples-wasm-pack/web-build-no-module.ps1 +++ b/examples-wasm-pack/web-build-no-module.ps1 @@ -1,2 +1 @@ -$env:RUSTFLAGS="-C target-feature=+atomics,+bulk-memory,+mutable-globals" wasm-pack build --dev --out-dir ./no-module/target --target no-modules diff --git a/examples-wasm-pack/web-build-no-module.sh b/examples-wasm-pack/web-build-no-module.sh index 9fa6bac..1fc58d4 100644 --- a/examples-wasm-pack/web-build-no-module.sh +++ b/examples-wasm-pack/web-build-no-module.sh @@ -1,4 +1,3 @@ #!/bin/sh -RUSTFLAGS="-C target-feature=+atomics,+bulk-memory,+mutable-globals" \ - wasm-pack build --dev --out-dir ./no-module/target --target no-modules +wasm-pack build --dev --out-dir ./no-module/target --target no-modules diff --git a/examples-wasm-pack/web-build.ps1 b/examples-wasm-pack/web-build.ps1 index 89d0e0a..35d7793 100644 --- a/examples-wasm-pack/web-build.ps1 +++ b/examples-wasm-pack/web-build.ps1 @@ -1,2 +1 @@ -$env:RUSTFLAGS="-C target-feature=+atomics,+bulk-memory,+mutable-globals" wasm-pack build --dev --out-dir ./module/target --target web --features wasm_thread/es_modules diff --git a/examples-wasm-pack/web-build.sh b/examples-wasm-pack/web-build.sh index a3e0e7c..965bef5 100644 --- a/examples-wasm-pack/web-build.sh +++ b/examples-wasm-pack/web-build.sh @@ -1,4 +1,3 @@ #!/bin/sh -RUSTFLAGS="-C target-feature=+atomics,+bulk-memory,+mutable-globals" \ - wasm-pack build --dev --out-dir ./module/target --target web --features wasm_thread/es_modules +wasm-pack build --dev --out-dir ./module/target --target web --features wasm_thread/es_modules From 2b6105ef70d143c0151ba6fe35c96b97f765e942 Mon Sep 17 00:00:00 2001 From: chemicstry Date: Fri, 10 Mar 2023 14:58:21 +0200 Subject: [PATCH 11/24] Use modules by default --- Cargo.toml | 4 +++- build_wasm.sh | 2 +- examples-wasm-pack/.cargo/Config.toml | 4 ++-- examples-wasm-pack/Cargo.toml | 2 +- examples/simple.html | 6 +++--- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4dd1ec4..59b5ffe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ categories = ["concurrency", "wasm"] readme = "README.md" [features] +default = ["es_modules"] es_modules = [] [dependencies] @@ -34,7 +35,8 @@ futures = "0.3" [dev-dependencies] log = "0.4" -env_logger = "0.7" +env_logger = "0.10" +wasm-bindgen-test = "0.3" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] console_log = { version = "1.0", features = ["color"] } diff --git a/build_wasm.sh b/build_wasm.sh index ae988b2..cafdd88 100755 --- a/build_wasm.sh +++ b/build_wasm.sh @@ -20,4 +20,4 @@ RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals' \ wasm-bindgen \ target/wasm32-unknown-unknown/release/examples/simple.wasm \ --out-dir ./examples/target/ \ - --target no-modules + --target web diff --git a/examples-wasm-pack/.cargo/Config.toml b/examples-wasm-pack/.cargo/Config.toml index ad87c56..bbe29f9 100644 --- a/examples-wasm-pack/.cargo/Config.toml +++ b/examples-wasm-pack/.cargo/Config.toml @@ -2,7 +2,7 @@ target = "wasm32-unknown-unknown" [target.'cfg(target_arch = "wasm32")'] -rustflags = ["-C", "target-feature=+simd128,+atomics,+bulk-memory,+mutable-globals"] +rustflags = ["-C", "target-feature=+atomics,+bulk-memory,+mutable-globals"] [unstable] -build-std = ["panic_abort", "std"] \ No newline at end of file +build-std = ["panic_abort", "std"] diff --git a/examples-wasm-pack/Cargo.toml b/examples-wasm-pack/Cargo.toml index 4f0d600..9b0f540 100644 --- a/examples-wasm-pack/Cargo.toml +++ b/examples-wasm-pack/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -wasm_thread = { path = "../" } +wasm_thread = { path = "../", default-features = false } log = "0.4" wasm-bindgen = "0.2" console_log = { version = "1.0", features = ["color"] } diff --git a/examples/simple.html b/examples/simple.html index c278964..67626a7 100644 --- a/examples/simple.html +++ b/examples/simple.html @@ -5,9 +5,9 @@ - - From 28bc56fb3dc4d5dd4674b731e54951d0c81e994b Mon Sep 17 00:00:00 2001 From: chemicstry Date: Fri, 10 Mar 2023 14:58:29 +0200 Subject: [PATCH 12/24] Fix race condition in Signal --- src/wasm32/signal.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/wasm32/signal.rs b/src/wasm32/signal.rs index 0d7c27f..8c53d84 100644 --- a/src/wasm32/signal.rs +++ b/src/wasm32/signal.rs @@ -53,10 +53,11 @@ impl Signal { /// Asynchronously waits until [Self::signal] is called. pub async fn wait_async(&self) { poll_fn(|cx| { + self.waiters.lock_spin().unwrap().push(cx.waker().clone()); + if self.value.load(Ordering::Relaxed) == 1 { Poll::Ready(()) } else { - self.waiters.lock_spin().unwrap().push(cx.waker().clone()); Poll::Pending } }) From 321f8e88aeffe2edaca299a4c542d48c8baa1c68 Mon Sep 17 00:00:00 2001 From: chemicstry Date: Fri, 10 Mar 2023 15:26:52 +0200 Subject: [PATCH 13/24] Add tests --- .github/workflows/ci.yaml | 24 +++++++++++++++ Cargo.toml | 3 ++ test_wasm.sh | 4 +++ tests/native.rs | 40 +++++++++++++++++++++++++ tests/wasm.rs | 62 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 133 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100644 test_wasm.sh create mode 100644 tests/native.rs create mode 100644 tests/wasm.rs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..b7b8ca6 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,24 @@ +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + + - name: Rustfmt + run: cargo fmt --all --check + + - name: Test native + run: cargo test + + - name: Test wasm + run: ./test_wasm.sh + + - name: Build docs + env: + RUSTDOCFLAGS: "-Dwarnings" + run: cargo doc --no-deps --lib --target wasm32-unknown-unknown \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 59b5ffe..a6fb863 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,3 +41,6 @@ wasm-bindgen-test = "0.3" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] console_log = { version = "1.0", features = ["color"] } console_error_panic_hook = "0.1" + +[package.metadata.docs.rs] +targets = ["wasm32-unknown-unknown"] diff --git a/test_wasm.sh b/test_wasm.sh new file mode 100644 index 0000000..3bcf734 --- /dev/null +++ b/test_wasm.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals' \ + wasm-pack test --headless --firefox -- -Z build-std=panic_abort,std \ No newline at end of file diff --git a/tests/native.rs b/tests/native.rs new file mode 100644 index 0000000..5e8a0fe --- /dev/null +++ b/tests/native.rs @@ -0,0 +1,40 @@ +//! Trivial tests to ensure that native thread API is unchanged. + +use wasm_thread as thread; + +#[test] +fn thread_join() { + let handle = thread::spawn(|| 1234); + + assert_eq!(handle.join().unwrap(), 1234); +} + +#[test] +fn thread_scope() { + let mut a = vec![1, 2, 3]; + let mut x = 0; + + thread::scope(|s| { + s.spawn(|| { + println!("hello from the first scoped thread {:?}", thread::current().id()); + // We can borrow `a` here. + dbg!(&a) + }); + + s.spawn(|| { + println!("hello from the second scoped thread {:?}", thread::current().id()); + // We can even mutably borrow `x` here, + // because no other threads are using it. + x += a[0] + a[2]; + }); + + println!( + "Hello from scope \"main\" thread {:?} inside scope.", + thread::current().id() + ); + }); + + // After the scope, we can modify and access our variables again: + a.push(4); + assert_eq!(x, a.len()); +} diff --git a/tests/wasm.rs b/tests/wasm.rs new file mode 100644 index 0000000..769cc09 --- /dev/null +++ b/tests/wasm.rs @@ -0,0 +1,62 @@ +#![cfg(target_arch = "wasm32")] + +use wasm_bindgen_test::*; +use wasm_thread as thread; + +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +async fn thread_join_async() { + let handle = thread::spawn(|| 1234); + + assert_eq!(handle.join_async().await.unwrap(), 1234); +} + +#[wasm_bindgen_test] +async fn thread_join_sync() { + // synchronous join only allowed inside threads + thread::spawn(|| { + let handle = thread::spawn(|| 1234); + + assert_eq!(handle.join().unwrap(), 1234); + }) + .join_async() + .await + .unwrap(); +} + +#[wasm_bindgen_test] +async fn thread_scope_sync() { + // synchronous scope only allowed inside threads + thread::spawn(|| { + let mut a = vec![1, 2, 3]; + let mut x = 0; + + thread::scope(|s| { + s.spawn(|| { + println!("hello from the first scoped thread {:?}", thread::current().id()); + // We can borrow `a` here. + dbg!(&a) + }); + + s.spawn(|| { + println!("hello from the second scoped thread {:?}", thread::current().id()); + // We can even mutably borrow `x` here, + // because no other threads are using it. + x += a[0] + a[2]; + }); + + println!( + "Hello from scope \"main\" thread {:?} inside scope.", + thread::current().id() + ); + }); + + // After the scope, we can modify and access our variables again: + a.push(4); + assert_eq!(x, a.len()); + }) + .join_async() + .await + .unwrap(); +} From 15ee3e24519d03342ec5a3803969e37d617cfabe Mon Sep 17 00:00:00 2001 From: chemicstry Date: Fri, 10 Mar 2023 15:30:40 +0200 Subject: [PATCH 14/24] Fix tests --- src/wasm32/mod.rs | 4 ++-- src/wasm32/scoped.rs | 2 +- test_wasm.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) mode change 100644 => 100755 test_wasm.sh diff --git a/src/wasm32/mod.rs b/src/wasm32/mod.rs index 48e3fca..96d938d 100644 --- a/src/wasm32/mod.rs +++ b/src/wasm32/mod.rs @@ -139,7 +139,7 @@ impl Builder { } /// Spawns a new thread by taking ownership of the `Builder`, and returns an - /// [`io::Result`] to its [`JoinHandle`]. + /// [std::io::Result] to its [`JoinHandle`]. pub fn spawn(self, f: F) -> std::io::Result> where F: FnOnce() -> T, @@ -150,7 +150,7 @@ impl Builder { } /// Spawns a new thread without any lifetime restrictions by taking ownership - /// of the `Builder`, and returns an [`io::Result`] to its [`JoinHandle`]. + /// of the `Builder`, and returns an [std::io::Result] to its [`JoinHandle`]. /// /// # Safety /// diff --git a/src/wasm32/scoped.rs b/src/wasm32/scoped.rs index 5396ebf..cd18532 100644 --- a/src/wasm32/scoped.rs +++ b/src/wasm32/scoped.rs @@ -160,7 +160,7 @@ impl<'scope, 'env> Scope<'scope, 'env> { impl Builder { /// Spawns a new scoped thread using the settings set through this `Builder`. /// - /// Unlike [Scope::spawn], this method yields an [`io::Result`] to + /// Unlike [Scope::spawn], this method yields an [std::io::Result] to /// capture any failure to create the thread at the OS level. pub fn spawn_scoped<'scope, 'env, F, T>( self, diff --git a/test_wasm.sh b/test_wasm.sh old mode 100644 new mode 100755 index 3bcf734..f80d6f0 --- a/test_wasm.sh +++ b/test_wasm.sh @@ -1,4 +1,4 @@ #!/bin/sh RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals' \ - wasm-pack test --headless --firefox -- -Z build-std=panic_abort,std \ No newline at end of file + wasm-pack test --headless --firefox --chrome -- -Z build-std=panic_abort,std \ No newline at end of file From a8897d2c36df6358ffaf110f04b77b713865143f Mon Sep 17 00:00:00 2001 From: chemicstry Date: Fri, 10 Mar 2023 15:32:59 +0200 Subject: [PATCH 15/24] Optimize CI --- .github/workflows/ci.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b7b8ca6..feb7ddf 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,6 +2,10 @@ on: [push, pull_request] jobs: test: + # Prevent running double CI when pull request is on the same repository + if: | + (github.event_name != 'pull_request') + || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 From 43f5398bfc3480ee8a8123c3b70f71d718d256fd Mon Sep 17 00:00:00 2001 From: chemicstry Date: Wed, 15 Mar 2023 00:10:32 +0200 Subject: [PATCH 16/24] Fix newlines --- .github/workflows/ci.yaml | 2 +- examples-wasm-pack/module/simple.html | 2 +- examples-wasm-pack/no-module/simple.html | 2 +- examples/simple.html | 2 +- test_wasm.sh | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index feb7ddf..69354ed 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -25,4 +25,4 @@ jobs: - name: Build docs env: RUSTDOCFLAGS: "-Dwarnings" - run: cargo doc --no-deps --lib --target wasm32-unknown-unknown \ No newline at end of file + run: cargo doc --no-deps --lib --target wasm32-unknown-unknown diff --git a/examples-wasm-pack/module/simple.html b/examples-wasm-pack/module/simple.html index 67626a7..2c8feb5 100644 --- a/examples-wasm-pack/module/simple.html +++ b/examples-wasm-pack/module/simple.html @@ -11,4 +11,4 @@ - \ No newline at end of file + diff --git a/examples-wasm-pack/no-module/simple.html b/examples-wasm-pack/no-module/simple.html index c278964..cd66321 100644 --- a/examples-wasm-pack/no-module/simple.html +++ b/examples-wasm-pack/no-module/simple.html @@ -11,4 +11,4 @@ - \ No newline at end of file + diff --git a/examples/simple.html b/examples/simple.html index 67626a7..2c8feb5 100644 --- a/examples/simple.html +++ b/examples/simple.html @@ -11,4 +11,4 @@ - \ No newline at end of file + diff --git a/test_wasm.sh b/test_wasm.sh index f80d6f0..62830c6 100755 --- a/test_wasm.sh +++ b/test_wasm.sh @@ -1,4 +1,4 @@ #!/bin/sh RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals' \ - wasm-pack test --headless --firefox --chrome -- -Z build-std=panic_abort,std \ No newline at end of file + wasm-pack test --headless --firefox --chrome -- -Z build-std=panic_abort,std From ad021277caa2155f5c61cfc48fb3fbc013686c93 Mon Sep 17 00:00:00 2001 From: chemicstry Date: Wed, 15 Mar 2023 00:47:09 +0200 Subject: [PATCH 17/24] Add more scope tests --- tests/wasm.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/wasm.rs b/tests/wasm.rs index 769cc09..92532ba 100644 --- a/tests/wasm.rs +++ b/tests/wasm.rs @@ -1,5 +1,10 @@ #![cfg(target_arch = "wasm32")] +use core::{ + sync::atomic::{AtomicBool, Ordering}, + time::Duration, +}; + use wasm_bindgen_test::*; use wasm_thread as thread; @@ -60,3 +65,35 @@ async fn thread_scope_sync() { .await .unwrap(); } + +#[wasm_bindgen_test] +async fn thread_scope_sync_block() { + // synchronous scope only allowed inside threads + thread::spawn(|| { + let t1_done = AtomicBool::new(false); + let t2_done = AtomicBool::new(false); + + thread::scope(|s| { + s.spawn(|| { + thread::sleep(Duration::from_millis(100)); + t1_done.store(true, Ordering::Relaxed); + }); + + s.spawn(|| { + thread::sleep(Duration::from_millis(100)); + t2_done.store(true, Ordering::Relaxed); + }); + + // Threads should be in sleep and not yet done + assert_eq!(t1_done.load(Ordering::Relaxed), false); + assert_eq!(t2_done.load(Ordering::Relaxed), false); + }); + + // Scope should block until both threads terminate + assert_eq!(t1_done.load(Ordering::Relaxed), true); + assert_eq!(t2_done.load(Ordering::Relaxed), true); + }) + .join_async() + .await + .unwrap(); +} From bfc2658d10373b3508731110ff19b3ca72859f86 Mon Sep 17 00:00:00 2001 From: chemicstry Date: Mon, 26 Jun 2023 12:27:08 +0300 Subject: [PATCH 18/24] Add async channel tests --- Cargo.toml | 2 ++ tests/wasm.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index a6fb863..146db44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,8 @@ futures = "0.3" log = "0.4" env_logger = "0.10" wasm-bindgen-test = "0.3" +async-channel = "1.8" +wasm-bindgen-futures = "0.4" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] console_log = { version = "1.0", features = ["color"] } diff --git a/tests/wasm.rs b/tests/wasm.rs index 92532ba..ac1f238 100644 --- a/tests/wasm.rs +++ b/tests/wasm.rs @@ -97,3 +97,31 @@ async fn thread_scope_sync_block() { .await .unwrap(); } + +#[wasm_bindgen_test] +async fn thread_async_channel() { + // Exchange a series of messages over async channel. + // This test ensures that web worker thread does not terminate prematurely. + let (thread_tx, main_rx) = async_channel::unbounded::(); + let (main_tx, thread_rx) = async_channel::unbounded::(); + + thread::spawn(|| { + // Spawn async closure into browser's event loop. + // Synchronous thread closure will terminate shortly, + // but the webworker should continue running. + wasm_bindgen_futures::spawn_local(async move { + thread::sleep(Duration::from_millis(100)); + thread_tx.send("Hello".to_string()).await.unwrap(); + let mut msg = thread_rx.recv().await.unwrap(); + msg.push_str("!"); + thread_tx.send(msg).await.unwrap(); + }) + }); + + let mut msg = main_rx.recv().await.unwrap(); + msg.push_str(" world"); + main_tx.send(msg).await.unwrap(); + + let result = main_rx.recv().await.unwrap(); + assert_eq!(result, "Hello world!"); +} From ff3628ab924e3b5b2fe82fe87ee6bb12a8b8627a Mon Sep 17 00:00:00 2001 From: chemicstry Date: Sun, 17 Mar 2024 14:04:17 +0200 Subject: [PATCH 19/24] chrome driver seems broken at the moment --- test_wasm.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_wasm.sh b/test_wasm.sh index 62830c6..48c5232 100755 --- a/test_wasm.sh +++ b/test_wasm.sh @@ -1,4 +1,4 @@ #!/bin/sh RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals' \ - wasm-pack test --headless --firefox --chrome -- -Z build-std=panic_abort,std + wasm-pack test --headless --firefox -- -Z build-std=panic_abort,std From 34e379a2b09123299a72c4a6029ed6e26ab5ae2f Mon Sep 17 00:00:00 2001 From: chemicstry Date: Sun, 17 Mar 2024 14:04:38 +0200 Subject: [PATCH 20/24] Nightly is no longer needed --- Cargo.toml | 2 +- build_wasm.sh | 2 +- rust-toolchain.toml | 6 ------ src/lib.rs | 2 +- 4 files changed, 3 insertions(+), 9 deletions(-) delete mode 100644 rust-toolchain.toml diff --git a/Cargo.toml b/Cargo.toml index 146db44..8edba80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ futures = "0.3" log = "0.4" env_logger = "0.10" wasm-bindgen-test = "0.3" -async-channel = "1.8" +async-channel = "2.2" wasm-bindgen-futures = "0.4" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] diff --git a/build_wasm.sh b/build_wasm.sh index cafdd88..b4bbc42 100755 --- a/build_wasm.sh +++ b/build_wasm.sh @@ -13,7 +13,7 @@ set -ex # shared memory, passive segments, etc. RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals' \ - cargo +nightly build --example simple --target wasm32-unknown-unknown --release -Z build-std=std,panic_abort + cargo build --example simple --target wasm32-unknown-unknown --release -Z build-std=std,panic_abort # Note the usage of `--target no-modules` here which is required for passing # the memory import to each wasm module. diff --git a/rust-toolchain.toml b/rust-toolchain.toml deleted file mode 100644 index f81cd8b..0000000 --- a/rust-toolchain.toml +++ /dev/null @@ -1,6 +0,0 @@ -# Before upgrading check that everything is available on all tier1 targets here: -# https://rust-lang.github.io/rustup-components-history -[toolchain] -channel = "nightly-2023-02-07" -components = ["rust-src", "rustfmt"] -targets = ["wasm32-unknown-unknown"] diff --git a/src/lib.rs b/src/lib.rs index fead769..a2685c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![cfg_attr(target_arch = "wasm32", feature(stdsimd))] +#![cfg_attr(target_arch = "wasm32", feature(stdarch_wasm_atomic_wait))] // Import reusable APIs from std pub use std::thread::{current, sleep, Result, Thread, ThreadId}; From e919f9b15349297d1c6ec0c02dc3d4af8433061b Mon Sep 17 00:00:00 2001 From: chemicstry Date: Sun, 17 Mar 2024 14:05:35 +0200 Subject: [PATCH 21/24] Fix async channel test --- tests/wasm.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/wasm.rs b/tests/wasm.rs index ac1f238..9c90ec1 100644 --- a/tests/wasm.rs +++ b/tests/wasm.rs @@ -101,15 +101,11 @@ async fn thread_scope_sync_block() { #[wasm_bindgen_test] async fn thread_async_channel() { // Exchange a series of messages over async channel. - // This test ensures that web worker thread does not terminate prematurely. let (thread_tx, main_rx) = async_channel::unbounded::(); let (main_tx, thread_rx) = async_channel::unbounded::(); thread::spawn(|| { - // Spawn async closure into browser's event loop. - // Synchronous thread closure will terminate shortly, - // but the webworker should continue running. - wasm_bindgen_futures::spawn_local(async move { + futures::executor::block_on(async move { thread::sleep(Duration::from_millis(100)); thread_tx.send("Hello".to_string()).await.unwrap(); let mut msg = thread_rx.recv().await.unwrap(); From 71e6124b825f9015ac403e163d4160f6a9a902ab Mon Sep 17 00:00:00 2001 From: chemicstry Date: Sun, 17 Mar 2024 14:10:09 +0200 Subject: [PATCH 22/24] Revert "Nightly is no longer needed" This reverts commit 34e379a2b09123299a72c4a6029ed6e26ab5ae2f. --- Cargo.toml | 2 +- build_wasm.sh | 2 +- rust-toolchain.toml | 6 ++++++ src/lib.rs | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 rust-toolchain.toml diff --git a/Cargo.toml b/Cargo.toml index 8edba80..146db44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ futures = "0.3" log = "0.4" env_logger = "0.10" wasm-bindgen-test = "0.3" -async-channel = "2.2" +async-channel = "1.8" wasm-bindgen-futures = "0.4" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] diff --git a/build_wasm.sh b/build_wasm.sh index b4bbc42..cafdd88 100755 --- a/build_wasm.sh +++ b/build_wasm.sh @@ -13,7 +13,7 @@ set -ex # shared memory, passive segments, etc. RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals' \ - cargo build --example simple --target wasm32-unknown-unknown --release -Z build-std=std,panic_abort + cargo +nightly build --example simple --target wasm32-unknown-unknown --release -Z build-std=std,panic_abort # Note the usage of `--target no-modules` here which is required for passing # the memory import to each wasm module. diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..f81cd8b --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,6 @@ +# Before upgrading check that everything is available on all tier1 targets here: +# https://rust-lang.github.io/rustup-components-history +[toolchain] +channel = "nightly-2023-02-07" +components = ["rust-src", "rustfmt"] +targets = ["wasm32-unknown-unknown"] diff --git a/src/lib.rs b/src/lib.rs index a2685c4..fead769 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![cfg_attr(target_arch = "wasm32", feature(stdarch_wasm_atomic_wait))] +#![cfg_attr(target_arch = "wasm32", feature(stdsimd))] // Import reusable APIs from std pub use std::thread::{current, sleep, Result, Thread, ThreadId}; From b273e538309baa6ad48e85f286f25d0345b73b05 Mon Sep 17 00:00:00 2001 From: chemicstry Date: Sun, 17 Mar 2024 14:13:14 +0200 Subject: [PATCH 23/24] Update nightly --- Cargo.toml | 2 +- rust-toolchain.toml | 2 +- src/lib.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 146db44..8edba80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ futures = "0.3" log = "0.4" env_logger = "0.10" wasm-bindgen-test = "0.3" -async-channel = "1.8" +async-channel = "2.2" wasm-bindgen-futures = "0.4" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f81cd8b..4887225 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,6 +1,6 @@ # Before upgrading check that everything is available on all tier1 targets here: # https://rust-lang.github.io/rustup-components-history [toolchain] -channel = "nightly-2023-02-07" +channel = "nightly-2024-03-16" components = ["rust-src", "rustfmt"] targets = ["wasm32-unknown-unknown"] diff --git a/src/lib.rs b/src/lib.rs index fead769..a2685c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![cfg_attr(target_arch = "wasm32", feature(stdsimd))] +#![cfg_attr(target_arch = "wasm32", feature(stdarch_wasm_atomic_wait))] // Import reusable APIs from std pub use std::thread::{current, sleep, Result, Thread, ThreadId}; From 2ea79c4399f8a2ad77167cab96c8a4e3a504b329 Mon Sep 17 00:00:00 2001 From: chemicstry Date: Sun, 17 Mar 2024 14:16:20 +0200 Subject: [PATCH 24/24] Bump version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8edba80..a313b6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wasm_thread" -version = "0.2.0" +version = "0.3.0" authors = ["Jurgis Balciunas "] edition = "2018" description = "An std thread replacement for wasm32 target"