7 | #### 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /threads/examples/wasi_workers/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |7 | #### 8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |7 | #### 8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads_channel/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |7 | #### 8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /threads/src/index.ts: -------------------------------------------------------------------------------- 1 | import { WASIFarmAnimal } from "./animals.js"; 2 | import { WASIFarm } from "./farm.js"; 3 | import { WASIFarmRef } from "./ref.js"; 4 | export { thread_spawn_on_worker } from "./shared_array_buffer/index.js"; 5 | export { WASIFarm, WASIFarmRef, WASIFarmAnimal }; 6 | -------------------------------------------------------------------------------- /threads/import-module-test/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | 3 | export default defineConfig({ 4 | server: { 5 | headers: { 6 | "Cross-Origin-Embedder-Policy": "require-corp", 7 | "Cross-Origin-Opener-Policy": "same-origin", 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 3 | parser: "@typescript-eslint/parser", 4 | plugins: ["@typescript-eslint"], 5 | rules: { 6 | "@typescript-eslint/no-this-alias": "off", 7 | "@typescript-eslint/ban-ts-comment": "off", 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /threads/import-module-test/src/counter.ts: -------------------------------------------------------------------------------- 1 | export function setupCounter(element: HTMLButtonElement) { 2 | let counter = 0; 3 | const setCounter = (count: number) => { 4 | counter = count; 5 | element.innerHTML = `count is ${counter}`; 6 | }; 7 | element.addEventListener("click", () => setCounter(counter + 1)); 8 | setCounter(0); 9 | } 10 | -------------------------------------------------------------------------------- /threads/import-module-test/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | 4 | let _: std::thread::JoinHandle<()> = std::thread::spawn(|| { 5 | for i in 1..1000 { 6 | println!("hi number {} from the spawned thread!", i); 7 | } 8 | }); 9 | 10 | for i in 1..1000 { 11 | println!("hi number {} from the main thread!", i); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /threads/src/shared_array_buffer/worker_background/index.ts: -------------------------------------------------------------------------------- 1 | import type { WorkerBackgroundRefObject } from "./worker_export.js"; 2 | import { WorkerBackgroundRef, WorkerRef } from "./worker_background_ref.js"; 3 | import { url as worker_background_worker_url } from "./worker_blob.js"; 4 | 5 | export { 6 | WorkerBackgroundRef, 7 | WorkerRef, 8 | type WorkerBackgroundRefObject, 9 | worker_background_worker_url, 10 | }; 11 | -------------------------------------------------------------------------------- /test/adapters/shared/adapter.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import pathlib 3 | import sys 4 | import os 5 | 6 | run_wasi_mjs = pathlib.Path(__file__).parent / "run-wasi.mjs" 7 | args = sys.argv[1:] 8 | cmd = ["node", str(run_wasi_mjs)] + args 9 | if os.environ.get("VERBOSE_ADAPTER") is not None: 10 | print(" ".join(map(lambda x: f"'{x}'", cmd))) 11 | 12 | result = subprocess.run(cmd, check=False) 13 | sys.exit(result.returncode) 14 | -------------------------------------------------------------------------------- /threads/src/shared_array_buffer/worker_background/spack.config.cjs: -------------------------------------------------------------------------------- 1 | // https://swc.rs/docs/configuration/bundling 2 | 3 | const { config } = require("@swc/core/spack"); 4 | 5 | console.log(__dirname); 6 | 7 | module.exports = config({ 8 | entry: { 9 | web: `${__dirname}/worker.ts`, 10 | }, 11 | output: { 12 | path: `${__dirname}/../../../dist/workers/`, 13 | name: "worker_background_worker.js", 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /threads/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/swcrc", 3 | "jsc": { 4 | "parser": { 5 | "syntax": "typescript", 6 | "tsx": false, 7 | "dynamicImport": false, 8 | "decorators": false, 9 | "dts": true 10 | }, 11 | "transform": {}, 12 | "target": "esnext", 13 | "loose": false, 14 | "externalHelpers": false, 15 | "keepClassNames": true 16 | }, 17 | "minify": true 18 | } 19 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads_rustc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /threads/import-module-test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |24 | Note: the failure to invoke the linker at the end is expected. 25 | WASI doesn't have a way to invoke external processes and rustc doesn't have a builtin linker. 26 | This demo highlights how far `rustc` can get on this polyfill before failing due to other reasons. 27 |
28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /threads/vite.config.ts: -------------------------------------------------------------------------------- 1 | // https://zenn.dev/seapolis/articles/3605c4befc8465 2 | 3 | import { resolve } from "node:path"; 4 | import { defineConfig } from "vite"; 5 | import dts from "vite-plugin-dts"; 6 | import swc from "unplugin-swc"; 7 | 8 | export default defineConfig({ 9 | server: { 10 | headers: { 11 | "Cross-Origin-Embedder-Policy": "require-corp", 12 | "Cross-Origin-Opener-Policy": "same-origin", 13 | }, 14 | }, 15 | build: { 16 | lib: { 17 | entry: resolve(__dirname, "src/index.ts"), 18 | name: "wasi-shim-threads", 19 | formats: ["es", "umd", "cjs"], 20 | fileName: (format) => `browser-wasi-shim-threads.${format}.js`, 21 | }, 22 | sourcemap: true, 23 | minify: true, 24 | copyPublicDir: false, 25 | }, 26 | // plugins: [dts({ rollupTypes: true })], 27 | plugins: [swc.vite(), swc.rollup(), dts({ rollupTypes: true })], 28 | }); 29 | -------------------------------------------------------------------------------- /test/skip.json: -------------------------------------------------------------------------------- 1 | { 2 | "WASI Assemblyscript tests": { 3 | }, 4 | "WASI C tests": { 5 | "sock_shutdown-invalid_fd": "not implemented yet", 6 | "stat-dev-ino": "fail", 7 | "sock_shutdown-not_sock": "fail", 8 | "fdopendir-with-access": "fail" 9 | }, 10 | "WASI Rust tests": { 11 | "path_exists": "fail", 12 | "fd_filestat_set": "fail", 13 | "symlink_create": "fail", 14 | "path_open_read_write": "fail", 15 | "path_rename_dir_trailing_slashes": "fail", 16 | "fd_flags_set": "fail", 17 | "path_filestat": "fail", 18 | "path_link": "fail", 19 | "fd_fdstat_set_rights": "fail", 20 | "readlink": "fail", 21 | "path_symlink_trailing_slashes": "fail", 22 | "poll_oneoff_stdio": "fail", 23 | "dangling_symlink": "fail", 24 | "nofollow_errors": "fail", 25 | "path_open_preopen": "fail", 26 | "symlink_filestat": "fail", 27 | "symlink_loop": "fail" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/adapters/shared/parseArgs.mjs: -------------------------------------------------------------------------------- 1 | /// Parse command line arguments given by `adapter.py` through 2 | /// `wasi-testsuite`'s test runner. 3 | export function parseArgs() { 4 | const args = process.argv.slice(2); 5 | const options = { 6 | "version": false, 7 | "test-file": null, 8 | "arg": [], 9 | "env": [], 10 | "dir": [], 11 | }; 12 | while (args.length > 0) { 13 | const arg = args.shift(); 14 | if (arg.startsWith("--")) { 15 | let [name, value] = arg.split("="); 16 | name = name.slice(2); 17 | if (Object.prototype.hasOwnProperty.call(options, name)) { 18 | if (value === undefined) { 19 | value = args.shift() || true; 20 | } 21 | if (Array.isArray(options[name])) { 22 | options[name].push(value); 23 | } else { 24 | options[name] = value; 25 | } 26 | } 27 | } 28 | } 29 | 30 | return options; 31 | } 32 | -------------------------------------------------------------------------------- /threads/examples/wasi_workers/worker1.ts: -------------------------------------------------------------------------------- 1 | import { WASIFarmAnimal } from "../../src"; 2 | 3 | self.onmessage = async (e) => { 4 | const { wasi_ref, wasi_ref2 } = e.data; 5 | 6 | console.dir(wasi_ref); 7 | console.dir(wasi_ref2); 8 | 9 | const wasi = new WASIFarmAnimal( 10 | [wasi_ref2, wasi_ref], 11 | [ 12 | "echo_and_rewrite", 13 | "hello2/hello2.txt", 14 | "world", 15 | "new_world", 16 | "0", 17 | "100", 18 | "100", 19 | ], // args 20 | [""], // env 21 | // options 22 | ); 23 | 24 | console.dir(wasi, { depth: null }); 25 | 26 | const wasm = await fetch("./echo_and_rewrite.wasm"); 27 | const buff = await wasm.arrayBuffer(); 28 | const { instance } = await WebAssembly.instantiate(buff, { 29 | wasi_snapshot_preview1: wasi.wasiImport, 30 | }); 31 | wasi.start( 32 | instance as unknown as { 33 | exports: { memory: WebAssembly.Memory; _start: () => unknown }; 34 | }, 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads/worker.ts: -------------------------------------------------------------------------------- 1 | import { WASIFarmAnimal } from "../../src"; 2 | 3 | self.onmessage = async (e) => { 4 | const { wasi_ref } = e.data; 5 | 6 | const wasm = await WebAssembly.compileStreaming( 7 | fetch("./multi_thread_echo.wasm"), 8 | ); 9 | 10 | const wasi = new WASIFarmAnimal( 11 | wasi_ref, 12 | [], // args 13 | [], // env 14 | { 15 | can_thread_spawn: true, 16 | thread_spawn_worker_url: new URL("./thread_spawn.ts", import.meta.url) 17 | .href, 18 | thread_spawn_wasm: wasm, 19 | }, 20 | ); 21 | 22 | await wasi.wait_worker_background_worker(); 23 | 24 | const inst = await WebAssembly.instantiate(wasm, { 25 | env: { 26 | memory: wasi.get_share_memory(), 27 | }, 28 | wasi: wasi.wasiThreadImport, 29 | wasi_snapshot_preview1: wasi.wasiImport, 30 | }); 31 | 32 | wasi.start( 33 | inst as unknown as { 34 | exports: { memory: WebAssembly.Memory; _start: () => unknown }; 35 | }, 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads_channel/worker.ts: -------------------------------------------------------------------------------- 1 | import { WASIFarmAnimal } from "../../src"; 2 | 3 | self.onmessage = async (e) => { 4 | const { wasi_ref } = e.data; 5 | 6 | const wasm = await WebAssembly.compileStreaming(fetch("./channel.wasm")); 7 | 8 | const wasi = new WASIFarmAnimal( 9 | wasi_ref, 10 | [], // args 11 | [], // env 12 | { 13 | can_thread_spawn: true, 14 | thread_spawn_worker_url: new URL("./thread_spawn.ts", import.meta.url) 15 | .href, 16 | // thread_spawn_worker_url: "./thread_spawn.ts", 17 | thread_spawn_wasm: wasm, 18 | }, 19 | ); 20 | 21 | await wasi.wait_worker_background_worker(); 22 | 23 | const inst = await WebAssembly.instantiate(wasm, { 24 | env: { 25 | memory: wasi.get_share_memory(), 26 | }, 27 | wasi: wasi.wasiThreadImport, 28 | wasi_snapshot_preview1: wasi.wasiImport, 29 | }); 30 | 31 | wasi.start( 32 | inst as unknown as { 33 | exports: { memory: WebAssembly.Memory; _start: () => unknown }; 34 | }, 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/debug.ts: -------------------------------------------------------------------------------- 1 | class Debug { 2 | prefix?: string = "wasi:"; 3 | log: (...args: unknown[]) => void; 4 | 5 | constructor(private isEnabled: boolean) { 6 | this.enable(isEnabled); 7 | } 8 | 9 | // Recreate the logger function with the new enabled state. 10 | enable(enabled?: boolean) { 11 | this.log = createLogger( 12 | enabled === undefined ? true : enabled, 13 | this.prefix, 14 | ); 15 | } 16 | 17 | // Getter for the private isEnabled property. 18 | get enabled(): boolean { 19 | return this.isEnabled; 20 | } 21 | } 22 | 23 | // The createLogger() creates either an empty function or a bound console.log 24 | // function so we can retain accurate line lumbers on Debug.log() calls. 25 | function createLogger( 26 | enabled: boolean, 27 | prefix?: string, 28 | ): (...args: unknown[]) => void { 29 | if (enabled) { 30 | const a = console.log.bind(console, "%c%s", "color: #265BA0", prefix); 31 | return a; 32 | } else { 33 | return () => {}; 34 | } 35 | } 36 | 37 | export const debug = new Debug(false); 38 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /threads/src/shared_array_buffer/worker_background/minify.js: -------------------------------------------------------------------------------- 1 | import swc from "@swc/core"; 2 | 3 | import { readFileSync, writeFileSync } from "node:fs"; 4 | 5 | const old_code = readFileSync( 6 | "./dist/workers/worker_background_worker.js", 7 | "utf8", 8 | ); 9 | 10 | const { code } = await swc.minify(old_code, { 11 | compress: { 12 | reduce_funcs: true, 13 | arguments: true, 14 | booleans_as_integers: true, 15 | hoist_funs: false, 16 | keep_classnames: false, 17 | unsafe: true, 18 | }, 19 | mangle: true, 20 | }); 21 | 22 | writeFileSync( 23 | "./dist/workers/worker_background_worker_minify.js", 24 | code, 25 | "utf8", 26 | ); 27 | 28 | // \n -> \\n 29 | 30 | const wrapper_code = `export const url = () => { 31 | const code = 32 | '${code.replace(/\\/g, "\\\\")}'; 33 | 34 | const blob = new Blob([code], { type: "application/javascript" }); 35 | 36 | const url = URL.createObjectURL(blob); 37 | 38 | return url; 39 | }; 40 | `; 41 | 42 | writeFileSync( 43 | "./src/shared_array_buffer/worker_background/worker_blob.ts", 44 | wrapper_code, 45 | "utf8", 46 | ); 47 | -------------------------------------------------------------------------------- /test/adapters/shared/walkFs.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import path from 'path'; 3 | 4 | /** 5 | * Walks a directory recursively and returns the result of combining the found entries 6 | * using the given reducer function. 7 | * 8 | * @typedef {{ kind: "dir", contents: any } | { kind: "file", buffer: Buffer }} Entry 9 | * @param {string} dir 10 | * @param {(name: string, entry: Entry, out: any) => any} nextPartialResult 11 | * @param {() => any} initial 12 | */ 13 | export async function walkFs(dir, nextPartialResult, initial) { 14 | let result = initial(); 15 | const srcContents = await fs.readdir(dir, { withFileTypes: true }); 16 | for (let entry of srcContents) { 17 | const entryPath = path.join(dir, entry.name); 18 | if (entry.isDirectory()) { 19 | const contents = await walkFs(entryPath, nextPartialResult, initial); 20 | result = nextPartialResult(entry.name, { kind: "dir", contents }, result); 21 | } else { 22 | const buffer = await fs.readFile(entryPath); 23 | result = nextPartialResult(entry.name, { kind: "file", buffer }, result); 24 | } 25 | } 26 | return result; 27 | } 28 | -------------------------------------------------------------------------------- /threads/import-module-test/src/main.ts: -------------------------------------------------------------------------------- 1 | import "./style.css"; 2 | import typescriptLogo from "./typescript.svg"; 3 | import viteLogo from "/vite.svg"; 4 | import { setupCounter } from "./counter.ts"; 5 | 6 | // biome-ignore lint/style/noNonNullAssertion:20 | Click on the Vite and TypeScript logos to learn more 21 |
22 |27 | Note: the failure to invoke the linker at the end is expected. 28 | WASI doesn't have a way to invoke external processes and rustc doesn't have a builtin linker. 29 | This demo highlights how far `rustc` can get on this polyfill before failing due to other reasons. 30 |
31 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /threads/src/shared_array_buffer/sender.ts: -------------------------------------------------------------------------------- 1 | export type ToRefSenderUseArrayBufferObject = { 2 | data_size: number; 3 | share_arrays_memory?: SharedArrayBuffer; 4 | }; 5 | 6 | // To ref sender abstract class 7 | export abstract class ToRefSenderUseArrayBuffer { 8 | // The structure is similar to an allocator, but the mechanism is different 9 | 10 | // Example of fd management 11 | // This needs to be handled 12 | // 1. Start of path_open 13 | // 2. Removed by fd_close 14 | // 2.1 Sent by ToRefSender 15 | // 3. Reassigned by path_open 16 | // < Closed by ToRefSender 17 | // 3.1 The person who opened it can use it 18 | // < Closed by ToRefSender — this alone will cause a bug 19 | // Structurally, this shouldn't happen in the farm 20 | 21 | // In the end, when receiving from this function, it should be done on the first call of each function 22 | 23 | // The first 4 bytes are for lock value: i32 24 | // The next 4 bytes are the current number of data: m: i32 25 | // The next 4 bytes are the length of the area used by share_arrays_memory: n: i32 26 | // Data header 27 | // 4 bytes: remaining target count 28 | // 4 bytes: target count (n) 29 | // n * 4 bytes: target allocation numbers 30 | // Data 31 | // data_size bytes: data 32 | private share_arrays_memory: SharedArrayBuffer; 33 | 34 | // The size of the data 35 | private data_size: number; 36 | 37 | constructor( 38 | // data is Uint32Array 39 | // and data_size is data.length 40 | data_size: number, 41 | max_share_arrays_memory: number = 100 * 1024, 42 | share_arrays_memory?: SharedArrayBuffer, 43 | ) { 44 | this.data_size = data_size; 45 | if (share_arrays_memory) { 46 | this.share_arrays_memory = share_arrays_memory; 47 | } else { 48 | this.share_arrays_memory = new SharedArrayBuffer(max_share_arrays_memory); 49 | } 50 | const view = new Int32Array(this.share_arrays_memory); 51 | Atomics.store(view, 0, 0); 52 | Atomics.store(view, 1, 0); 53 | Atomics.store(view, 2, 12); 54 | } 55 | 56 | protected static init_self_inner(sl: ToRefSenderUseArrayBufferObject): { 57 | data_size: number; 58 | max_share_arrays_memory: number; 59 | share_arrays_memory: SharedArrayBuffer; 60 | } { 61 | return { 62 | data_size: sl.data_size, 63 | // biome-ignore lint/style/noNonNullAssertion: