├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── build_wasm.ps1 ├── build_wasm.sh ├── examples-wasm-pack ├── .cargo │ └── Config.toml ├── Cargo.toml ├── module │ └── simple.html ├── no-module │ └── simple.html ├── src │ └── lib.rs ├── web-build-no-module.ps1 ├── web-build-no-module.sh ├── web-build.ps1 └── web-build.sh ├── examples ├── simple.html └── simple.rs ├── rust-toolchain.toml ├── rustfmt.toml ├── src ├── lib.rs └── wasm32 │ ├── js │ ├── module_workers_polyfill.min.js │ ├── script_path.js │ ├── web_worker.js │ └── web_worker_module.js │ ├── mod.rs │ ├── scoped.rs │ ├── signal.rs │ └── utils.rs ├── test_wasm.sh └── tests ├── native.rs └── wasm.rs /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | jobs: 4 | test: 5 | # Prevent running double CI when pull request is on the same repository 6 | if: | 7 | (github.event_name != 'pull_request') 8 | || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Install 14 | run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 15 | 16 | - name: Rustfmt 17 | run: cargo fmt --all --check 18 | 19 | - name: Test native 20 | run: cargo test 21 | 22 | - name: Test wasm 23 | run: ./test_wasm.sh 24 | 25 | - name: Build docs 26 | env: 27 | RUSTDOCFLAGS: "-Dwarnings" 28 | run: cargo doc --no-deps --lib --target wasm32-unknown-unknown 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | /examples/target 4 | /examples-wasm-pack/target 5 | .vscode/ 6 | !.vscode/settings.json 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm_thread" 3 | version = "0.3.3" 4 | authors = ["Jurgis Balciunas "] 5 | edition = "2018" 6 | description = "An std thread replacement for wasm32 target" 7 | license = "Apache-2.0 OR MIT" 8 | repository = "https://github.com/chemicstry/wasm_thread" 9 | homepage = "https://github.com/chemicstry/wasm_thread" 10 | documentation = "https://docs.rs/wasm_thread" 11 | keywords = ["thread", "wasm", "parallel", "concurrency"] 12 | categories = ["concurrency", "wasm"] 13 | readme = "README.md" 14 | 15 | [features] 16 | default = ["es_modules"] 17 | es_modules = [] 18 | 19 | [target.'cfg(target_arch = "wasm32")'.dependencies] 20 | wasm-bindgen = "0.2.95" 21 | web-sys = { version = "0.3.72", features = [ 22 | "Blob", 23 | "DedicatedWorkerGlobalScope", 24 | "MessageEvent", 25 | "Url", 26 | "Worker", 27 | "WorkerType", 28 | "WorkerOptions", 29 | "Window", 30 | "Navigator", 31 | "WorkerNavigator", 32 | ] } 33 | js-sys = "0.3.72" 34 | futures = "0.3.31" 35 | 36 | [dev-dependencies] 37 | log = "0.4.22" 38 | env_logger = "0.11.5" 39 | 40 | [target.'cfg(target_arch = "wasm32")'.dev-dependencies] 41 | console_log = { version = "1.0.0", features = ["color"] } 42 | console_error_panic_hook = "0.1.7" 43 | wasm-bindgen-test = "0.3.45" 44 | async-channel = "2.3.1" 45 | 46 | [package.metadata.docs.rs] 47 | targets = ["wasm32-unknown-unknown"] 48 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wasm_thread 2 | 3 | [![License](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](https://github.com/chemicstry/wasm_thread) 4 | [![Cargo](https://img.shields.io/crates/v/wasm_thread.svg)](https://crates.io/crates/wasm_thread) 5 | [![Documentation](https://docs.rs/wasm_thread/badge.svg)](https://docs.rs/wasm_thread) 6 | 7 | An `std::thread` replacement for wasm32 target. 8 | 9 | This crate tries to closely replicate `std::thread` API. Namely, it doesn't require you to bundle worker scripts and resolves wasm-bindgen shim URL automatically. 10 | 11 | Note that some API is still missing and may be even impossible to implement given wasm limitations. 12 | 13 | ## Using as a library 14 | 15 | - Add `wasm_thread` to your `Cargo.toml`. 16 | - This project supports `wasm-pack` targets `web` and `no-modules`. `es_modules` feature is enabled by default, if building for `no-modules`, use `default-features = false` when specifying dependency. 17 | - Replace `use std::thread` with `use wasm_thread as thread`. Note that some API might be missing. 18 | - Build normally using `wasm-pack` or adapt [build_wasm.sh](build_wasm.sh) to your project. 19 | 20 | ## Notes on wasm limitations 21 | 22 | - In order for multiple wasm instances to share the same memory, `SharedArrayBuffer` is required. This means that the COOP and COEP security headers for the webpage will need to be set (see [Mozilla's documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer)). These may be enabled by adjusting webserver settings or using a [service worker](https://github.com/gzuidhof/coi-serviceworker). 23 | - Any blocking API (`thread.join()`, `futures::block_on()`, etc) on the main thread will freeze the browser for as long as lock is maintained. This also freezes any proxied functions, which means that worker spawning, network fetches and other similar asynchronous APIs will block also and can cause a deadlock. To avoid this, either run your `main()` in a worker thread or use async futures. 24 | - Atomic locks (`i32.atomic.wait` to be specific) will panic on the main thread. This means that `mutex.lock()` will likely crash. Solution is the same as above. 25 | - Web workers are normally spawned by providing a script URL, however, to avoid bundling scripts this library uses URL encoded blob [web_worker.js](src/wasm32/js/web_worker.js) to avoid HTTP fetch. `wasm_bindgen` generated `.js` shim script is still needed and a [hack](src/wasm32/js/script_path.js) is used to obtain its URL. If this for some reason does not work in your setup, please report an issue or use `Builder::wasm_bindgen_shim_url()` to specify explicit URL. 26 | - For additional information on wasm threading look at [this](https://rustwasm.github.io/2018/10/24/multithreading-rust-and-wasm.html) blogpost or [raytrace-parallel](https://rustwasm.github.io/wasm-bindgen/examples/raytrace.html) example. 27 | 28 | ## Alternatives 29 | 30 | For a higher-level threading solution, see [wasm-bindgen-rayon](https://github.com/RReverser/wasm-bindgen-rayon), which allows one to utilize a fixed-size threadpool in web browsers. 31 | 32 | ## Running examples 33 | 34 | ### Simple 35 | 36 | #### Native 37 | 38 | - Just `cargo run --example simple` 39 | 40 | #### wasm-bindgen 41 | 42 | - Install nightly toolchain and dependencies: 43 | ```bash 44 | rustup toolchain install nightly 45 | rustup component add rust-src --toolchain nightly 46 | cargo install wasm-bindgen-cli 47 | ``` 48 | - 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). 49 | - 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`. 50 | 51 | #### wasm-pack 52 | 53 | - Install `wasm-pack`: 54 | ```bash 55 | cargo install wasm-pack 56 | ``` 57 | - 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`. 58 | - Serve `./examples-wasm-pack/module` or `./examples-wasm-pack/no-module`, respectively, over HTTP and open `simple.html` in browser. Inspect console output. 59 | 60 | ### Example output 61 | 62 | Native: 63 | ``` 64 | hi number 1 from the spawned thread ThreadId(2)! 65 | hi number 1 from the main thread ThreadId(1)! 66 | hi number 1 from the spawned thread ThreadId(3)! 67 | hi number 2 from the main thread ThreadId(1)! 68 | hi number 2 from the spawned thread ThreadId(2)! 69 | hi number 2 from the spawned thread ThreadId(3)! 70 | ``` 71 | 72 | Wasm: 73 | ``` 74 | hi number 1 from the main thread ThreadId(1)! 75 | hi number 2 from the main thread ThreadId(1)! 76 | hi number 1 from the spawned thread ThreadId(2)! 77 | hi number 1 from the spawned thread ThreadId(3)! 78 | hi number 2 from the spawned thread ThreadId(2)! 79 | hi number 2 from the spawned thread ThreadId(3)! 80 | ``` 81 | 82 | As you can see wasm threads are only spawned after `main()` returns, because browser event loop cannot continue while main thread is blocked. 83 | 84 | ## License 85 | 86 | Licensed under either of 87 | 88 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 89 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 90 | 91 | at your option. 92 | 93 | #### Contribution 94 | 95 | Unless you explicitly state otherwise, any contribution intentionally submitted 96 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 97 | dual licensed as above, without any additional terms or conditions. 98 | -------------------------------------------------------------------------------- /build_wasm.ps1: -------------------------------------------------------------------------------- 1 | $env:RUSTFLAGS="-C target-feature=+atomics,+bulk-memory,+mutable-globals" 2 | cargo +nightly build --example simple --target wasm32-unknown-unknown --release -Z build-std=std,panic_abort 3 | 4 | wasm-bindgen target/wasm32-unknown-unknown/release/examples/simple.wasm --out-dir ./examples/target/ --target no-modules 5 | -------------------------------------------------------------------------------- /build_wasm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ex 4 | 5 | # A couple of steps are necessary to get this build working which makes it slightly 6 | # nonstandard compared to most other builds. 7 | # 8 | # * First, the Rust standard library needs to be recompiled with atomics 9 | # enabled. to do that we use Cargo's unstable `-Zbuild-std` feature. 10 | # 11 | # * Next we need to compile everything with the `atomics` and `bulk-memory` 12 | # features enabled, ensuring that LLVM will generate atomic instructions, 13 | # shared memory, passive segments, etc. 14 | 15 | RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals' \ 16 | cargo +nightly build --example simple --target wasm32-unknown-unknown --release -Z build-std=std,panic_abort 17 | 18 | # Note the usage of `--target no-modules` here which is required for passing 19 | # the memory import to each wasm module. 20 | wasm-bindgen \ 21 | target/wasm32-unknown-unknown/release/examples/simple.wasm \ 22 | --out-dir ./examples/target/ \ 23 | --target web 24 | -------------------------------------------------------------------------------- /examples-wasm-pack/.cargo/Config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-unknown-unknown" 3 | 4 | [target.'cfg(target_arch = "wasm32")'] 5 | rustflags = ["-C", "target-feature=+atomics,+bulk-memory,+mutable-globals"] 6 | 7 | [unstable] 8 | build-std = ["panic_abort", "std"] 9 | -------------------------------------------------------------------------------- /examples-wasm-pack/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simple" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | crate-type = ["cdylib", "rlib"] 10 | 11 | [dependencies] 12 | wasm_thread = { path = "../", default-features = false } 13 | log = "0.4" 14 | wasm-bindgen = "0.2" 15 | console_log = { version = "1.0", features = ["color"] } 16 | console_error_panic_hook = "0.1" 17 | -------------------------------------------------------------------------------- /examples-wasm-pack/module/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples-wasm-pack/no-module/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples-wasm-pack/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use wasm_bindgen::prelude::*; 4 | use wasm_thread as thread; 5 | 6 | #[wasm_bindgen(start)] 7 | fn main() { 8 | console_log::init().unwrap(); 9 | console_error_panic_hook::set_once(); 10 | 11 | for _ in 0..2 { 12 | thread::spawn(|| { 13 | for i in 1..3 { 14 | log::info!("hi number {} from the spawned thread {:?}!", i, thread::current().id()); 15 | thread::sleep(Duration::from_millis(1)); 16 | } 17 | }); 18 | } 19 | 20 | for i in 1..3 { 21 | log::info!("hi number {} from the main thread {:?}!", i, thread::current().id()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples-wasm-pack/web-build-no-module.ps1: -------------------------------------------------------------------------------- 1 | wasm-pack build --dev --out-dir ./no-module/target --target no-modules 2 | -------------------------------------------------------------------------------- /examples-wasm-pack/web-build-no-module.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | wasm-pack build --dev --out-dir ./no-module/target --target no-modules 4 | -------------------------------------------------------------------------------- /examples-wasm-pack/web-build.ps1: -------------------------------------------------------------------------------- 1 | wasm-pack build --dev --out-dir ./module/target --target web --features wasm_thread/es_modules 2 | -------------------------------------------------------------------------------- /examples-wasm-pack/web-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | wasm-pack build --dev --out-dir ./module/target --target web --features wasm_thread/es_modules 4 | -------------------------------------------------------------------------------- /examples/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use wasm_thread as thread; 4 | 5 | fn main() { 6 | #[cfg(target_arch = "wasm32")] 7 | { 8 | console_log::init().unwrap(); 9 | console_error_panic_hook::set_once(); 10 | } 11 | 12 | #[cfg(not(target_arch = "wasm32"))] 13 | env_logger::init_from_env(env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info")); 14 | 15 | log::info!("Available parallelism: {:?}", thread::available_parallelism()); 16 | 17 | let mut threads = vec![]; 18 | 19 | for _ in 0..2 { 20 | threads.push(thread::spawn(|| { 21 | for i in 1..3 { 22 | log::info!("hi number {} from the spawned thread {:?}!", i, thread::current().id()); 23 | thread::sleep(Duration::from_millis(1)); 24 | } 25 | })); 26 | } 27 | 28 | for i in 1..3 { 29 | log::info!("hi number {} from the main thread {:?}!", i, thread::current().id()); 30 | } 31 | 32 | // It's not possible to do a scope on the main thread, because blocking waits are not supported, but we can use 33 | // scope inside web workers. 34 | threads.push(thread::spawn(|| { 35 | log::info!("Start scope test on thread {:?}", thread::current().id()); 36 | 37 | let mut a = vec![1, 2, 3]; 38 | let mut x = 0; 39 | 40 | thread::scope(|s| { 41 | let handle = s.spawn(|| { 42 | log::info!("hello from the first scoped thread {:?}", thread::current().id()); 43 | // We can borrow `a` here. 44 | log::info!("a = {:?}", &a); 45 | // Return a subslice of borrowed `a` 46 | &a[0..2] 47 | }); 48 | 49 | // Wait for the returned value from first thread 50 | log::info!("a[0..2] = {:?}", handle.join().unwrap()); 51 | 52 | s.spawn(|| { 53 | log::info!("hello from the second scoped thread {:?}", thread::current().id()); 54 | // We can even mutably borrow `x` here, 55 | // because no other threads are using it. 56 | x += a[0] + a[2]; 57 | }); 58 | 59 | log::info!( 60 | "Hello from scope \"main\" thread {:?} inside scope.", 61 | thread::current().id() 62 | ); 63 | }); 64 | 65 | // After the scope, we can modify and access our variables again: 66 | a.push(4); 67 | assert_eq!(x, a.len()); 68 | log::info!("Scope done x = {}, a.len() = {}", x, a.len()); 69 | })); 70 | 71 | // Wait for all threads, otherwise program exits before threads finish execution. 72 | // We can't do blocking join on wasm main thread though, but the browser window will continue running. 73 | #[cfg(not(target_arch = "wasm32"))] 74 | for handle in threads { 75 | handle.join().unwrap(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | # Before upgrading check that everything is available on all tier1 targets here: 2 | # https://rust-lang.github.io/rustup-components-history 3 | [toolchain] 4 | channel = "nightly-2024-03-16" 5 | components = ["rust-src", "rustfmt"] 6 | targets = ["wasm32-unknown-unknown"] 7 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | unstable_features = true 2 | max_width = 120 3 | comment_width = 120 4 | wrap_comments = true 5 | format_code_in_doc_comments = true 6 | group_imports = "StdExternalCrate" 7 | imports_granularity = "Crate" 8 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(target_arch = "wasm32", feature(stdarch_wasm_atomic_wait))] 2 | 3 | // Import reusable APIs from std 4 | pub use std::thread::{current, sleep, Result, Thread, ThreadId}; 5 | 6 | #[cfg(target_arch = "wasm32")] 7 | mod wasm32; 8 | 9 | #[cfg(not(target_arch = "wasm32"))] 10 | pub use std::thread::*; 11 | 12 | #[cfg(target_arch = "wasm32")] 13 | pub use wasm32::*; 14 | -------------------------------------------------------------------------------- /src/wasm32/js/module_workers_polyfill.min.js: -------------------------------------------------------------------------------- 1 | if(Worker._$P !== true) { 2 | let polyfill = "!function(e){if(!e||!0!==e._$P){if(e){var n,r=Object.defineProperty({},\"type\",{get:function(){n=!0}});try{var t=URL.createObjectURL(new Blob([\"\"],{type:\"text/javascript\"}));new e(t,r).terminate(),URL.revokeObjectURL(t)}catch(e){}if(!n)try{new e(\"data:text/javascript,\",r).terminate()}catch(e){}if(n)return;(self.Worker=function(n,r){return r&&\"module\"==r.type&&(r={name:n+\"\\n\"+(r.name||\"\")},n=\"undefined\"==typeof document?location.href:document.currentScript&&document.currentScript.src||(new Error).stack.match(/[(@]((file|https?):\\/\\/[^)]+?):\\d+(:\\d+)?(?:\\)|$)/m)[1]),new e(n,r)})._$P=!0}\"undefined\"==typeof document&&function(){var e={},n={};function r(e,n){for(n=n.replace(/^(\\.\\.\\/|\\.\\/)/,e.replace(/[^/]+$/g,\"\")+\"$1\");n!==(n=n.replace(/[^/]+\\/\\.\\.\\//g,\"\")););return n.replace(/\\.\\//g,\"\")}var t=[],s=t.push.bind(t);addEventListener(\"message\",s);var a=self.name.match(/^[^\\n]+/)[0];self.name=self.name.replace(/^[^\\n]*\\n/g,\"\"),function t(s,a){var u,o=s;return a&&(s=r(a,s)),e[s]||(e[s]=fetch(s).then((function(a){if((o=a.url)!==s){if(null!=e[o])return e[o];e[o]=e[s]}return a.text().then((function(e){if(!a.ok)throw e;var c={exports:{}};u=n[o]||(n[o]=c.exports);var i=function(e){return t(e,o)},f=[];return e=function(e,n){n=n||[];var r,t=[],a=0;function u(e,n){for(var s,a=/(?:^|,)\\s*([\\w$]+)(?:\\s+as\\s+([\\w$]+))?\\s*/g,u=[];s=a.exec(e);)n?t.push((s[2]||s[1])+\":\"+s[1]):u.push((s[2]||s[1])+\"=\"+r+\".\"+s[1]);return u}return(e=e.replace(/(^\\s*|[;}\\s\\n]\\s*)import\\s*(?:(?:([\\w$]+)(?:\\s*\\,\\s*\\{([^}]+)\\})?|(?:\\*\\s*as\\s+([\\w$]+))|\\{([^}]*)\\})\\s*from)?\\s*(['\"])(.+?)\\6/g,(function(e,t,s,o,c,i,f,p){return n.push(p),t+=\"var \"+(r=\"$im$\"+ ++a)+\"=$require(\"+f+p+f+\")\",s&&(t+=\";var \"+s+\" = 'default' in \"+r+\" ? \"+r+\".default : \"+r),c&&(t+=\";var \"+c+\" = \"+r),(o=o||i)&&(t+=\";var \"+u(o,!1)),t})).replace(/((?:^|[;}\\s\\n])\\s*)export\\s*(?:\\s+(default)\\s+|((?:async\\s+)?function\\s*\\*?|class|const\\s|let\\s|var\\s)\\s*([a-zA-Z0-9$_{[]+))/g,(function(e,n,r,s,u){if(r){var o=\"$im$\"+ ++a;return t.push(\"default:\"+o),n+\"var \"+o+\"=\"}return t.push(u+\":\"+u),n+s+\" \"+u})).replace(/((?:^|[;}\\s\\n])\\s*)export\\s*\\{([^}]+)\\}\\s*;?/g,(function(e,n,r){return u(r,!0),n})).replace(/((?:^|[^a-zA-Z0-9$_@`'\".])\\s*)(import\\s*\\([\\s\\S]+?\\))/g,\"$1$$$2\")).replace(/((?:^|[^a-zA-Z0-9$_@`'\".])\\s*)import\\.meta\\.url/g,\"$1\"+JSON.stringify(s))+\"\\n$module.exports={\"+t.join(\",\")+\"}\"}(e,f),Promise.all(f.map((function(e){var s=r(o,e);return s in n?n[s]:t(s)}))).then((function(n){e+=\"\\n//# sourceURL=\"+s;try{var r=new Function(\"$import\",\"$require\",\"$module\",\"$exports\",e)}catch(n){var t=n.line-1,a=n.column,o=e.split(\"\\n\"),p=(o[t-2]||\"\")+\"\\n\"+o[t-1]+\"\\n\"+(null==a?\"\":new Array(a).join(\"-\")+\"^\\n\")+(o[t]||\"\"),l=new Error(n.message+\"\\n\\n\"+p,s,t);throw l.sourceURL=l.fileName=s,l.line=t,l.column=a,l}var m=r(i,(function(e){return n[f.indexOf(e)]}),c,c.exports);return null!=m&&(c.exports=m),Object.assign(u,c.exports),c.exports}))}))})))}(a).then((function(){removeEventListener(\"message\",s),t.map(dispatchEvent)})).catch((function(e){setTimeout((function(){throw e}))}))}()}}(self.Worker);"; 3 | let blob = new Blob([polyfill], { type: 'text/javascript' }); 4 | let blobUrl = URL.createObjectURL(blob); 5 | !function(e){if(!e||!0!==e._$P){if(e){var n,r=Object.defineProperty({},"type",{get:function(){n=!0}});try{var t=URL.createObjectURL(new Blob([""],{type:"text/javascript"}));new e(t,r).terminate(),URL.revokeObjectURL(t)}catch(e){}if(!n)try{new e("data:text/javascript,",r).terminate()}catch(e){}if(n)return;(self.Worker=function(n,r){return r&&"module"==r.type&&(r={name:n+"\n"+(r.name||"")},n=blobUrl),new e(n,r)})._$P=!0}"undefined"==typeof document&&function(){var e={},n={};function r(e,n){for(n=n.replace(/^(\.\.\/|\.\/)/,e.replace(/[^/]+$/g,"")+"$1");n!==(n=n.replace(/[^/]+\/\.\.\//g,"")););return n.replace(/\.\//g,"")}var t=[],s=t.push.bind(t);addEventListener("message",s);var a=self.name.match(/^[^\n]+/)[0];self.name=self.name.replace(/^[^\n]*\n/g,""),function t(s,a){var u,o=s;return a&&(s=r(a,s)),e[s]||(e[s]=fetch(s).then((function(a){if((o=a.url)!==s){if(null!=e[o])return e[o];e[o]=e[s]}return a.text().then((function(e){if(!a.ok)throw e;var c={exports:{}};u=n[o]||(n[o]=c.exports);var i=function(e){return t(e,o)},f=[];return e=function(e,n){n=n||[];var r,t=[],a=0;function u(e,n){for(var s,a=/(?:^|,)\s*([\w$]+)(?:\s+as\s+([\w$]+))?\s*/g,u=[];s=a.exec(e);)n?t.push((s[2]||s[1])+":"+s[1]):u.push((s[2]||s[1])+"="+r+"."+s[1]);return u}return(e=e.replace(/(^\s*|[;}\s\n]\s*)import\s*(?:(?:([\w$]+)(?:\s*\,\s*\{([^}]+)\})?|(?:\*\s*as\s+([\w$]+))|\{([^}]*)\})\s*from)?\s*(['"])(.+?)\6/g,(function(e,t,s,o,c,i,f,p){return n.push(p),t+="var "+(r="$im$"+ ++a)+"=$require("+f+p+f+")",s&&(t+=";var "+s+" = 'default' in "+r+" ? "+r+".default : "+r),c&&(t+=";var "+c+" = "+r),(o=o||i)&&(t+=";var "+u(o,!1)),t})).replace(/((?:^|[;}\s\n])\s*)export\s*(?:\s+(default)\s+|((?:async\s+)?function\s*\*?|class|const\s|let\s|var\s)\s*([a-zA-Z0-9$_{[]+))/g,(function(e,n,r,s,u){if(r){var o="$im$"+ ++a;return t.push("default:"+o),n+"var "+o+"="}return t.push(u+":"+u),n+s+" "+u})).replace(/((?:^|[;}\s\n])\s*)export\s*\{([^}]+)\}\s*;?/g,(function(e,n,r){return u(r,!0),n})).replace(/((?:^|[^a-zA-Z0-9$_@`'".])\s*)(import\s*\([\s\S]+?\))/g,"$1$$$2")).replace(/((?:^|[^a-zA-Z0-9$_@`'".])\s*)import\.meta\.url/g,"$1"+JSON.stringify(s))+"\n$module.exports={"+t.join(",")+"}"}(e,f),Promise.all(f.map((function(e){var s=r(o,e);return s in n?n[s]:t(s)}))).then((function(n){e+="\n//# sourceURL="+s;try{var r=new Function("$import","$require","$module","$exports",e)}catch(n){var t=n.line-1,a=n.column,o=e.split("\n"),p=(o[t-2]||"")+"\n"+o[t-1]+"\n"+(null==a?"":new Array(a).join("-")+"^\n")+(o[t]||""),l=new Error(n.message+"\n\n"+p,s,t);throw l.sourceURL=l.fileName=s,l.line=t,l.column=a,l}var m=r(i,(function(e){return n[f.indexOf(e)]}),c,c.exports);return null!=m&&(c.exports=m),Object.assign(u,c.exports),c.exports}))}))})))}(a).then((function(){removeEventListener("message",s),t.map(dispatchEvent)})).catch((function(e){setTimeout((function(){throw e}))}))}()}}(self.Worker); 6 | } -------------------------------------------------------------------------------- /src/wasm32/js/script_path.js: -------------------------------------------------------------------------------- 1 | /// Extracts current script file path from artificially generated stack trace 2 | function script_path() { 3 | try { 4 | throw new Error(); 5 | } catch (e) { 6 | let parts = e.stack.match(/(?:\(|@)(\S+):\d+:\d+/); 7 | return parts[1]; 8 | } 9 | } 10 | 11 | script_path() -------------------------------------------------------------------------------- /src/wasm32/js/web_worker.js: -------------------------------------------------------------------------------- 1 | // synchronously, using the browser, import wasm_bindgen shim JS scripts 2 | importScripts('WASM_BINDGEN_SHIM_URL'); 3 | 4 | // Wait for the main thread to send us the shared module/memory and work context. 5 | // Once we've got it, initialize it all with the `wasm_bindgen` global we imported via 6 | // `importScripts`. 7 | self.onmessage = event => { 8 | let [ module, memory, work ] = event.data; 9 | 10 | wasm_bindgen(module, memory).catch(err => { 11 | console.log(err); 12 | 13 | // Propagate to main `onerror`: 14 | setTimeout(() => { 15 | throw err; 16 | }); 17 | // Rethrow to keep promise rejected and prevent execution of further commands: 18 | throw err; 19 | }).then(wasm => { 20 | // Enter rust code by calling entry point defined in `lib.rs`. 21 | // This executes closure defined by work context. 22 | wasm.wasm_thread_entry_point(work); 23 | 24 | // Once done, terminate web worker 25 | close(); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /src/wasm32/js/web_worker_module.js: -------------------------------------------------------------------------------- 1 | // synchronously, using the browser, import wasm_bindgen shim JS scripts 2 | import init, {wasm_thread_entry_point} from "WASM_BINDGEN_SHIM_URL"; 3 | 4 | // Wait for the main thread to send us the shared module/memory and work context. 5 | // Once we've got it, initialize it all with the `wasm_bindgen` global we imported via 6 | // `importScripts`. 7 | self.onmessage = event => { 8 | let [ module, memory, work ] = event.data; 9 | 10 | init(module, memory).catch(err => { 11 | console.log(err); 12 | 13 | // Propagate to main `onerror`: 14 | setTimeout(() => { 15 | throw err; 16 | }); 17 | // Rethrow to keep promise rejected and prevent execution of further commands: 18 | throw err; 19 | }).then(() => { 20 | // Enter rust code by calling entry point defined in `lib.rs`. 21 | // This executes closure defined by work context. 22 | wasm_thread_entry_point(work); 23 | 24 | // Once done, terminate web worker 25 | close(); 26 | }); 27 | }; -------------------------------------------------------------------------------- /src/wasm32/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cell::UnsafeCell, 3 | fmt, 4 | marker::PhantomData, 5 | mem, 6 | panic::{catch_unwind, AssertUnwindSafe}, 7 | rc::Rc, 8 | sync::{Arc, Mutex}, 9 | thread::{Result, Thread}, 10 | }; 11 | 12 | use scoped::ScopeData; 13 | pub use scoped::{scope, Scope, ScopedJoinHandle}; 14 | use signal::Signal; 15 | use utils::SpinLockMutex; 16 | pub use utils::{available_parallelism, get_wasm_bindgen_shim_script_path, get_worker_script, is_web_worker_thread}; 17 | use wasm_bindgen::prelude::*; 18 | use web_sys::{DedicatedWorkerGlobalScope, Worker, WorkerOptions, WorkerType}; 19 | 20 | mod scoped; 21 | mod signal; 22 | mod utils; 23 | 24 | struct WebWorkerContext { 25 | func: Box, 26 | } 27 | 28 | /// Entry point for web workers 29 | #[wasm_bindgen] 30 | pub fn wasm_thread_entry_point(ptr: u32) { 31 | let ctx = unsafe { Box::from_raw(ptr as *mut WebWorkerContext) }; 32 | (ctx.func)(); 33 | WorkerMessage::ThreadComplete.post(); 34 | } 35 | 36 | /// Used to relay spawn requests from web workers to main thread 37 | struct BuilderRequest { 38 | builder: Builder, 39 | context: WebWorkerContext, 40 | } 41 | 42 | impl BuilderRequest { 43 | pub unsafe fn spawn(self) { 44 | self.builder.spawn_for_context(self.context); 45 | } 46 | } 47 | 48 | /// Web worker to main thread messages 49 | enum WorkerMessage { 50 | /// Request to spawn thread 51 | SpawnThread(BuilderRequest), 52 | /// Thread has completed execution 53 | ThreadComplete, 54 | } 55 | 56 | impl WorkerMessage { 57 | pub fn post(self) { 58 | let req = Box::new(self); 59 | 60 | js_sys::eval("self") 61 | .unwrap() 62 | .dyn_into::() 63 | .unwrap() 64 | .post_message(&JsValue::from(Box::into_raw(req) as u32)) 65 | .unwrap(); 66 | } 67 | } 68 | 69 | static DEFAULT_BUILDER: Mutex> = Mutex::new(None); 70 | 71 | /// Thread factory, which can be used in order to configure the properties of a new thread. 72 | #[derive(Debug, Clone)] 73 | pub struct Builder { 74 | // A name for the thread-to-be, for identification in panic messages 75 | name: Option, 76 | // A prefix for the thread-to-be, for identification in panic messages 77 | prefix: Option, 78 | // The URL of the web worker script to use as web worker thread script 79 | worker_script_url: Option, 80 | // The size of the stack for the spawned thread in bytes 81 | stack_size: Option, 82 | // Url of the `wasm_bindgen` generated shim `.js` script to use as web worker entry point 83 | wasm_bindgen_shim_url: Option, 84 | } 85 | 86 | impl Default for Builder { 87 | fn default() -> Self { 88 | DEFAULT_BUILDER.lock_spin().unwrap().clone().unwrap_or(Self::empty()) 89 | } 90 | } 91 | 92 | impl Builder { 93 | /// Creates a builder inheriting global configuration options set by [Self::set_default]. 94 | pub fn new() -> Builder { 95 | Builder::default() 96 | } 97 | 98 | /// Creates a builder without inheriting global options set by [Self::set_default]. 99 | pub fn empty() -> Builder { 100 | Self { 101 | name: None, 102 | prefix: None, 103 | worker_script_url: None, 104 | stack_size: None, 105 | wasm_bindgen_shim_url: None, 106 | } 107 | } 108 | 109 | /// Sets current values as global default for all new builders created with [Builder::new] or [Builder::default]. 110 | pub fn set_default(self) { 111 | *DEFAULT_BUILDER.lock_spin().unwrap() = Some(self); 112 | } 113 | 114 | /// Sets the prefix of the thread. 115 | pub fn prefix(mut self, prefix: String) -> Builder { 116 | self.prefix = Some(prefix); 117 | self 118 | } 119 | 120 | pub fn worker_script_url(mut self, worker_script_url: String) -> Builder { 121 | self.worker_script_url = Some(worker_script_url); 122 | self 123 | } 124 | 125 | /// Sets the name of the thread. 126 | /// 127 | /// If not set, the default name is autogenerated. 128 | pub fn name(mut self, name: String) -> Builder { 129 | self.name = Some(name); 130 | self 131 | } 132 | 133 | /// Sets the size of the stack (in bytes) for the new thread. 134 | /// 135 | /// # Warning 136 | /// 137 | /// This is currently not supported by wasm, but provided for API consistency with [std::thread]. 138 | pub fn stack_size(mut self, size: usize) -> Builder { 139 | self.stack_size = Some(size); 140 | self 141 | } 142 | 143 | /// Sets the URL of wasm_bindgen generated shim script. 144 | pub fn wasm_bindgen_shim_url(mut self, url: String) -> Builder { 145 | self.wasm_bindgen_shim_url = Some(url); 146 | self 147 | } 148 | 149 | /// Spawns a new thread by taking ownership of the `Builder`, and returns an 150 | /// [std::io::Result] to its [`JoinHandle`]. 151 | pub fn spawn(self, f: F) -> std::io::Result> 152 | where 153 | F: FnOnce() -> T, 154 | F: Send + 'static, 155 | T: Send + 'static, 156 | { 157 | unsafe { self.spawn_unchecked(f) } 158 | } 159 | 160 | /// Spawns a new thread without any lifetime restrictions by taking ownership 161 | /// of the `Builder`, and returns an [std::io::Result] to its [`JoinHandle`]. 162 | /// 163 | /// # Safety 164 | /// 165 | /// The caller has to ensure that no references in the supplied thread closure 166 | /// or its return type can outlive the spawned thread's lifetime. This can be 167 | /// guaranteed in two ways: 168 | /// 169 | /// - ensure that [`join`][`JoinHandle::join`] is called before any referenced 170 | /// data is dropped 171 | /// - use only types with `'static` lifetime bounds, i.e., those with no or only 172 | /// `'static` references (both [`Builder::spawn`] 173 | /// and [`spawn`] enforce this property statically) 174 | pub unsafe fn spawn_unchecked<'a, F, T>(self, f: F) -> std::io::Result> 175 | where 176 | F: FnOnce() -> T, 177 | F: Send + 'a, 178 | T: Send + 'a, 179 | { 180 | Ok(JoinHandle(unsafe { self.spawn_unchecked_(f, None) }?)) 181 | } 182 | 183 | pub(crate) unsafe fn spawn_unchecked_<'a, 'scope, F, T>( 184 | self, 185 | f: F, 186 | scope_data: Option>, 187 | ) -> std::io::Result> 188 | where 189 | F: FnOnce() -> T, 190 | F: Send + 'a, 191 | T: Send + 'a, 192 | 'scope: 'a, 193 | { 194 | let my_signal = Arc::new(Signal::new()); 195 | let their_signal = my_signal.clone(); 196 | 197 | let my_packet: Arc> = Arc::new(Packet { 198 | scope: scope_data, 199 | result: UnsafeCell::new(None), 200 | _marker: PhantomData, 201 | }); 202 | let their_packet = my_packet.clone(); 203 | 204 | // Pass `f` in `MaybeUninit` because actually that closure might *run longer than the lifetime of `F`*. 205 | // See for more details. 206 | // To prevent leaks we use a wrapper that drops its contents. 207 | #[repr(transparent)] 208 | struct MaybeDangling(mem::MaybeUninit); 209 | impl MaybeDangling { 210 | fn new(x: T) -> Self { 211 | MaybeDangling(mem::MaybeUninit::new(x)) 212 | } 213 | fn into_inner(self) -> T { 214 | // SAFETY: we are always initiailized. 215 | let ret = unsafe { self.0.assume_init_read() }; 216 | // Make sure we don't drop. 217 | mem::forget(self); 218 | ret 219 | } 220 | } 221 | impl Drop for MaybeDangling { 222 | fn drop(&mut self) { 223 | // SAFETY: we are always initiailized. 224 | unsafe { self.0.assume_init_drop() }; 225 | } 226 | } 227 | 228 | let f = MaybeDangling::new(f); 229 | let main = Box::new(move || { 230 | // SAFETY: we constructed `f` initialized. 231 | let f = f.into_inner(); 232 | // Execute the closure and catch any panics 233 | let try_result = catch_unwind(AssertUnwindSafe(|| f())); 234 | // SAFETY: `their_packet` as been built just above and moved by the 235 | // closure (it is an Arc<...>) and `my_packet` will be stored in the 236 | // same `JoinInner` as this closure meaning the mutation will be 237 | // safe (not modify it and affect a value far away). 238 | unsafe { *their_packet.result.get() = Some(try_result) }; 239 | // Here `their_packet` gets dropped, and if this is the last `Arc` for that packet that 240 | // will call `decrement_num_running_threads` and therefore signal that this thread is 241 | // done. 242 | drop(their_packet); 243 | // Notify waiting handles 244 | their_signal.signal(); 245 | // Here, the lifetime `'a` and even `'scope` can end. `main` keeps running for a bit 246 | // after that before returning itself. 247 | }); 248 | 249 | // Erase lifetime 250 | let context = WebWorkerContext { 251 | func: mem::transmute::, Box>(main), 252 | }; 253 | 254 | if is_web_worker_thread() { 255 | WorkerMessage::SpawnThread(BuilderRequest { builder: self, context }).post(); 256 | } else { 257 | self.spawn_for_context(context); 258 | } 259 | 260 | if let Some(scope) = &my_packet.scope { 261 | scope.increment_num_running_threads(); 262 | } 263 | 264 | Ok(JoinInner { 265 | signal: my_signal, 266 | packet: my_packet, 267 | }) 268 | } 269 | 270 | unsafe fn spawn_for_context(self, ctx: WebWorkerContext) { 271 | let Builder { 272 | name, 273 | prefix, 274 | worker_script_url, 275 | wasm_bindgen_shim_url, 276 | .. 277 | } = self; 278 | 279 | // Get worker script as URL encoded blob 280 | let script = worker_script_url.unwrap_or(get_worker_script(wasm_bindgen_shim_url)); 281 | 282 | // Todo: figure out how to set stack size 283 | let options = WorkerOptions::new(); 284 | match (name, prefix) { 285 | (Some(name), Some(prefix)) => { 286 | options.set_name(&format!("{}:{}", prefix, name)); 287 | } 288 | (Some(name), None) => { 289 | options.set_name(&name); 290 | } 291 | (None, Some(prefix)) => { 292 | let random = (js_sys::Math::random() * 10e10) as u64; 293 | options.set_name(&format!("{}:{}", prefix, random)); 294 | } 295 | (None, None) => {} 296 | }; 297 | 298 | #[cfg(feature = "es_modules")] 299 | { 300 | js_sys::eval(include_str!("js/module_workers_polyfill.min.js")).unwrap(); 301 | options.set_type(WorkerType::Module); 302 | } 303 | #[cfg(not(feature = "es_modules"))] 304 | { 305 | options.set_type(WorkerType::Classic); 306 | } 307 | 308 | // Spawn the worker 309 | let worker = Rc::new(Worker::new_with_options(script.as_str(), &options).unwrap()); 310 | 311 | // Make copy and keep a reference in callback handler so that GC does not despawn worker 312 | let mut their_worker = Some(worker.clone()); 313 | 314 | let callback = Closure::wrap(Box::new(move |x: &web_sys::MessageEvent| { 315 | // All u32 bits map to f64 mantisa so it's safe to cast like that 316 | let req = Box::from_raw(x.data().as_f64().unwrap() as u32 as *mut WorkerMessage); 317 | 318 | match *req { 319 | WorkerMessage::SpawnThread(builder) => { 320 | builder.spawn(); 321 | } 322 | WorkerMessage::ThreadComplete => { 323 | // Drop worker reference so it can be cleaned up by GC 324 | their_worker.take(); 325 | } 326 | }; 327 | }) as Box); 328 | worker.set_onmessage(Some(callback.as_ref().unchecked_ref())); 329 | 330 | // TODO: cleanup this leak somehow 331 | callback.forget(); 332 | 333 | let ctx_ptr = Box::into_raw(Box::new(ctx)); 334 | 335 | // Pack shared wasm (module and memory) and work as a single JS array 336 | let init = js_sys::Array::new(); 337 | init.push(&wasm_bindgen::module()); 338 | init.push(&wasm_bindgen::memory()); 339 | init.push(&JsValue::from(ctx_ptr as u32)); 340 | 341 | // Send initialization message 342 | match worker.post_message(&init) { 343 | Ok(()) => Ok(worker), 344 | Err(e) => { 345 | drop(Box::from_raw(ctx_ptr)); 346 | Err(e) 347 | } 348 | } 349 | .unwrap(); 350 | } 351 | } 352 | 353 | // This packet is used to communicate the return value between the spawned 354 | // thread and the rest of the program. It is shared through an `Arc` and 355 | // there's no need for a mutex here because synchronization happens with `join()` 356 | // (the caller will never read this packet until the thread has exited). 357 | // 358 | // An Arc to the packet is stored into a `JoinInner` which in turns is placed 359 | // in `JoinHandle`. 360 | struct Packet<'scope, T> { 361 | scope: Option>, 362 | result: UnsafeCell>>, 363 | _marker: PhantomData>, 364 | } 365 | 366 | // Due to the usage of `UnsafeCell` we need to manually implement Sync. 367 | // The type `T` should already always be Send (otherwise the thread could not 368 | // have been created) and the Packet is Sync because all access to the 369 | // `UnsafeCell` synchronized (by the `join()` boundary), and `ScopeData` is Sync. 370 | unsafe impl<'scope, T: Send> Sync for Packet<'scope, T> {} 371 | 372 | impl<'scope, T> Drop for Packet<'scope, T> { 373 | fn drop(&mut self) { 374 | // If this packet was for a thread that ran in a scope, the thread 375 | // panicked, and nobody consumed the panic payload, we make sure 376 | // the scope function will panic. 377 | let unhandled_panic = matches!(self.result.get_mut(), Some(Err(_))); 378 | // Drop the result without causing unwinding. 379 | // This is only relevant for threads that aren't join()ed, as 380 | // join() will take the `result` and set it to None, such that 381 | // there is nothing left to drop here. 382 | // If this panics, we should handle that, because we're outside the 383 | // outermost `catch_unwind` of our thread. 384 | // We just abort in that case, since there's nothing else we can do. 385 | // (And even if we tried to handle it somehow, we'd also need to handle 386 | // the case where the panic payload we get out of it also panics on 387 | // drop, and so on. See issue #86027.) 388 | if let Err(_) = catch_unwind(AssertUnwindSafe(|| { 389 | *self.result.get_mut() = None; 390 | })) { 391 | panic!("thread result panicked on drop"); 392 | } 393 | // Book-keeping so the scope knows when it's done. 394 | if let Some(scope) = &self.scope { 395 | // Now that there will be no more user code running on this thread 396 | // that can use 'scope, mark the thread as 'finished'. 397 | // It's important we only do this after the `result` has been dropped, 398 | // since dropping it might still use things it borrowed from 'scope. 399 | scope.decrement_num_running_threads(unhandled_panic); 400 | } 401 | } 402 | } 403 | 404 | /// Inner representation for JoinHandle 405 | pub(crate) struct JoinInner<'scope, T> { 406 | packet: Arc>, 407 | signal: Arc, 408 | } 409 | 410 | impl<'scope, T> JoinInner<'scope, T> { 411 | pub fn join(mut self) -> Result { 412 | self.signal.wait(); 413 | Arc::get_mut(&mut self.packet).unwrap().result.get_mut().take().unwrap() 414 | } 415 | 416 | pub async fn join_async(mut self) -> Result { 417 | self.signal.wait_async().await; 418 | Arc::get_mut(&mut self.packet).unwrap().result.get_mut().take().unwrap() 419 | } 420 | } 421 | 422 | /// An owned permission to join on a thread (block on its termination). 423 | pub struct JoinHandle(JoinInner<'static, T>); 424 | 425 | impl JoinHandle { 426 | /// Extracts a handle to the underlying thread. 427 | pub fn thread(&self) -> &Thread { 428 | unimplemented!(); 429 | //&self.0.thread 430 | } 431 | 432 | /// Waits for the associated thread to finish. 433 | pub fn join(self) -> Result { 434 | self.0.join() 435 | } 436 | 437 | /// Waits for the associated thread to finish asynchronously. 438 | pub async fn join_async(self) -> Result { 439 | self.0.join_async().await 440 | } 441 | 442 | /// Checks if the associated thread has finished running its main function. 443 | pub fn is_finished(&self) -> bool { 444 | Arc::strong_count(&self.0.packet) == 1 445 | } 446 | } 447 | 448 | impl fmt::Debug for JoinHandle { 449 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 450 | f.pad("JoinHandle { .. }") 451 | } 452 | } 453 | 454 | /// Spawns a new thread, returning a JoinHandle for it. 455 | pub fn spawn(f: F) -> JoinHandle 456 | where 457 | F: FnOnce() -> T, 458 | F: Send + 'static, 459 | T: Send + 'static, 460 | { 461 | Builder::new().spawn(f).expect("failed to spawn thread") 462 | } 463 | -------------------------------------------------------------------------------- /src/wasm32/scoped.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | marker::PhantomData, 3 | panic::{catch_unwind, resume_unwind, AssertUnwindSafe}, 4 | sync::{ 5 | atomic::{AtomicBool, AtomicUsize, Ordering}, 6 | Arc, 7 | }, 8 | }; 9 | 10 | use super::{signal::Signal, utils::is_web_worker_thread, Builder, JoinInner}; 11 | 12 | /// A scope to spawn scoped threads in. 13 | /// 14 | /// See [`scope`] for details. 15 | pub struct Scope<'scope, 'env: 'scope> { 16 | data: Arc, 17 | /// Invariance over 'scope, to make sure 'scope cannot shrink, 18 | /// which is necessary for soundness. 19 | /// 20 | /// Without invariance, this would compile fine but be unsound: 21 | /// 22 | /// ```compile_fail,E0373 23 | /// std::thread::scope(|s| { 24 | /// s.spawn(|| { 25 | /// let a = String::from("abcd"); 26 | /// s.spawn(|| println!("{a:?}")); // might run after `a` is dropped 27 | /// }); 28 | /// }); 29 | /// ``` 30 | scope: PhantomData<&'scope mut &'scope ()>, 31 | env: PhantomData<&'env mut &'env ()>, 32 | } 33 | 34 | /// An owned permission to join on a scoped thread (block on its termination). 35 | /// 36 | /// See [`Scope::spawn`] for details. 37 | pub struct ScopedJoinHandle<'scope, T>(JoinInner<'scope, T>); 38 | 39 | pub(crate) struct ScopeData { 40 | num_running_threads: AtomicUsize, 41 | a_thread_panicked: AtomicBool, 42 | signal: Signal, 43 | } 44 | 45 | impl ScopeData { 46 | pub(crate) fn increment_num_running_threads(&self) { 47 | // We check for 'overflow' with usize::MAX / 2, to make sure there's no 48 | // chance it overflows to 0, which would result in unsoundness. 49 | if self.num_running_threads.fetch_add(1, Ordering::Relaxed) > usize::MAX / 2 { 50 | // This can only reasonably happen by mem::forget()'ing a lot of ScopedJoinHandles. 51 | self.decrement_num_running_threads(false); 52 | panic!("too many running threads in thread scope"); 53 | } 54 | } 55 | 56 | pub(crate) fn decrement_num_running_threads(&self, panic: bool) { 57 | if panic { 58 | self.a_thread_panicked.store(true, Ordering::Relaxed); 59 | } 60 | 61 | if self.num_running_threads.fetch_sub(1, Ordering::Release) == 1 { 62 | // All threads have terminated 63 | self.signal.signal(); 64 | } 65 | } 66 | } 67 | 68 | /// Create a scope for spawning scoped threads. 69 | /// 70 | /// The function passed to `scope` will be provided a [`Scope`] object, 71 | /// through which scoped threads can be [spawned][`Scope::spawn`]. 72 | /// 73 | /// Unlike non-scoped threads, scoped threads can borrow non-`'static` data, 74 | /// as the scope guarantees all threads will be joined at the end of the scope. 75 | /// 76 | /// All threads spawned within the scope that haven't been manually joined 77 | /// will be automatically joined before this function returns. 78 | /// 79 | /// # Panics 80 | /// 81 | /// If any of the automatically joined threads panicked, this function will panic. 82 | /// 83 | /// If you want to handle panics from spawned threads, 84 | /// [`join`][ScopedJoinHandle::join] them before the end of the scope. 85 | /// 86 | /// On wasm, this will panic on main thread because blocking join is not allowed. 87 | pub fn scope<'env, F, T>(f: F) -> T 88 | where 89 | F: for<'scope> FnOnce(&'scope Scope<'scope, 'env>) -> T, 90 | { 91 | // Fail early to avoid flaky panics that depend on execution time 92 | if !is_web_worker_thread() { 93 | panic!("scope is not allowed on the main thread"); 94 | } 95 | 96 | // We put the `ScopeData` into an `Arc` so that other threads can finish their 97 | // `decrement_num_running_threads` even after this function returns. 98 | let scope = Scope { 99 | data: Arc::new(ScopeData { 100 | num_running_threads: AtomicUsize::new(0), 101 | a_thread_panicked: AtomicBool::new(false), 102 | signal: Signal::new(), 103 | }), 104 | env: PhantomData, 105 | scope: PhantomData, 106 | }; 107 | 108 | // Run `f`, but catch panics so we can make sure to wait for all the threads to join. 109 | let result = catch_unwind(AssertUnwindSafe(|| f(&scope))); 110 | 111 | // Wait until all the threads are finished. 112 | while scope.data.num_running_threads.load(Ordering::Acquire) != 0 { 113 | scope.data.signal.wait(); 114 | } 115 | 116 | // Throw any panic from `f`, or the return value of `f` if no thread panicked. 117 | match result { 118 | Err(e) => resume_unwind(e), 119 | Ok(_) if scope.data.a_thread_panicked.load(Ordering::Relaxed) => { 120 | panic!("a scoped thread panicked") 121 | } 122 | Ok(result) => result, 123 | } 124 | } 125 | 126 | impl<'scope, 'env> Scope<'scope, 'env> { 127 | /// Spawns a new thread within a scope, returning a [`ScopedJoinHandle`] for it. 128 | /// 129 | /// Unlike non-scoped threads, threads spawned with this function may 130 | /// borrow non-`'static` data from the outside the scope. See [`scope`] for 131 | /// details. 132 | /// 133 | /// The join handle provides a [`join`] method that can be used to join the spawned 134 | /// thread. If the spawned thread panics, [`join`] will return an [`Err`] containing 135 | /// the panic payload. 136 | /// 137 | /// If the join handle is dropped, the spawned thread will implicitly joined at the 138 | /// end of the scope. In that case, if the spawned thread panics, [`scope`] will 139 | /// panic after all threads are joined. 140 | /// 141 | /// This call will create a thread using default parameters of [`Builder`]. 142 | /// If you want to specify the stack size or the name of the thread, use 143 | /// [`Builder::spawn_scoped`] instead. 144 | /// 145 | /// # Panics 146 | /// 147 | /// Panics if the OS fails to create a thread; use [`Builder::spawn_scoped`] 148 | /// to recover from such errors. 149 | /// 150 | /// [`join`]: ScopedJoinHandle::join 151 | pub fn spawn(&'scope self, f: F) -> ScopedJoinHandle<'scope, T> 152 | where 153 | F: FnOnce() -> T + Send + 'scope, 154 | T: Send + 'scope, 155 | { 156 | Builder::new().spawn_scoped(self, f).expect("failed to spawn thread") 157 | } 158 | } 159 | 160 | impl Builder { 161 | /// Spawns a new scoped thread using the settings set through this `Builder`. 162 | /// 163 | /// Unlike [Scope::spawn], this method yields an [std::io::Result] to 164 | /// capture any failure to create the thread at the OS level. 165 | pub fn spawn_scoped<'scope, 'env, F, T>( 166 | self, 167 | scope: &'scope Scope<'scope, 'env>, 168 | f: F, 169 | ) -> std::io::Result> 170 | where 171 | F: FnOnce() -> T + Send + 'scope, 172 | T: Send + 'scope, 173 | { 174 | Ok(ScopedJoinHandle(unsafe { 175 | self.spawn_unchecked_(f, Some(scope.data.clone())) 176 | }?)) 177 | } 178 | } 179 | 180 | impl<'scope, T> ScopedJoinHandle<'scope, T> { 181 | pub fn join(self) -> super::Result { 182 | self.0.join() 183 | } 184 | 185 | pub async fn join_async(self) -> super::Result { 186 | self.0.join_async().await 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/wasm32/signal.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | arch::wasm32, 3 | sync::{ 4 | atomic::{AtomicU32, Ordering}, 5 | Mutex, 6 | }, 7 | task::{Poll, Waker}, 8 | }; 9 | 10 | use futures::future::poll_fn; 11 | 12 | use super::utils::SpinLockMutex; 13 | 14 | /// A combined sync/async synchronization primitive that allows waiting for a condition. 15 | pub struct Signal { 16 | waiters: Mutex>, 17 | // Starts with 0 and changes to 1 when signaled 18 | value: AtomicU32, 19 | } 20 | 21 | impl Signal { 22 | pub fn new() -> Self { 23 | Self { 24 | waiters: Mutex::new(Default::default()), 25 | value: AtomicU32::new(0), 26 | } 27 | } 28 | 29 | /// Sends a signal and unlocks all waiters. 30 | pub fn signal(&self) { 31 | self.value.store(1, Ordering::SeqCst); 32 | 33 | // Wake all blocking waiters 34 | unsafe { 35 | wasm32::memory_atomic_notify(&self.value as *const AtomicU32 as *mut i32, i32::MAX as u32); 36 | } 37 | 38 | // Wake all async waiters 39 | for waiter in self.waiters.lock_spin().unwrap().drain(..) { 40 | waiter.wake(); 41 | } 42 | } 43 | 44 | /// Synchronously waits until [Self::signal] is called. 45 | pub fn wait(&self) { 46 | while self.value.load(Ordering::Relaxed) == 0 { 47 | unsafe { 48 | wasm32::memory_atomic_wait32(&self.value as *const AtomicU32 as *mut i32, 0, -1); 49 | } 50 | } 51 | } 52 | 53 | /// Asynchronously waits until [Self::signal] is called. 54 | pub async fn wait_async(&self) { 55 | poll_fn(|cx| { 56 | self.waiters.lock_spin().unwrap().push(cx.waker().clone()); 57 | 58 | if self.value.load(Ordering::Relaxed) == 1 { 59 | Poll::Ready(()) 60 | } else { 61 | Poll::Pending 62 | } 63 | }) 64 | .await 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/wasm32/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | num::NonZeroUsize, 4 | sync::{LockResult, Mutex, MutexGuard, TryLockError}, 5 | }; 6 | 7 | use wasm_bindgen::prelude::*; 8 | use web_sys::{Blob, Url, WorkerGlobalScope}; 9 | 10 | pub fn available_parallelism() -> io::Result { 11 | if let Some(window) = web_sys::window() { 12 | return Ok(NonZeroUsize::new(window.navigator().hardware_concurrency() as usize).unwrap()); 13 | } 14 | 15 | if let Ok(worker) = js_sys::eval("self").unwrap().dyn_into::() { 16 | return Ok(NonZeroUsize::new(worker.navigator().hardware_concurrency() as usize).unwrap()); 17 | } 18 | 19 | Err(io::Error::new( 20 | io::ErrorKind::Unsupported, 21 | "hardware_concurrency unsupported", 22 | )) 23 | } 24 | 25 | pub fn is_web_worker_thread() -> bool { 26 | js_sys::eval("self").unwrap().dyn_into::().is_ok() 27 | } 28 | 29 | /// Extracts path of the `wasm_bindgen` generated .js shim script. 30 | /// 31 | /// Internally, this intentionally generates a javascript exception to obtain a stacktrace containing the current script 32 | /// URL. 33 | pub fn get_wasm_bindgen_shim_script_path() -> String { 34 | js_sys::eval(include_str!("js/script_path.js")) 35 | .unwrap() 36 | .as_string() 37 | .unwrap() 38 | } 39 | 40 | /// Generates worker entry script as URL encoded blob 41 | pub fn get_worker_script(wasm_bindgen_shim_url: Option) -> String { 42 | // Cache URL so that subsequent calls are less expensive 43 | static CACHED_URL: Mutex> = Mutex::new(None); 44 | 45 | if let Some(url) = CACHED_URL.lock_spin().unwrap().clone() { 46 | return url; 47 | } 48 | 49 | // If wasm bindgen shim url is not provided, try to obtain one automatically 50 | let wasm_bindgen_shim_url = wasm_bindgen_shim_url.unwrap_or_else(get_wasm_bindgen_shim_script_path); 51 | 52 | // Generate script from template 53 | #[cfg(feature = "es_modules")] 54 | let template = include_str!("js/web_worker_module.js"); 55 | #[cfg(not(feature = "es_modules"))] 56 | let template = include_str!("js/web_worker.js"); 57 | 58 | let script = template.replace("WASM_BINDGEN_SHIM_URL", &wasm_bindgen_shim_url); 59 | 60 | // Create url encoded blob 61 | let arr = js_sys::Array::new(); 62 | arr.set(0, JsValue::from_str(&script)); 63 | let blob = Blob::new_with_str_sequence(&arr).unwrap(); 64 | let url = Url::create_object_url_with_blob( 65 | &blob 66 | .slice_with_f64_and_f64_and_content_type(0.0, blob.size(), "text/javascript") 67 | .unwrap(), 68 | ) 69 | .unwrap(); 70 | 71 | *CACHED_URL.lock_spin().unwrap() = Some(url.clone()); 72 | 73 | url 74 | } 75 | 76 | /// A spin lock mutex extension. 77 | /// 78 | /// Atomic wait panics in wasm main thread so we can't use `Mutex::lock()`. 79 | /// This is a helper, which implement spinlock by calling `Mutex::try_lock()` in a loop. 80 | /// Care must be taken not to introduce deadlocks when using this trait. 81 | pub trait SpinLockMutex { 82 | type Inner; 83 | 84 | fn lock_spin<'a>(&'a self) -> LockResult>; 85 | } 86 | 87 | impl SpinLockMutex for Mutex { 88 | type Inner = T; 89 | 90 | fn lock_spin<'a>(&'a self) -> LockResult> { 91 | loop { 92 | match self.try_lock() { 93 | Ok(guard) => break Ok(guard), 94 | Err(TryLockError::WouldBlock) => {} 95 | Err(TryLockError::Poisoned(e)) => break Err(e), 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /test_wasm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals' \ 4 | wasm-pack test --headless --firefox -- -Z build-std=panic_abort,std 5 | -------------------------------------------------------------------------------- /tests/native.rs: -------------------------------------------------------------------------------- 1 | //! Trivial tests to ensure that native thread API is unchanged. 2 | 3 | use wasm_thread as thread; 4 | 5 | #[test] 6 | fn thread_join() { 7 | let handle = thread::spawn(|| 1234); 8 | 9 | assert_eq!(handle.join().unwrap(), 1234); 10 | } 11 | 12 | #[test] 13 | fn thread_scope() { 14 | let mut a = vec![1, 2, 3]; 15 | let mut x = 0; 16 | 17 | thread::scope(|s| { 18 | s.spawn(|| { 19 | println!("hello from the first scoped thread {:?}", thread::current().id()); 20 | // We can borrow `a` here. 21 | dbg!(&a) 22 | }); 23 | 24 | s.spawn(|| { 25 | println!("hello from the second scoped thread {:?}", thread::current().id()); 26 | // We can even mutably borrow `x` here, 27 | // because no other threads are using it. 28 | x += a[0] + a[2]; 29 | }); 30 | 31 | println!( 32 | "Hello from scope \"main\" thread {:?} inside scope.", 33 | thread::current().id() 34 | ); 35 | }); 36 | 37 | // After the scope, we can modify and access our variables again: 38 | a.push(4); 39 | assert_eq!(x, a.len()); 40 | } 41 | -------------------------------------------------------------------------------- /tests/wasm.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_arch = "wasm32")] 2 | 3 | use core::{ 4 | sync::atomic::{AtomicBool, Ordering}, 5 | time::Duration, 6 | }; 7 | 8 | use wasm_bindgen_test::*; 9 | use wasm_thread as thread; 10 | 11 | wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); 12 | 13 | #[wasm_bindgen_test] 14 | async fn thread_join_async() { 15 | let handle = thread::spawn(|| 1234); 16 | 17 | assert_eq!(handle.join_async().await.unwrap(), 1234); 18 | } 19 | 20 | #[wasm_bindgen_test] 21 | async fn thread_join_sync() { 22 | // synchronous join only allowed inside threads 23 | thread::spawn(|| { 24 | let handle = thread::spawn(|| 1234); 25 | 26 | assert_eq!(handle.join().unwrap(), 1234); 27 | }) 28 | .join_async() 29 | .await 30 | .unwrap(); 31 | } 32 | 33 | #[wasm_bindgen_test] 34 | async fn thread_scope_sync() { 35 | // synchronous scope only allowed inside threads 36 | thread::spawn(|| { 37 | let mut a = vec![1, 2, 3]; 38 | let mut x = 0; 39 | 40 | thread::scope(|s| { 41 | s.spawn(|| { 42 | println!("hello from the first scoped thread {:?}", thread::current().id()); 43 | // We can borrow `a` here. 44 | dbg!(&a) 45 | }); 46 | 47 | s.spawn(|| { 48 | println!("hello from the second scoped thread {:?}", thread::current().id()); 49 | // We can even mutably borrow `x` here, 50 | // because no other threads are using it. 51 | x += a[0] + a[2]; 52 | }); 53 | 54 | println!( 55 | "Hello from scope \"main\" thread {:?} inside scope.", 56 | thread::current().id() 57 | ); 58 | }); 59 | 60 | // After the scope, we can modify and access our variables again: 61 | a.push(4); 62 | assert_eq!(x, a.len()); 63 | }) 64 | .join_async() 65 | .await 66 | .unwrap(); 67 | } 68 | 69 | #[wasm_bindgen_test] 70 | async fn thread_scope_sync_block() { 71 | // synchronous scope only allowed inside threads 72 | thread::spawn(|| { 73 | let t1_done = AtomicBool::new(false); 74 | let t2_done = AtomicBool::new(false); 75 | 76 | thread::scope(|s| { 77 | s.spawn(|| { 78 | thread::sleep(Duration::from_millis(100)); 79 | t1_done.store(true, Ordering::Relaxed); 80 | }); 81 | 82 | s.spawn(|| { 83 | thread::sleep(Duration::from_millis(100)); 84 | t2_done.store(true, Ordering::Relaxed); 85 | }); 86 | 87 | // Threads should be in sleep and not yet done 88 | assert_eq!(t1_done.load(Ordering::Relaxed), false); 89 | assert_eq!(t2_done.load(Ordering::Relaxed), false); 90 | }); 91 | 92 | // Scope should block until both threads terminate 93 | assert_eq!(t1_done.load(Ordering::Relaxed), true); 94 | assert_eq!(t2_done.load(Ordering::Relaxed), true); 95 | }) 96 | .join_async() 97 | .await 98 | .unwrap(); 99 | } 100 | 101 | #[wasm_bindgen_test] 102 | async fn thread_async_channel() { 103 | // Exchange a series of messages over async channel. 104 | let (thread_tx, main_rx) = async_channel::unbounded::(); 105 | let (main_tx, thread_rx) = async_channel::unbounded::(); 106 | 107 | thread::spawn(|| { 108 | futures::executor::block_on(async move { 109 | thread::sleep(Duration::from_millis(100)); 110 | thread_tx.send("Hello".to_string()).await.unwrap(); 111 | let mut msg = thread_rx.recv().await.unwrap(); 112 | msg.push_str("!"); 113 | thread_tx.send(msg).await.unwrap(); 114 | }) 115 | }); 116 | 117 | let mut msg = main_rx.recv().await.unwrap(); 118 | msg.push_str(" world"); 119 | main_tx.send(msg).await.unwrap(); 120 | 121 | let result = main_rx.recv().await.unwrap(); 122 | assert_eq!(result, "Hello world!"); 123 | } 124 | --------------------------------------------------------------------------------