├── test ├── adapters │ ├── browser │ │ ├── adapter.py │ │ ├── run-test.html │ │ └── run-wasi.mjs │ ├── node │ │ ├── adapter.py │ │ └── run-wasi.mjs │ └── shared │ │ ├── adapter.py │ │ ├── parseArgs.mjs │ │ └── walkFs.mjs ├── run-testsuite.sh └── skip.json ├── .gitignore ├── threads ├── .gitignore ├── import-module-test │ ├── src │ │ ├── vite-env.d.ts │ │ ├── counter.ts │ │ ├── main.ts │ │ ├── typescript.svg │ │ └── style.css │ ├── vite.config.ts │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── tsconfig.json │ └── public │ │ └── vite.svg ├── bun.lockb ├── examples │ ├── bun.lockb │ ├── wasi_workers │ │ ├── echo_and_rewrite.wasm │ │ ├── index.html │ │ ├── worker.ts │ │ ├── worker2.ts │ │ ├── worker3.ts │ │ ├── worker1.ts │ │ ├── index.ts │ │ └── main.rs │ ├── wasi_multi_threads_rustc │ │ ├── tre_opt.wasm │ │ ├── thread_spawn.ts │ │ ├── index.html │ │ ├── rustc_with_lld.ts │ │ ├── tree.ts │ │ ├── rustc.ts │ │ ├── index.ts │ │ ├── save_stdout.ts │ │ ├── depend_clang_files.ts │ │ ├── tmp_dir.ts │ │ ├── clang.ts │ │ ├── depend_rustc_files.ts │ │ ├── depend_rustc_with_lld.ts │ │ └── worker.ts │ ├── wasi_multi_threads │ │ ├── multi_thread_echo.wasm │ │ ├── thread_spawn.ts │ │ ├── index.html │ │ ├── main.rs │ │ ├── index.ts │ │ └── worker.ts │ ├── wasi_multi_threads_channel │ │ ├── channel.wasm │ │ ├── thread_spawn.ts │ │ ├── index.html │ │ ├── main.rs │ │ ├── index.ts │ │ └── worker.ts │ ├── wasi_workers_single │ │ ├── echo_and_rewrite.wasm │ │ ├── index.html │ │ └── index.ts │ ├── package.json │ └── wasi_workers_rustc │ │ ├── index.html │ │ ├── worker.ts │ │ └── index.ts ├── architecture │ ├── architecture.pptx │ └── slide2.svg ├── src │ ├── sender.ts │ ├── index.ts │ ├── shared_array_buffer │ │ ├── worker_background │ │ │ ├── index.ts │ │ │ ├── spack.config.cjs │ │ │ ├── worker_export.ts │ │ │ ├── minify.js │ │ │ ├── worker_blob.ts │ │ │ └── worker_background_ref.ts │ │ ├── index.ts │ │ ├── serialize_error.ts │ │ ├── util.ts │ │ ├── fd_close_sender.ts │ │ ├── sender.ts │ │ └── allocator.ts │ ├── farm.ts │ ├── polyfill.js │ └── ref.ts ├── RELEASE.md ├── .swcrc ├── biome.json ├── tsconfig.json ├── vite.config.ts ├── index.html ├── README.md └── package.json ├── .prettierrc ├── .npmignore ├── .eslintrc.cjs ├── src ├── index.ts ├── strace.ts ├── debug.ts ├── fd.ts ├── fs_opfs.ts └── wasi_defs.ts ├── .gitmodules ├── .swcrc ├── .github └── workflows │ ├── release.yml │ └── check.yml ├── examples ├── package.json ├── package-lock.json └── rustc.html ├── LICENSE-MIT ├── package.json ├── README.md ├── tsconfig.json └── LICENSE-APACHE /test/adapters/browser/adapter.py: -------------------------------------------------------------------------------- 1 | ../shared/adapter.py -------------------------------------------------------------------------------- /test/adapters/node/adapter.py: -------------------------------------------------------------------------------- 1 | ../shared/adapter.py -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | typings/ 4 | .vscode/ 5 | -------------------------------------------------------------------------------- /threads/.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /dist 3 | /node_modules 4 | /types 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "printWidth": 80 4 | } 5 | -------------------------------------------------------------------------------- /threads/import-module-test/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /threads/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrium/browser_wasi_shim/main/threads/bun.lockb -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.wasm 3 | *.wasm.gz 4 | /examples 5 | /test 6 | /threads 7 | .github/ 8 | src/ 9 | -------------------------------------------------------------------------------- /threads/examples/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrium/browser_wasi_shim/main/threads/examples/bun.lockb -------------------------------------------------------------------------------- /threads/architecture/architecture.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrium/browser_wasi_shim/main/threads/architecture/architecture.pptx -------------------------------------------------------------------------------- /threads/examples/wasi_workers/echo_and_rewrite.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrium/browser_wasi_shim/main/threads/examples/wasi_workers/echo_and_rewrite.wasm -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads_rustc/tre_opt.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrium/browser_wasi_shim/main/threads/examples/wasi_multi_threads_rustc/tre_opt.wasm -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads/multi_thread_echo.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrium/browser_wasi_shim/main/threads/examples/wasi_multi_threads/multi_thread_echo.wasm -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads_channel/channel.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrium/browser_wasi_shim/main/threads/examples/wasi_multi_threads_channel/channel.wasm -------------------------------------------------------------------------------- /threads/examples/wasi_workers_single/echo_and_rewrite.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrium/browser_wasi_shim/main/threads/examples/wasi_workers_single/echo_and_rewrite.wasm -------------------------------------------------------------------------------- /threads/src/sender.ts: -------------------------------------------------------------------------------- 1 | export interface FdCloseSender { 2 | send(targets: Array, fd: number): Promise; 3 | get(id: number): Array | undefined; 4 | } 5 | -------------------------------------------------------------------------------- /threads/RELEASE.md: -------------------------------------------------------------------------------- 1 | # v0.1.0 2 | first release 3 | 4 | # v0.1.1 5 | - fix: Fixed a bug where the main thread could not recover after an error occurred following block_start_on_thread. 6 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads/thread_spawn.ts: -------------------------------------------------------------------------------- 1 | import { thread_spawn_on_worker } from "../../src"; 2 | 3 | self.onmessage = (event) => { 4 | thread_spawn_on_worker(event.data); 5 | }; 6 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads_channel/thread_spawn.ts: -------------------------------------------------------------------------------- 1 | import { thread_spawn_on_worker } from "../../src"; 2 | 3 | self.onmessage = (event) => { 4 | thread_spawn_on_worker(event.data); 5 | }; 6 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads_rustc/thread_spawn.ts: -------------------------------------------------------------------------------- 1 | import { thread_spawn_on_worker } from "../../src"; 2 | 3 | self.onmessage = async (event) => { 4 | await thread_spawn_on_worker(event.data); 5 | }; 6 | -------------------------------------------------------------------------------- /threads/examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@oligami/shared-object": "^0.1.1", 4 | "@xterm/xterm": "^5.5", 5 | "xterm-addon-fit": "^0.8.0", 6 | "@bjorn3/browser_wasi_shim": "^0.3.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /threads/examples/wasi_workers_single/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

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 | Vite + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import WASI, { WASIProcExit } from "./wasi.js"; 2 | export { WASI, WASIProcExit }; 3 | 4 | export { Fd, Inode } from "./fd.js"; 5 | export { 6 | File, 7 | Directory, 8 | OpenFile, 9 | OpenDirectory, 10 | PreopenDirectory, 11 | ConsoleStdout, 12 | } from "./fs_mem.js"; 13 | export { SyncOPFSFile, OpenSyncOPFSFile } from "./fs_opfs.js"; 14 | export { strace } from "./strace.js"; 15 | export * as wasi from "./wasi_defs.js"; 16 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads_channel/main.rs: -------------------------------------------------------------------------------- 1 | // https://doc.rust-lang.org/book/ch16-02-message-passing.html 2 | 3 | use std::sync::mpsc; 4 | use std::thread; 5 | 6 | fn main() { 7 | let (tx, rx) = mpsc::channel(); 8 | 9 | thread::spawn(move || { 10 | let val = String::from("hi"); 11 | tx.send(val).unwrap(); 12 | }); 13 | 14 | let received = rx.recv().unwrap(); 15 | println!("Got: {received}"); 16 | } 17 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "examples/wasm-rustc"] 2 | path = examples/wasm-rustc 3 | url = git@github.com:bjorn3/wasm-rustc.git 4 | [submodule "test/wasi-testsuite"] 5 | path = test/wasi-testsuite 6 | url = https://github.com/WebAssembly/wasi-testsuite 7 | branch = prod/testsuite-base 8 | [submodule "threads/examples/wasi_multi_threads_rustc/rust_wasm"] 9 | path = threads/examples/wasi_multi_threads_rustc/rust_wasm 10 | url = https://github.com/oligamiq/rust_wasm 11 | shallow = true 12 | -------------------------------------------------------------------------------- /.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": false 10 | }, 11 | "transform": {}, 12 | "target": "es2020", 13 | "loose": false, 14 | "externalHelpers": false, 15 | "keepClassNames": true 16 | }, 17 | "minify": true 18 | } 19 | -------------------------------------------------------------------------------- /threads/import-module-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "import-module-test", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "devDependencies": { 12 | "typescript": "^5.5.3", 13 | "vite": "^5.4.8" 14 | }, 15 | "dependencies": { 16 | "@oligami/browser_wasi_shim-threads": "file:..", 17 | "import-module-test": "file:" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /threads/src/shared_array_buffer/index.ts: -------------------------------------------------------------------------------- 1 | import { WASIFarmParkUseArrayBuffer } from "./park.js"; 2 | import { WASIFarmRefUseArrayBuffer } from "./ref.js"; 3 | import type { WASIFarmRefUseArrayBufferObject } from "./ref.js"; 4 | import { ThreadSpawner } from "./thread_spawn.js"; 5 | import { thread_spawn_on_worker } from "./thread_spawn.js"; 6 | 7 | export { 8 | WASIFarmRefUseArrayBuffer, 9 | type WASIFarmRefUseArrayBufferObject, 10 | WASIFarmParkUseArrayBuffer, 11 | ThreadSpawner, 12 | thread_spawn_on_worker, 13 | }; 14 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads_channel/index.ts: -------------------------------------------------------------------------------- 1 | import { OpenFile, File, ConsoleStdout } from "@bjorn3/browser_wasi_shim"; 2 | import { WASIFarm } from "../../src"; 3 | 4 | const farm = new WASIFarm( 5 | new OpenFile(new File([])), // stdin 6 | ConsoleStdout.lineBuffered((msg) => console.log(`[WASI stdout] ${msg}`)), 7 | ConsoleStdout.lineBuffered((msg) => console.warn(`[WASI stderr] ${msg}`)), 8 | [], 9 | ); 10 | 11 | const worker = new Worker("./worker.ts", { type: "module" }); 12 | 13 | worker.postMessage({ 14 | wasi_ref: farm.get_ref(), 15 | }); 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | # Setup .npmrc file to publish to npm 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: "16.x" 14 | registry-url: "https://registry.npmjs.org" 15 | - run: npm ci 16 | - run: npm run check 17 | - run: npm publish 18 | env: 19 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check types 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - run: git submodule update --init test/wasi-testsuite 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: "16.x" 14 | registry-url: "https://registry.npmjs.org" 15 | - run: npm ci 16 | - run: npm run check 17 | - run: python3 -m pip install -r ./test/wasi-testsuite/test-runner/requirements.txt 18 | - run: npm test 19 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browser_wasi_shim_examples", 3 | "publish": false, 4 | "version": "0.0.0", 5 | "license": "MIT OR Apache-2.0", 6 | "description": "", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/bjorn3/browser_wasi_shim.git" 10 | }, 11 | "author": "bjorn3", 12 | "bugs": { 13 | "url": "https://github.com/bjorn3/browser_wasi_shim/issues" 14 | }, 15 | "homepage": "https://github.com/bjorn3/browser_wasi_shim#readme", 16 | "dependencies": { 17 | "xterm": "^4.18.0", 18 | "xterm-addon-fit": "^0.5.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /threads/biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.1/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": [] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "space" 15 | }, 16 | "organizeImports": { 17 | "enabled": true 18 | }, 19 | "linter": { 20 | "enabled": true, 21 | "rules": { 22 | "recommended": true 23 | } 24 | }, 25 | "javascript": { 26 | "formatter": { 27 | "quoteStyle": "double" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /threads/import-module-test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true 21 | }, 22 | "include": ["src"] 23 | } 24 | -------------------------------------------------------------------------------- /src/strace.ts: -------------------------------------------------------------------------------- 1 | export function strace( 2 | imports: T, 3 | no_trace: Array, 4 | ) { 5 | return new Proxy(imports, { 6 | get(target, prop, receiver) { 7 | const f = Reflect.get(target, prop, receiver); 8 | if (no_trace.includes(prop)) { 9 | return f; 10 | } 11 | return function (...args: undefined[]) { 12 | console.log(prop, "(", ...args, ")"); 13 | // eslint-disable-next-line @typescript-eslint/ban-types 14 | const result = Reflect.apply(f as Function, receiver, args); 15 | console.log(" =", result); 16 | return result; 17 | }; 18 | }, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /test/run-testsuite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | TEST_DIR="$(cd "$(dirname $0)" && pwd)" 6 | TESTSUITE_ROOT="$TEST_DIR/wasi-testsuite" 7 | ADAPTER="node" 8 | # Take the first argument as the adapter name if given 9 | if [ $# -gt 0 ]; then 10 | ADAPTER="$1" 11 | shift 12 | fi 13 | 14 | python3 "$TESTSUITE_ROOT/test-runner/wasi_test_runner.py" \ 15 | --test-suite "$TESTSUITE_ROOT/tests/assemblyscript/testsuite/" \ 16 | "$TESTSUITE_ROOT/tests/c/testsuite/" \ 17 | "$TESTSUITE_ROOT/tests/rust/testsuite/" \ 18 | --runtime-adapter "$TEST_DIR/adapters/$ADAPTER/adapter.py" \ 19 | --exclude-filter "$TEST_DIR/skip.json" \ 20 | $@ 21 | -------------------------------------------------------------------------------- /threads/examples/wasi_workers/worker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ConsoleStdout, 3 | type Inode, 4 | PreopenDirectory, 5 | File, 6 | } from "@bjorn3/browser_wasi_shim"; 7 | import { WASIFarm } from "../../src"; 8 | 9 | const dir = new Map(); 10 | dir.set("hello2.txt", new File(new TextEncoder().encode("Hello, world!!!!!"))); 11 | 12 | const wasi_farm = new WASIFarm( 13 | undefined, 14 | ConsoleStdout.lineBuffered((msg) => 15 | console.log(`[WASI stdout on worker] ${msg}`), 16 | ), 17 | undefined, 18 | [new PreopenDirectory("hello2", dir)], 19 | ); 20 | 21 | console.log("WASI farm created"); 22 | const wasi_ref = await wasi_farm.get_ref(); 23 | self.postMessage({ wasi_ref }); 24 | -------------------------------------------------------------------------------- /threads/src/shared_array_buffer/serialize_error.ts: -------------------------------------------------------------------------------- 1 | export type SerializedError = { 2 | message: string; 3 | name: string; 4 | stack?: string; 5 | cause?: unknown; 6 | }; 7 | 8 | export const serialize = (error: Error): SerializedError => { 9 | return { 10 | message: error.message, 11 | name: error.name, 12 | stack: error.stack, 13 | cause: error.cause, 14 | }; 15 | }; 16 | 17 | export const deserialize = (serializedError: SerializedError): Error => { 18 | const error = new Error(serializedError.message); 19 | error.name = serializedError.name; 20 | error.stack = serializedError.stack?.replace(/.wasm:/g, ".wasm\n"); 21 | error.cause = serializedError.cause; 22 | return error; 23 | }; 24 | -------------------------------------------------------------------------------- /threads/src/shared_array_buffer/worker_background/worker_export.ts: -------------------------------------------------------------------------------- 1 | import type { AllocatorUseArrayBufferObject } from "../allocator.js"; 2 | 3 | export type WorkerBackgroundRefObject = { 4 | allocator: AllocatorUseArrayBufferObject; 5 | lock: SharedArrayBuffer; 6 | signature_input: SharedArrayBuffer; 7 | }; 8 | 9 | export const WorkerBackgroundRefObjectConstructor = 10 | (): WorkerBackgroundRefObject => { 11 | return { 12 | allocator: { 13 | share_arrays_memory: new SharedArrayBuffer(10 * 1024), 14 | }, 15 | lock: new SharedArrayBuffer(20), 16 | signature_input: new SharedArrayBuffer(24), 17 | }; 18 | }; 19 | 20 | export type WorkerOptions = { 21 | type: "module" | ""; 22 | }; 23 | -------------------------------------------------------------------------------- /threads/examples/wasi_workers/worker2.ts: -------------------------------------------------------------------------------- 1 | import { WASIFarmAnimal } from "../../src"; 2 | 3 | self.onmessage = async (e) => { 4 | const { wasi_ref, wasi_ref2 } = e.data; 5 | 6 | const wasi = new WASIFarmAnimal( 7 | [wasi_ref2, wasi_ref], 8 | ["echo_and_rewrite", "hello.txt", "world", "new_world", "100", "200"], // args 9 | [""], // env 10 | // options 11 | ); 12 | const wasm = await fetch("./echo_and_rewrite.wasm"); 13 | const buff = await wasm.arrayBuffer(); 14 | const { instance } = await WebAssembly.instantiate(buff, { 15 | wasi_snapshot_preview1: wasi.wasiImport, 16 | }); 17 | wasi.start( 18 | instance as unknown as { 19 | exports: { memory: WebAssembly.Memory; _start: () => unknown }; 20 | }, 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /threads/examples/wasi_workers/worker3.ts: -------------------------------------------------------------------------------- 1 | import { WASIFarmAnimal } from "../../src"; 2 | 3 | self.onmessage = async (e) => { 4 | const { wasi_ref, wasi_ref2 } = e.data; 5 | 6 | const wasi = new WASIFarmAnimal( 7 | [wasi_ref, wasi_ref2], 8 | ["echo_and_rewrite", "hello.txt", "world", "new_world", "200", "300"], // args 9 | [""], // env 10 | // options 11 | ); 12 | const wasm = await fetch("./echo_and_rewrite.wasm"); 13 | const buff = await wasm.arrayBuffer(); 14 | const { instance } = await WebAssembly.instantiate(buff, { 15 | wasi_snapshot_preview1: wasi.wasiImport, 16 | }); 17 | wasi.start( 18 | instance as unknown as { 19 | exports: { memory: WebAssembly.Memory; _start: () => unknown }; 20 | }, 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /threads/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "skipLibCheck": true, 8 | /* Bundler mode */ 9 | "incremental": true, 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "emitDeclarationOnly": true, 15 | "declaration": true, 16 | "declarationMap": true, 17 | "outDir": "./dist", 18 | "declarationDir": "./types", 19 | "esModuleInterop": true, 20 | /* Linting */ 21 | "strict": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noFallthroughCasesInSwitch": true 25 | }, 26 | "include": ["./src"] 27 | } 28 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads/index.ts: -------------------------------------------------------------------------------- 1 | import { WASIFarm, WASIFarmAnimal } from "../../src"; 2 | import { 3 | File, 4 | OpenFile, 5 | ConsoleStdout, 6 | PreopenDirectory, 7 | } from "@bjorn3/browser_wasi_shim"; 8 | 9 | const farm = new WASIFarm( 10 | new OpenFile(new File([])), // stdin 11 | ConsoleStdout.lineBuffered((msg) => console.log(`[WASI stdout] ${msg}`)), 12 | ConsoleStdout.lineBuffered((msg) => console.warn(`[WASI stderr] ${msg}`)), 13 | [], 14 | ); 15 | 16 | console.log(farm); 17 | 18 | const worker = new Worker("worker.ts", { type: "module" }); 19 | // const worker = new Worker(new URL("./worker.js", import.meta.url).href, { type: "module" }); 20 | 21 | console.log(worker); 22 | 23 | worker.postMessage({ 24 | wasi_ref: farm.get_ref(), 25 | }); 26 | 27 | console.log("Sent WASI ref to worker"); 28 | -------------------------------------------------------------------------------- /threads/examples/wasi_workers_rustc/index.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 19 | 20 | 21 |
22 |
23 |

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: 7 | document.querySelector("#app")!.innerHTML = ` 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |

Vite + TypeScript

16 |
17 | 18 |
19 |

20 | Click on the Vite and TypeScript logos to learn more 21 |

22 |
23 | `; 24 | 25 | // biome-ignore lint/style/noNonNullAssertion: 26 | setupCounter(document.querySelector("#counter")!); 27 | 28 | import { WASIFarm } from "@oligami/browser_wasi_shim-threads"; 29 | 30 | // Create a new WASI farm 31 | const farm = new WASIFarm(); 32 | console.dir(farm, { depth: null }); 33 | -------------------------------------------------------------------------------- /threads/examples/wasi_workers_single/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OpenFile, 3 | File, 4 | WASI, 5 | ConsoleStdout, 6 | PreopenDirectory, 7 | } from "@bjorn3/browser_wasi_shim"; 8 | 9 | // sleep 1000 10 | await new Promise((resolve) => setTimeout(resolve, 1000)); 11 | 12 | const toMap = (arr: Array<[string, File]>) => new Map(arr); 13 | 14 | const wasi = new WASI( 15 | ["echo_and_rewrite", "hello.txt", "world", "new_world"], // args 16 | ["FOO=bar"], // env 17 | [ 18 | new OpenFile(new File([])), // stdin 19 | ConsoleStdout.lineBuffered((msg) => console.log(`[WASI stdout] ${msg}`)), 20 | ConsoleStdout.lineBuffered((msg) => console.warn(`[WASI stderr] ${msg}`)), 21 | new PreopenDirectory( 22 | ".", 23 | toMap([ 24 | ["hello.txt", new File(new TextEncoder().encode("Hello, world!"))], 25 | ]), 26 | ), 27 | ], 28 | { debug: true }, 29 | ); 30 | const wasm = await fetch("./echo_and_rewrite.wasm"); 31 | const buff = await wasm.arrayBuffer(); 32 | const { instance } = await WebAssembly.instantiate(buff, { 33 | wasi_snapshot_preview1: wasi.wasiImport, 34 | }); 35 | 36 | // sleep 1000ms 37 | await new Promise((resolve) => setTimeout(resolve, 1000)); 38 | 39 | wasi.start( 40 | instance as unknown as { 41 | exports: { memory: WebAssembly.Memory; _start: () => unknown }; 42 | }, 43 | ); 44 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads_rustc/rustc_with_lld.ts: -------------------------------------------------------------------------------- 1 | import { SharedObject } from "@oligami/shared-object"; 2 | import { WASIFarmAnimal } from "../../src"; 3 | 4 | let wasi: WASIFarmAnimal; 5 | let wasm: WebAssembly.Module; 6 | let shared: SharedObject; 7 | 8 | globalThis.onmessage = async (e) => { 9 | const { wasi_refs } = e.data; 10 | 11 | if (wasi_refs) { 12 | wasm = await WebAssembly.compileStreaming( 13 | fetch("./rust_wasm/rustc_llvm_with_lld/rustc_opt.wasm"), 14 | ); 15 | 16 | wasi = new WASIFarmAnimal( 17 | wasi_refs, 18 | [], // args 19 | ["RUST_MIN_STACK=16777216"], // env 20 | { 21 | // debug: true, 22 | can_thread_spawn: true, 23 | thread_spawn_worker_url: new URL("./thread_spawn.ts", import.meta.url) 24 | .href, 25 | // thread_spawn_worker_url: "./thread_spawn.ts", 26 | thread_spawn_wasm: wasm, 27 | }, 28 | ); 29 | 30 | await wasi.wait_worker_background_worker(); 31 | 32 | wasi.get_share_memory().grow(200); 33 | 34 | console.log("Waiting for worker background worker..."); 35 | 36 | shared = new SharedObject((...args) => { 37 | wasi.args = ["rustc_with_lld", ...args]; 38 | wasi.block_start_on_thread(); 39 | console.log("wasi.start done"); 40 | }, "rustc_with_lld"); 41 | 42 | postMessage({ ready: true }); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads_rustc/tree.ts: -------------------------------------------------------------------------------- 1 | import { SharedObject } from "@oligami/shared-object"; 2 | import { WASIFarmAnimal } from "../../src"; 3 | 4 | let inst: { exports: { memory: WebAssembly.Memory; _start: () => unknown } }; 5 | let wasi: WASIFarmAnimal; 6 | let wasm: WebAssembly.Module; 7 | 8 | let shared: SharedObject; 9 | 10 | globalThis.onmessage = async (e) => { 11 | const { wasi_refs } = e.data; 12 | 13 | if (wasi_refs) { 14 | wasm = await WebAssembly.compileStreaming(fetch("./tre_opt.wasm")); 15 | 16 | wasi = new WASIFarmAnimal( 17 | wasi_refs, 18 | ["tre"], // args 19 | [], // env 20 | ); 21 | 22 | // Memory is rewritten at this time. 23 | inst = (await WebAssembly.instantiate(wasm, { 24 | wasi_snapshot_preview1: wasi.wasiImport, 25 | })) as unknown as { 26 | exports: { memory: WebAssembly.Memory; _start: () => unknown }; 27 | }; 28 | 29 | const memory_reset = inst.exports.memory.buffer; 30 | const memory_reset_view = new Uint8Array(memory_reset).slice(); 31 | 32 | shared = new SharedObject((...args) => { 33 | // If I don't reset memory, I get some kind of error. 34 | wasi.args = ["tre", ...args]; 35 | const memory_view = new Uint8Array(inst.exports.memory.buffer); 36 | memory_view.set(memory_reset_view); 37 | wasi.start(inst); 38 | }, "tree"); 39 | 40 | postMessage({ ready: true }); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /threads/import-module-test/src/typescript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads_rustc/rustc.ts: -------------------------------------------------------------------------------- 1 | import { SharedObject } from "@oligami/shared-object"; 2 | import { WASIFarmAnimal } from "../../src"; 3 | 4 | const { promise, resolve } = Promise.withResolvers(); 5 | import("@oligami/shared-object").then(resolve); 6 | 7 | let wasi: WASIFarmAnimal; 8 | let wasm: WebAssembly.Module; 9 | let shared: SharedObject; 10 | 11 | globalThis.onmessage = async (e) => { 12 | const { wasi_refs } = e.data; 13 | 14 | if (wasi_refs) { 15 | wasm = await WebAssembly.compileStreaming( 16 | fetch("./rust_wasm/rustc_llvm/rustc_opt.wasm"), 17 | ); 18 | 19 | wasi = new WASIFarmAnimal( 20 | wasi_refs, 21 | [], // args 22 | ["RUST_MIN_STACK=16777216"], // env 23 | { 24 | // debug: true, 25 | can_thread_spawn: true, 26 | thread_spawn_worker_url: new URL("./thread_spawn.ts", import.meta.url) 27 | .href, 28 | // thread_spawn_worker_url: "./thread_spawn.ts", 29 | thread_spawn_wasm: wasm, 30 | }, 31 | ); 32 | 33 | await wasi.wait_worker_background_worker(); 34 | 35 | wasi.get_share_memory().grow(200); 36 | 37 | console.log("Waiting for worker background worker..."); 38 | 39 | await promise; 40 | 41 | shared = new SharedObject((...args) => { 42 | wasi.args = ["rustc", ...args]; 43 | wasi.block_start_on_thread(); 44 | console.log("wasi.start done"); 45 | }, "rustc"); 46 | 47 | postMessage({ ready: true }); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /threads/import-module-test/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /threads/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples 7 | 34 | 35 | 36 | 37 |

all link

38 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /threads/examples/wasi_workers/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OpenFile, 3 | File, 4 | ConsoleStdout, 5 | PreopenDirectory, 6 | type Inode, 7 | Directory, 8 | } from "@bjorn3/browser_wasi_shim"; 9 | import { WASIFarm } from "../../src"; 10 | 11 | const worker = new Worker("./worker.ts", { type: "module" }); 12 | worker.onmessage = (event) => { 13 | const { wasi_ref: wasi_ref2 } = event.data; 14 | 15 | (async () => { 16 | const current_directory = new Map(); 17 | current_directory.set( 18 | "hello.txt", 19 | new File(new TextEncoder().encode("Hello, world!")), 20 | ); 21 | current_directory.set("hello2", new Directory(new Map())); 22 | 23 | const wasi_farm = new WASIFarm( 24 | new OpenFile(new File([])), // stdin 25 | ConsoleStdout.lineBuffered((msg) => 26 | console.log(`[WASI stdout on main thread] ${msg}`), 27 | ), 28 | ConsoleStdout.lineBuffered((msg) => console.warn(`[WASI stderr] ${msg}`)), 29 | [new PreopenDirectory(".", current_directory)], 30 | ); 31 | console.log("WASI farm created"); 32 | const wasi_ref = await wasi_farm.get_ref(); 33 | 34 | const myWorker1 = new Worker("./worker1.ts", { type: "module" }); 35 | myWorker1.postMessage({ wasi_ref, wasi_ref2 }); 36 | const myWorker2 = new Worker("./worker2.ts", { type: "module" }); 37 | myWorker2.postMessage({ wasi_ref, wasi_ref2 }); 38 | const myWorker3 = new Worker("./worker3.ts", { type: "module" }); 39 | myWorker3.postMessage({ wasi_ref, wasi_ref2 }); 40 | })(); 41 | }; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bjorn3/browser_wasi_shim", 3 | "version": "0.4.1", 4 | "license": "MIT OR Apache-2.0", 5 | "description": "A pure javascript shim for WASI", 6 | "type": "module", 7 | "scripts": { 8 | "build": "swc src -d dist -s true && tsc --emitDeclarationOnly", 9 | "prepare": "swc src -d dist && tsc --emitDeclarationOnly", 10 | "test:node": "./test/run-testsuite.sh node", 11 | "test:browser": "playwright-core install && ./test/run-testsuite.sh browser", 12 | "test": "npm run test:node && npm run test:browser", 13 | "check": "tsc --noEmit && prettier src -c && eslint src/" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/bjorn3/browser_wasi_shim.git" 18 | }, 19 | "author": "bjorn3", 20 | "bugs": { 21 | "url": "https://github.com/bjorn3/browser_wasi_shim/issues" 22 | }, 23 | "main": "dist/index.js", 24 | "types": "dist/index.d.ts", 25 | "exports": { 26 | ".": { 27 | "types": "./typings/index.d.ts", 28 | "import": "./dist/index.js", 29 | "default": "./dist/index.js" 30 | } 31 | }, 32 | "typings": "./typings/index.d.ts", 33 | "homepage": "https://github.com/bjorn3/browser_wasi_shim#readme", 34 | "devDependencies": { 35 | "@swc/cli": "^0.1.62", 36 | "@swc/core": "^1.3.37", 37 | "@typescript-eslint/eslint-plugin": "^6.7.4", 38 | "@typescript-eslint/parser": "^6.7.4", 39 | "eslint": "^8.50.0", 40 | "playwright": "^1.40.1", 41 | "prettier": "^3.0.3", 42 | "typescript": "^4.9.5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browser_wasi_shim_examples", 3 | "version": "0.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "browser_wasi_shim_examples", 9 | "version": "0.0.0", 10 | "license": "MIT OR Apache-2.0", 11 | "dependencies": { 12 | "xterm": "^4.18.0", 13 | "xterm-addon-fit": "^0.5.0" 14 | } 15 | }, 16 | "node_modules/xterm": { 17 | "version": "4.18.0", 18 | "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.18.0.tgz", 19 | "integrity": "sha512-JQoc1S0dti6SQfI0bK1AZvGnAxH4MVw45ZPFSO6FHTInAiau3Ix77fSxNx3mX4eh9OL4AYa8+4C8f5UvnSfppQ==" 20 | }, 21 | "node_modules/xterm-addon-fit": { 22 | "version": "0.5.0", 23 | "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz", 24 | "integrity": "sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==", 25 | "peerDependencies": { 26 | "xterm": "^4.0.0" 27 | } 28 | } 29 | }, 30 | "dependencies": { 31 | "xterm": { 32 | "version": "4.18.0", 33 | "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.18.0.tgz", 34 | "integrity": "sha512-JQoc1S0dti6SQfI0bK1AZvGnAxH4MVw45ZPFSO6FHTInAiau3Ix77fSxNx3mX4eh9OL4AYa8+4C8f5UvnSfppQ==" 35 | }, 36 | "xterm-addon-fit": { 37 | "version": "0.5.0", 38 | "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz", 39 | "integrity": "sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==", 40 | "requires": {} 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /threads/examples/wasi_workers/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | // ファイル名を受けとる 3 | let args: Vec = std::env::args().collect(); 4 | let filename = &args[1]; 5 | 6 | println!("reading file: {}", filename); 7 | 8 | // ファイルを読み込む 9 | let file = std::fs::read_to_string(filename).expect("ファイルが読み込めませんでした"); 10 | 11 | // ファイルの内容を表示 12 | println!("{}", file); 13 | 14 | // ファイルの内容を書き換え 15 | // 二つ目の引数の文字列を二つ目の文字列に書き換える 16 | let replaced = file.replace(&args[2], &args[3]); 17 | 18 | // 書き換えた内容を表示 19 | println!("{}", replaced); 20 | 21 | println!("random replace start"); 22 | 23 | let start = std::env::args().nth(4).unwrap().parse::().unwrap(); 24 | let end = std::env::args().nth(5).unwrap().parse::().unwrap(); 25 | 26 | // 新しいfileを作る 27 | let new_file = format!("{}-{}~{}.txt", filename, start, end); 28 | std::fs::write(&new_file, "$$$$$$$$$").expect("ファイルが書き込めませんでした"); 29 | 30 | let loop_n = std::env::args().nth(6).unwrap_or("100".to_string()).parse::().unwrap(); 31 | 32 | // loop { 33 | for _ in 0..loop_n { 34 | // ファイルを読み込む 35 | let file = std::fs::read_to_string(filename).expect("ファイルが読み込めませんでした"); 36 | 37 | // ランダムな数値を生成 38 | let random = rand::random::() % (end - start) + start; 39 | 40 | // 生成した数値をkファイルの内容に書き換える 41 | let replaced = format!("{}, {}", file, random); 42 | 43 | // 書き換えた内容を表示 44 | println!("{}", replaced); 45 | 46 | // ファイルを書き換える 47 | std::fs::write(filename, &replaced).expect("ファイルが書き込めませんでした"); 48 | 49 | // 1秒待つ 50 | // std::thread::sleep(std::time::Duration::from_secs(1)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads_rustc/index.ts: -------------------------------------------------------------------------------- 1 | import { Terminal } from "@xterm/xterm"; 2 | import { WASIFarm } from "../../src"; 3 | import { FitAddon } from "xterm-addon-fit"; 4 | import { SharedObject } from "@oligami/shared-object"; 5 | import { Fd } from "@bjorn3/browser_wasi_shim"; 6 | 7 | import "@xterm/xterm/css/xterm.css"; 8 | 9 | const term = new Terminal({ 10 | convertEol: true, 11 | }); 12 | const terminalElement = document.getElementById("terminal"); 13 | 14 | if (!terminalElement) { 15 | throw new Error("No terminal element found"); 16 | } 17 | 18 | term.open(terminalElement); 19 | 20 | const fitAddon = new FitAddon(); 21 | term.loadAddon(fitAddon); 22 | fitAddon.fit(); 23 | 24 | // term.onData(data => { 25 | // term.write(data); 26 | // }); 27 | 28 | const shared = new SharedObject( 29 | { 30 | writeln(data) { 31 | term.writeln(data); 32 | }, 33 | writeUtf8(data) { 34 | term.write(new TextDecoder().decode(data)); 35 | }, 36 | }, 37 | "term", 38 | ); 39 | 40 | term.writeln("Initializing WASI..."); 41 | 42 | class XtermStdio extends Fd { 43 | term: Terminal; 44 | 45 | constructor(term: Terminal) { 46 | super(); 47 | this.term = term; 48 | } 49 | fd_write(data: Uint8Array) /*: {ret: number, nwritten: number}*/ { 50 | this.term.write(new TextDecoder().decode(data)); 51 | return { ret: 0, nwritten: data.byteLength }; 52 | } 53 | } 54 | 55 | const farm = new WASIFarm( 56 | new XtermStdio(term), 57 | new XtermStdio(term), 58 | new XtermStdio(term), 59 | [], 60 | // { debug: true }, 61 | ); 62 | 63 | const worker = new Worker("./worker.ts", { type: "module" }); 64 | 65 | worker.postMessage({ 66 | wasi_ref: farm.get_ref(), 67 | }); 68 | -------------------------------------------------------------------------------- /threads/src/shared_array_buffer/util.ts: -------------------------------------------------------------------------------- 1 | export const get_func_name_from_number = (num: number): string => { 2 | switch (num) { 3 | case 7: 4 | return "fd_advise"; 5 | case 8: 6 | return "fd_allocate"; 7 | case 9: 8 | return "fd_close"; 9 | case 10: 10 | return "fd_datasync"; 11 | case 11: 12 | return "fd_fdstat_get"; 13 | case 12: 14 | return "fd_fdstat_set_flags"; 15 | case 13: 16 | return "fd_fdstat_set_rights"; 17 | case 14: 18 | return "fd_filestat_get"; 19 | case 15: 20 | return "fd_filestat_set_size"; 21 | case 16: 22 | return "fd_filestat_set_times"; 23 | case 17: 24 | return "fd_pread"; 25 | case 18: 26 | return "fd_prestat_get"; 27 | case 19: 28 | return "fd_prestat_dir_name"; 29 | case 20: 30 | return "fd_pwrite"; 31 | case 21: 32 | return "fd_read"; 33 | case 22: 34 | return "fd_readdir"; 35 | case 23: 36 | return "fd_renumber"; 37 | case 24: 38 | return "fd_seek"; 39 | case 25: 40 | return "fd_sync"; 41 | case 26: 42 | return "fd_tell"; 43 | case 27: 44 | return "fd_write"; 45 | case 28: 46 | return "path_create_directory"; 47 | case 29: 48 | return "path_filestat_get"; 49 | case 30: 50 | return "path_filestat_set_times"; 51 | case 31: 52 | return "path_link"; 53 | case 32: 54 | return "path_open"; 55 | case 33: 56 | return "path_readlink"; 57 | case 34: 58 | return "path_remove_directory"; 59 | case 35: 60 | return "path_rename"; 61 | case 36: 62 | return "path_symlink"; 63 | case 37: 64 | return "path_unlink_file"; 65 | default: 66 | return "unknown"; 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /threads/README.md: -------------------------------------------------------------------------------- 1 | # A pure javascript shim for WASI Preview 1 threads 2 | 3 | > [!WARNING] 4 | > The code in this directory is less production ready than the main browser_wasi_shim code. 5 | > This code requires `SharedArrayBuffer`, `waitAsync` and `Atomics` to be enabled in the browser, so it may not work in all browsers. 6 | > For example, Firefox failed to run the demo in this directory. 7 | > Chrome worked fine. 8 | > This library require `cross-origin isolation` to be enabled in the browser. 9 | 10 | This project implement threads on browser_wasi_shim 11 | 12 | # Features 13 | - [x] thread creation 14 | - [x] Filesystem wrapper accessible by multiple workers 15 | - [ ] thread pool 16 | 17 | # Building 18 | ```sh 19 | $ npm install 20 | $ npm run build 21 | ``` 22 | 23 | # Running the demo 24 | ```sh 25 | $ git submodule update --init 26 | $ cd examples && npm install && npm run dev 27 | ``` 28 | And visit http://localhost 29 | 30 | # Architecture 31 | In the thread that maintains the file system for `@bjorn3/browser_wasi_shim`, file access is awaited asynchronously, while in the thread executing the WASM, file access is performed synchronously. 32 | 33 | Since the functions called within the WASM are invoked synchronously, it is not possible to properly create web workers. Therefore, it is necessary to first create a dedicated thread for generating web workers. 34 | 35 | Please refer to the diagram below for more details. 36 | 37 | 38 | ![slide1](./architecture/slide1.svg) 39 | 40 | Additionally, since data of unknown size is being exchanged, an allocator is essential. A simple allocator is implemented on the assumption that the allocated memory will be released promptly. 41 | 42 | 43 | ![slide2](./architecture/slide2.svg) 44 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads_rustc/save_stdout.ts: -------------------------------------------------------------------------------- 1 | import { SharedObject, SharedObjectRef } from "@oligami/shared-object"; 2 | import { WASIFarm } from "../../src"; 3 | import { Fd } from "@bjorn3/browser_wasi_shim"; 4 | import { resolve } from "node:path"; 5 | 6 | const term = new SharedObjectRef("term").proxy<{ 7 | writeln: (data) => Promise; 8 | writeUtf8: (data) => Promise; 9 | }>(); 10 | 11 | class Stdout extends Fd { 12 | buffer = new Uint8Array(0); 13 | 14 | fd_write(data: Uint8Array): { ret: number; nwritten: number } { 15 | const { promise, resolve } = Promise.withResolvers(); 16 | 17 | (async () => { 18 | const new_buffer = new Uint8Array(this.buffer.length + data.length); 19 | new_buffer.set(this.buffer); 20 | new_buffer.set(data, this.buffer.length); 21 | this.buffer = new_buffer; 22 | await term.writeUtf8(data); 23 | 24 | resolve({ ret: 0, nwritten: data.byteLength }); 25 | })(); 26 | 27 | return promise as unknown as { ret: number; nwritten: number }; 28 | } 29 | } 30 | 31 | const std_out = new Stdout(); 32 | 33 | const std_err = new Stdout(); 34 | 35 | const shared_std_out = new SharedObject( 36 | { 37 | get() { 38 | return new TextDecoder().decode(std_out.buffer); 39 | }, 40 | reset() { 41 | std_out.buffer = new Uint8Array(0); 42 | }, 43 | }, 44 | "std_out_keep", 45 | ); 46 | 47 | const shared_std_err = new SharedObject( 48 | { 49 | get() { 50 | return new TextDecoder().decode(std_err.buffer); 51 | }, 52 | reset() { 53 | std_err.buffer = new Uint8Array(0); 54 | }, 55 | }, 56 | "std_err_keep", 57 | ); 58 | 59 | const farm = new WASIFarm(undefined, std_out, std_err); 60 | 61 | const ret = await farm.get_ref(); 62 | 63 | postMessage({ wasi_ref: ret }); 64 | -------------------------------------------------------------------------------- /threads/import-module-test/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | a:hover { 22 | color: #535bf2; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | display: flex; 28 | place-items: center; 29 | min-width: 320px; 30 | min-height: 100vh; 31 | } 32 | 33 | h1 { 34 | font-size: 3.2em; 35 | line-height: 1.1; 36 | } 37 | 38 | #app { 39 | max-width: 1280px; 40 | margin: 0 auto; 41 | padding: 2rem; 42 | text-align: center; 43 | } 44 | 45 | .logo { 46 | height: 6em; 47 | padding: 1.5em; 48 | will-change: filter; 49 | transition: filter 300ms; 50 | } 51 | .logo:hover { 52 | filter: drop-shadow(0 0 2em #646cffaa); 53 | } 54 | .logo.vanilla:hover { 55 | filter: drop-shadow(0 0 2em #3178c6aa); 56 | } 57 | 58 | .card { 59 | padding: 2em; 60 | } 61 | 62 | .read-the-docs { 63 | color: #888; 64 | } 65 | 66 | button { 67 | border-radius: 8px; 68 | border: 1px solid transparent; 69 | padding: 0.6em 1.2em; 70 | font-size: 1em; 71 | font-weight: 500; 72 | font-family: inherit; 73 | background-color: #1a1a1a; 74 | cursor: pointer; 75 | transition: border-color 0.25s; 76 | } 77 | button:hover { 78 | border-color: #646cff; 79 | } 80 | button:focus, 81 | button:focus-visible { 82 | outline: 4px auto -webkit-focus-ring-color; 83 | } 84 | 85 | @media (prefers-color-scheme: light) { 86 | :root { 87 | color: #213547; 88 | background-color: #ffffff; 89 | } 90 | a:hover { 91 | color: #747bff; 92 | } 93 | button { 94 | background-color: #f9f9f9; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /threads/src/shared_array_buffer/fd_close_sender.ts: -------------------------------------------------------------------------------- 1 | import type { FdCloseSender } from "../sender.js"; 2 | import { 3 | ToRefSenderUseArrayBuffer, 4 | type ToRefSenderUseArrayBufferObject, 5 | } from "./sender.js"; 6 | 7 | export type FdCloseSenderUseArrayBufferObject = { 8 | max_share_arrays_memory?: number; 9 | share_arrays_memory?: SharedArrayBuffer; 10 | } & ToRefSenderUseArrayBufferObject; 11 | 12 | // Object to tell other processes, 13 | // such as child processes, 14 | // that the file descriptor has been closed 15 | export class FdCloseSenderUseArrayBuffer 16 | extends ToRefSenderUseArrayBuffer 17 | implements FdCloseSender 18 | { 19 | // Should be able to change the size of memory as it accumulates more and more on memory 20 | constructor( 21 | max_share_arrays_memory?: number, 22 | share_arrays_memory?: SharedArrayBuffer, 23 | ) { 24 | super(4, max_share_arrays_memory, share_arrays_memory); 25 | } 26 | 27 | // Send the closed file descriptor to the target process 28 | async send(targets: Array, fd: number): Promise { 29 | if (targets === undefined || targets.length === 0) { 30 | throw new Error("targets is empty"); 31 | } 32 | // console.log("fd_close_sender send", targets, fd); 33 | 34 | await this.async_send(targets, new Uint32Array([fd])); 35 | } 36 | 37 | // Get the closed file descriptor from the target process 38 | get(id: number): Array | undefined { 39 | const data = this.get_data(id); 40 | if (data === undefined) { 41 | return undefined; 42 | } 43 | 44 | // console.log("fd_close_sender get", data); 45 | 46 | const array = []; 47 | for (const i of data) { 48 | array.push(i[0]); 49 | } 50 | 51 | return array; 52 | } 53 | 54 | // Initialize the class from object 55 | static init_self(sl: FdCloseSenderUseArrayBufferObject): FdCloseSender { 56 | const sel = ToRefSenderUseArrayBuffer.init_self_inner(sl); 57 | return new FdCloseSenderUseArrayBuffer( 58 | sel.max_share_arrays_memory, 59 | sel.share_arrays_memory, 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /threads/examples/wasi_workers_rustc/worker.ts: -------------------------------------------------------------------------------- 1 | import { strace } from "@bjorn3/browser_wasi_shim"; 2 | import { WASIFarmAnimal } from "../../src"; 3 | 4 | console.log("worker.js"); 5 | 6 | globalThis.onmessage = async (e) => { 7 | const wasm = await WebAssembly.compileStreaming( 8 | fetch( 9 | "../wasi_multi_threads_rustc/rust_wasm/rustc_cranelift/rustc_opt.wasm", 10 | ), 11 | ); 12 | 13 | console.log("worker.js onmessage", e.data); 14 | 15 | const { wasi_ref } = e.data; 16 | 17 | const args = [ 18 | "rustc", 19 | "/hello.rs", 20 | "--sysroot", 21 | "/sysroot", 22 | "--target", 23 | "x86_64-unknown-linux-gnu", 24 | "-Cpanic=abort", 25 | "-Ccodegen-units=1", 26 | ]; 27 | const env = ["RUSTC_LOG=info"]; 28 | 29 | console.log("wasi_ref", wasi_ref); 30 | 31 | const w = new WASIFarmAnimal(wasi_ref, args, env, {}); 32 | 33 | let next_thread_id = 1; 34 | 35 | const inst = (await WebAssembly.instantiate(wasm, { 36 | env: { 37 | memory: new WebAssembly.Memory({ 38 | initial: 256, 39 | maximum: 16384, 40 | shared: true, 41 | }), 42 | }, 43 | wasi: { 44 | "thread-spawn": (start_arg) => { 45 | console.log("thread-spawn", start_arg); 46 | 47 | const thread_id = next_thread_id++; 48 | inst.exports.wasi_thread_start(thread_id, start_arg); 49 | return thread_id; 50 | }, 51 | }, 52 | wasi_snapshot_preview1: strace(w.wasiImport, ["fd_prestat_get"]), 53 | })) as unknown as { 54 | exports: { 55 | wasi_thread_start: (thread_id: number, start_arg: number) => void; 56 | memory: WebAssembly.Memory; 57 | _start: () => unknown; 58 | }; 59 | }; 60 | 61 | postMessage({ 62 | term: "\x1B[93mExecuting\x1B[0m", 63 | }); 64 | console.log(inst.exports); 65 | try { 66 | w.start(inst); 67 | } catch (e) { 68 | postMessage({ 69 | term: `Exception: ${e.message}`, 70 | }); 71 | // /*term.writeln("backtrace:"); term.writeln(e.stack);*/ 72 | } 73 | postMessage({ 74 | term: "\x1B[92mDone\x1B[0m", 75 | }); 76 | }; 77 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads_rustc/depend_clang_files.ts: -------------------------------------------------------------------------------- 1 | import { WASIFarm } from "../../src"; 2 | import { 3 | Directory, 4 | File, 5 | type Inode, 6 | PreopenDirectory, 7 | } from "@bjorn3/browser_wasi_shim"; 8 | 9 | async function load_external_file(path) { 10 | return new File(await (await (await fetch(path)).blob()).arrayBuffer()); 11 | } 12 | 13 | const wasi_libs = new Map(); 14 | for (const file of [ 15 | "crt1-command.o", 16 | "crt1-reactor.o", 17 | "crt1.o", 18 | "libc-printscan-long-double.a", 19 | "libc-printscan-no-floating-point.a", 20 | "libc.a", 21 | "libc.imports", 22 | "libc++.a", 23 | "libc++.modules.json", 24 | "libc++abi.a", 25 | "libc++experimental.a", 26 | "libcrypt.a", 27 | "libdl.a", 28 | "libm.a", 29 | "libpthread.a", 30 | "libresolv.a", 31 | "librt.a", 32 | "libsetjmp.a", 33 | "libutil.a", 34 | "libwasi-emulated-getpid.a", 35 | "libwasi-emulated-mman.a", 36 | "libwasi-emulated-process-clocks.a", 37 | "libwasi-emulated-signal.a", 38 | "libxnet.a", 39 | ]) { 40 | wasi_libs.set( 41 | file, 42 | await load_external_file( 43 | `./rust_wasm/llvm-tools/dist/lib/wasm32-wasip1/${file}`, 44 | ), 45 | ); 46 | } 47 | 48 | const toMap = (arr: Array<[string, Inode]>) => new Map(arr); 49 | 50 | const farm = new WASIFarm( 51 | undefined, 52 | undefined, 53 | undefined, 54 | [ 55 | new PreopenDirectory( 56 | "/sysroot-clang", 57 | toMap([ 58 | [ 59 | "lib", 60 | new Directory([ 61 | [ 62 | "wasm32-unknown-wasip1", 63 | new Directory([ 64 | [ 65 | "libclang_rt.builtins.a", 66 | await load_external_file( 67 | "./rust_wasm/llvm-tools/dist/lib/wasm32-unknown-wasip1/libclang_rt.builtins.a", 68 | ), 69 | ], 70 | ]), 71 | ], 72 | ["wasm32-wasip1", new Directory(wasi_libs)], 73 | ]), 74 | ], 75 | ]), 76 | ), 77 | ], 78 | { 79 | // allocator_size: 1024 * 1024 * 1024, 80 | // debug: true, 81 | }, 82 | ); 83 | 84 | const ret = await farm.get_ref(); 85 | 86 | postMessage({ wasi_ref: ret }); 87 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads_rustc/tmp_dir.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Inode, 3 | PreopenDirectory, 4 | File, 5 | Directory, 6 | } from "@bjorn3/browser_wasi_shim"; 7 | import { WASIFarm } from "../../src"; 8 | import { SharedObject } from "@oligami/shared-object"; 9 | 10 | const toMap = (arr: Array<[string, Inode]>) => new Map(arr); 11 | 12 | const root_dir = new PreopenDirectory( 13 | "/", 14 | toMap([ 15 | [ 16 | "hello.rs", 17 | new File( 18 | new TextEncoder().encode(`fn main() { println!("Hello World!"); }`), 19 | ), 20 | ], 21 | ["sysroot", new Directory([])], 22 | ["sysroot-with-lld", new Directory([])], 23 | ["tmp", new Directory([])], 24 | ]), 25 | ); 26 | 27 | const farm = new WASIFarm( 28 | undefined, 29 | undefined, 30 | undefined, 31 | [ 32 | // new PreopenDirectory(".", [ 33 | // ["tmp-tmp", new File(new TextEncoder("utf-8").encode("Hello World!"))], 34 | // ["tmp-dir", new Directory([])], 35 | // ]), 36 | // new PreopenDirectory("tmp-dir", [ 37 | // [ 38 | // "tmp-dir_inner", 39 | // new Directory([ 40 | // [ 41 | // "tmp-dir_inner-file", 42 | // new File(new TextEncoder("utf-8").encode("Hello World!!!!!")), 43 | // ], 44 | // ]), 45 | // ], 46 | // ]), 47 | new PreopenDirectory("/tmp", toMap([])), 48 | root_dir, 49 | new PreopenDirectory( 50 | "~", 51 | toMap([ 52 | [ 53 | "####.rs", 54 | new File( 55 | new TextEncoder().encode(`fn main() { println!("Hello World!"); }`), 56 | ), 57 | ], 58 | ["sysroot", new Directory([])], 59 | ]), 60 | ), 61 | ], 62 | // { debug: true }, 63 | ); 64 | 65 | const ret = await farm.get_ref(); 66 | 67 | const shared = new SharedObject( 68 | { 69 | get_file(path_str) { 70 | console.log(root_dir); 71 | const path = { 72 | parts: [path_str], 73 | is_dir: false, 74 | }; 75 | // biome-ignore lint/suspicious/noExplicitAny: 76 | const { ret, entry } = root_dir.dir.get_entry_for_path(path as any); 77 | if (ret !== 0 || entry === null) { 78 | throw new Error(`get_file: ${path_str} failed`); 79 | } 80 | return (entry as File).data; 81 | }, 82 | }, 83 | "root_dir", 84 | ); 85 | 86 | postMessage({ wasi_ref: ret }); 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A pure javascript shim for WASI 2 | 3 | Implementation status: A subset of wasi_snapshot_preview1 is implemented. The rest either throws an exception, returns an error or is incorrectly implemented. 4 | 5 | ## Usage 6 | 7 | ``` 8 | npm install @bjorn3/browser_wasi_shim --save 9 | ``` 10 | 11 | ```javascript 12 | import { WASI, File, OpenFile, ConsoleStdout, PreopenDirectory } from "@bjorn3/browser_wasi_shim"; 13 | 14 | let args = ["bin", "arg1", "arg2"]; 15 | let env = ["FOO=bar"]; 16 | let fds = [ 17 | new OpenFile(new File([])), // stdin 18 | ConsoleStdout.lineBuffered(msg => console.log(`[WASI stdout] ${msg}`)), 19 | ConsoleStdout.lineBuffered(msg => console.warn(`[WASI stderr] ${msg}`)), 20 | new PreopenDirectory(".", [ 21 | ["example.c", new File(new TextEncoder("utf-8").encode(`#include "a"`))], 22 | ["hello.rs", new File(new TextEncoder("utf-8").encode(`fn main() { println!("Hello World!"); }`))], 23 | ]), 24 | ]; 25 | let wasi = new WASI(args, env, fds); 26 | 27 | let wasm = await WebAssembly.compileStreaming(fetch("bin.wasm")); 28 | let inst = await WebAssembly.instantiate(wasm, { 29 | "wasi_snapshot_preview1": wasi.wasiImport, 30 | }); 31 | wasi.start(inst); 32 | ``` 33 | 34 | ## Building 35 | 36 | ``` 37 | $ npm install 38 | $ npm run build 39 | ``` 40 | 41 | ## Running the demo 42 | 43 | The demo requires the wasm rustc artifacts and the xterm js package. To get them run: 44 | 45 | ``` 46 | $ git submodule update --init 47 | $ cd examples && npm install 48 | ``` 49 | 50 | Run the demo with a static web server from the root of this project: 51 | 52 | ``` 53 | $ npx http-server 54 | ``` 55 | 56 | And visit [http://127.0.0.1:8080/examples/rustc.html]() in your browser. 57 | 58 | A more complete demo by [@LyonSyonII](https://github.com/LyonSyonII) which runs 59 | miri inside the browser can be found at . (source 60 | at ) 61 | 62 | ## Testing 63 | 64 | ``` 65 | $ python3 -m pip install -r ./test/wasi-testsuite/test-runner/requirements.txt 66 | $ npm test 67 | ``` 68 | 69 | ## License 70 | 71 | Licensed under either of 72 | 73 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or 74 | http://www.apache.org/licenses/LICENSE-2.0) 75 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or 76 | http://opensource.org/licenses/MIT) 77 | 78 | at your option. 79 | 80 | ### Contribution 81 | 82 | Unless you explicitly state otherwise, any contribution intentionally submitted 83 | for inclusion in the work by you shall be dual licensed as above, without any 84 | additional terms or conditions. 85 | -------------------------------------------------------------------------------- /test/adapters/browser/run-test.html: -------------------------------------------------------------------------------- 1 | 2 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /threads/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@oligami/browser_wasi_shim-threads", 3 | "version": "0.1.1", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "npm run worker && vite build", 8 | "prepare": "npm run build", 9 | "fmt": "biome format --write .", 10 | "lint": "biome lint src examples import-module-test", 11 | "check": "biome check && tsc --noEmit", 12 | "worker": "spack --config ./src/shared_array_buffer/worker_background/spack.config.cjs && node src/shared_array_buffer/worker_background/minify.js", 13 | "watch": "npm-watch worker" 14 | }, 15 | "watch": { 16 | "worker": "src/shared_array_buffer/worker_background/*.ts" 17 | }, 18 | "devDependencies": { 19 | "@biomejs/biome": "1.9.4", 20 | "@swc/cli": "^0.5.1", 21 | "@swc/core": "^1.9.3", 22 | "better-typescript-lib": "^2.10.0", 23 | "npm-watch": "^0.13.0", 24 | "typescript": "^5.7.2", 25 | "unplugin-swc": "^1.5.1", 26 | "vite": "^6.0.1", 27 | "vite-plugin-dts": "^4.3.0" 28 | }, 29 | "dependencies": { 30 | "@bjorn3/browser_wasi_shim": "^0.3.0", 31 | "@oligami/browser_wasi_shim-threads": "file:" 32 | }, 33 | "peerDependencies": { 34 | "@bjorn3/browser_wasi_shim": "^0.3.0" 35 | }, 36 | "private": false, 37 | "publishConfig": { 38 | "access": "public" 39 | }, 40 | "author": "oligami (https://github.com/oligamiq)", 41 | "license": "MIT OR Apache-2.0", 42 | "description": "A pure javascript shim for WASI with support for threads", 43 | "homepage": "https://github.com/bjorn3/browser_wasi_shim/tree/main/threads#readme", 44 | "repository": { 45 | "type": "git", 46 | "url": "git+https://github.com/bjorn3/browser_wasi_shim.git" 47 | }, 48 | "bugs": { 49 | "url": "https://github.com/bjorn3/browser_wasi_shim/issues" 50 | }, 51 | "main": "./dist/browser-wasi-shim-threads.es.js", 52 | "types": "./dist/index.d.ts", 53 | "exports": { 54 | ".": { 55 | "import": { 56 | "types": "./dist/index.d.ts", 57 | "default": "./dist/browser-wasi-shim-threads.es.js" 58 | }, 59 | "require": { 60 | "types": "./dist/index.d.ts", 61 | "default": "./dist/browser-wasi-shim-threads.cjs.js" 62 | }, 63 | "node": { 64 | "types": "./dist/index.d.ts", 65 | "default": "./dist/browser-wasi-shim-threads.cjs.js" 66 | }, 67 | "types": "./dist/index.d.ts", 68 | "default": "./dist/browser-wasi-shim-threads.es.js" 69 | } 70 | }, 71 | "files": ["dist", "src"], 72 | "module": "dist/browser-wasi-shim-threads.es.js", 73 | "keywords": ["wasi", "webassembly", "threads", "worker", "webworker"] 74 | } 75 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads_rustc/clang.ts: -------------------------------------------------------------------------------- 1 | import { strace } from "@bjorn3/browser_wasi_shim"; 2 | import { WASIFarmAnimal } from "../../src"; 3 | import { SharedObject } from "@oligami/shared-object"; 4 | 5 | let wasi: WASIFarmAnimal; 6 | let inst: { 7 | exports: { memory: WebAssembly.Memory; _start: () => unknown }; 8 | }; 9 | let wasm: WebAssembly.Module; 10 | 11 | let shared_clang: SharedObject; 12 | let shared_tools: SharedObject; 13 | let shared_wasm_ld: SharedObject; 14 | 15 | globalThis.onmessage = async (e) => { 16 | const { wasi_refs } = e.data; 17 | 18 | if (wasi_refs) { 19 | wasm = await WebAssembly.compileStreaming( 20 | fetch("./rust_wasm/llvm-tools/llvm-opt.wasm"), 21 | ); 22 | 23 | wasi = new WASIFarmAnimal( 24 | wasi_refs, 25 | ["llvm"], // args 26 | [], // env 27 | ); 28 | 29 | // Memory is rewritten at this time. 30 | // inst = await WebAssembly.instantiate(wasm, { 31 | // wasi_snapshot_preview1: wasi.wasiImport, 32 | // }); 33 | inst = (await WebAssembly.instantiate(wasm, { 34 | wasi_snapshot_preview1: strace(wasi.wasiImport, []), 35 | })) as unknown as { 36 | exports: { memory: WebAssembly.Memory; _start: () => unknown }; 37 | }; 38 | 39 | console.log("wasi.start, inst", inst); 40 | 41 | const memory_reset = inst.exports.memory.buffer; 42 | const memory_reset_view = new Uint8Array(memory_reset).slice(); 43 | 44 | shared_clang = new SharedObject((...args) => { 45 | console.log("clang args", args); 46 | // If I don't reset memory, I get some kind of error. 47 | wasi.args = ["llvm", "clang", ...args]; 48 | const memory_view = new Uint8Array(inst.exports.memory.buffer); 49 | memory_view.set(memory_reset_view); 50 | wasi.start(inst); 51 | console.log("clang wasi.start done"); 52 | }, "clang"); 53 | 54 | shared_tools = new SharedObject((...args) => { 55 | console.log("tools args", args); 56 | // If I don't reset memory, I get some kind of error. 57 | wasi.args = ["llvm-tools", ...args]; 58 | const memory_view = new Uint8Array(inst.exports.memory.buffer); 59 | memory_view.set(memory_reset_view); 60 | wasi.start(inst); 61 | console.log("tools wasi.start done"); 62 | }, "llvm-tools"); 63 | 64 | shared_wasm_ld = new SharedObject((...args) => { 65 | console.log("wasm-ld args", args); 66 | // If I don't reset memory, I get some kind of error. 67 | wasi.args = ["llvm-tools", "wasm-ld", ...args]; 68 | const memory_view = new Uint8Array(inst.exports.memory.buffer); 69 | memory_view.set(memory_reset_view); 70 | wasi.start(inst); 71 | console.log("wasm-ld wasi.start done"); 72 | }, "wasm-ld"); 73 | 74 | postMessage({ ready: true }); 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /test/adapters/node/run-wasi.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'fs/promises'; 4 | import { WASI, wasi, strace, OpenFile, File, Directory, PreopenDirectory, Fd, Inode } from "../../../dist/index.js" 5 | import { parseArgs } from "../shared/parseArgs.mjs" 6 | import { walkFs } from "../shared/walkFs.mjs" 7 | 8 | class NodeStdout extends Fd { 9 | constructor(out) { 10 | super(); 11 | this.out = out; 12 | this.ino = Inode.issue_ino(); 13 | } 14 | 15 | fd_filestat_get() { 16 | const filestat = new wasi.Filestat( 17 | this.ino, 18 | wasi.FILETYPE_CHARACTER_DEVICE, 19 | BigInt(0), 20 | ); 21 | return { ret: 0, filestat }; 22 | } 23 | 24 | fd_fdstat_get() { 25 | const fdstat = new wasi.Fdstat(wasi.FILETYPE_CHARACTER_DEVICE, 0); 26 | fdstat.fs_rights_base = BigInt(wasi.RIGHTS_FD_WRITE); 27 | return { ret: 0, fdstat }; 28 | } 29 | 30 | fd_write(data) { 31 | this.out.write(data); 32 | return { ret: 0, nwritten: data.byteLength }; 33 | } 34 | } 35 | 36 | async function derivePreopens(dirs) { 37 | const preopens = []; 38 | for (let dir of dirs) { 39 | const contents = await walkFs(dir, (name, entry, out) => { 40 | switch (entry.kind) { 41 | case "dir": 42 | entry = new Directory(entry.contents); 43 | break; 44 | case "file": 45 | entry = new File(entry.buffer); 46 | break; 47 | default: 48 | throw new Error(`Unexpected entry kind: ${entry.kind}`); 49 | } 50 | out.set(name, entry); 51 | return out; 52 | }, () => new Map()) 53 | const preopen = new PreopenDirectory(dir, contents); 54 | preopens.push(preopen); 55 | } 56 | return preopens; 57 | } 58 | 59 | async function runWASI(options) { 60 | const testFile = options["test-file"] 61 | if (!testFile) { 62 | throw new Error("Missing --test-file"); 63 | } 64 | 65 | // arg0 is the given test file 66 | const args = [testFile].concat(options.arg) 67 | const fds = [ 68 | new OpenFile(new File([])), 69 | new NodeStdout(process.stdout), 70 | new NodeStdout(process.stderr), 71 | ]; 72 | const preopens = await derivePreopens(options.dir); 73 | fds.push(...preopens); 74 | const wasi = new WASI(args, options.env, fds, { debug: false }) 75 | 76 | let wasiImport = wasi.wasiImport; 77 | if (process.env["STRACE"]) { 78 | wasiImport = strace(wasiImport, []); 79 | } 80 | const importObject = { wasi_snapshot_preview1: wasiImport } 81 | 82 | const wasm = await WebAssembly.compile(await fs.readFile(testFile)); 83 | const instance = await WebAssembly.instantiate(wasm, importObject); 84 | const status = wasi.start(instance); 85 | process.exit(status); 86 | } 87 | 88 | async function main() { 89 | const options = parseArgs(); 90 | if (options.version) { 91 | const pkg = JSON.parse(await fs.readFile(new URL("../../../package.json", import.meta.url))); 92 | console.log(`${pkg.name} v${pkg.version}`); 93 | return; 94 | } 95 | runWASI(options); 96 | } 97 | 98 | await main(); 99 | -------------------------------------------------------------------------------- /threads/src/farm.ts: -------------------------------------------------------------------------------- 1 | import type { Fd } from "@bjorn3/browser_wasi_shim"; 2 | import type { WASIFarmPark } from "./park.js"; 3 | import type { WASIFarmRefObject } from "./ref.js"; 4 | import { WASIFarmParkUseArrayBuffer } from "./shared_array_buffer/index.js"; 5 | 6 | export class WASIFarm { 7 | private fds: Array; 8 | private park: WASIFarmPark; 9 | 10 | private can_array_buffer: boolean; 11 | 12 | constructor( 13 | stdin?: Fd, 14 | stdout?: Fd, 15 | stderr?: Fd, 16 | fds: Array = [], 17 | options: { 18 | allocator_size?: number; 19 | } = {}, 20 | ) { 21 | const new_fds = []; 22 | let stdin_ = undefined; 23 | let stdout_ = undefined; 24 | let stderr_ = undefined; 25 | if (stdin) { 26 | new_fds.push(stdin); 27 | stdin_ = new_fds.length - 1; 28 | } 29 | if (stdout) { 30 | new_fds.push(stdout); 31 | stdout_ = new_fds.length - 1; 32 | } 33 | if (stderr) { 34 | new_fds.push(stderr); 35 | stderr_ = new_fds.length - 1; 36 | } 37 | new_fds.push(...fds); 38 | 39 | const default_allow_fds = []; 40 | for (let i = 0; i < new_fds.length; i++) { 41 | default_allow_fds.push(i); 42 | } 43 | 44 | this.fds = new_fds; 45 | 46 | // WebAssembly.Memory can be used to create a SharedArrayBuffer, but it cannot be transferred by postMessage. 47 | // Uncaught (in promise) DataCloneError: 48 | // Failed to execute 'postMessage' on 'Worker': 49 | // SharedArrayBuffer transfer requires self.crossOriginIsolated. 50 | try { 51 | new SharedArrayBuffer(4); 52 | this.can_array_buffer = true; 53 | } catch (e) { 54 | this.can_array_buffer = false; 55 | console.warn("SharedArrayBuffer is not supported:", e); 56 | 57 | if ( 58 | !(globalThis as unknown as { crossOriginIsolated: unknown }) 59 | .crossOriginIsolated 60 | ) { 61 | console.warn( 62 | "SharedArrayBuffer is not supported because crossOriginIsolated is not enabled.", 63 | ); 64 | } 65 | } 66 | 67 | if (this.can_array_buffer) { 68 | this.park = new WASIFarmParkUseArrayBuffer( 69 | this.fds_ref(), 70 | stdin_, 71 | stdout_, 72 | stderr_, 73 | default_allow_fds, 74 | options?.allocator_size, 75 | ); 76 | } else { 77 | throw new Error("Non SharedArrayBuffer is not supported yet"); 78 | } 79 | 80 | this.park.listen(); 81 | } 82 | 83 | private fds_ref(): Array { 84 | const fds = new Proxy([] as Array, { 85 | get: (_, prop) => { 86 | if (prop === "push") { 87 | return (fd: Fd) => { 88 | const len = this.fds.push(fd); 89 | return len; 90 | }; 91 | } 92 | // @ts-ignore 93 | return this.fds[prop]; 94 | }, 95 | 96 | set: (_, prop, value) => { 97 | // @ts-ignore 98 | this.fds[prop] = value; 99 | return true; 100 | }, 101 | }); 102 | 103 | return fds; 104 | } 105 | 106 | get_ref(): WASIFarmRefObject { 107 | return this.park.get_ref(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/fd.ts: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-unused-vars:0 */ 2 | import * as wasi from "./wasi_defs.js"; 3 | 4 | export abstract class Fd { 5 | fd_allocate(offset: bigint, len: bigint): number { 6 | return wasi.ERRNO_NOTSUP; 7 | } 8 | fd_close(): number { 9 | return 0; 10 | } 11 | fd_fdstat_get(): { ret: number; fdstat: wasi.Fdstat | null } { 12 | return { ret: wasi.ERRNO_NOTSUP, fdstat: null }; 13 | } 14 | fd_fdstat_set_flags(flags: number): number { 15 | return wasi.ERRNO_NOTSUP; 16 | } 17 | fd_fdstat_set_rights( 18 | fs_rights_base: bigint, 19 | fs_rights_inheriting: bigint, 20 | ): number { 21 | return wasi.ERRNO_NOTSUP; 22 | } 23 | fd_filestat_get(): { ret: number; filestat: wasi.Filestat | null } { 24 | return { ret: wasi.ERRNO_NOTSUP, filestat: null }; 25 | } 26 | fd_filestat_set_size(size: bigint): number { 27 | return wasi.ERRNO_NOTSUP; 28 | } 29 | fd_filestat_set_times(atim: bigint, mtim: bigint, fst_flags: number): number { 30 | return wasi.ERRNO_NOTSUP; 31 | } 32 | fd_pread(size: number, offset: bigint): { ret: number; data: Uint8Array } { 33 | return { ret: wasi.ERRNO_NOTSUP, data: new Uint8Array() }; 34 | } 35 | fd_prestat_get(): { ret: number; prestat: wasi.Prestat | null } { 36 | return { ret: wasi.ERRNO_NOTSUP, prestat: null }; 37 | } 38 | fd_pwrite( 39 | data: Uint8Array, 40 | offset: bigint, 41 | ): { ret: number; nwritten: number } { 42 | return { ret: wasi.ERRNO_NOTSUP, nwritten: 0 }; 43 | } 44 | fd_read(size: number): { ret: number; data: Uint8Array } { 45 | return { ret: wasi.ERRNO_NOTSUP, data: new Uint8Array() }; 46 | } 47 | fd_readdir_single(cookie: bigint): { 48 | ret: number; 49 | dirent: wasi.Dirent | null; 50 | } { 51 | return { ret: wasi.ERRNO_NOTSUP, dirent: null }; 52 | } 53 | fd_seek(offset: bigint, whence: number): { ret: number; offset: bigint } { 54 | return { ret: wasi.ERRNO_NOTSUP, offset: 0n }; 55 | } 56 | fd_sync(): number { 57 | return 0; 58 | } 59 | fd_tell(): { ret: number; offset: bigint } { 60 | return { ret: wasi.ERRNO_NOTSUP, offset: 0n }; 61 | } 62 | fd_write(data: Uint8Array): { ret: number; nwritten: number } { 63 | return { ret: wasi.ERRNO_NOTSUP, nwritten: 0 }; 64 | } 65 | path_create_directory(path: string): number { 66 | return wasi.ERRNO_NOTSUP; 67 | } 68 | path_filestat_get( 69 | flags: number, 70 | path: string, 71 | ): { ret: number; filestat: wasi.Filestat | null } { 72 | return { ret: wasi.ERRNO_NOTSUP, filestat: null }; 73 | } 74 | path_filestat_set_times( 75 | flags: number, 76 | path: string, 77 | atim: bigint, 78 | mtim: bigint, 79 | fst_flags: number, 80 | ): number { 81 | return wasi.ERRNO_NOTSUP; 82 | } 83 | path_link(path: string, inode: Inode, allow_dir: boolean): number { 84 | return wasi.ERRNO_NOTSUP; 85 | } 86 | path_unlink(path: string): { ret: number; inode_obj: Inode | null } { 87 | return { ret: wasi.ERRNO_NOTSUP, inode_obj: null }; 88 | } 89 | path_lookup( 90 | path: string, 91 | dirflags: number, 92 | ): { ret: number; inode_obj: Inode | null } { 93 | return { ret: wasi.ERRNO_NOTSUP, inode_obj: null }; 94 | } 95 | path_open( 96 | dirflags: number, 97 | path: string, 98 | oflags: number, 99 | fs_rights_base: bigint, 100 | fs_rights_inheriting: bigint, 101 | fd_flags: number, 102 | ): { ret: number; fd_obj: Fd | null } { 103 | return { ret: wasi.ERRNO_NOTDIR, fd_obj: null }; 104 | } 105 | path_readlink(path: string): { ret: number; data: string | null } { 106 | return { ret: wasi.ERRNO_NOTSUP, data: null }; 107 | } 108 | path_remove_directory(path: string): number { 109 | return wasi.ERRNO_NOTSUP; 110 | } 111 | path_rename(old_path: string, new_fd: number, new_path: string): number { 112 | return wasi.ERRNO_NOTSUP; 113 | } 114 | path_unlink_file(path: string): number { 115 | return wasi.ERRNO_NOTSUP; 116 | } 117 | } 118 | 119 | export abstract class Inode { 120 | ino: bigint; 121 | 122 | constructor() { 123 | this.ino = Inode.issue_ino(); 124 | } 125 | 126 | // NOTE: ino 0 is reserved for the root directory 127 | private static next_ino: bigint = 1n; 128 | static issue_ino(): bigint { 129 | return Inode.next_ino++; 130 | } 131 | static root_ino(): bigint { 132 | return 0n; 133 | } 134 | 135 | abstract path_open( 136 | oflags: number, 137 | fs_rights_base: bigint, 138 | fd_flags: number, 139 | ): { ret: number; fd_obj: Fd | null }; 140 | 141 | abstract stat(): wasi.Filestat; 142 | } 143 | -------------------------------------------------------------------------------- /test/adapters/browser/run-wasi.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'fs/promises'; 4 | import path from 'path'; 5 | import { chromium } from "playwright" 6 | import { parseArgs } from "../shared/parseArgs.mjs" 7 | import { walkFs } from "../shared/walkFs.mjs" 8 | 9 | async function derivePreopens(dirs) { 10 | const preopens = []; 11 | for (let dir of dirs) { 12 | const contents = await walkFs(dir, (name, entry, out) => { 13 | if (entry.kind === "file") { 14 | // Convert buffer to array to make it serializable. 15 | entry.buffer = Array.from(entry.buffer); 16 | } 17 | return { ...out, [name]: entry }; 18 | }, () => {}); 19 | preopens.push({ dir, contents }); 20 | } 21 | return preopens; 22 | } 23 | 24 | /** 25 | * Configure routes for the browser harness. 26 | * 27 | * @param {import('playwright').BrowserContext} context 28 | * @param {string} harnessURL 29 | */ 30 | async function configureRoutes(context, harnessURL) { 31 | 32 | // Serve the main test page. 33 | context.route(`${harnessURL}/run-test.html`, async route => { 34 | const dirname = new URL(".", import.meta.url).pathname; 35 | const body = await fs.readFile(path.join(dirname, "run-test.html"), "utf8"); 36 | route.fulfill({ 37 | status: 200, 38 | contentType: 'text/html', 39 | // Browsers reduce the precision of performance.now() if the page is not 40 | // isolated. To keep the precision for `clock_get_time` we need to set the 41 | // following headers. 42 | // See: https://developer.mozilla.org/en-US/docs/Web/API/Performance/now#security_requirements 43 | headers: { 44 | "Cross-Origin-Opener-Policy": "same-origin", 45 | "Cross-Origin-Embedder-Policy": "require-corp", 46 | }, 47 | body, 48 | }); 49 | }) 50 | 51 | // Serve wasi-testsuite files. 52 | // e.g. http://browser-wasi-shim.localhost/home/me/browser_wasi_shim/test/wasi-testsuite/xxx 53 | let projectDir = path.join(new URL("../../wasi-testsuite", import.meta.url).pathname); 54 | projectDir = path.resolve(projectDir); 55 | context.route(`${harnessURL}${projectDir}/**/*`, async route => { 56 | const pathname = new URL(route.request().url()).pathname; 57 | const relativePath = pathname.slice(pathname.indexOf(projectDir) + projectDir.length); 58 | const content = await fs.readFile(path.join(projectDir, relativePath)); 59 | route.fulfill({ 60 | status: 200, 61 | contentType: 'application/javascript', 62 | body: content, 63 | }); 64 | }); 65 | 66 | // Serve transpiled browser_wasi_shim files under ./dist. 67 | context.route(`${harnessURL}/dist/*.js`, async route => { 68 | const pathname = new URL(route.request().url()).pathname; 69 | const distRelativePath = pathname.slice(pathname.indexOf("/dist/")); 70 | const distDir = new URL("../../..", import.meta.url); 71 | const distPath = path.join(distDir.pathname, distRelativePath); 72 | const content = await fs.readFile(distPath); 73 | route.fulfill({ 74 | status: 200, 75 | contentType: 'application/javascript', 76 | body: content, 77 | }); 78 | }); 79 | } 80 | 81 | async function runWASIOnBrowser(options) { 82 | const browser = await chromium.launch(); 83 | const context = await browser.newContext(); 84 | const harnessURL = 'http://browser-wasi-shim.localhost' 85 | 86 | await configureRoutes(context, harnessURL); 87 | 88 | const page = await context.newPage(); 89 | // Expose stdout/stderr bindings to allow test driver to capture output. 90 | page.exposeBinding("bindingWriteIO", (_, buffer, destination) => { 91 | buffer = Buffer.from(buffer); 92 | switch (destination) { 93 | case "stdout": 94 | process.stdout.write(buffer); 95 | break; 96 | case "stderr": 97 | process.stderr.write(buffer); 98 | break; 99 | default: 100 | throw new Error(`Unknown destination ${destination}`); 101 | } 102 | }); 103 | // Expose a way to serialize preopened directories to the browser. 104 | page.exposeBinding("bindingDerivePreopens", async (_, dirs) => { 105 | return await derivePreopens(dirs); 106 | }); 107 | 108 | page.on('console', msg => console.log(msg.text())); 109 | page.on('pageerror', ({ message }) => { 110 | console.log('PAGE ERROR:', message) 111 | process.exit(1); // Unexpected error. 112 | }); 113 | 114 | await page.goto(`${harnessURL}/run-test.html`, { waitUntil: "load" }) 115 | const status = await page.evaluate(async (o) => await window.runWASI(o), options) 116 | await page.close(); 117 | process.exit(status); 118 | } 119 | 120 | async function main() { 121 | const options = parseArgs(); 122 | if (options.version) { 123 | const pkg = JSON.parse(await fs.readFile(new URL("../../../package.json", import.meta.url))); 124 | console.log(`${pkg.name} v${pkg.version}`); 125 | return; 126 | } 127 | 128 | await runWASIOnBrowser(options); 129 | } 130 | 131 | await main(); 132 | -------------------------------------------------------------------------------- /src/fs_opfs.ts: -------------------------------------------------------------------------------- 1 | import * as wasi from "./wasi_defs.js"; 2 | import { Fd, Inode } from "./fd.js"; 3 | 4 | // Shim for https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle 5 | // This is not part of the public interface. 6 | export interface FileSystemSyncAccessHandle { 7 | close(): void; 8 | flush(): void; 9 | getSize(): number; 10 | read(buffer: ArrayBuffer | ArrayBufferView, options?: { at: number }): number; 11 | truncate(to: number): void; 12 | write( 13 | buffer: ArrayBuffer | ArrayBufferView, 14 | options?: { at: number }, 15 | ): number; 16 | } 17 | 18 | // Synchronous access to an individual file in the origin private file system. 19 | // Only allowed inside a WebWorker. 20 | export class SyncOPFSFile extends Inode { 21 | handle: FileSystemSyncAccessHandle; 22 | readonly: boolean; 23 | 24 | // FIXME needs a close() method to be called after start() to release the underlying handle 25 | constructor( 26 | handle: FileSystemSyncAccessHandle, 27 | options?: Partial<{ 28 | readonly: boolean; 29 | }>, 30 | ) { 31 | super(); 32 | this.handle = handle; 33 | this.readonly = !!options?.readonly; 34 | } 35 | 36 | path_open(oflags: number, fs_rights_base: bigint, fd_flags: number) { 37 | if ( 38 | this.readonly && 39 | (fs_rights_base & BigInt(wasi.RIGHTS_FD_WRITE)) == 40 | BigInt(wasi.RIGHTS_FD_WRITE) 41 | ) { 42 | // no write permission to file 43 | return { ret: wasi.ERRNO_PERM, fd_obj: null }; 44 | } 45 | 46 | if ((oflags & wasi.OFLAGS_TRUNC) == wasi.OFLAGS_TRUNC) { 47 | if (this.readonly) return { ret: wasi.ERRNO_PERM, fd_obj: null }; 48 | this.handle.truncate(0); 49 | } 50 | 51 | const file = new OpenSyncOPFSFile(this); 52 | if (fd_flags & wasi.FDFLAGS_APPEND) file.fd_seek(0n, wasi.WHENCE_END); 53 | return { ret: wasi.ERRNO_SUCCESS, fd_obj: file }; 54 | } 55 | 56 | get size(): bigint { 57 | return BigInt(this.handle.getSize()); 58 | } 59 | 60 | stat(): wasi.Filestat { 61 | return new wasi.Filestat(this.ino, wasi.FILETYPE_REGULAR_FILE, this.size); 62 | } 63 | } 64 | 65 | export class OpenSyncOPFSFile extends Fd { 66 | file: SyncOPFSFile; 67 | position: bigint = 0n; 68 | ino: bigint; 69 | 70 | constructor(file: SyncOPFSFile) { 71 | super(); 72 | this.file = file; 73 | this.ino = Inode.issue_ino(); 74 | } 75 | 76 | fd_allocate(offset: bigint, len: bigint): number { 77 | if (BigInt(this.file.handle.getSize()) > offset + len) { 78 | // already big enough 79 | } else { 80 | // extend 81 | this.file.handle.truncate(Number(offset + len)); 82 | } 83 | return wasi.ERRNO_SUCCESS; 84 | } 85 | 86 | fd_fdstat_get(): { ret: number; fdstat: wasi.Fdstat | null } { 87 | return { ret: 0, fdstat: new wasi.Fdstat(wasi.FILETYPE_REGULAR_FILE, 0) }; 88 | } 89 | 90 | fd_filestat_get(): { ret: number; filestat: wasi.Filestat } { 91 | return { 92 | ret: 0, 93 | filestat: new wasi.Filestat( 94 | this.ino, 95 | wasi.FILETYPE_REGULAR_FILE, 96 | BigInt(this.file.handle.getSize()), 97 | ), 98 | }; 99 | } 100 | 101 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 102 | fd_filestat_set_size(size: bigint): number { 103 | this.file.handle.truncate(Number(size)); 104 | return wasi.ERRNO_SUCCESS; 105 | } 106 | 107 | fd_read(size: number): { ret: number; data: Uint8Array } { 108 | const buf = new Uint8Array(size); 109 | const n = this.file.handle.read(buf, { at: Number(this.position) }); 110 | this.position += BigInt(n); 111 | return { ret: 0, data: buf.slice(0, n) }; 112 | } 113 | 114 | fd_seek( 115 | offset: number | bigint, 116 | whence: number, 117 | ): { ret: number; offset: bigint } { 118 | let calculated_offset: bigint; 119 | switch (whence) { 120 | case wasi.WHENCE_SET: 121 | calculated_offset = BigInt(offset); 122 | break; 123 | case wasi.WHENCE_CUR: 124 | calculated_offset = this.position + BigInt(offset); 125 | break; 126 | case wasi.WHENCE_END: 127 | calculated_offset = BigInt(this.file.handle.getSize()) + BigInt(offset); 128 | break; 129 | default: 130 | return { ret: wasi.ERRNO_INVAL, offset: 0n }; 131 | } 132 | if (calculated_offset < 0) { 133 | return { ret: wasi.ERRNO_INVAL, offset: 0n }; 134 | } 135 | this.position = calculated_offset; 136 | return { ret: wasi.ERRNO_SUCCESS, offset: this.position }; 137 | } 138 | 139 | fd_write(data: Uint8Array): { ret: number; nwritten: number } { 140 | if (this.file.readonly) return { ret: wasi.ERRNO_BADF, nwritten: 0 }; 141 | 142 | // don't need to extend file manually, just write 143 | const n = this.file.handle.write(data, { at: Number(this.position) }); 144 | this.position += BigInt(n); 145 | return { ret: wasi.ERRNO_SUCCESS, nwritten: n }; 146 | } 147 | 148 | fd_sync(): number { 149 | this.file.handle.flush(); 150 | return wasi.ERRNO_SUCCESS; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /threads/src/polyfill.js: -------------------------------------------------------------------------------- 1 | // https://github.com/tc39/proposal-atomics-wait-async/blob/master/polyfill.js 2 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | * 7 | * Author: Lars T Hansen, lhansen@mozilla.com 8 | */ 9 | 10 | /* Polyfill for Atomics.waitAsync() for web browsers. 11 | * 12 | * Any kind of agent that is able to create a new Worker can use this polyfill. 13 | * 14 | * Load this file in all agents that will use Atomics.waitAsync. 15 | * 16 | * Agents that don't call Atomics.waitAsync need do nothing special. 17 | * 18 | * Any kind of agent can wake another agent that is sleeping in 19 | * Atomics.waitAsync by just calling Atomics.notify for the location being slept 20 | * on, as normal. 21 | * 22 | * The implementation is not completely faithful to the proposed semantics: in 23 | * the case where an agent first asyncWaits and then waits on the same location: 24 | * when it is woken, the two waits will be woken in order, while in the real 25 | * semantics, the sync wait will be woken first. 26 | * 27 | * In this polyfill Atomics.waitAsync is not very fast. 28 | */ 29 | 30 | /* Implementation: 31 | * 32 | * For every wait we fork off a Worker to perform the wait. Workers are reused 33 | * when possible. The worker communicates with its parent using postMessage. 34 | */ 35 | 36 | (() => { 37 | if (typeof Atomics.waitAsync === "function") return; 38 | 39 | const helperCode = ` 40 | onmessage = function (ev) { 41 | try { 42 | switch (ev.data[0]) { 43 | case 'wait': { 44 | let [_, ia, index, value, timeout] = ev.data; 45 | let result = Atomics.wait(ia, index, value, timeout) 46 | postMessage(['ok', result]); 47 | break; 48 | } 49 | default: { 50 | throw new Error("Bogus message sent to wait helper: " + ev.data.join(',')); 51 | } 52 | } 53 | } catch (e) { 54 | console.log("Exception in wait helper"); 55 | postMessage(['error', 'Exception']); 56 | } 57 | } 58 | `; 59 | 60 | const helpers = []; 61 | 62 | function allocHelper() { 63 | if (helpers.length > 0) return helpers.pop(); 64 | const h = new Worker( 65 | `data:application/javascript,${encodeURIComponent(helperCode)}`, 66 | ); 67 | return h; 68 | } 69 | 70 | function freeHelper(h) { 71 | helpers.push(h); 72 | } 73 | 74 | // Atomics.waitAsync always returns a promise. Throws standard errors 75 | // for parameter validation. The promise is resolved with a string as from 76 | // Atomics.wait, or, in the case something went completely wrong, it is 77 | // rejected with an error string. 78 | 79 | function waitAsync(ia, index_, value_, timeout_) { 80 | if ( 81 | typeof ia !== "object" || 82 | !(ia instanceof Int32Array) || 83 | !(ia.buffer instanceof SharedArrayBuffer) 84 | ) 85 | throw new TypeError("Expected shared memory"); 86 | 87 | // These conversions only approximate the desired semantics but are 88 | // close enough for the polyfill. 89 | 90 | const index = index_ | 0; 91 | const value = value_ | 0; 92 | const timeout = 93 | timeout_ === undefined ? Number.POSITIVE_INFINITY : +timeout_; 94 | 95 | // Range checking for the index. 96 | 97 | ia[index]; 98 | 99 | // Optimization, avoid the helper thread in this common case. 100 | 101 | if (Atomics.load(ia, index) !== value) return Promise.resolve("not-equal"); 102 | 103 | // General case, we must wait. 104 | 105 | return new Promise((resolve, reject) => { 106 | const h = allocHelper(); 107 | h.onmessage = (ev) => { 108 | // Free the helper early so that it can be reused if the resolution 109 | // needs a helper. 110 | freeHelper(h); 111 | switch (ev.data[0]) { 112 | case "ok": 113 | resolve(ev.data[1]); 114 | break; 115 | case "error": 116 | // Note, rejection is not in the spec, it is an artifact of the polyfill. 117 | // The helper already printed an error to the console. 118 | reject(ev.data[1]); 119 | break; 120 | } 121 | }; 122 | 123 | // It's possible to do better here if the ia is already known to the 124 | // helper. In that case we can communicate the other data through 125 | // shared memory and wake the agent. And it is possible to make ia 126 | // known to the helper by waking it with a special value so that it 127 | // checks its messages, and then posting the ia to the helper. Some 128 | // caching / decay scheme is useful no doubt, to improve performance 129 | // and avoid leaks. 130 | // 131 | // In the event we wake the helper directly, we can micro-wait here 132 | // for a quick result. We'll need to restructure some code to make 133 | // that work out properly, and some synchronization is necessary for 134 | // the helper to know that we've picked up the result and no 135 | // postMessage is necessary. 136 | 137 | h.postMessage(["wait", ia, index, value, timeout]); 138 | }); 139 | } 140 | 141 | Object.defineProperty(Atomics, "waitAsync", { 142 | value: waitAsync, 143 | configurable: true, 144 | enumerable: false, 145 | writable: true, 146 | }); 147 | })(); 148 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads_rustc/depend_rustc_files.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directory, 3 | PreopenDirectory, 4 | File, 5 | type Inode, 6 | } from "@bjorn3/browser_wasi_shim"; 7 | import { WASIFarm } from "../../src"; 8 | 9 | async function load_external_file(path) { 10 | return new File(await (await (await fetch(path)).blob()).arrayBuffer()); 11 | } 12 | 13 | const linux_libs_promise = (async () => { 14 | const dir = new Map(); 15 | for (const file of [ 16 | "libaddr2line-b8754aeb03c02354.rlib", 17 | "libadler-05c3545f6cd12159.rlib", 18 | "liballoc-0dab879bc41cd6bd.rlib", 19 | "libcfg_if-c7fd2cef50341546.rlib", 20 | "libcompiler_builtins-a99947d020d809d6.rlib", 21 | "libcore-4b8e8a815d049db3.rlib", 22 | "libgetopts-bbb75529e85d129d.rlib", 23 | "libgimli-598847d27d7a3cbf.rlib", 24 | "libhashbrown-d2ff91fdf93cacb2.rlib", 25 | "liblibc-dc63949c664c3fce.rlib", 26 | "libmemchr-2d3a423be1a6cb96.rlib", 27 | "libminiz_oxide-b109506a0ccc4c6a.rlib", 28 | "libobject-7b48def7544c748b.rlib", 29 | "libpanic_abort-c93441899b93b849.rlib", 30 | "libpanic_unwind-11d9ba05b60bf694.rlib", 31 | "libproc_macro-1a7f7840bb9983dc.rlib", 32 | "librustc_demangle-59342a335246393d.rlib", 33 | "librustc_std_workspace_alloc-552b185085090ff6.rlib", 34 | "librustc_std_workspace_core-5d8a121daa7eeaa9.rlib", 35 | "librustc_std_workspace_std-97f43841ce452f7d.rlib", 36 | "libstd-bdedb7706a556da2.rlib", 37 | "libstd-bdedb7706a556da2.so", 38 | "libstd_detect-cca21eebc4281add.rlib", 39 | "libsysroot-f654e185be3ffebd.rlib", 40 | "libtest-f06fa3fbc201c558.rlib", 41 | "libunicode_width-19a0dcd589fa0877.rlib", 42 | "libunwind-747b693f90af9445.rlib", 43 | ]) { 44 | dir.set( 45 | file, 46 | await load_external_file( 47 | `./rust_wasm/rustc_llvm/dist/lib/rustlib/x86_64-unknown-linux-gnu/lib/${file}`, 48 | ), 49 | ); 50 | } 51 | return dir; 52 | })(); 53 | 54 | const threads_libs_promise = (async () => { 55 | const dir = new Map(); 56 | for (const file of [ 57 | "libaddr2line-a47658bebc67c3a1.rlib", 58 | "libadler-38ddbcf07afd45fc.rlib", 59 | "liballoc-1fc4f6ca1d836e4c.rlib", 60 | "libcfg_if-fd15f5d506df7899.rlib", 61 | "libcompiler_builtins-3dc6223f56552b05.rlib", 62 | "libcore-0ec7cb16e8553802.rlib", 63 | "libgetopts-6248a91c42a854a0.rlib", 64 | "libgimli-4425159eeeeb18dd.rlib", 65 | "libhashbrown-243f98c4e4e641ea.rlib", 66 | "liblibc-9149392e3841960d.rlib", 67 | "libmemchr-9ac950afd37fa4c7.rlib", 68 | "libminiz_oxide-91aaa0ee7402d39e.rlib", 69 | "libobject-361b96ef5df8a7f9.rlib", 70 | "libpanic_abort-f91052098501e46b.rlib", 71 | "libpanic_unwind-fc376dcf47815f10.rlib", 72 | "libproc_macro-9cab37e4d11f0e52.rlib", 73 | "librustc_demangle-1af142f261139812.rlib", 74 | "librustc_std_workspace_alloc-f0d62212c413dd0e.rlib", 75 | "librustc_std_workspace_core-ea396731d16229a8.rlib", 76 | "librustc_std_workspace_std-7434133be68a4a89.rlib", 77 | "libstd_detect-083332b3c8180bc9.rlib", 78 | "libstd-5ddf10249e9580fe.rlib", 79 | "libsysroot-8b3608099dad3b42.rlib", 80 | "libtest-8ebd431ae5608538.rlib", 81 | "libunicode_width-7e2396fcd7049a8b.rlib", 82 | "libunwind-e7408208cf4a3c79.rlib", 83 | "libwasi-f0b9e157c50fe586.rlib", 84 | ]) { 85 | dir.set( 86 | file, 87 | await load_external_file( 88 | `./rust_wasm/rustc_llvm/dist/lib/rustlib/wasm32-wasip1-threads/lib/${file}`, 89 | ), 90 | ); 91 | } 92 | return dir; 93 | })(); 94 | 95 | const threads_self_contained_promise = (async () => { 96 | const dir = new Map(); 97 | for (const file of ["crt1-command.o", "crt1-reactor.o", "libc.a"]) { 98 | dir.set( 99 | file, 100 | await load_external_file( 101 | `./rust_wasm/rustc_llvm/dist/lib/rustlib/wasm32-wasip1-threads/lib/self-contained/${file}`, 102 | ), 103 | ); 104 | } 105 | return dir; 106 | })(); 107 | 108 | const [linux_libs, threads_libs, threads_self_contained, components] = 109 | await Promise.all([ 110 | linux_libs_promise, 111 | threads_libs_promise, 112 | threads_self_contained_promise, 113 | await load_external_file( 114 | "./rust_wasm/rustc_llvm/dist/lib/rustlib/components", 115 | ), 116 | ]); 117 | 118 | threads_libs.set("self-contained", new Directory(threads_self_contained)); 119 | 120 | const toMap = (arr: Array<[string, Inode]>) => new Map(arr); 121 | 122 | const farm = new WASIFarm( 123 | undefined, 124 | undefined, 125 | undefined, 126 | [ 127 | new PreopenDirectory( 128 | "/sysroot", 129 | toMap([ 130 | [ 131 | "lib", 132 | new Directory([ 133 | [ 134 | "rustlib", 135 | new Directory([ 136 | ["components", components], 137 | [ 138 | "wasm32-wasip1-threads", 139 | new Directory([["lib", new Directory(threads_libs)]]), 140 | ], 141 | [ 142 | "x86_64-unknown-linux-gnu", 143 | new Directory([["lib", new Directory(linux_libs)]]), 144 | ], 145 | ]), 146 | ], 147 | ]), 148 | ], 149 | ]), 150 | ), 151 | ], 152 | { 153 | allocator_size: 1024 * 1024 * 1024, 154 | // debug: true, 155 | }, 156 | ); 157 | 158 | const ret = await farm.get_ref(); 159 | 160 | postMessage({ wasi_ref: ret }); 161 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads_rustc/depend_rustc_with_lld.ts: -------------------------------------------------------------------------------- 1 | import { WASIFarm } from "../../src"; 2 | 3 | import { 4 | File, 5 | Directory, 6 | PreopenDirectory, 7 | type Inode, 8 | } from "@bjorn3/browser_wasi_shim"; 9 | 10 | async function load_external_file(path) { 11 | return new File(await (await (await fetch(path)).blob()).arrayBuffer()); 12 | } 13 | 14 | const linux_libs_promise = (async () => { 15 | const dir = new Map(); 16 | for (const file of [ 17 | "libaddr2line-d2445d88f0df8258.rlib", 18 | "libadler-6095b59b8443d84e.rlib", 19 | "liballoc-19b196c8c1c1b105.rlib", 20 | "libcfg_if-d04d665f43a7a5c9.rlib", 21 | "libcompiler_builtins-f3e5bc67a3085e50.rlib", 22 | "libcore-24518d8502db248c.rlib", 23 | "libgetopts-4bb89b05e2b4cc6e.rlib", 24 | "libgimli-85e2d283537da979.rlib", 25 | "libhashbrown-1436b1713a4e6650.rlib", 26 | "liblibc-a012fa771333437a.rlib", 27 | "libmemchr-b50e0c33c9e9768d.rlib", 28 | "libminiz_oxide-937a56bed56199f4.rlib", 29 | "libobject-4e591f1863579f49.rlib", 30 | "libpanic_abort-90cfe20cb97a5e4c.rlib", 31 | "libpanic_unwind-9ad8c03a583f4dc7.rlib", 32 | "libproc_macro-32b9efef039a24fe.rlib", 33 | "librustc_demangle-ca292a161705fdb4.rlib", 34 | "librustc_std_workspace_alloc-bba69521e853a996.rlib", 35 | "librustc_std_workspace_core-3da34b7f5e59869a.rlib", 36 | "librustc_std_workspace_std-c35ef78edc033606.rlib", 37 | "libstd_detect-776a1ebea822ca12.rlib", 38 | "libstd-6924b036e1bec7ce.rlib", 39 | "libstd-6924b036e1bec7ce.so", 40 | "libsysroot-0b7644c6027c414e.rlib", 41 | "libtest-3d5766d8038a0e74.rlib", 42 | "libunicode_width-dff2b02e7e936b79.rlib", 43 | "libunwind-c99628283276f21f.rlib", 44 | ]) { 45 | dir.set( 46 | file, 47 | await load_external_file( 48 | `./rust_wasm/rustc_llvm_with_lld/dist/lib/rustlib/x86_64-unknown-linux-gnu/lib/${file}`, 49 | ), 50 | ); 51 | } 52 | return dir; 53 | })(); 54 | 55 | const threads_libs_promise = (async () => { 56 | const dir = new Map(); 57 | for (const file of [ 58 | "libaddr2line-02cc3b87379ea949.rlib", 59 | "libgimli-a80bd3f5fe54def6.rlib", 60 | "libpanic_unwind-05c50d12758b6d01.rlib", 61 | "libstd_detect-a196185ed7f17cfc.rlib", 62 | "libadler-a6b90b86640ec179.rlib", 63 | "libhashbrown-35a68b834152af94.rlib", 64 | "libproc_macro-2aa22b2ed111e644.rlib", 65 | "libsysroot-856cc79af2fd1ee2.rlib", 66 | "liballoc-be9b2b68a2d6adbd.rlib", 67 | "liblibc-374b1b6cc5790f18.rlib", 68 | "librustc_demangle-710d955f336192eb.rlib", 69 | "libtest-4fcaddbdfa4f37f9.rlib", 70 | "libcfg_if-75a956684c8aceef.rlib", 71 | "libmemchr-b8580e949eadd97a.rlib", 72 | "librustc_std_workspace_alloc-9c960f87e9d5a453.rlib", 73 | "libunicode_width-be3becba43cd1b78.rlib", 74 | "libcompiler_builtins-3a0795e4489d8e8b.rlib", 75 | "libminiz_oxide-0bc4b046969a6755.rlib", 76 | "librustc_std_workspace_core-4e237761d66d6cde.rlib", 77 | "libunwind-2e570680d1c4cd0a.rlib", 78 | "libcore-3e70038323b3a06e.rlib", 79 | "libobject-3d83ea5d7ed5636f.rlib", 80 | "librustc_std_workspace_std-5aa56e0a1970dc72.rlib", 81 | "libwasi-f7d0229d2fe97cfd.rlib", 82 | "libgetopts-b3d0219ad62c74a7.rlib", 83 | "libpanic_abort-431cc2501a123c59.rlib", 84 | "libstd-c7f97b33ddfcbfbf.rlib", 85 | ]) { 86 | dir.set( 87 | file, 88 | await load_external_file( 89 | `./rust_wasm/rustc_llvm_with_lld/dist/lib/rustlib/wasm32-wasip1/lib/${file}`, 90 | ), 91 | ); 92 | } 93 | return dir; 94 | })(); 95 | 96 | const threads_self_contained_promise = (async () => { 97 | const dir = new Map(); 98 | for (const file of ["crt1-command.o", "crt1-reactor.o", "libc.a"]) { 99 | dir.set( 100 | file, 101 | await load_external_file( 102 | `./rust_wasm/rustc_llvm_with_lld/dist/lib/rustlib/wasm32-wasip1/lib/self-contained/${file}`, 103 | ), 104 | ); 105 | } 106 | return dir; 107 | })(); 108 | 109 | const [linux_libs, threads_libs, threads_self_contained, components] = 110 | await Promise.all([ 111 | linux_libs_promise, 112 | threads_libs_promise, 113 | threads_self_contained_promise, 114 | await load_external_file( 115 | "./rust_wasm/rustc_llvm_with_lld/dist/lib/rustlib/components", 116 | ), 117 | ]); 118 | 119 | threads_libs.set("self-contained", new Directory(threads_self_contained)); 120 | 121 | const toMap = (arr: Array<[string, Inode]>) => new Map(arr); 122 | 123 | const farm = new WASIFarm( 124 | undefined, 125 | undefined, 126 | undefined, 127 | [ 128 | new PreopenDirectory( 129 | "/sysroot-with-lld", 130 | toMap([ 131 | [ 132 | "lib", 133 | new Directory([ 134 | [ 135 | "rustlib", 136 | new Directory([ 137 | ["components", components], 138 | [ 139 | "wasm32-wasip1", 140 | new Directory([["lib", new Directory(threads_libs)]]), 141 | ], 142 | [ 143 | "x86_64-unknown-linux-gnu", 144 | new Directory([["lib", new Directory(linux_libs)]]), 145 | ], 146 | ]), 147 | ], 148 | ]), 149 | ], 150 | ]), 151 | ), 152 | ], 153 | { 154 | allocator_size: 1024 * 1024 * 1024, 155 | // debug: true, 156 | }, 157 | ); 158 | 159 | const ret = await farm.get_ref(); 160 | 161 | postMessage({ wasi_ref: ret }); 162 | -------------------------------------------------------------------------------- /threads/src/ref.ts: -------------------------------------------------------------------------------- 1 | import type { FdCloseSender } from "./sender.js"; 2 | import type { wasi } from "@bjorn3/browser_wasi_shim"; 3 | 4 | export type WASIFarmRefObject = { 5 | stdin: number | undefined; 6 | stdout: number | undefined; 7 | stderr: number | undefined; 8 | fd_close_receiver: FdCloseSender; 9 | default_fds: Array; 10 | }; 11 | 12 | export abstract class WASIFarmRef { 13 | abstract get_fds_len(): number; 14 | // please implement this method 15 | // abstract init_self(sl: WASIFarmRef): WASIFarmRef; 16 | 17 | protected stdin: number | undefined; 18 | protected stdout: number | undefined; 19 | protected stderr: number | undefined; 20 | 21 | protected id!: number; 22 | 23 | fd_close_receiver: FdCloseSender; 24 | 25 | default_fds: Array = []; 26 | 27 | async send(targets: Array, fd: number): Promise { 28 | await this.fd_close_receiver.send(targets, fd); 29 | } 30 | 31 | get(id: number): Array | undefined { 32 | return this.fd_close_receiver.get(id); 33 | } 34 | 35 | abstract set_park_fds_map(fds: Array): void; 36 | 37 | abstract set_id(): number; 38 | 39 | constructor( 40 | stdin: number | undefined, 41 | stdout: number | undefined, 42 | stderr: number | undefined, 43 | fd_close_receiver: FdCloseSender, 44 | default_fds?: Array, 45 | ) { 46 | this.stdin = stdin; 47 | this.stdout = stdout; 48 | this.stderr = stderr; 49 | this.fd_close_receiver = fd_close_receiver; 50 | if (default_fds !== undefined) { 51 | this.default_fds = default_fds; 52 | } 53 | } 54 | 55 | get_stdin(): number | undefined { 56 | return this.stdin; 57 | } 58 | 59 | get_stdout(): number | undefined { 60 | return this.stdout; 61 | } 62 | 63 | get_stderr(): number | undefined { 64 | return this.stderr; 65 | } 66 | 67 | abstract fd_advise(fd: number | undefined): number; 68 | abstract fd_allocate( 69 | fd: number | undefined, 70 | offset: bigint, 71 | len: bigint, 72 | ): number; 73 | abstract fd_close(fd: number | undefined): number; 74 | abstract fd_datasync(fd: number | undefined): number; 75 | abstract fd_fdstat_get( 76 | fd: number | undefined, 77 | ): [wasi.Fdstat | undefined, number]; 78 | abstract fd_fdstat_set_flags(fd: number | undefined, flags: number): number; 79 | abstract fd_fdstat_set_rights( 80 | fd: number | undefined, 81 | fs_rights_base: bigint, 82 | fs_rights_inheriting: bigint, 83 | ): number; 84 | abstract fd_filestat_get( 85 | fd: number | undefined, 86 | ): [wasi.Filestat | undefined, number]; 87 | abstract fd_filestat_set_size(fd: number | undefined, size: bigint): number; 88 | abstract fd_filestat_set_times( 89 | fd: number | undefined, 90 | atim: bigint, 91 | mtim: bigint, 92 | fst_flags: number, 93 | ): number; 94 | abstract fd_pread( 95 | fd: number | undefined, 96 | iovs: Uint32Array, 97 | offset: bigint, 98 | ): [[number, Uint8Array] | undefined, number]; 99 | abstract fd_prestat_get( 100 | fd: number | undefined, 101 | ): [[number, number] | undefined, number]; 102 | abstract fd_prestat_dir_name( 103 | fd: number | undefined, 104 | path_len: number, 105 | ): [Uint8Array | undefined, number]; 106 | abstract fd_pwrite( 107 | fd: number | undefined, 108 | iovs: Uint8Array, 109 | offset: bigint, 110 | ): [number | undefined, number]; 111 | abstract fd_read( 112 | fd: number | undefined, 113 | iovs: Uint32Array, 114 | ): [[number, Uint8Array] | undefined, number]; 115 | abstract fd_readdir( 116 | fd: number | undefined, 117 | limit_buf_len: number, 118 | cookie: bigint, 119 | ): [[Uint8Array, number] | undefined, number]; 120 | // abstract fd_renumber(fd: number | undefined, to: number): number; 121 | abstract fd_seek( 122 | fd: number | undefined, 123 | offset: bigint, 124 | whence: number, 125 | ): [bigint | undefined, number]; 126 | abstract fd_sync(fd: number | undefined): number; 127 | abstract fd_tell(fd: number | undefined): [bigint | undefined, number]; 128 | abstract fd_write( 129 | fd: number | undefined, 130 | iovs: Uint8Array, 131 | ): [number | undefined, number]; 132 | abstract path_create_directory( 133 | fd: number | undefined, 134 | path: Uint8Array, 135 | ): number; 136 | abstract path_filestat_get( 137 | fd: number | undefined, 138 | flags: number, 139 | path: Uint8Array, 140 | ): [wasi.Filestat | undefined, number]; 141 | abstract path_filestat_set_times( 142 | fd: number | undefined, 143 | flags: number, 144 | path: Uint8Array, 145 | st_atim: bigint, 146 | st_mtim: bigint, 147 | fst_flags: number, 148 | ): number; 149 | abstract path_link( 150 | old_fd: number | undefined, 151 | old_flags: number, 152 | old_path: Uint8Array, 153 | new_fd: number | undefined, 154 | new_path: Uint8Array, 155 | ): number; 156 | abstract path_open( 157 | fd: number | undefined, 158 | dirflags: number, 159 | path: Uint8Array, 160 | oflags: number, 161 | fs_rights_base: bigint, 162 | fs_rights_inheriting: bigint, 163 | fs_flags: number, 164 | ): [number | undefined, number]; 165 | abstract path_readlink( 166 | fd: number | undefined, 167 | path: Uint8Array, 168 | buf_len: number, 169 | ): [Uint8Array | undefined, number]; 170 | abstract path_remove_directory( 171 | fd: number | undefined, 172 | path: Uint8Array, 173 | ): number; 174 | abstract path_rename( 175 | old_fd: number | undefined, 176 | old_path: Uint8Array, 177 | new_fd: number | undefined, 178 | new_path: Uint8Array, 179 | ): number; 180 | abstract path_symlink( 181 | old_path: Uint8Array, 182 | fd: number | undefined, 183 | new_path: Uint8Array, 184 | ): number; 185 | abstract path_unlink_file(fd: number | undefined, path: Uint8Array): number; 186 | } 187 | -------------------------------------------------------------------------------- /threads/src/shared_array_buffer/worker_background/worker_blob.ts: -------------------------------------------------------------------------------- 1 | export const url = () => { 2 | const code = 3 | 'let worker_background;let serialize=r=>({message:r.message,name:r.name,stack:r.stack,cause:r.cause});class AllocatorUseArrayBuffer{share_arrays_memory;constructor(r=new SharedArrayBuffer(0xa00000)){this.share_arrays_memory=r;let e=new Int32Array(this.share_arrays_memory);Atomics.store(e,0,0),Atomics.store(e,1,0),Atomics.store(e,2,12)}static init_self(r){return new AllocatorUseArrayBuffer(r.share_arrays_memory)}async async_write(r,e,t){let o=new Int32Array(this.share_arrays_memory);for(;;){let{value:s}=Atomics.waitAsync(o,0,1);if("timed-out"===(s instanceof Promise?await s:s))throw Error("timed-out lock");if(0!==Atomics.compareExchange(o,0,0,1))continue;let i=this.write_inner(r,e,t);return Atomics.store(o,0,0),Atomics.notify(o,0,1),i}}block_write(r,e,t){for(;;){let o=new Int32Array(this.share_arrays_memory);if("timed-out"===Atomics.wait(o,0,1))throw Error("timed-out lock");if(0!==Atomics.compareExchange(o,0,0,1))continue;let s=this.write_inner(r,e,t);return Atomics.store(o,0,0),Atomics.notify(o,0,1),s}}write_inner(r,e,t){let o,s;let i=new Int32Array(this.share_arrays_memory),a=new Uint8Array(this.share_arrays_memory);o=0===Atomics.add(i,1,1)?Atomics.store(i,2,12):Atomics.load(i,2);let n=this.share_arrays_memory.byteLength,l=r.byteLength,c=o+l;if(n{console.log("gen_worker");let r=Atomics.load(e,1),t=Atomics.load(e,2),o=this.allocator.get_memory(r,t);this.allocator.free(r,t);let s=new TextDecoder().decode(o),i=1===Atomics.load(e,3);return new Worker(s,{type:i?"module":"classic"})},a=()=>{console.log("gen_obj");let r=Atomics.load(e,4),t=Atomics.load(e,5),o=this.allocator.get_memory(r,t);this.allocator.free(r,t);let s=new TextDecoder().decode(o);return JSON.parse(s)};switch(Atomics.load(e,0)){case 1:{let r=i(),t=a(),o=this.assign_worker_id();console.log("new worker "+o),this.workers[o]=r;let{promise:s,resolve:n}=Promise.withResolvers();r.onmessage=async r=>{let{msg:e}=r.data;if("ready"===e&&n(),"done"===e&&(this.workers[o].terminate(),this.workers[o]=void 0,console.log(`worker ${o} done so terminate`)),"error"===e){this.workers[o].terminate(),this.workers[o]=void 0;let e=0;for(let r of this.workers)void 0!==r&&(r.terminate(),console.warn("wasi throw error but child process exists, terminate "+e)),e++;void 0!==this.start_worker&&(this.start_worker.terminate(),console.warn("wasi throw error but wasi exists, terminate wasi")),this.workers=[void 0],this.start_worker=void 0;let t=r.data.error,s=new Int32Array(this.lock,8),i=serialize(t),[a,n]=await this.allocator.async_write(new TextEncoder().encode(JSON.stringify(i)),this.lock,3),l=Atomics.compareExchange(s,0,0,1);if(0!==l){console.error("what happened?"),this.allocator.free(a,n);return}let c=Atomics.notify(s,0);0===c&&(console.error(t),this.allocator.free(a,n),Atomics.store(s,0,0))}},r.postMessage({...this.override_object,...t,worker_id:o,worker_background_ref:this.ref()}),await s,Atomics.store(e,0,o);break}case 2:{this.start_worker=i();let r=a();this.start_worker.onmessage=async r=>{let{msg:e}=r.data;if("done"===e){let r=0;for(let e of this.workers)void 0!==e&&(e.terminate(),console.warn("wasi done but worker exists, terminate "+r)),r++;this.start_worker.terminate(),this.start_worker=void 0,console.log("start worker done so terminate")}if("error"===e){let e=0;for(let r of this.workers)void 0!==r&&(r.terminate(),console.warn("wasi throw error but worker exists, terminate "+e)),e++;void 0!==this.start_worker&&(this.start_worker.terminate(),console.warn("wasi throw error but wasi exists, terminate start worker")),this.workers=[void 0],this.start_worker=void 0;let t=r.data.error,o=new Int32Array(this.lock,8),s=serialize(t),[i,a]=await this.allocator.async_write(new TextEncoder().encode(JSON.stringify(s)),this.lock,3),n=Atomics.compareExchange(o,0,0,1);if(0!==n){console.error("what happened?"),this.allocator.free(i,a);return}let l=Atomics.notify(o,0);0===l&&(console.error(t),this.allocator.free(i,a),Atomics.store(o,0,0))}},this.start_worker.postMessage({...this.override_object,...r,worker_background_ref:this.ref()})}}let n=Atomics.exchange(r,1,0);if(1!==n)throw Error("Lock is already set");let l=Atomics.notify(r,1,1);if(1!==l){if(0===l){console.warn("notify failed, waiter is late");continue}throw Error("notify failed: "+l)}}catch(r){console.error(r),await new Promise(r=>setTimeout(r,1e3))}}};globalThis.onmessage=r=>{let{override_object:e,worker_background_ref_object:t}=r.data;worker_background=WorkerBackground.init_self(e,t),postMessage("ready")};'; 4 | 5 | const blob = new Blob([code], { type: "application/javascript" }); 6 | 7 | const url = URL.createObjectURL(blob); 8 | 9 | return url; 10 | }; 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | "incremental": true /* Enable incremental compilation */, 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "ESNext" /* Specify what module code is generated. */, 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 51 | "declarationDir": "./typings", 52 | 53 | /* Interop Constraints */ 54 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 55 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 56 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 57 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 58 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 59 | 60 | /* Type Checking */ 61 | "strict": false /* Enable all strict type-checking options. */, 62 | 63 | /* Completeness */ 64 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 65 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 66 | }, 67 | "include": ["src"] 68 | } 69 | -------------------------------------------------------------------------------- /threads/examples/wasi_workers_rustc/index.ts: -------------------------------------------------------------------------------- 1 | import { WASIFarm } from "../../src"; 2 | import { 3 | Directory, 4 | Fd, 5 | File, 6 | type Inode, 7 | PreopenDirectory, 8 | } from "@bjorn3/browser_wasi_shim"; 9 | import { Terminal } from "@xterm/xterm"; 10 | import { FitAddon } from "xterm-addon-fit"; 11 | import "@xterm/xterm/css/xterm.css"; 12 | 13 | const term = new Terminal({ 14 | convertEol: true, 15 | }); 16 | const terminalElement = document.getElementById("terminal"); 17 | if (!terminalElement) { 18 | throw new Error("Terminal element not found"); 19 | } 20 | term.open(terminalElement); 21 | 22 | const fitAddon = new FitAddon(); 23 | term.loadAddon(fitAddon); 24 | fitAddon.fit(); 25 | 26 | class XtermStdio extends Fd { 27 | /*:: term: Terminal*/ 28 | term: Terminal; 29 | 30 | constructor(term: Terminal) { 31 | super(); 32 | this.term = term; 33 | } 34 | fd_write(data /*: Uint8Array*/) /*: {ret: number, nwritten: number}*/ { 35 | console.log(data); 36 | this.term.write(new TextDecoder().decode(data)); 37 | return { ret: 0, nwritten: data.byteLength }; 38 | } 39 | } 40 | 41 | term.writeln("\x1B[93mDownloading\x1B[0m"); 42 | //let wasm = await WebAssembly.compileStreaming(fetch("/rust_out.wasm")); 43 | term.writeln("\x1B[93mInstantiating\x1B[0m"); 44 | 45 | async function load_external_file(path) { 46 | return new File(await (await (await fetch(path)).blob()).arrayBuffer()); 47 | } 48 | 49 | const toMap = (arr: Array<[string, Inode]>) => { 50 | const map = new Map(); 51 | for (const [key, value] of arr) { 52 | map.set(key, value); 53 | } 54 | return map; 55 | }; 56 | 57 | const fds = [ 58 | new PreopenDirectory("/tmp", toMap([])), 59 | new PreopenDirectory( 60 | "/sysroot", 61 | toMap([ 62 | [ 63 | "lib", 64 | new Directory([ 65 | [ 66 | "rustlib", 67 | new Directory([ 68 | ["wasm32-wasi", new Directory([["lib", new Directory([])]])], 69 | [ 70 | "x86_64-unknown-linux-gnu", 71 | new Directory([ 72 | [ 73 | "lib", 74 | new Directory( 75 | await (async () => { 76 | const dir = new Map(); 77 | for (const file of [ 78 | "libaddr2line-b8754aeb03c02354.rlib", 79 | "libadler-05c3545f6cd12159.rlib", 80 | "liballoc-0dab879bc41cd6bd.rlib", 81 | "libcfg_if-c7fd2cef50341546.rlib", 82 | "libcompiler_builtins-a99947d020d809d6.rlib", 83 | "libcore-4b8e8a815d049db3.rlib", 84 | "libgetopts-bbb75529e85d129d.rlib", 85 | "libgimli-598847d27d7a3cbf.rlib", 86 | "libhashbrown-d2ff91fdf93cacb2.rlib", 87 | "liblibc-dc63949c664c3fce.rlib", 88 | "libmemchr-2d3a423be1a6cb96.rlib", 89 | "libminiz_oxide-b109506a0ccc4c6a.rlib", 90 | "libobject-7b48def7544c748b.rlib", 91 | "libpanic_abort-c93441899b93b849.rlib", 92 | "libpanic_unwind-11d9ba05b60bf694.rlib", 93 | "libproc_macro-1a7f7840bb9983dc.rlib", 94 | "librustc_demangle-59342a335246393d.rlib", 95 | "librustc_std_workspace_alloc-552b185085090ff6.rlib", 96 | "librustc_std_workspace_core-5d8a121daa7eeaa9.rlib", 97 | "librustc_std_workspace_std-97f43841ce452f7d.rlib", 98 | "libstd-bdedb7706a556da2.rlib", 99 | "libstd-bdedb7706a556da2.so", 100 | "libstd_detect-cca21eebc4281add.rlib", 101 | "libsysroot-f654e185be3ffebd.rlib", 102 | "libtest-f06fa3fbc201c558.rlib", 103 | "libunicode_width-19a0dcd589fa0877.rlib", 104 | "libunwind-747b693f90af9445.rlib", 105 | ]) { 106 | dir.set( 107 | file, 108 | await load_external_file( 109 | `../wasi_multi_threads_rustc/rust_wasm/rustc_cranelift/dist/lib/rustlib/x86_64-unknown-linux-gnu/lib/${file}`, 110 | ), 111 | ); 112 | } 113 | return dir; 114 | })(), 115 | ), 116 | ], 117 | ]), 118 | ], 119 | ]), 120 | ], 121 | ]), 122 | ], 123 | ]), 124 | ), 125 | new PreopenDirectory( 126 | "/", 127 | toMap([ 128 | [ 129 | "hello.rs", 130 | new File( 131 | new TextEncoder().encode(`fn main() { println!("Hello World!"); }`), 132 | ), 133 | ], 134 | ]), 135 | ), 136 | ]; 137 | 138 | const wasi_farm = new WASIFarm( 139 | new XtermStdio(term), 140 | new XtermStdio(term), 141 | new XtermStdio(term), 142 | fds, 143 | { 144 | allocator_size: 1024 * 1024 * 1024, 145 | }, 146 | ); 147 | 148 | const wasi_ref = await wasi_farm.get_ref(); 149 | 150 | const worker = new Worker("./worker.ts", { type: "module" }); 151 | worker.postMessage({ wasi_ref }); 152 | 153 | console.log("worker created", worker); 154 | 155 | worker.onmessage = (event) => { 156 | const data = event.data; 157 | const { term: write } = data; 158 | 159 | term.writeln(write); 160 | }; 161 | 162 | const downloadsElement = document.getElementById("downloads"); 163 | 164 | if (!downloadsElement) { 165 | throw new Error("Downloads element not found"); 166 | } 167 | 168 | let content: Inode | undefined; 169 | while (true) { 170 | content = fds[2].dir.contents.get( 171 | "hello.hello.65c991d23c885d45-cgu.0.rcgu.o", 172 | ); 173 | if (content) { 174 | break; 175 | } 176 | await new Promise((resolve) => setTimeout(resolve, 1000)); 177 | } 178 | await new Promise((resolve) => setTimeout(resolve, 1000)); 179 | downloadsElement.innerHTML += `
Download object`; 182 | let content2: Inode | undefined; 183 | while (true) { 184 | content2 = fds[2].dir.contents.get("hello.allocator_shim.rcgu.o"); 185 | if (content2) { 186 | break; 187 | } 188 | await new Promise((resolve) => setTimeout(resolve, 1000)); 189 | } 190 | await new Promise((resolve) => setTimeout(resolve, 1000)); 191 | downloadsElement.innerHTML += `
Download allocator shim`; 196 | -------------------------------------------------------------------------------- /examples/rustc.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 22 | 23 | 24 |
25 |
26 |

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: 64 | max_share_arrays_memory: sl.share_arrays_memory!.byteLength, 65 | // biome-ignore lint/style/noNonNullAssertion: 66 | share_arrays_memory: sl.share_arrays_memory!, 67 | }; 68 | } 69 | 70 | private async async_lock(): Promise { 71 | const view = new Int32Array(this.share_arrays_memory); 72 | // eslint-disable-next-line no-constant-condition 73 | while (true) { 74 | let lock: "not-equal" | "timed-out" | "ok"; 75 | const { value } = Atomics.waitAsync(view, 0, 1); 76 | if (value instanceof Promise) { 77 | lock = await value; 78 | } else { 79 | lock = value; 80 | } 81 | if (lock === "timed-out") { 82 | throw new Error("timed-out lock"); 83 | } 84 | const old = Atomics.compareExchange(view, 0, 0, 1); 85 | if (old !== 0) { 86 | continue; 87 | } 88 | break; 89 | } 90 | } 91 | 92 | private block_lock(): void { 93 | // eslint-disable-next-line no-constant-condition 94 | while (true) { 95 | const view = new Int32Array(this.share_arrays_memory); 96 | const lock = Atomics.wait(view, 0, 1); 97 | if (lock === "timed-out") { 98 | throw new Error("timed-out lock"); 99 | } 100 | const old = Atomics.compareExchange(view, 0, 0, 1); 101 | if (old !== 0) { 102 | continue; 103 | } 104 | break; 105 | } 106 | } 107 | 108 | private release_lock(): void { 109 | const view = new Int32Array(this.share_arrays_memory); 110 | Atomics.store(view, 0, 0); 111 | Atomics.notify(view, 0, 1); 112 | } 113 | 114 | protected async async_send( 115 | targets: Array, 116 | data: Uint32Array, 117 | ): Promise { 118 | await this.async_lock(); 119 | 120 | const view = new Int32Array(this.share_arrays_memory); 121 | const used_len = Atomics.load(view, 2); 122 | const data_len = data.byteLength; 123 | if (data_len !== this.data_size) { 124 | throw new Error(`invalid data size: ${data_len} !== ${this.data_size}`); 125 | } 126 | const new_used_len = used_len + data_len + 8 + targets.length * 4; 127 | if (new_used_len > this.share_arrays_memory.byteLength) { 128 | throw new Error("over memory"); 129 | } 130 | 131 | Atomics.store(view, 2, new_used_len); 132 | 133 | const header = new Int32Array(this.share_arrays_memory, used_len); 134 | header[0] = targets.length; 135 | header[1] = targets.length; 136 | header.set(targets, 2); 137 | 138 | const data_view = new Uint32Array( 139 | this.share_arrays_memory, 140 | used_len + 8 + targets.length * 4, 141 | ); 142 | data_view.set(data); 143 | 144 | // console.log("async_send send", targets, data); 145 | 146 | Atomics.add(view, 1, 1); 147 | 148 | this.release_lock(); 149 | } 150 | 151 | protected get_data(id: number): Array | undefined { 152 | const view = new Int32Array(this.share_arrays_memory); 153 | const data_num_tmp = Atomics.load(view, 1); 154 | if (data_num_tmp === 0) { 155 | return undefined; 156 | } 157 | 158 | this.block_lock(); 159 | 160 | const data_num = Atomics.load(view, 1); 161 | 162 | const return_data: Array = []; 163 | 164 | let offset = 12; 165 | // console.log("data_num", data_num); 166 | for (let i = 0; i < data_num; i++) { 167 | // console.log("this.share_arrays_memory", this.share_arrays_memory); 168 | const header = new Int32Array(this.share_arrays_memory, offset); 169 | const target_num = header[1]; 170 | const targets = new Int32Array( 171 | this.share_arrays_memory, 172 | offset + 8, 173 | target_num, 174 | ); 175 | const data_len = this.data_size; 176 | if (targets.includes(id)) { 177 | const data = new Uint32Array( 178 | this.share_arrays_memory, 179 | offset + 8 + target_num * 4, 180 | data_len / 4, 181 | ); 182 | 183 | // なぜかわからないが、上では正常に動作せず、以下のようにすると動作する 184 | // return_data.push(new Uint32Array(data)); 185 | return_data.push(new Uint32Array([...data])); 186 | 187 | const target_index = targets.indexOf(id); 188 | Atomics.store(targets, target_index, -1); 189 | const old_left_targets_num = Atomics.sub(header, 0, 1); 190 | if (old_left_targets_num === 1) { 191 | // rm data 192 | Atomics.sub(view, 1, 1); 193 | const used_len = Atomics.load(view, 2); 194 | const new_used_len = used_len - data_len - 8 - target_num * 4; 195 | Atomics.store(view, 2, new_used_len); 196 | const next_data_offset = offset + data_len + 8 + target_num * 4; 197 | const next_tail = new Int32Array( 198 | this.share_arrays_memory, 199 | next_data_offset, 200 | ); 201 | const now_tail = new Int32Array(this.share_arrays_memory, offset); 202 | now_tail.set(next_tail); 203 | // console.log("new_used_len", new_used_len); 204 | } else { 205 | offset += data_len + 8 + target_num * 4; 206 | } 207 | } else { 208 | offset += data_len + 8 + target_num * 4; 209 | } 210 | // console.log("offset", offset); 211 | } 212 | 213 | if (offset !== Atomics.load(view, 2)) { 214 | throw new Error(`invalid offset: ${offset} !== ${Atomics.load(view, 2)}`); 215 | } 216 | 217 | this.release_lock(); 218 | 219 | // console.log("get_data get: return_data", return_data); 220 | 221 | return return_data; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /threads/src/shared_array_buffer/allocator.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | // import { debug } from "../../debug.js"; 3 | // import "../polyfill.js"; 4 | 5 | export type AllocatorUseArrayBufferObject = { 6 | share_arrays_memory: SharedArrayBuffer; 7 | }; 8 | 9 | export class AllocatorUseArrayBuffer { 10 | // Pass a !Sized type 11 | // The first 4 bytes are for a lock value: i32 12 | // The next 4 bytes are for the current number of arrays: m: i32 13 | // The next 4 bytes are for the length of the occupied space in share_arrays_memory: n: i32 14 | // Once it is no longer busy, it should become empty immediately, so reset only when it is empty. 15 | // Even if it becomes too long, it should be fine due to the browser's virtualization. 16 | // Using an algorithm even simpler than First-Fit 17 | // SharedArrayBuffer.grow is supported by all major browsers except Android WebView, 18 | // which does not support SharedArrayBuffer in the first place, 19 | // but es2024 and the type system does not support it, 20 | // so the size is fixed from the beginning 21 | 22 | // share_arrays_memory: SharedArrayBuffer = new SharedArrayBuffer(12, { 23 | // // 10MB 24 | // maxByteLength: 10 * 1024 * 1024, 25 | // }); 26 | 27 | // Even if 100MB is allocated, due to browser virtualization, 28 | // the memory should not actually be used until it is needed. 29 | share_arrays_memory: SharedArrayBuffer; 30 | 31 | // When adding data, use Atomics.wait to wait until the first 4 bytes become 0. 32 | // After that, use Atomics.compareExchange to set the first 4 bytes to 1. 33 | // Then, use Atomics.add to increment the next 4 bytes by 1. 34 | // If the return value is 0, proceed to *1. 35 | // If the return value is 1, use Atomics.wait to wait until the first 4 bytes become 0. 36 | // *1: Increment the second by 1 using Atomics.add. If the return value is 0, reset it. 37 | // Add the data. Extend if there is not enough space. 38 | // To release, just decrement by 1 using Atomics.sub. 39 | 40 | // Since postMessage makes the class an object, 41 | // it must be able to receive and assign a SharedArrayBuffer. 42 | constructor( 43 | share_arrays_memory: SharedArrayBuffer = new SharedArrayBuffer( 44 | 10 * 1024 * 1024, 45 | ), 46 | ) { 47 | this.share_arrays_memory = share_arrays_memory; 48 | const view = new Int32Array(this.share_arrays_memory); 49 | Atomics.store(view, 0, 0); 50 | Atomics.store(view, 1, 0); 51 | Atomics.store(view, 2, 12); 52 | } 53 | 54 | // Since postMessage converts classes to objects, 55 | // it must be able to convert objects to classes. 56 | static init_self(sl: AllocatorUseArrayBufferObject): AllocatorUseArrayBuffer { 57 | return new AllocatorUseArrayBuffer(sl.share_arrays_memory); 58 | } 59 | 60 | // Writes without blocking threads when acquiring locks 61 | async async_write( 62 | data: Uint8Array | Uint32Array, 63 | memory: SharedArrayBuffer, 64 | // ptr, len 65 | // Pass I32Array ret_ptr 66 | ret_ptr: number, 67 | ): Promise<[number, number]> { 68 | const view = new Int32Array(this.share_arrays_memory); 69 | // eslint-disable-next-line no-constant-condition 70 | while (true) { 71 | let lock: "not-equal" | "timed-out" | "ok"; 72 | const { value } = Atomics.waitAsync(view, 0, 1); 73 | if (value instanceof Promise) { 74 | lock = await value; 75 | } else { 76 | lock = value; 77 | } 78 | if (lock === "timed-out") { 79 | throw new Error("timed-out lock"); 80 | } 81 | const old = Atomics.compareExchange(view, 0, 0, 1); 82 | if (old !== 0) { 83 | continue; 84 | } 85 | 86 | const ret = this.write_inner(data, memory, ret_ptr); 87 | 88 | // release lock 89 | Atomics.store(view, 0, 0); 90 | Atomics.notify(view, 0, 1); 91 | 92 | return ret; 93 | } 94 | } 95 | 96 | // Blocking threads for writing when acquiring locks 97 | block_write( 98 | data: Uint8Array | Uint32Array, 99 | memory: SharedArrayBuffer, 100 | // ptr, len 101 | ret_ptr: number, 102 | ): [number, number] { 103 | // eslint-disable-next-line no-constant-condition 104 | while (true) { 105 | const view = new Int32Array(this.share_arrays_memory); 106 | const lock = Atomics.wait(view, 0, 1); 107 | if (lock === "timed-out") { 108 | throw new Error("timed-out lock"); 109 | } 110 | const old = Atomics.compareExchange(view, 0, 0, 1); 111 | if (old !== 0) { 112 | continue; 113 | } 114 | 115 | const ret = this.write_inner(data, memory, ret_ptr); 116 | 117 | // release lock 118 | Atomics.store(view, 0, 0); 119 | Atomics.notify(view, 0, 1); 120 | 121 | return ret; 122 | } 123 | } 124 | 125 | // Function to write after acquiring a lock 126 | write_inner( 127 | data: Uint8Array | Uint32Array, 128 | memory: SharedArrayBuffer, 129 | // ptr, len 130 | ret_ptr: number, 131 | ): [number, number] { 132 | // console.log("data", data); 133 | 134 | const view = new Int32Array(this.share_arrays_memory); 135 | const view8 = new Uint8Array(this.share_arrays_memory); 136 | 137 | // Indicates more users using memory 138 | const old_num = Atomics.add(view, 1, 1); 139 | let share_arrays_memory_kept: number; 140 | if (old_num === 0) { 141 | // Reset because there were no users. 142 | // debug.log("reset allocator"); 143 | share_arrays_memory_kept = Atomics.store(view, 2, 12); 144 | } else { 145 | share_arrays_memory_kept = Atomics.load(view, 2); 146 | } 147 | // console.log("num", Atomics.load(view, 1)); 148 | 149 | const memory_len = this.share_arrays_memory.byteLength; 150 | const len = data.byteLength; 151 | const new_memory_len = share_arrays_memory_kept + len; 152 | if (memory_len < new_memory_len) { 153 | // extend memory 154 | // support from es2024 155 | // this.share_arrays_memory.grow(new_memory_len); 156 | throw new Error( 157 | "size is bigger than memory. \nTODO! fix memory limit. support big size another way.", 158 | ); 159 | } 160 | 161 | let data8: Uint8Array; 162 | if (data instanceof Uint8Array) { 163 | data8 = data; 164 | } else if (data instanceof Uint32Array) { 165 | // data to uint8 166 | const tmp = new ArrayBuffer(data.byteLength); 167 | new Uint32Array(tmp).set(data); 168 | data8 = new Uint8Array(tmp); 169 | } 170 | 171 | // biome-ignore lint/style/noNonNullAssertion: 172 | view8.set(new Uint8Array(data8!), share_arrays_memory_kept); 173 | Atomics.store(view, 2, new_memory_len); 174 | 175 | const memory_view = new Int32Array(memory); 176 | Atomics.store(memory_view, ret_ptr, share_arrays_memory_kept); 177 | Atomics.store(memory_view, ret_ptr + 1, len); 178 | 179 | // console.log("allocator: allocate", share_arrays_memory_kept, len); 180 | 181 | return [share_arrays_memory_kept, len]; 182 | } 183 | 184 | // free allocated memory 185 | free(_pointer: number, _len: number) { 186 | Atomics.sub(new Int32Array(this.share_arrays_memory), 1, 1); 187 | 188 | // console.log("allocator: free", pointer, len); 189 | } 190 | 191 | // get memory from pointer and length 192 | get_memory(ptr: number, len: number): ArrayBuffer { 193 | const data = new ArrayBuffer(len); 194 | const view = new Uint8Array(data); 195 | view.set(new Uint8Array(this.share_arrays_memory).slice(ptr, ptr + len)); 196 | return data; 197 | } 198 | 199 | // Write again to the memory before releasing 200 | // Not used because the situation for using it does not exist. 201 | use_defined_memory(ptr: number, len: number, data: ArrayBufferLike) { 202 | const memory = new Uint8Array(this.share_arrays_memory); 203 | memory.set(new Uint8Array(data).slice(0, len), ptr); 204 | } 205 | 206 | get_object(): AllocatorUseArrayBufferObject { 207 | return { 208 | share_arrays_memory: this.share_arrays_memory, 209 | }; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /threads/architecture/slide2.svg: -------------------------------------------------------------------------------- 1 | Simple allocatormallocfreeIncrement the allocation count.Return the previously allocated memory location.allocatedMemoryLocation+= size;Decrement the allocation countallocationCount--;If the allocation count is 0,reset pointerif (allocationCount=== 0) {allocatedMemoryLocation= 0;}The lock is in place to prevent deadlocks. -------------------------------------------------------------------------------- /threads/src/shared_array_buffer/worker_background/worker_background_ref.ts: -------------------------------------------------------------------------------- 1 | import { AllocatorUseArrayBuffer } from "../allocator.js"; 2 | import type { 3 | WorkerBackgroundRefObject, 4 | WorkerOptions, 5 | } from "./worker_export.js"; 6 | import * as Serializer from "../serialize_error.js"; 7 | 8 | export class WorkerBackgroundRef { 9 | private allocator: AllocatorUseArrayBuffer; 10 | private lock: SharedArrayBuffer; 11 | private signature_input: SharedArrayBuffer; 12 | 13 | constructor( 14 | allocator: AllocatorUseArrayBuffer, 15 | lock: SharedArrayBuffer, 16 | signature_input: SharedArrayBuffer, 17 | ) { 18 | this.allocator = allocator; 19 | this.lock = lock; 20 | this.signature_input = signature_input; 21 | } 22 | 23 | private block_lock_base_func(): void { 24 | const view = new Int32Array(this.lock); 25 | // eslint-disable-next-line no-constant-condition 26 | while (true) { 27 | const lock = Atomics.wait(view, 0, 1); 28 | if (lock === "timed-out") { 29 | throw new Error("timed-out lock"); 30 | } 31 | const old = Atomics.compareExchange(view, 0, 0, 1); 32 | if (old !== 0) { 33 | continue; 34 | } 35 | break; 36 | } 37 | } 38 | 39 | private async async_lock_base_func(): Promise { 40 | const view = new Int32Array(this.lock); 41 | // eslint-disable-next-line no-constant-condition 42 | while (true) { 43 | let value: "timed-out" | "not-equal" | "ok"; 44 | const { value: _value } = Atomics.waitAsync(view, 0, 1); 45 | if (_value instanceof Promise) { 46 | value = await _value; 47 | } else { 48 | value = _value; 49 | } 50 | if (value === "timed-out") { 51 | throw new Error("timed-out lock"); 52 | } 53 | const old = Atomics.compareExchange(view, 0, 0, 1); 54 | if (old !== 0) { 55 | continue; 56 | } 57 | break; 58 | } 59 | } 60 | 61 | private call_base_func(): void { 62 | const view = new Int32Array(this.lock); 63 | const old = Atomics.exchange(view, 1, 1); 64 | if (old !== 0) { 65 | console.error("what happened?"); 66 | } 67 | Atomics.notify(view, 1, 1); 68 | } 69 | 70 | // wait base_func 71 | private block_wait_base_func(): void { 72 | const view = new Int32Array(this.lock); 73 | const lock = Atomics.wait(view, 1, 1); 74 | if (lock === "timed-out") { 75 | throw new Error("timed-out lock"); 76 | } 77 | } 78 | 79 | private async async_wait_base_func(): Promise { 80 | const view = new Int32Array(this.lock); 81 | let value: "timed-out" | "not-equal" | "ok"; 82 | const { value: _value } = Atomics.waitAsync(view, 1, 1); 83 | if (_value instanceof Promise) { 84 | value = await _value; 85 | } else { 86 | value = _value; 87 | } 88 | if (value === "timed-out") { 89 | throw new Error("timed-out lock"); 90 | } 91 | } 92 | 93 | // release base_func 94 | private release_base_func(): void { 95 | const view = new Int32Array(this.lock); 96 | Atomics.store(view, 0, 0); 97 | Atomics.notify(view, 0, 1); 98 | } 99 | 100 | new_worker( 101 | url: string, 102 | options?: WorkerOptions, 103 | post_obj?: unknown, 104 | ): WorkerRef { 105 | this.block_lock_base_func(); 106 | const view = new Int32Array(this.signature_input); 107 | Atomics.store(view, 0, 1); 108 | const url_buffer = new TextEncoder().encode(url); 109 | this.allocator.block_write(url_buffer, this.signature_input, 1); 110 | Atomics.store(view, 3, options?.type === "module" ? 1 : 0); 111 | const obj_json = JSON.stringify(post_obj); 112 | const obj_buffer = new TextEncoder().encode(obj_json); 113 | this.allocator.block_write(obj_buffer, this.signature_input, 4); 114 | this.call_base_func(); 115 | this.block_wait_base_func(); 116 | 117 | const id = Atomics.load(view, 0); 118 | 119 | this.release_base_func(); 120 | 121 | return new WorkerRef(id); 122 | } 123 | 124 | async async_start_on_thread( 125 | url: string, 126 | options: WorkerOptions | undefined, 127 | post_obj: unknown, 128 | ) { 129 | await this.async_lock_base_func(); 130 | const view = new Int32Array(this.signature_input); 131 | Atomics.store(view, 0, 2); 132 | const url_buffer = new TextEncoder().encode(url); 133 | await this.allocator.async_write(url_buffer, this.signature_input, 1); 134 | Atomics.store(view, 3, options?.type === "module" ? 1 : 0); 135 | const obj_json = JSON.stringify(post_obj); 136 | const obj_buffer = new TextEncoder().encode(obj_json); 137 | await this.allocator.async_write(obj_buffer, this.signature_input, 4); 138 | this.call_base_func(); 139 | await this.async_wait_base_func(); 140 | 141 | this.release_base_func(); 142 | } 143 | 144 | block_start_on_thread( 145 | url: string, 146 | options: WorkerOptions | undefined, 147 | post_obj: unknown, 148 | ) { 149 | this.block_lock_base_func(); 150 | const view = new Int32Array(this.signature_input); 151 | Atomics.store(view, 0, 2); 152 | const url_buffer = new TextEncoder().encode(url); 153 | this.allocator.block_write(url_buffer, this.signature_input, 1); 154 | Atomics.store(view, 3, options?.type === "module" ? 1 : 0); 155 | const obj_json = JSON.stringify(post_obj); 156 | const obj_buffer = new TextEncoder().encode(obj_json); 157 | this.allocator.block_write(obj_buffer, this.signature_input, 4); 158 | this.call_base_func(); 159 | this.block_wait_base_func(); 160 | 161 | this.release_base_func(); 162 | } 163 | 164 | static init_self(sl: WorkerBackgroundRefObject): WorkerBackgroundRef { 165 | return new WorkerBackgroundRef( 166 | AllocatorUseArrayBuffer.init_self(sl.allocator), 167 | sl.lock, 168 | sl.signature_input, 169 | ); 170 | } 171 | 172 | done_notify(code: number): void { 173 | const notify_view = new Int32Array(this.lock, 8); 174 | 175 | // notify done = code 2 176 | const old = Atomics.compareExchange(notify_view, 0, 0, 2); 177 | 178 | if (old !== 0) { 179 | console.error("what happened?"); 180 | 181 | return; 182 | } 183 | 184 | Atomics.store(notify_view, 1, code); 185 | 186 | const num = Atomics.notify(notify_view, 0); 187 | 188 | if (num === 0) { 189 | Atomics.store(notify_view, 0, 0); 190 | } 191 | } 192 | 193 | async async_wait_done_or_error(): Promise { 194 | const notify_view = new Int32Array(this.lock, 8); 195 | 196 | Atomics.store(notify_view, 0, 0); 197 | 198 | let value: "timed-out" | "not-equal" | "ok"; 199 | const { value: _value } = Atomics.waitAsync(notify_view, 0, 0); 200 | if (_value instanceof Promise) { 201 | value = await _value; 202 | } else { 203 | value = _value; 204 | } 205 | 206 | if (value === "timed-out") { 207 | throw new Error("timed-out"); 208 | } 209 | 210 | if (value === "not-equal") { 211 | throw new Error("not-equal"); 212 | } 213 | 214 | const code = Atomics.load(notify_view, 0); 215 | 216 | if (code === 2) { 217 | const old = Atomics.compareExchange(notify_view, 0, 2, 0); 218 | 219 | const code = Atomics.load(notify_view, 1); 220 | 221 | if (old !== 2) { 222 | console.error("what happened?"); 223 | } 224 | 225 | return code; 226 | } 227 | 228 | if (code !== 1) { 229 | throw new Error("unknown code"); 230 | } 231 | 232 | // get error 233 | const ptr = Atomics.load(notify_view, 1); 234 | const size = Atomics.load(notify_view, 2); 235 | const error_buffer = this.allocator.get_memory(ptr, size); 236 | const error_txt = new TextDecoder().decode(error_buffer); 237 | const error_serialized = JSON.parse( 238 | error_txt, 239 | ) as Serializer.SerializedError; 240 | const error = Serializer.deserialize(error_serialized); 241 | 242 | const old = Atomics.compareExchange(notify_view, 0, 1, 0); 243 | 244 | if (old !== 1) { 245 | console.error("what happened?"); 246 | } 247 | 248 | throw error; 249 | } 250 | 251 | block_wait_done_or_error(): number { 252 | const notify_view = new Int32Array(this.lock, 8); 253 | 254 | Atomics.store(notify_view, 0, 0); 255 | 256 | const value = Atomics.wait(notify_view, 0, 0); 257 | 258 | if (value === "timed-out") { 259 | throw new Error("timed-out"); 260 | } 261 | 262 | if (value === "not-equal") { 263 | throw new Error("not-equal"); 264 | } 265 | 266 | const code = Atomics.load(notify_view, 0); 267 | 268 | if (code === 2) { 269 | const old = Atomics.compareExchange(notify_view, 0, 2, 0); 270 | 271 | const code = Atomics.load(notify_view, 1); 272 | 273 | if (old !== 2) { 274 | console.error("what happened?"); 275 | } 276 | 277 | return code; 278 | } 279 | 280 | if (code !== 1) { 281 | throw new Error("unknown code"); 282 | } 283 | 284 | // get error 285 | const ptr = Atomics.load(notify_view, 1); 286 | const size = Atomics.load(notify_view, 2); 287 | const error_buffer = this.allocator.get_memory(ptr, size); 288 | const error_txt = new TextDecoder().decode(error_buffer); 289 | const error_serialized = JSON.parse( 290 | error_txt, 291 | ) as Serializer.SerializedError; 292 | const error = Serializer.deserialize(error_serialized); 293 | 294 | const old = Atomics.compareExchange(notify_view, 0, 1, 0); 295 | 296 | if (old !== 1) { 297 | console.error("what happened?"); 298 | } 299 | 300 | throw error; 301 | } 302 | } 303 | 304 | export class WorkerRef { 305 | private id: number; 306 | 307 | constructor(id: number) { 308 | this.id = id; 309 | } 310 | 311 | get_id(): number { 312 | return this.id; 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /threads/examples/wasi_multi_threads_rustc/worker.ts: -------------------------------------------------------------------------------- 1 | import { SharedObjectRef } from "@oligami/shared-object"; 2 | import { WASIFarmAnimal, type WASIFarmRef } from "../../src"; 3 | 4 | let tree: (...args: string[]) => void; 5 | let term: { 6 | writeln: (data) => Promise; 7 | writeUtf8: (data) => Promise; 8 | }; 9 | let rustc: (...args: string[]) => void; 10 | let rustc_with_lld: (...args: string[]) => void; 11 | let clang: (...args: string[]) => void; 12 | let llvm_tools: (...args: string[]) => void; 13 | let wasm_ld: (...args: string[]) => void; 14 | let std_out_keep: { 15 | get: () => Promise; 16 | reset: () => void; 17 | }; 18 | let std_err_keep: { 19 | get: () => Promise; 20 | reset: () => void; 21 | }; 22 | let root_dir: { 23 | get_file: (path_str: string) => Promise; 24 | }; 25 | 26 | const blueText = "\x1b[34m"; 27 | const resetText = "\x1b[0m"; 28 | 29 | self.onmessage = async (e) => { 30 | const { wasi_ref } = e.data; 31 | 32 | const { 33 | promise: depend_rustc_files_promise, 34 | resolve: depend_rustc_files_resolve, 35 | } = Promise.withResolvers<{ wasi_ref: WASIFarmRef }>(); 36 | const { 37 | promise: depend_rustc_with_lld_promise, 38 | resolve: depend_rustc_with_lld_resolve, 39 | } = Promise.withResolvers<{ wasi_ref: WASIFarmRef }>(); 40 | const { 41 | promise: depend_clang_files_promise, 42 | resolve: depend_clang_files_resolve, 43 | } = Promise.withResolvers<{ wasi_ref: WASIFarmRef }>(); 44 | const { promise: tmp_dir_promise, resolve: tmp_dir_resolve } = 45 | Promise.withResolvers<{ wasi_ref: WASIFarmRef }>(); 46 | const { promise: save_stdout_promise, resolve: save_stdout_resolve } = 47 | Promise.withResolvers<{ wasi_ref: WASIFarmRef }>(); 48 | 49 | const depend_rustc_files_manage_worker = new Worker("depend_rustc_files.ts", { 50 | type: "module", 51 | }); 52 | depend_rustc_files_manage_worker.onmessage = (e) => { 53 | depend_rustc_files_resolve(e.data); 54 | }; 55 | 56 | const depend_rustc_with_lld_manage_worker = new Worker( 57 | "depend_rustc_with_lld.ts", 58 | { 59 | type: "module", 60 | }, 61 | ); 62 | depend_rustc_with_lld_manage_worker.onmessage = (e) => { 63 | depend_rustc_with_lld_resolve(e.data); 64 | }; 65 | 66 | const depend_clang_files_manage_worker = new Worker("depend_clang_files.ts", { 67 | type: "module", 68 | }); 69 | depend_clang_files_manage_worker.onmessage = (e) => { 70 | depend_clang_files_resolve(e.data); 71 | }; 72 | 73 | const tmp_dir_manage_worker = new Worker("tmp_dir.ts", { 74 | type: "module", 75 | }); 76 | tmp_dir_manage_worker.onmessage = (e) => { 77 | tmp_dir_resolve(e.data); 78 | }; 79 | 80 | const save_stdout_manage_worker = new Worker("save_stdout.ts", { 81 | type: "module", 82 | }); 83 | save_stdout_manage_worker.onmessage = (e) => { 84 | save_stdout_resolve(e.data); 85 | }; 86 | 87 | const [ 88 | depend_rustc_files, 89 | depend_rustc_with_lld, 90 | depend_clang_files, 91 | tmp_dir, 92 | save_stdout, 93 | ] = await Promise.all([ 94 | depend_rustc_files_promise, 95 | depend_rustc_with_lld_promise, 96 | depend_clang_files_promise, 97 | tmp_dir_promise, 98 | save_stdout_promise, 99 | ]); 100 | const { wasi_ref: wasi_ref_depend_rustc_files } = depend_rustc_files; 101 | const { wasi_ref: wasi_ref_depend_rustc_with_lld_files } = 102 | depend_rustc_with_lld; 103 | const { wasi_ref: wasi_ref_depend_clang_files } = depend_clang_files; 104 | const { wasi_ref: wasi_ref_tmp_dir } = tmp_dir; 105 | const { wasi_ref: wasi_ref_save_stdout } = save_stdout; 106 | 107 | const wasi_refs = [ 108 | wasi_ref_depend_rustc_files, 109 | wasi_ref_depend_rustc_with_lld_files, 110 | wasi_ref_depend_clang_files, 111 | wasi_ref_tmp_dir, 112 | wasi_ref, 113 | ]; 114 | 115 | const { promise: tree_promise, resolve: tree_resolve } = 116 | Promise.withResolvers(); 117 | const { promise: rustc_promise, resolve: rustc_resolve } = 118 | Promise.withResolvers(); 119 | const { promise: rustc_with_lld_promise, resolve: rustc_with_lld_resolve } = 120 | Promise.withResolvers(); 121 | const { promise: clang_promise, resolve: clang_resolve } = 122 | Promise.withResolvers(); 123 | 124 | const tree_worker = new Worker("tree.ts", { 125 | type: "module", 126 | }); 127 | tree_worker.onmessage = (e) => { 128 | console.log("tree onmessage"); 129 | tree_resolve(e.data); 130 | }; 131 | 132 | const rustc_worker = new Worker("rustc.ts", { 133 | type: "module", 134 | }); 135 | rustc_worker.onmessage = (e) => { 136 | console.log("rustc onmessage"); 137 | rustc_resolve(e.data); 138 | }; 139 | 140 | const rustc_with_lld_worker = new Worker("rustc_with_lld.ts", { 141 | type: "module", 142 | }); 143 | rustc_with_lld_worker.onmessage = (e) => { 144 | console.log("rustc_with_lld onmessage"); 145 | rustc_with_lld_resolve(e.data); 146 | }; 147 | 148 | const clang_worker = new Worker("clang.ts", { 149 | type: "module", 150 | }); 151 | clang_worker.onmessage = (e) => { 152 | console.log("clang onmessage"); 153 | clang_resolve(e.data); 154 | }; 155 | 156 | tree_worker.postMessage({ 157 | wasi_refs, 158 | }); 159 | rustc_worker.postMessage({ 160 | wasi_refs: [wasi_ref_save_stdout, ...wasi_refs], 161 | }); 162 | rustc_with_lld_worker.postMessage({ 163 | wasi_refs, 164 | }); 165 | clang_worker.postMessage({ 166 | wasi_refs, 167 | }); 168 | 169 | console.log("Waiting for tree and rustc to finish..."); 170 | 171 | await Promise.all([ 172 | tree_promise, 173 | rustc_promise, 174 | rustc_with_lld_promise, 175 | clang_promise, 176 | ]); 177 | 178 | console.log("Sending run message..."); 179 | 180 | tree = new SharedObjectRef("tree").proxy<(...args: string[]) => void>(); 181 | 182 | term = new SharedObjectRef("term").proxy<{ 183 | writeln: (data) => Promise; 184 | writeUtf8: (data) => Promise; 185 | }>(); 186 | 187 | rustc = new SharedObjectRef("rustc").proxy<(...args: string[]) => void>(); 188 | 189 | rustc_with_lld = new SharedObjectRef("rustc_with_lld").proxy< 190 | (...args: string[]) => void 191 | >(); 192 | 193 | clang = new SharedObjectRef("clang").proxy<(...args: string[]) => void>(); 194 | 195 | llvm_tools = new SharedObjectRef("llvm-tools").proxy< 196 | (...args: string[]) => void 197 | >(); 198 | 199 | wasm_ld = new SharedObjectRef("wasm-ld").proxy<(...args: string[]) => void>(); 200 | 201 | std_out_keep = new SharedObjectRef("std_out_keep").proxy<{ 202 | get: () => Promise; 203 | reset: () => void; 204 | }>(); 205 | 206 | std_err_keep = new SharedObjectRef("std_err_keep").proxy<{ 207 | get: () => Promise; 208 | reset: () => void; 209 | }>(); 210 | 211 | root_dir = new SharedObjectRef("root_dir").proxy<{ 212 | get_file: (path_str: string) => Promise; 213 | }>(); 214 | 215 | // llvm-tools 216 | await term.writeln(`$${blueText} llvm-tools${resetText}`); 217 | await llvm_tools(); 218 | 219 | // clang -h 220 | await term.writeln(`\n$${blueText} clang --help${resetText}`); 221 | await clang("--help"); 222 | 223 | // clang -v 224 | await term.writeln(`\n$${blueText} clang -v${resetText}`); 225 | await clang("-v"); 226 | 227 | // wasm-ld --help 228 | await term.writeln(`\n$${blueText} wasm-ld --help${resetText}`); 229 | await wasm_ld("--help"); 230 | 231 | // wasm-ld -v 232 | await term.writeln(`\n$${blueText} wasm-ld -v${resetText}`); 233 | await wasm_ld("-v"); 234 | 235 | // tree -h 236 | await term.writeln(`\n$${blueText} tree -h${resetText}`); 237 | await tree("-h"); 238 | 239 | // tree / 240 | await term.writeln(`\n$${blueText} tree /${resetText}`); 241 | await tree("/"); 242 | 243 | // rustc -h 244 | await std_out_keep.reset(); 245 | await std_err_keep.reset(); 246 | await term.writeln(`\n$${blueText} rustc -h${resetText}`); 247 | await rustc("-h"); 248 | const rustc_help = await std_out_keep.get(); 249 | const rustc_help_err = await std_err_keep.get(); 250 | console.log(rustc_help); 251 | console.warn(rustc_help_err); 252 | 253 | // rustc /hello.rs --sysroot /sysroot --target wasm32-wasip1-threads -Csave-temps --out-dir /tmp 254 | await term.writeln( 255 | `\n$${blueText} rustc /hello.rs --sysroot /sysroot --target wasm32-wasip1-threads -Csave-temps --out-dir /tmp${resetText}`, 256 | ); 257 | try { 258 | await std_out_keep.reset(); 259 | await std_err_keep.reset(); 260 | await rustc( 261 | "/hello.rs", 262 | "--sysroot", 263 | "/sysroot", 264 | "--target", 265 | "wasm32-wasip1-threads", 266 | "-Csave-temps", 267 | "--out-dir", 268 | "/tmp", 269 | ); 270 | } catch (e) { 271 | console.error(e); 272 | } 273 | const out = await std_out_keep.get(); 274 | const err = await std_err_keep.get(); 275 | console.log(out); 276 | console.warn(err); 277 | 278 | // tree / 279 | await term.writeln(`\n$${blueText} tree /${resetText}`); 280 | await tree("/"); 281 | 282 | // If the glob pattern is used, 283 | // it seems to be passed directly to path_open, 284 | // so it is necessary to specify it carefully. 285 | if (!err.includes("error: could not exec the linker")) { 286 | throw new Error("cannot get lld arguments"); 287 | } 288 | // extract lld arguments line 289 | const lld_args_and_etc = err 290 | .split("\n") 291 | .find((line) => line.includes("rust-lld") && line.includes("note: ")); 292 | if (!lld_args_and_etc) { 293 | throw new Error("cannot get lld arguments"); 294 | } 295 | const lld_args_str = lld_args_and_etc.split('"rust-lld"')[1].trim(); 296 | const lld_args_with_wrap = lld_args_str.split(" "); 297 | let lld_args = lld_args_with_wrap.map((arg) => arg.slice(1, -1)); 298 | // rm -flavor wasm 299 | lld_args = lld_args.filter((arg) => arg !== "-flavor" && arg !== "wasm"); 300 | // rm -Wl 301 | lld_args = lld_args.map((arg) => { 302 | if (arg.includes("-Wl,")) { 303 | return arg.slice(4); 304 | } 305 | return arg; 306 | }); 307 | console.log(lld_args); 308 | 309 | await term.writeln( 310 | `\n$${blueText} wasm-ld ${lld_args.join(" ")}${resetText}`, 311 | ); 312 | try { 313 | await wasm_ld(...lld_args); 314 | } catch (error) { 315 | console.error(error); 316 | const redText = "\x1b[31m"; 317 | const boldText = "\x1b[1m"; 318 | 319 | const message = `${boldText}${redText}Error: ${error.message}${resetText}\n`; 320 | const stack = `${redText}Stack Trace: ${error.stack}${resetText}`; 321 | 322 | await term.writeln(message + stack); 323 | } 324 | 325 | // tree / 326 | await term.writeln(`\n$${blueText} tree /${resetText}`); 327 | await tree("/"); 328 | 329 | // rustc_with_lld /hello.rs --sysroot /sysroot --target wasm32-wasip1 330 | await term.writeln( 331 | `\n$${blueText} rustc_with_lld /hello.rs --sysroot /sysroot-with-lld --target wasm32-wasip1${resetText}`, 332 | ); 333 | try { 334 | await rustc_with_lld( 335 | "/hello.rs", 336 | "--sysroot", 337 | "/sysroot-with-lld", 338 | "--target", 339 | "wasm32-wasip1", 340 | ); 341 | } catch (e) { 342 | console.error(e); 343 | } 344 | 345 | // tree / 346 | await term.writeln(`\n$${blueText} tree /${resetText}`); 347 | await tree("/"); 348 | 349 | // run /hello.wasm 350 | await term.writeln(`\n$${blueText} run /hello.wasm${resetText}`); 351 | const created_wasm_buffer = await root_dir.get_file("hello.wasm"); 352 | const created_wasm = await WebAssembly.compile(created_wasm_buffer); 353 | const wasi = new WASIFarmAnimal( 354 | wasi_refs, 355 | [], // args 356 | [], // env 357 | ); 358 | 359 | // Memory is rewritten at this time. 360 | const inst = (await WebAssembly.instantiate(created_wasm, { 361 | wasi_snapshot_preview1: wasi.wasiImport, 362 | })) as unknown as { 363 | exports: { memory: WebAssembly.Memory; _start: () => unknown }; 364 | }; 365 | 366 | wasi.start(inst); 367 | 368 | // all done 369 | await term.writeln(`\n$${blueText} All done!${resetText}`); 370 | }; 371 | -------------------------------------------------------------------------------- /src/wasi_defs.ts: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/bytecodealliance/wasi/tree/d3c7a34193cb33d994b11104b22d234530232b5f 2 | 3 | export const FD_STDIN = 0; 4 | export const FD_STDOUT = 1; 5 | export const FD_STDERR = 2; 6 | 7 | export const CLOCKID_REALTIME = 0; 8 | export const CLOCKID_MONOTONIC = 1; 9 | export const CLOCKID_PROCESS_CPUTIME_ID = 2; 10 | export const CLOCKID_THREAD_CPUTIME_ID = 3; 11 | 12 | export const ERRNO_SUCCESS = 0; 13 | export const ERRNO_2BIG = 1; 14 | export const ERRNO_ACCES = 2; 15 | export const ERRNO_ADDRINUSE = 3; 16 | export const ERRNO_ADDRNOTAVAIL = 4; 17 | export const ERRNO_AFNOSUPPORT = 5; 18 | export const ERRNO_AGAIN = 6; 19 | export const ERRNO_ALREADY = 7; 20 | export const ERRNO_BADF = 8; 21 | export const ERRNO_BADMSG = 9; 22 | export const ERRNO_BUSY = 10; 23 | export const ERRNO_CANCELED = 11; 24 | export const ERRNO_CHILD = 12; 25 | export const ERRNO_CONNABORTED = 13; 26 | export const ERRNO_CONNREFUSED = 14; 27 | export const ERRNO_CONNRESET = 15; 28 | export const ERRNO_DEADLK = 16; 29 | export const ERRNO_DESTADDRREQ = 17; 30 | export const ERRNO_DOM = 18; 31 | export const ERRNO_DQUOT = 19; 32 | export const ERRNO_EXIST = 20; 33 | export const ERRNO_FAULT = 21; 34 | export const ERRNO_FBIG = 22; 35 | export const ERRNO_HOSTUNREACH = 23; 36 | export const ERRNO_IDRM = 24; 37 | export const ERRNO_ILSEQ = 25; 38 | export const ERRNO_INPROGRESS = 26; 39 | export const ERRNO_INTR = 27; 40 | export const ERRNO_INVAL = 28; 41 | export const ERRNO_IO = 29; 42 | export const ERRNO_ISCONN = 30; 43 | export const ERRNO_ISDIR = 31; 44 | export const ERRNO_LOOP = 32; 45 | export const ERRNO_MFILE = 33; 46 | export const ERRNO_MLINK = 34; 47 | export const ERRNO_MSGSIZE = 35; 48 | export const ERRNO_MULTIHOP = 36; 49 | export const ERRNO_NAMETOOLONG = 37; 50 | export const ERRNO_NETDOWN = 38; 51 | export const ERRNO_NETRESET = 39; 52 | export const ERRNO_NETUNREACH = 40; 53 | export const ERRNO_NFILE = 41; 54 | export const ERRNO_NOBUFS = 42; 55 | export const ERRNO_NODEV = 43; 56 | export const ERRNO_NOENT = 44; 57 | export const ERRNO_NOEXEC = 45; 58 | export const ERRNO_NOLCK = 46; 59 | export const ERRNO_NOLINK = 47; 60 | export const ERRNO_NOMEM = 48; 61 | export const ERRNO_NOMSG = 49; 62 | export const ERRNO_NOPROTOOPT = 50; 63 | export const ERRNO_NOSPC = 51; 64 | export const ERRNO_NOSYS = 52; 65 | export const ERRNO_NOTCONN = 53; 66 | export const ERRNO_NOTDIR = 54; 67 | export const ERRNO_NOTEMPTY = 55; 68 | export const ERRNO_NOTRECOVERABLE = 56; 69 | export const ERRNO_NOTSOCK = 57; 70 | export const ERRNO_NOTSUP = 58; 71 | export const ERRNO_NOTTY = 59; 72 | export const ERRNO_NXIO = 60; 73 | export const ERRNO_OVERFLOW = 61; 74 | export const ERRNO_OWNERDEAD = 62; 75 | export const ERRNO_PERM = 63; 76 | export const ERRNO_PIPE = 64; 77 | export const ERRNO_PROTO = 65; 78 | export const ERRNO_PROTONOSUPPORT = 66; 79 | export const ERRNO_PROTOTYPE = 67; 80 | export const ERRNO_RANGE = 68; 81 | export const ERRNO_ROFS = 69; 82 | export const ERRNO_SPIPE = 70; 83 | export const ERRNO_SRCH = 71; 84 | export const ERRNO_STALE = 72; 85 | export const ERRNO_TIMEDOUT = 73; 86 | export const ERRNO_TXTBSY = 74; 87 | export const ERRNO_XDEV = 75; 88 | export const ERRNO_NOTCAPABLE = 76; 89 | 90 | export const RIGHTS_FD_DATASYNC = 1 << 0; 91 | export const RIGHTS_FD_READ = 1 << 1; 92 | export const RIGHTS_FD_SEEK = 1 << 2; 93 | export const RIGHTS_FD_FDSTAT_SET_FLAGS = 1 << 3; 94 | export const RIGHTS_FD_SYNC = 1 << 4; 95 | export const RIGHTS_FD_TELL = 1 << 5; 96 | export const RIGHTS_FD_WRITE = 1 << 6; 97 | export const RIGHTS_FD_ADVISE = 1 << 7; 98 | export const RIGHTS_FD_ALLOCATE = 1 << 8; 99 | export const RIGHTS_PATH_CREATE_DIRECTORY = 1 << 9; 100 | export const RIGHTS_PATH_CREATE_FILE = 1 << 10; 101 | export const RIGHTS_PATH_LINK_SOURCE = 1 << 11; 102 | export const RIGHTS_PATH_LINK_TARGET = 1 << 12; 103 | export const RIGHTS_PATH_OPEN = 1 << 13; 104 | export const RIGHTS_FD_READDIR = 1 << 14; 105 | export const RIGHTS_PATH_READLINK = 1 << 15; 106 | export const RIGHTS_PATH_RENAME_SOURCE = 1 << 16; 107 | export const RIGHTS_PATH_RENAME_TARGET = 1 << 17; 108 | export const RIGHTS_PATH_FILESTAT_GET = 1 << 18; 109 | export const RIGHTS_PATH_FILESTAT_SET_SIZE = 1 << 19; 110 | export const RIGHTS_PATH_FILESTAT_SET_TIMES = 1 << 20; 111 | export const RIGHTS_FD_FILESTAT_GET = 1 << 21; 112 | export const RIGHTS_FD_FILESTAT_SET_SIZE = 1 << 22; 113 | export const RIGHTS_FD_FILESTAT_SET_TIMES = 1 << 23; 114 | export const RIGHTS_PATH_SYMLINK = 1 << 24; 115 | export const RIGHTS_PATH_REMOVE_DIRECTORY = 1 << 25; 116 | export const RIGHTS_PATH_UNLINK_FILE = 1 << 26; 117 | export const RIGHTS_POLL_FD_READWRITE = 1 << 27; 118 | export const RIGHTS_SOCK_SHUTDOWN = 1 << 28; 119 | 120 | export class Iovec { 121 | //@ts-ignore strictPropertyInitialization 122 | buf: number; 123 | //@ts-ignore strictPropertyInitialization 124 | buf_len: number; 125 | 126 | static read_bytes(view: DataView, ptr: number): Iovec { 127 | const iovec = new Iovec(); 128 | iovec.buf = view.getUint32(ptr, true); 129 | iovec.buf_len = view.getUint32(ptr + 4, true); 130 | return iovec; 131 | } 132 | 133 | static read_bytes_array( 134 | view: DataView, 135 | ptr: number, 136 | len: number, 137 | ): Array { 138 | const iovecs = []; 139 | for (let i = 0; i < len; i++) { 140 | iovecs.push(Iovec.read_bytes(view, ptr + 8 * i)); 141 | } 142 | return iovecs; 143 | } 144 | } 145 | 146 | export class Ciovec { 147 | //@ts-ignore strictPropertyInitialization 148 | buf: number; 149 | //@ts-ignore strictPropertyInitialization 150 | buf_len: number; 151 | 152 | static read_bytes(view: DataView, ptr: number): Ciovec { 153 | const iovec = new Ciovec(); 154 | iovec.buf = view.getUint32(ptr, true); 155 | iovec.buf_len = view.getUint32(ptr + 4, true); 156 | return iovec; 157 | } 158 | 159 | static read_bytes_array( 160 | view: DataView, 161 | ptr: number, 162 | len: number, 163 | ): Array { 164 | const iovecs = []; 165 | for (let i = 0; i < len; i++) { 166 | iovecs.push(Ciovec.read_bytes(view, ptr + 8 * i)); 167 | } 168 | return iovecs; 169 | } 170 | } 171 | 172 | export const WHENCE_SET = 0; 173 | export const WHENCE_CUR = 1; 174 | export const WHENCE_END = 2; 175 | 176 | export const FILETYPE_UNKNOWN = 0; 177 | export const FILETYPE_BLOCK_DEVICE = 1; 178 | export const FILETYPE_CHARACTER_DEVICE = 2; 179 | export const FILETYPE_DIRECTORY = 3; 180 | export const FILETYPE_REGULAR_FILE = 4; 181 | export const FILETYPE_SOCKET_DGRAM = 5; 182 | export const FILETYPE_SOCKET_STREAM = 6; 183 | export const FILETYPE_SYMBOLIC_LINK = 7; 184 | 185 | export class Dirent { 186 | d_next: bigint; 187 | d_ino: bigint; 188 | d_namlen: number; 189 | d_type: number; 190 | dir_name: Uint8Array; 191 | 192 | constructor(next_cookie: bigint, d_ino: bigint, name: string, type: number) { 193 | const encoded_name = new TextEncoder().encode(name); 194 | 195 | this.d_next = next_cookie; 196 | this.d_ino = d_ino; 197 | this.d_namlen = encoded_name.byteLength; 198 | this.d_type = type; 199 | this.dir_name = encoded_name; 200 | } 201 | 202 | head_length(): number { 203 | return 24; 204 | } 205 | 206 | name_length(): number { 207 | return this.dir_name.byteLength; 208 | } 209 | 210 | write_head_bytes(view: DataView, ptr: number) { 211 | view.setBigUint64(ptr, this.d_next, true); 212 | view.setBigUint64(ptr + 8, this.d_ino, true); 213 | view.setUint32(ptr + 16, this.dir_name.length, true); // d_namlen 214 | view.setUint8(ptr + 20, this.d_type); 215 | } 216 | 217 | write_name_bytes(view8: Uint8Array, ptr: number, buf_len: number) { 218 | view8.set( 219 | this.dir_name.slice(0, Math.min(this.dir_name.byteLength, buf_len)), 220 | ptr, 221 | ); 222 | } 223 | } 224 | 225 | export const ADVICE_NORMAL = 0; 226 | export const ADVICE_SEQUENTIAL = 1; 227 | export const ADVICE_RANDOM = 2; 228 | export const ADVICE_WILLNEED = 3; 229 | export const ADVICE_DONTNEED = 4; 230 | export const ADVICE_NOREUSE = 5; 231 | 232 | export const FDFLAGS_APPEND = 1 << 0; 233 | export const FDFLAGS_DSYNC = 1 << 1; 234 | export const FDFLAGS_NONBLOCK = 1 << 2; 235 | export const FDFLAGS_RSYNC = 1 << 3; 236 | export const FDFLAGS_SYNC = 1 << 4; 237 | 238 | export class Fdstat { 239 | fs_filetype: number; 240 | fs_flags: number; 241 | fs_rights_base: bigint = 0n; 242 | fs_rights_inherited: bigint = 0n; 243 | 244 | constructor(filetype: number, flags: number) { 245 | this.fs_filetype = filetype; 246 | this.fs_flags = flags; 247 | } 248 | 249 | write_bytes(view: DataView, ptr: number) { 250 | view.setUint8(ptr, this.fs_filetype); 251 | view.setUint16(ptr + 2, this.fs_flags, true); 252 | view.setBigUint64(ptr + 8, this.fs_rights_base, true); 253 | view.setBigUint64(ptr + 16, this.fs_rights_inherited, true); 254 | } 255 | } 256 | 257 | export const FSTFLAGS_ATIM = 1 << 0; 258 | export const FSTFLAGS_ATIM_NOW = 1 << 1; 259 | export const FSTFLAGS_MTIM = 1 << 2; 260 | export const FSTFLAGS_MTIM_NOW = 1 << 3; 261 | 262 | export const OFLAGS_CREAT = 1 << 0; 263 | export const OFLAGS_DIRECTORY = 1 << 1; 264 | export const OFLAGS_EXCL = 1 << 2; 265 | export const OFLAGS_TRUNC = 1 << 3; 266 | 267 | export class Filestat { 268 | dev: bigint = 0n; 269 | ino: bigint; 270 | filetype: number; 271 | nlink: bigint = 0n; 272 | size: bigint; 273 | atim: bigint = 0n; 274 | mtim: bigint = 0n; 275 | ctim: bigint = 0n; 276 | 277 | constructor(ino: bigint, filetype: number, size: bigint) { 278 | this.ino = ino; 279 | this.filetype = filetype; 280 | this.size = size; 281 | } 282 | 283 | write_bytes(view: DataView, ptr: number) { 284 | view.setBigUint64(ptr, this.dev, true); 285 | view.setBigUint64(ptr + 8, this.ino, true); 286 | view.setUint8(ptr + 16, this.filetype); 287 | view.setBigUint64(ptr + 24, this.nlink, true); 288 | view.setBigUint64(ptr + 32, this.size, true); 289 | view.setBigUint64(ptr + 38, this.atim, true); 290 | view.setBigUint64(ptr + 46, this.mtim, true); 291 | view.setBigUint64(ptr + 52, this.ctim, true); 292 | } 293 | } 294 | 295 | export const EVENTTYPE_CLOCK = 0; 296 | export const EVENTTYPE_FD_READ = 1; 297 | export const EVENTTYPE_FD_WRITE = 2; 298 | 299 | export const EVENTRWFLAGS_FD_READWRITE_HANGUP = 1 << 0; 300 | 301 | export const SUBCLOCKFLAGS_SUBSCRIPTION_CLOCK_ABSTIME = 1 << 0; 302 | 303 | export const SIGNAL_NONE = 0; 304 | export const SIGNAL_HUP = 1; 305 | export const SIGNAL_INT = 2; 306 | export const SIGNAL_QUIT = 3; 307 | export const SIGNAL_ILL = 4; 308 | export const SIGNAL_TRAP = 5; 309 | export const SIGNAL_ABRT = 6; 310 | export const SIGNAL_BUS = 7; 311 | export const SIGNAL_FPE = 8; 312 | export const SIGNAL_KILL = 9; 313 | export const SIGNAL_USR1 = 10; 314 | export const SIGNAL_SEGV = 11; 315 | export const SIGNAL_USR2 = 12; 316 | export const SIGNAL_PIPE = 13; 317 | export const SIGNAL_ALRM = 14; 318 | export const SIGNAL_TERM = 15; 319 | export const SIGNAL_CHLD = 16; 320 | export const SIGNAL_CONT = 17; 321 | export const SIGNAL_STOP = 18; 322 | export const SIGNAL_TSTP = 19; 323 | export const SIGNAL_TTIN = 20; 324 | export const SIGNAL_TTOU = 21; 325 | export const SIGNAL_URG = 22; 326 | export const SIGNAL_XCPU = 23; 327 | export const SIGNAL_XFSZ = 24; 328 | export const SIGNAL_VTALRM = 25; 329 | export const SIGNAL_PROF = 26; 330 | export const SIGNAL_WINCH = 27; 331 | export const SIGNAL_POLL = 28; 332 | export const SIGNAL_PWR = 29; 333 | export const SIGNAL_SYS = 30; 334 | 335 | export const RIFLAGS_RECV_PEEK = 1 << 0; 336 | export const RIFLAGS_RECV_WAITALL = 1 << 1; 337 | 338 | export const ROFLAGS_RECV_DATA_TRUNCATED = 1 << 0; 339 | 340 | export const SDFLAGS_RD = 1 << 0; 341 | export const SDFLAGS_WR = 1 << 1; 342 | 343 | export const PREOPENTYPE_DIR = 0; 344 | 345 | export class PrestatDir { 346 | pr_name: Uint8Array; 347 | 348 | constructor(name: string) { 349 | this.pr_name = new TextEncoder().encode(name); 350 | } 351 | 352 | write_bytes(view: DataView, ptr: number) { 353 | view.setUint32(ptr, this.pr_name.byteLength, true); 354 | } 355 | } 356 | 357 | export class Prestat { 358 | //@ts-ignore strictPropertyInitialization 359 | tag: number; 360 | //@ts-ignore strictPropertyInitialization 361 | inner: PrestatDir; 362 | 363 | static dir(name: string): Prestat { 364 | const prestat = new Prestat(); 365 | prestat.tag = PREOPENTYPE_DIR; 366 | prestat.inner = new PrestatDir(name); 367 | return prestat; 368 | } 369 | 370 | write_bytes(view: DataView, ptr: number) { 371 | view.setUint32(ptr, this.tag, true); 372 | this.inner.write_bytes(view, ptr + 4); 373 | } 374 | } 375 | --------------------------------------------------------------------------------