├── examples ├── languages │ ├── swift │ │ └── main.swift │ ├── rust │ │ └── main.rs │ └── c │ │ └── main.c ├── package.json ├── main.js └── Makefile ├── .npmignore ├── .gitignore ├── .gitmodules ├── tsconfig.esm.json ├── tsconfig.cjs.json ├── jest.config.js ├── tsconfig.base.json ├── .github └── workflows │ └── test.yml ├── src ├── features │ ├── proc.ts │ ├── random.ts │ ├── tracing.ts │ ├── all.ts │ ├── args.ts │ ├── environ.ts │ ├── clock.ts │ └── fd.ts ├── options.ts ├── index.ts └── abi.ts ├── package.json ├── test ├── fd.test.mjs ├── wasi.skip.json └── wasi.test.mjs └── README.md /examples/languages/swift/main.swift: -------------------------------------------------------------------------------- 1 | print(CommandLine.arguments) 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /examples 2 | /third_party 3 | /.github 4 | /test 5 | /src 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /lib 3 | /examples/build 4 | /examples/package-lock.json 5 | -------------------------------------------------------------------------------- /examples/languages/rust/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "dependencies": { 4 | "uwasi": "../" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/languages/c/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(void) { 4 | printf("Hello, World!\n"); 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "third_party/wasi-testsuite"] 2 | path = third_party/wasi-testsuite 3 | url = https://github.com/WebAssembly/wasi-testsuite 4 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "outDir": "./lib/esm" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "esModuleInterop": true, 6 | "outDir": "./lib/cjs" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: "node", 3 | preset: "ts-jest", 4 | globals: { 5 | "ts-jest": { 6 | tsconfig: "tsconfig.cjs.json" 7 | } 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "rootDir": "src", 5 | "strict": true, 6 | "target": "es2017", 7 | "lib": ["es2020", "DOM"], 8 | "skipLibCheck": true 9 | }, 10 | "include": ["src/**/*"], 11 | "exclude": ["node_modules"] 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | submodules: true 17 | 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: 22.14.0 21 | 22 | - run: npm install 23 | - run: npm test 24 | 25 | -------------------------------------------------------------------------------- /examples/main.js: -------------------------------------------------------------------------------- 1 | import { WASI, useAll, useTrace } from "uwasi"; 2 | import fs from "node:fs/promises"; 3 | 4 | async function main() { 5 | const wasi = new WASI({ 6 | args: process.argv.slice(2), 7 | features: [useTrace([useAll()])], 8 | }); 9 | const bytes = await fs.readFile(process.argv[2]); 10 | const { instance } = await WebAssembly.instantiate(bytes, { 11 | wasi_snapshot_preview1: wasi.wasiImport, 12 | }); 13 | const exitCode = wasi.start(instance); 14 | console.log("exit code:", exitCode); 15 | } 16 | 17 | main() 18 | -------------------------------------------------------------------------------- /src/features/proc.ts: -------------------------------------------------------------------------------- 1 | import { WASIAbi, WASIProcExit } from "../abi.js"; 2 | import { WASIOptions } from "../options.js"; 3 | 4 | /** 5 | * A feature provider that provides `proc_exit` and `proc_raise` by JavaScript's exception. 6 | */ 7 | export function useProc( 8 | options: WASIOptions, 9 | abi: WASIAbi, 10 | memoryView: () => DataView, 11 | ): WebAssembly.ModuleImports { 12 | return { 13 | proc_exit: (code: number) => { 14 | throw new WASIProcExit(code); 15 | }, 16 | proc_raise: (signal: number) => { 17 | // TODO: Implement 18 | return WASIAbi.WASI_ESUCCESS; 19 | }, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /examples/Makefile: -------------------------------------------------------------------------------- 1 | build/swift.wasm: 2 | swiftc -target wasm32-unknown-wasi languages/swift/main.swift -o build/swift.wasm 3 | 4 | build/ruby.wasm: 5 | mkdir -p build/ruby 6 | cd build/ruby && curl -L curl -L https://github.com/ruby/ruby.wasm/releases/download/2022-05-03-a/ruby-head-wasm32-unknown-wasi-minimal.tar.gz | tar xz --strip-components=1 7 | mv build/ruby/usr/local/bin/ruby build/ruby.wasm 8 | rm -rf build/ruby 9 | 10 | build/rust.wasm: 11 | rustc --target wasm32-wasi languages/rust/main.rs -o build/rust.wasm 12 | 13 | build/c.wasm: 14 | clang -target wasm32-wasi languages/c/main.c -o build/c.wasm 15 | 16 | .PHONY: all 17 | all: build/swift.wasm build/ruby.wasm build/rust.wasm 18 | -------------------------------------------------------------------------------- /src/features/random.ts: -------------------------------------------------------------------------------- 1 | import { WASIAbi } from "../abi.js"; 2 | import { WASIFeatureProvider } from "../options.js"; 3 | 4 | /** 5 | * Create a feature provider that provides `random_get` with `crypto` APIs as backend by default. 6 | */ 7 | export function useRandom( 8 | useOptions: { 9 | randomFillSync?: (buffer: Uint8Array) => void; 10 | } = {}, 11 | ): WASIFeatureProvider { 12 | const randomFillSync = useOptions.randomFillSync || crypto.getRandomValues; 13 | return (options, abi, memoryView) => { 14 | return { 15 | random_get: (bufferOffset: number, length: number) => { 16 | const view = memoryView(); 17 | 18 | const buffer = new Uint8Array(view.buffer, bufferOffset, length); 19 | randomFillSync(buffer); 20 | 21 | return WASIAbi.WASI_ESUCCESS; 22 | }, 23 | }; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/features/tracing.ts: -------------------------------------------------------------------------------- 1 | import { WASIFeatureProvider } from "../options.js"; 2 | 3 | export function useTrace(features: WASIFeatureProvider[]): WASIFeatureProvider { 4 | return (options, abi, memoryView) => { 5 | let wasiImport: WebAssembly.ModuleImports = {}; 6 | for (const useFeature of features) { 7 | const imports = useFeature(options, abi, memoryView); 8 | wasiImport = { ...wasiImport, ...imports }; 9 | } 10 | for (const key in wasiImport) { 11 | const original = wasiImport[key]; 12 | if (typeof original !== "function") { 13 | continue; 14 | } 15 | wasiImport[key] = (...args: any[]) => { 16 | const result = original(...args); 17 | console.log(`[uwasi-tracing] ${key}(${args.join(", ")}) => ${result}`); 18 | return result; 19 | }; 20 | } 21 | return wasiImport; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/features/all.ts: -------------------------------------------------------------------------------- 1 | import { WASIAbi } from "../abi.js"; 2 | import { WASIFeatureProvider, WASIOptions } from "../options.js"; 3 | import { useArgs } from "./args.js"; 4 | import { useClock } from "./clock.js"; 5 | import { useEnviron } from "./environ.js"; 6 | import { useMemoryFS } from "./fd.js"; 7 | import { useProc } from "./proc.js"; 8 | import { useRandom } from "./random.js"; 9 | 10 | type Options = Parameters[0] & 11 | Parameters[0]; 12 | 13 | export function useAll(useOptions: Options = {}): WASIFeatureProvider { 14 | return (options: WASIOptions, abi: WASIAbi, memoryView: () => DataView) => { 15 | const features = [ 16 | useMemoryFS(useOptions), 17 | useEnviron, 18 | useArgs, 19 | useClock, 20 | useProc, 21 | useRandom(useOptions), 22 | ]; 23 | return features.reduce((acc, fn) => { 24 | return { ...acc, ...fn(options, abi, memoryView) }; 25 | }, {}); 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uwasi", 3 | "version": "1.4.1", 4 | "description": "Micro modularized WASI runtime for JavaScript", 5 | "exports": { 6 | ".": { 7 | "import": "./lib/esm/index.js", 8 | "require": "./lib/cjs/index.js" 9 | } 10 | }, 11 | "scripts": { 12 | "build": "tsc -p tsconfig.esm.json && tsc -p tsconfig.cjs.json && echo '{ \"type\": \"module\" }' > lib/esm/package.json", 13 | "test": "node --test test/*.test.mjs", 14 | "format": "prettier --write ./src ./test", 15 | "prepare": "npm run build" 16 | }, 17 | "keywords": [ 18 | "webassembly", 19 | "wasm", 20 | "wasi" 21 | ], 22 | "bugs": { 23 | "url": "https://github.com/swiftwasm/uwasi/issues" 24 | }, 25 | "homepage": "https://github.com/swiftwasm/uwasi", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/swiftwasm/uwasi.git" 29 | }, 30 | "publishConfig": { 31 | "access": "public" 32 | }, 33 | "author": "SwiftWasm Team", 34 | "license": "MIT", 35 | "devDependencies": { 36 | "@types/node": "^17.0.31", 37 | "prettier": "^3.5.2", 38 | "typescript": "^4.6.4" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/fd.test.mjs: -------------------------------------------------------------------------------- 1 | import { ReadableTextProxy } from "../lib/esm/features/fd.js"; 2 | import { describe, it } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | describe("fd.ReadableTextProxy", () => { 6 | it("readv single buffer", () => { 7 | const input = "hello"; 8 | const inputs = [input]; 9 | const proxy = new ReadableTextProxy(() => inputs.shift() || ""); 10 | const buffer = new Uint8Array(10); 11 | const read = proxy.readv([buffer]); 12 | assert.strictEqual(read, 5); 13 | const expected = new TextEncoder().encode(input); 14 | assert.deepStrictEqual(buffer.slice(0, 5), expected); 15 | }); 16 | 17 | it("readv 2 buffer", () => { 18 | const input = "hello"; 19 | const inputs = [input]; 20 | const proxy = new ReadableTextProxy(() => inputs.shift() || ""); 21 | const buf0 = new Uint8Array(2); 22 | const buf1 = new Uint8Array(2); 23 | const read = proxy.readv([buf0, buf1]); 24 | assert.strictEqual(read, 4); 25 | const expected = new TextEncoder().encode(input); 26 | assert.deepStrictEqual(buf0, expected.slice(0, 2)); 27 | assert.deepStrictEqual(buf1, expected.slice(2, 4)); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | import { WASIAbi } from "./abi.js"; 2 | 3 | export type WASIFeatureProvider = ( 4 | options: WASIOptions, 5 | abi: WASIAbi, 6 | view: () => DataView, 7 | ) => WebAssembly.ModuleImports; 8 | 9 | export interface WASIOptions { 10 | /** 11 | * An array of strings that the WebAssembly application will 12 | * see as command line arguments. The first argument is the virtual path to the 13 | * WASI command itself. 14 | */ 15 | args?: string[] | undefined; 16 | /** 17 | * An object similar to `process.env` that the WebAssembly 18 | * application will see as its environment. 19 | */ 20 | env?: { [key: string]: string } | undefined; 21 | /** 22 | * An object that represents the WebAssembly application's 23 | * sandbox directory structure. The string keys of `preopens` are treated as 24 | * directories within the sandbox. The corresponding values in `preopens` are 25 | * the real paths to those directories on the host filesystem. 26 | */ 27 | preopens?: { [guestPath: string]: string } | undefined; 28 | 29 | /** 30 | * A list of functions that returns import object for the WebAssembly application. 31 | */ 32 | features?: WASIFeatureProvider[]; 33 | } 34 | -------------------------------------------------------------------------------- /src/features/args.ts: -------------------------------------------------------------------------------- 1 | import { WASIAbi } from "../abi.js"; 2 | import { WASIOptions } from "../options.js"; 3 | 4 | /** 5 | * A feature provider that provides `args_get` and `args_sizes_get` 6 | */ 7 | export function useArgs( 8 | options: WASIOptions, 9 | abi: WASIAbi, 10 | memoryView: () => DataView, 11 | ): WebAssembly.ModuleImports { 12 | const args = options.args || []; 13 | return { 14 | args_get: (argv: number, argvBuf: number) => { 15 | let offsetOffset = argv; 16 | let bufferOffset = argvBuf; 17 | const view = memoryView(); 18 | for (const arg of args) { 19 | view.setUint32(offsetOffset, bufferOffset, true); 20 | offsetOffset += 4; 21 | bufferOffset += abi.writeString(view, `${arg}\0`, bufferOffset); 22 | } 23 | return WASIAbi.WASI_ESUCCESS; 24 | }, 25 | args_sizes_get: (argc: number, argvBufSize: number) => { 26 | const view = memoryView(); 27 | view.setUint32(argc, args.length, true); 28 | const bufferSize = args.reduce( 29 | (acc, arg) => acc + abi.byteLength(arg) + 1, 30 | 0, 31 | ); 32 | view.setUint32(argvBufSize, bufferSize, true); 33 | return WASIAbi.WASI_ESUCCESS; 34 | }, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/features/environ.ts: -------------------------------------------------------------------------------- 1 | import { WASIAbi } from "../abi.js"; 2 | import { WASIOptions } from "../options.js"; 3 | 4 | /** 5 | * A feature provider that provides `environ_get` and `environ_sizes_get` 6 | */ 7 | export function useEnviron( 8 | options: WASIOptions, 9 | abi: WASIAbi, 10 | memoryView: () => DataView, 11 | ): WebAssembly.ModuleImports { 12 | return { 13 | environ_get: (environ: number, environBuf: number) => { 14 | let offsetOffset = environ; 15 | let bufferOffset = environBuf; 16 | const view = memoryView(); 17 | for (const key in options.env) { 18 | const value = options.env[key]; 19 | view.setUint32(offsetOffset, bufferOffset, true); 20 | offsetOffset += 4; 21 | bufferOffset += abi.writeString( 22 | view, 23 | `${key}=${value}\0`, 24 | bufferOffset, 25 | ); 26 | } 27 | return WASIAbi.WASI_ESUCCESS; 28 | }, 29 | environ_sizes_get: (environ: number, environBufSize: number) => { 30 | const view = memoryView(); 31 | view.setUint32(environ, Object.keys(options.env || {}).length, true); 32 | view.setUint32( 33 | environBufSize, 34 | Object.entries(options.env || {}).reduce((acc, [key, value]) => { 35 | return ( 36 | acc + 37 | abi.byteLength(key) /* = */ + 38 | 1 + 39 | abi.byteLength(value) /* \0 */ + 40 | 1 41 | ); 42 | }, 0), 43 | true, 44 | ); 45 | return WASIAbi.WASI_ESUCCESS; 46 | }, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /test/wasi.skip.json: -------------------------------------------------------------------------------- 1 | { 2 | "WASI Rust tests": { 3 | "sched_yield": "not implemented yet", 4 | "poll_oneoff_stdio": "not implemented yet", 5 | "path_rename": "not implemented yet", 6 | "fd_advise": "not implemented yet", 7 | "path_exists": "not implemented yet", 8 | "path_open_dirfd_not_dir": "not implemented yet", 9 | "fd_filestat_set": "not implemented yet", 10 | "symlink_create": "not implemented yet", 11 | "overwrite_preopen": "not implemented yet", 12 | "path_open_read_write": "not implemented yet", 13 | "path_rename_dir_trailing_slashes": "not implemented yet", 14 | "fd_flags_set": "not implemented yet", 15 | "path_filestat": "not implemented yet", 16 | "path_link": "not implemented yet", 17 | "fd_fdstat_set_rights": "not implemented yet", 18 | "readlink": "not implemented yet", 19 | "unlink_file_trailing_slashes": "not implemented yet", 20 | "path_symlink_trailing_slashes": "not implemented yet", 21 | "dangling_symlink": "not implemented yet", 22 | "dir_fd_op_failures": "not implemented yet", 23 | "file_allocate": "not implemented yet", 24 | "nofollow_errors": "not implemented yet", 25 | "path_open_preopen": "not implemented yet", 26 | "fd_readdir": "not implemented yet", 27 | "directory_seek": "not implemented yet", 28 | "symlink_filestat": "not implemented yet", 29 | "symlink_loop": "not implemented yet", 30 | "interesting_paths": "not implemented yet", 31 | 32 | "stdio": "fail", 33 | "renumber": "fail", 34 | "remove_nonempty_directory": "fail", 35 | "remove_directory_trailing_slashes": "fail", 36 | "fstflags_validate": "fail", 37 | "file_unbuffered_write": "fail", 38 | "file_seek_tell": "fail", 39 | "file_pread_pwrite": "fail", 40 | "close_preopen": "fail" 41 | }, 42 | "WASI C tests": { 43 | "sock_shutdown-invalid_fd": "not implemented yet", 44 | "stat-dev-ino": "not implemented yet", 45 | "sock_shutdown-not_sock": "not implemented yet", 46 | "fdopendir-with-access": "not implemented yet", 47 | 48 | "pwrite-with-append": "fail", 49 | "pwrite-with-access": "fail", 50 | "pread-with-access": "fail" 51 | }, 52 | "WASI AssemblyScript tests": {} 53 | } 54 | -------------------------------------------------------------------------------- /src/features/clock.ts: -------------------------------------------------------------------------------- 1 | import { WASIAbi } from "../abi.js"; 2 | import { WASIOptions } from "../options.js"; 3 | 4 | /** 5 | * A feature provider that provides `clock_res_get` and `clock_time_get` by JavaScript's Date. 6 | */ 7 | export function useClock( 8 | options: WASIOptions, 9 | abi: WASIAbi, 10 | memoryView: () => DataView, 11 | ): WebAssembly.ModuleImports { 12 | return { 13 | clock_res_get: (clockId: number, resolution: number) => { 14 | let resolutionValue: number; 15 | switch (clockId) { 16 | case WASIAbi.WASI_CLOCK_MONOTONIC: { 17 | // https://developer.mozilla.org/en-US/docs/Web/API/Performance/now 18 | resolutionValue = 5000; 19 | break; 20 | } 21 | case WASIAbi.WASI_CLOCK_REALTIME: { 22 | resolutionValue = 1000; 23 | break; 24 | } 25 | default: 26 | return WASIAbi.WASI_ENOSYS; 27 | } 28 | const view = memoryView(); 29 | // 64-bit integer, but only the lower 32 bits are used. 30 | view.setUint32(resolution, resolutionValue, true); 31 | return WASIAbi.WASI_ESUCCESS; 32 | }, 33 | clock_time_get: (clockId: number, precision: number, time: number) => { 34 | let nowMs: number = 0; 35 | switch (clockId) { 36 | case WASIAbi.WASI_CLOCK_MONOTONIC: { 37 | nowMs = performance.now(); 38 | break; 39 | } 40 | case WASIAbi.WASI_CLOCK_REALTIME: { 41 | nowMs = Date.now(); 42 | break; 43 | } 44 | default: 45 | return WASIAbi.WASI_ENOSYS; 46 | } 47 | const view = memoryView(); 48 | if (BigInt) { 49 | const msToNs = (ms: number) => { 50 | const msInt = Math.trunc(ms); 51 | const decimal = BigInt(Math.round((ms - msInt) * 1_000_000)); 52 | const ns = BigInt(msInt) * BigInt(1_000_000); 53 | return ns + decimal; 54 | }; 55 | const now = BigInt(msToNs(nowMs)); 56 | view.setBigUint64(time, now, true); 57 | } else { 58 | // Fallback to two 32-bit numbers losing precision 59 | const now = Date.now() * 1_000_000; 60 | view.setUint32(time, now & 0x0000ffff, true); 61 | view.setUint32(time + 4, now & 0xffff0000, true); 62 | } 63 | return WASIAbi.WASI_ESUCCESS; 64 | }, 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/uwasi.svg)](https://badge.fury.io/js/uwasi) 2 | [![.github/workflows/test.yml](https://github.com/swiftwasm/uwasi/actions/workflows/test.yml/badge.svg)](https://github.com/swiftwasm/uwasi/actions/workflows/test.yml) 3 | 4 | # μWASI 5 | 6 | This library provides a WASI implementation for Node.js and browsers in a tree-shaking friendly way. 7 | The system calls provided by this library are configurable. 8 | 9 | With minimal configuration, it provides WASI system calls which just return `WASI_ENOSYS`. 10 | 11 | ## Features 12 | 13 | - No dependencies 14 | - Tree-shaking friendly 15 | - 3 KB when minimal configuration 16 | - 6 KB when all features enabled 17 | - Almost compatible interface with [Node.js WASI implementation](https://nodejs.org/api/wasi.html) 18 | - Well tested, thanks to [wasi-test-suite by Casper Beyer](https://github.com/caspervonb/wasi-test-suite) 19 | 20 | ## Installation 21 | 22 | ```bash 23 | npm install uwasi 24 | ``` 25 | 26 | ## Example 27 | 28 | ### With all system calls enabled 29 | 30 | ```js 31 | import { WASI, useAll } from "uwasi"; 32 | import fs from "node:fs/promises"; 33 | 34 | async function main() { 35 | const wasi = new WASI({ 36 | args: process.argv.slice(2), 37 | features: [useAll()], 38 | }); 39 | const bytes = await fs.readFile(process.argv[2]); 40 | const { instance } = await WebAssembly.instantiate(bytes, { 41 | wasi_snapshot_preview1: wasi.wasiImport, 42 | }); 43 | const exitCode = wasi.start(instance); 44 | console.log("exit code:", exitCode); 45 | 46 | /* With Reactor model 47 | wasi.initialize(instance); 48 | */ 49 | } 50 | 51 | main() 52 | ``` 53 | 54 | ### With no system calls enabled 55 | 56 | ```js 57 | import { WASI, useAll } from "uwasi"; 58 | 59 | const wasi = new WASI({ 60 | features: [], 61 | }); 62 | ``` 63 | 64 | ### With `environ`, `args`, `clock`, `proc`, and `random` enabled 65 | 66 | ```js 67 | import { WASI, useArgs, useClock } from "uwasi"; 68 | 69 | const wasi = new WASI({ 70 | args: ["./a.out", "hello", "world"], 71 | features: [useEnviron, useArgs, useClock, useProc, useRandom()], 72 | }); 73 | ``` 74 | 75 | ### With `fd` (file descriptor) enabled only for stdio 76 | 77 | By default, `stdin` behaves like `/dev/null`, `stdout` and `stderr` print to the console. 78 | 79 | ```js 80 | import { WASI, useStdio } from "uwasi"; 81 | 82 | const wasi = new WASI({ 83 | features: [useStdio()], 84 | }); 85 | ``` 86 | 87 | You can use custom backends for stdio by passing handlers to `useStdio`. 88 | 89 | ```js 90 | import { WASI, useStdio } from "uwasi"; 91 | 92 | const inputs = ["Y", "N", "Y", "Y"]; 93 | const wasi = new WASI({ 94 | features: [useStdio({ 95 | stdin: () => inputs.shift() || "", 96 | stdout: (str) => document.body.innerHTML += str, 97 | stderr: (str) => document.body.innerHTML += str, 98 | })], 99 | }); 100 | ``` 101 | 102 | By default, the `stdout` and `stderr` handlers are passed strings. You can pass `outputBuffers: true` to get `Uint8Array` buffers instead. Along with that, you can also pass `Uint8Array` buffers to `stdin`. 103 | 104 | ```js 105 | import { WASI, useStdio } from "uwasi"; 106 | const wasi = new WASI({ 107 | features: [useStdio({ 108 | outputBuffers: true, 109 | stdin: () => new Uint8Array([1, 2, 3, 4, 5]), 110 | stdout: (buf) => console.log(buf), 111 | stderr: (buf) => console.error(buf), 112 | })], 113 | }); 114 | ``` 115 | 116 | ## Implementation Status 117 | 118 | Some of WASI system calls are not implemented yet. Contributions are welcome! 119 | 120 | | Syscall | Status | Notes | 121 | |-------|----------|---------| 122 | | `args_XXX` | ✅ | | 123 | | `clock_XXX` | ✅ | Monotonic clock is unavailable due to JS API limitation | 124 | | `environ_XXX` | ✅ | | 125 | | `fd_XXX` | 🚧 | stdin/stdout/stderr are supported | 126 | | `path_XXX` | ❌ | | 127 | | `poll_oneoff` | ❌ | | 128 | | `proc_XXX` | ✅ | | 129 | | `random_get` | ✅ | | 130 | | `sched_yield` | ❌ | | 131 | | `sock_XXX` | ❌ | | 132 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { WASIAbi, WASIProcExit } from "./abi.js"; 2 | export { WASIProcExit } from "./abi.js"; 3 | import { WASIOptions } from "./options.js"; 4 | 5 | export * from "./features/all.js"; 6 | export * from "./features/args.js"; 7 | export * from "./features/clock.js"; 8 | export * from "./features/environ.js"; 9 | export { 10 | useFS, 11 | useStdio, 12 | useMemoryFS, 13 | MemoryFileSystem, 14 | } from "./features/fd.js"; 15 | export * from "./features/proc.js"; 16 | export * from "./features/random.js"; 17 | export * from "./features/tracing.js"; 18 | 19 | export class WASI { 20 | /** 21 | * `wasiImport` is an object that implements the WASI system call API. This object 22 | * should be passed as the `wasi_snapshot_preview1` import during the instantiation 23 | * of a [`WebAssembly.Instance`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Instance). 24 | */ 25 | readonly wasiImport: WebAssembly.ModuleImports; 26 | private instance: WebAssembly.Instance | null = null; 27 | private isStarted: boolean = false; 28 | 29 | constructor(options?: WASIOptions) { 30 | this.wasiImport = {}; 31 | const abi = new WASIAbi(); 32 | if (options && options.features) { 33 | const importProviders: Record = {}; 34 | for (const useFeature of options.features) { 35 | const featureName = useFeature.name || "Unknown feature"; 36 | const imports = useFeature(options, abi, this.view.bind(this)); 37 | for (const key in imports) { 38 | importProviders[key] = featureName; 39 | } 40 | this.wasiImport = { ...this.wasiImport, ...imports }; 41 | } 42 | } 43 | // Provide default implementations for missing functions just returning ENOSYS. 44 | for (const key of WASIAbi.IMPORT_FUNCTIONS) { 45 | if (!(key in this.wasiImport)) { 46 | this.wasiImport[key] = () => { 47 | return WASIAbi.WASI_ENOSYS; 48 | }; 49 | } 50 | } 51 | } 52 | 53 | private view(): DataView { 54 | if (!this.instance) { 55 | throw new Error("wasi.start() or wasi.initialize() has not been called"); 56 | } 57 | if (!this.instance.exports.memory) { 58 | throw new Error("instance.exports.memory is undefined"); 59 | } 60 | if (!(this.instance.exports.memory instanceof WebAssembly.Memory)) { 61 | throw new Error("instance.exports.memory is not a WebAssembly.Memory"); 62 | } 63 | return new DataView(this.instance.exports.memory.buffer); 64 | } 65 | 66 | /** 67 | * Attempt to begin execution of `instance` as a WASI command by invoking its`_start()` export. If `instance` does not contain a `_start()` export, or if`instance` contains an `_initialize()` 68 | * export, then an exception is thrown. 69 | * 70 | * `start()` requires that `instance` exports a [`WebAssembly.Memory`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Memory) named`memory`. If 71 | * `instance` does not have a `memory` export an exception is thrown. 72 | * 73 | * If `start()` is called more than once, an exception is thrown. 74 | */ 75 | start(instance: WebAssembly.Instance): number { 76 | if (this.isStarted) { 77 | throw new Error( 78 | "wasi.start() or wasi.initialize() has already been called", 79 | ); 80 | } 81 | this.isStarted = true; 82 | this.instance = instance; 83 | if (!this.instance.exports._start) { 84 | throw new Error("instance.exports._start is undefined"); 85 | } 86 | if (typeof this.instance.exports._start !== "function") { 87 | throw new Error("instance.exports._start is not a function"); 88 | } 89 | try { 90 | this.instance.exports._start(); 91 | return WASIAbi.WASI_ESUCCESS; 92 | } catch (e) { 93 | if (e instanceof WASIProcExit) { 94 | return e.code; 95 | } 96 | throw e; 97 | } 98 | } 99 | /** 100 | * Attempt to initialize `instance` as a WASI reactor by invoking its`_initialize()` export, if it is present. If `instance` contains a `_start()`export, then an exception is thrown. 101 | * 102 | * `initialize()` requires that `instance` exports a [`WebAssembly.Memory`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Memory) named`memory`. 103 | * If `instance` does not have a `memory` export an exception is thrown. 104 | * 105 | * If `initialize()` is called more than once, an exception is thrown. 106 | */ 107 | initialize(instance: WebAssembly.Instance): void { 108 | if (this.isStarted) { 109 | throw new Error( 110 | "wasi.start() or wasi.initialize() has already been called", 111 | ); 112 | } 113 | this.isStarted = true; 114 | this.instance = instance; 115 | if (!this.instance.exports._initialize) { 116 | throw new Error("instance.exports._initialize is undefined"); 117 | } 118 | if (typeof this.instance.exports._initialize !== "function") { 119 | throw new Error("instance.exports._initialize is not a function"); 120 | } 121 | this.instance.exports._initialize(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/abi.ts: -------------------------------------------------------------------------------- 1 | export class WASIAbi { 2 | /** 3 | * No error occurred. System call completed successfully. 4 | */ 5 | static readonly WASI_ESUCCESS = 0; 6 | 7 | /** 8 | * Bad file descriptor. 9 | */ 10 | static readonly WASI_ERRNO_BADF = 8; 11 | 12 | /** 13 | * Function not supported. 14 | */ 15 | static readonly WASI_ENOSYS = 52; 16 | 17 | /** 18 | * The clock measuring real time. Time value zero corresponds with 1970-01-01T00:00:00Z. 19 | */ 20 | static readonly WASI_CLOCK_REALTIME = 0; 21 | /** 22 | * The store-wide monotonic clock, which is defined as a clock measuring real time, 23 | * whose value cannot be adjusted and which cannot have negative clock jumps. 24 | * The epoch of this clock is undefined. The absolute time value of this clock therefore has no meaning. 25 | */ 26 | static readonly WASI_CLOCK_MONOTONIC = 1; 27 | 28 | /** 29 | * The file descriptor or file refers to a directory. 30 | */ 31 | static readonly WASI_ERRNO_ISDIR = 31; 32 | /** 33 | * Invalid argument. 34 | */ 35 | static readonly WASI_ERRNO_INVAL = 28; 36 | /** 37 | * Not a directory or a symbolic link to a directory. 38 | */ 39 | static readonly WASI_ERRNO_NOTDIR = 54; 40 | /** 41 | * No such file or directory. 42 | */ 43 | static readonly WASI_ERRNO_NOENT = 44; 44 | /** 45 | * File exists. 46 | */ 47 | static readonly WASI_ERRNO_EXIST = 20; 48 | /** 49 | * I/O error. 50 | */ 51 | static readonly WASI_ERRNO_IO = 29; 52 | 53 | /** 54 | * The file descriptor or file refers to a character device inode. 55 | */ 56 | static readonly WASI_FILETYPE_CHARACTER_DEVICE = 2; 57 | /** 58 | * The file descriptor or file refers to a directory inode. 59 | */ 60 | static readonly WASI_FILETYPE_DIRECTORY = 3; 61 | /** 62 | * The file descriptor or file refers to a regular file inode. 63 | */ 64 | static readonly WASI_FILETYPE_REGULAR_FILE = 4; 65 | /** 66 | * Create file if it does not exist. 67 | */ 68 | static readonly WASI_OFLAGS_CREAT = 1 << 0; 69 | /** 70 | * Open directory. 71 | */ 72 | static readonly WASI_OFLAGS_DIRECTORY = 1 << 1; 73 | /** 74 | * Fail if not a directory. 75 | */ 76 | static readonly WASI_OFLAGS_EXCL = 1 << 2; 77 | /** 78 | * Truncate to zero length. 79 | */ 80 | static readonly WASI_OFLAGS_TRUNC = 1 << 3; 81 | 82 | static readonly IMPORT_FUNCTIONS = [ 83 | "args_get", 84 | "args_sizes_get", 85 | 86 | "clock_res_get", 87 | "clock_time_get", 88 | 89 | "environ_get", 90 | "environ_sizes_get", 91 | 92 | "fd_advise", 93 | "fd_allocate", 94 | "fd_close", 95 | "fd_datasync", 96 | "fd_fdstat_get", 97 | "fd_fdstat_set_flags", 98 | "fd_fdstat_set_rights", 99 | "fd_filestat_get", 100 | "fd_filestat_set_size", 101 | "fd_filestat_set_times", 102 | "fd_pread", 103 | "fd_prestat_dir_name", 104 | "fd_prestat_get", 105 | "fd_pwrite", 106 | "fd_read", 107 | "fd_readdir", 108 | "fd_renumber", 109 | "fd_seek", 110 | "fd_sync", 111 | "fd_tell", 112 | "fd_write", 113 | 114 | "path_create_directory", 115 | "path_filestat_get", 116 | "path_filestat_set_times", 117 | "path_link", 118 | "path_open", 119 | "path_readlink", 120 | "path_remove_directory", 121 | "path_rename", 122 | "path_symlink", 123 | "path_unlink_file", 124 | 125 | "poll_oneoff", 126 | 127 | "proc_exit", 128 | "proc_raise", 129 | 130 | "random_get", 131 | 132 | "sched_yield", 133 | 134 | "sock_accept", 135 | "sock_recv", 136 | "sock_send", 137 | "sock_shutdown", 138 | ]; 139 | 140 | private encoder: TextEncoder; 141 | private decoder: TextDecoder; 142 | 143 | constructor() { 144 | this.encoder = new TextEncoder(); 145 | this.decoder = new TextDecoder(); 146 | } 147 | 148 | writeString(memory: DataView, value: string, offset: number): number { 149 | const bytes = this.encoder.encode(value); 150 | const buffer = new Uint8Array(memory.buffer, offset, bytes.length); 151 | buffer.set(bytes); 152 | return bytes.length; 153 | } 154 | 155 | readString(memory: DataView, ptr: number, len: number): string { 156 | const buffer = new Uint8Array(memory.buffer, ptr, len); 157 | return this.decoder.decode(buffer); 158 | } 159 | 160 | byteLength(value: string): number { 161 | return this.encoder.encode(value).length; 162 | } 163 | 164 | private static readonly iovec_t = { 165 | size: 8, 166 | bufferOffset: 0, 167 | lengthOffset: 4, 168 | }; 169 | 170 | iovViews(memory: DataView, iovs: number, iovsLen: number): Uint8Array[] { 171 | const iovsBuffers: Uint8Array[] = []; 172 | let iovsOffset = iovs; 173 | 174 | for (let i = 0; i < iovsLen; i++) { 175 | const offset = memory.getUint32( 176 | iovsOffset + WASIAbi.iovec_t.bufferOffset, 177 | true, 178 | ); 179 | const len = memory.getUint32( 180 | iovsOffset + WASIAbi.iovec_t.lengthOffset, 181 | true, 182 | ); 183 | 184 | iovsBuffers.push(new Uint8Array(memory.buffer, offset, len)); 185 | iovsOffset += WASIAbi.iovec_t.size; 186 | } 187 | return iovsBuffers; 188 | } 189 | 190 | writeFilestat(memory: DataView, ptr: number, filetype: number): void { 191 | memory.setBigUint64(ptr, /* dev */ BigInt(0), true); 192 | memory.setBigUint64(ptr + 8, /* ino */ BigInt(0), true); 193 | memory.setUint8(ptr + 16, filetype); 194 | memory.setUint32(ptr + 24, /* nlink */ 0, true); 195 | memory.setBigUint64(ptr + 32, /* size */ BigInt(0), true); 196 | memory.setBigUint64(ptr + 40, /* atim */ BigInt(0), true); 197 | memory.setBigUint64(ptr + 48, /* mtim */ BigInt(0), true); 198 | } 199 | 200 | writeFdstat( 201 | memory: DataView, 202 | ptr: number, 203 | filetype: number, 204 | flags: number, 205 | ): void { 206 | memory.setUint8(ptr, filetype); 207 | memory.setUint16(ptr + 2, flags, true); 208 | memory.setBigUint64(ptr + 8, /* rights_base */ BigInt(0), true); 209 | memory.setBigUint64(ptr + 16, /* rights_inheriting */ BigInt(0), true); 210 | } 211 | } 212 | 213 | /** 214 | * An exception that is thrown when the process exits. 215 | **/ 216 | export class WASIProcExit { 217 | constructor(public readonly code: number) {} 218 | 219 | /** @deprecated Use 'code' instead. 220 | * Has been renamed to have loose compatibility 221 | * with other implementations **/ 222 | get exitCode() { 223 | return this.code; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /test/wasi.test.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import fs from "fs/promises"; 3 | import fsSync from "fs"; 4 | import path from "path"; 5 | import { useAll, WASI, MemoryFileSystem, useRandom } from "../lib/esm/index.js"; 6 | import { describe, it } from "node:test"; 7 | import assert from "node:assert"; 8 | import * as crypto from "crypto"; 9 | 10 | /** 11 | * @typedef {{ exit_code?: number, args?: string[], env?: Record, dirs?: string[] }} TestCaseConfig 12 | * @typedef {{ suite: string, wasmFile: string, testName: string, config: TestCaseConfig }} TestCase 13 | */ 14 | 15 | /** 16 | * Helper function to find test cases directory and files 17 | * 18 | * @param {string} testDir - The directory to search for test cases 19 | * @returns {Array} An array of test cases 20 | */ 21 | function findTestCases(testDir) { 22 | const testSuites = [ 23 | { path: "rust/testsuite", name: "WASI Rust tests" }, 24 | { path: "c/testsuite", name: "WASI C tests" }, 25 | { path: "assemblyscript/testsuite", name: "WASI AssemblyScript tests" }, 26 | ]; 27 | 28 | /** @type {Array} */ 29 | const allTests = []; 30 | 31 | for (const suite of testSuites) { 32 | const suitePath = path.join(testDir, suite.path); 33 | try { 34 | const files = fsSync.readdirSync(suitePath); 35 | const wasmFiles = files.filter((file) => file.endsWith(".wasm")); 36 | 37 | for (const wasmFile of wasmFiles) { 38 | // Find corresponding JSON config file 39 | const jsonFile = wasmFile.replace(".wasm", ".json"); 40 | const jsonPath = path.join(suitePath, jsonFile); 41 | 42 | let config = {}; 43 | try { 44 | const jsonContent = fsSync.readFileSync(jsonPath, "utf8"); 45 | config = JSON.parse(jsonContent); 46 | } catch (e) { 47 | // Use default config if no JSON config file found 48 | config = {}; 49 | } 50 | 51 | allTests.push({ 52 | suite: suite.name, 53 | wasmFile: path.join(suitePath, wasmFile), 54 | testName: path.basename(wasmFile, ".wasm"), 55 | config, 56 | }); 57 | } 58 | } catch (err) { 59 | console.warn(`Test suite ${suite.name} is not available: ${err.message}`); 60 | } 61 | } 62 | 63 | return allTests; 64 | } 65 | 66 | // Helper function to run a test 67 | async function runTest(testCase) { 68 | /** @type {string[]} */ 69 | const args = []; 70 | /** @type {Record} */ 71 | const env = {}; 72 | 73 | // Add args if specified 74 | if (testCase.config.args) { 75 | args.push(...testCase.config.args); 76 | } 77 | 78 | // Add env if specified 79 | if (testCase.config.env) { 80 | for (const [key, value] of Object.entries(testCase.config.env)) { 81 | env[key] = value; 82 | } 83 | } 84 | 85 | // Setup file system 86 | const fileSystem = new MemoryFileSystem( 87 | (testCase.config.dirs || []).reduce((obj, dir) => { 88 | obj[dir] = dir; 89 | return obj; 90 | }, {}), 91 | ); 92 | 93 | // Clone directories to memory file system 94 | if (testCase.config.dirs) { 95 | for (const dir of testCase.config.dirs) { 96 | const dirPath = path.join(path.dirname(testCase.wasmFile), dir); 97 | await cloneDirectoryToMemFS(fileSystem, dirPath, "/" + dir); 98 | } 99 | } 100 | 101 | // Create stdout and stderr buffers 102 | let stdoutData = ""; 103 | let stderrData = ""; 104 | 105 | // Create WASI instance 106 | const wasi = new WASI({ 107 | args: [path.basename(testCase.wasmFile), ...args], 108 | env: env, 109 | features: [ 110 | useAll({ 111 | withFileSystem: fileSystem, 112 | withStdio: { 113 | stdout: (data) => { 114 | if (typeof data === "string") { 115 | stdoutData += data; 116 | } else { 117 | stdoutData += new TextDecoder().decode(data); 118 | } 119 | }, 120 | stderr: (data) => { 121 | if (typeof data === "string") { 122 | stderrData += data; 123 | } else { 124 | stderrData += new TextDecoder().decode(data); 125 | } 126 | }, 127 | }, 128 | }), 129 | useRandom({ 130 | randomFillSync: crypto.randomFillSync, 131 | }), 132 | ], 133 | }); 134 | 135 | try { 136 | const wasmBytes = await fs.readFile(testCase.wasmFile); 137 | const wasmModule = await WebAssembly.compile(wasmBytes); 138 | const importObject = { wasi_snapshot_preview1: wasi.wasiImport }; 139 | const instance = await WebAssembly.instantiate(wasmModule, importObject); 140 | 141 | // Start WASI 142 | const exitCode = wasi.start(instance); 143 | 144 | return { 145 | exitCode, 146 | stdout: stdoutData, 147 | stderr: stderrData, 148 | }; 149 | } catch (error) { 150 | return { 151 | error: error.message, 152 | exitCode: 1, 153 | stdout: stdoutData, 154 | stderr: stderrData, 155 | }; 156 | } 157 | } 158 | 159 | /** 160 | * Helper function to clone a directory to memory file system 161 | * 162 | * @param {MemoryFileSystem} fileSystem 163 | * @param {string} sourceDir 164 | * @param {string} targetPath 165 | * @returns {Promise} 166 | */ 167 | async function cloneDirectoryToMemFS(fileSystem, sourceDir, targetPath) { 168 | // Check if directory exists 169 | const stats = await fs.stat(sourceDir); 170 | if (!stats.isDirectory()) { 171 | return; 172 | } 173 | 174 | // Create directory in file system 175 | fileSystem.ensureDir(targetPath); 176 | 177 | // Read directory contents 178 | const entries = await fs.readdir(sourceDir, { withFileTypes: true }); 179 | 180 | // Process each entry 181 | for (const entry of entries) { 182 | const sourcePath = path.join(sourceDir, entry.name); 183 | const targetFilePath = path.join(targetPath, entry.name); 184 | 185 | if (entry.isDirectory()) { 186 | // Recursively clone directory 187 | await cloneDirectoryToMemFS(fileSystem, sourcePath, targetFilePath); 188 | } else if (entry.isFile()) { 189 | // Read file content and add to file system 190 | const content = await fs.readFile(sourcePath); 191 | fileSystem.addFile(targetFilePath, content); 192 | } 193 | } 194 | } 195 | 196 | // Main test setup 197 | describe("WASI Test Suite", () => { 198 | const __dirname = path.dirname(new URL(import.meta.url).pathname); 199 | const testDir = path.join(__dirname, "../third_party/wasi-testsuite/tests"); 200 | const testCases = findTestCases(testDir); 201 | // Load the skip tests list 202 | let skipTests = {}; 203 | try { 204 | skipTests = JSON.parse( 205 | fsSync.readFileSync(path.join(__dirname, "./wasi.skip.json"), "utf8"), 206 | ); 207 | } catch (err) { 208 | console.warn("Could not load skip tests file. All tests will be run."); 209 | } 210 | 211 | // This test will dynamically create and run tests for each test case 212 | for (const testCase of testCases) { 213 | const isSkipped = 214 | skipTests[testCase.suite] && skipTests[testCase.suite][testCase.testName]; 215 | const defineTest = isSkipped ? it.skip : it; 216 | defineTest(`${testCase.suite} - ${testCase.testName}`, async () => { 217 | const result = await runTest(testCase); 218 | assert.strictEqual(result.error, undefined, result.stderr); 219 | assert.strictEqual( 220 | result.exitCode, 221 | testCase.config.exit_code || 0, 222 | result.stderr, 223 | ); 224 | }); 225 | } 226 | }); 227 | -------------------------------------------------------------------------------- /src/features/fd.ts: -------------------------------------------------------------------------------- 1 | import { WASIAbi } from "../abi.js"; 2 | import { WASIFeatureProvider, WASIOptions } from "../options.js"; 3 | 4 | interface FdEntry { 5 | writev(iovs: Uint8Array[]): number; 6 | readv(iovs: Uint8Array[]): number; 7 | close(): void; 8 | } 9 | 10 | class WritableTextProxy implements FdEntry { 11 | private decoder = new TextDecoder("utf-8"); 12 | constructor( 13 | private readonly handler: (lines: string | Uint8Array) => void, 14 | private readonly outputBuffers: boolean, 15 | ) {} 16 | 17 | writev(iovs: Uint8Array[]): number { 18 | const totalBufferSize = iovs.reduce((acc, iov) => acc + iov.byteLength, 0); 19 | let offset = 0; 20 | const concatBuffer = new Uint8Array(totalBufferSize); 21 | for (const buffer of iovs) { 22 | concatBuffer.set(buffer, offset); 23 | offset += buffer.byteLength; 24 | } 25 | 26 | if (this.outputBuffers) { 27 | this.handler(concatBuffer); 28 | } else { 29 | const lines = this.decoder.decode(concatBuffer); 30 | this.handler(lines); 31 | } 32 | 33 | return concatBuffer.length; 34 | } 35 | readv(_iovs: Uint8Array[]): number { 36 | return 0; 37 | } 38 | close(): void {} 39 | } 40 | 41 | export class ReadableTextProxy implements FdEntry { 42 | private encoder = new TextEncoder(); 43 | private pending: Uint8Array | null = null; 44 | constructor(private readonly consume: () => string | Uint8Array) {} 45 | 46 | writev(_iovs: Uint8Array[]): number { 47 | return 0; 48 | } 49 | consumePending(pending: Uint8Array, requestLength: number): Uint8Array { 50 | if (pending.byteLength < requestLength) { 51 | this.pending = null; 52 | return pending; 53 | } else { 54 | const result = pending.slice(0, requestLength); 55 | this.pending = pending.slice(requestLength); 56 | return result; 57 | } 58 | } 59 | readv(iovs: Uint8Array[]): number { 60 | let read = 0; 61 | for (const buffer of iovs) { 62 | let remaining = buffer.byteLength; 63 | if (this.pending) { 64 | const consumed = this.consumePending(this.pending, remaining); 65 | buffer.set(consumed, 0); 66 | remaining -= consumed.byteLength; 67 | read += consumed.byteLength; 68 | } 69 | while (remaining > 0) { 70 | const newData = this.consume(); 71 | let bytes: Uint8Array; 72 | 73 | if (newData instanceof Uint8Array) { 74 | bytes = newData; 75 | } else { 76 | bytes = this.encoder.encode(newData); 77 | } 78 | 79 | if (bytes.length == 0) { 80 | return read; 81 | } 82 | if (bytes.length > remaining) { 83 | buffer.set(bytes.slice(0, remaining), buffer.byteLength - remaining); 84 | this.pending = bytes.slice(remaining); 85 | read += remaining; 86 | remaining = 0; 87 | } else { 88 | buffer.set(bytes, buffer.byteLength - remaining); 89 | read += bytes.length; 90 | remaining -= bytes.length; 91 | } 92 | } 93 | } 94 | return read; 95 | } 96 | close(): void {} 97 | } 98 | 99 | export type StdioOptions = { 100 | stdin?: () => string | Uint8Array; 101 | stdout?: (lines: string | Uint8Array) => void; 102 | stderr?: (lines: string | Uint8Array) => void; 103 | outputBuffers?: boolean; 104 | }; 105 | 106 | function bindStdio( 107 | useOptions: StdioOptions = {}, 108 | ): (ReadableTextProxy | WritableTextProxy)[] { 109 | const outputBuffers = useOptions.outputBuffers || false; 110 | return [ 111 | new ReadableTextProxy( 112 | useOptions.stdin || 113 | (() => { 114 | return ""; 115 | }), 116 | ), 117 | new WritableTextProxy(useOptions.stdout || console.log, outputBuffers), 118 | new WritableTextProxy(useOptions.stderr || console.error, outputBuffers), 119 | ]; 120 | } 121 | 122 | /** 123 | * Create a feature provider that provides fd related features only for standard output and standard error 124 | * It uses JavaScript's `console` APIs as backend by default. 125 | * 126 | * ```js 127 | * const wasi = new WASI({ 128 | * features: [useStdio()], 129 | * }); 130 | * ``` 131 | * 132 | * To use a custom backend, you can pass stdout and stderr handlers. 133 | * 134 | * ```js 135 | * const wasi = new WASI({ 136 | * features: [ 137 | * useStdio({ 138 | * stdout: (lines) => document.write(lines), 139 | * stderr: (lines) => document.write(lines), 140 | * }) 141 | * ], 142 | * }); 143 | * ``` 144 | * 145 | * This provides `fd_write`, `fd_prestat_get` and `fd_prestat_dir_name` implementations to make libc work with minimal effort. 146 | */ 147 | export function useStdio(useOptions: StdioOptions = {}): WASIFeatureProvider { 148 | return (options, abi, memoryView) => { 149 | const fdTable = bindStdio(useOptions); 150 | return { 151 | fd_fdstat_get: (fd: number, buf: number) => { 152 | const fdEntry = fdTable[fd]; 153 | if (!fdEntry) return WASIAbi.WASI_ERRNO_BADF; 154 | const view = memoryView(); 155 | abi.writeFdstat(view, buf, WASIAbi.WASI_FILETYPE_CHARACTER_DEVICE, 0); 156 | return WASIAbi.WASI_ESUCCESS; 157 | }, 158 | fd_filestat_get: (fd: number, buf: number) => { 159 | const fdEntry = fdTable[fd]; 160 | if (!fdEntry) return WASIAbi.WASI_ERRNO_BADF; 161 | const view = memoryView(); 162 | abi.writeFilestat(view, buf, WASIAbi.WASI_FILETYPE_CHARACTER_DEVICE); 163 | }, 164 | fd_prestat_get: (fd: number, buf: number) => { 165 | return WASIAbi.WASI_ERRNO_BADF; 166 | }, 167 | fd_prestat_dir_name: (fd: number, buf: number) => { 168 | return WASIAbi.WASI_ERRNO_BADF; 169 | }, 170 | fd_write: ( 171 | fd: number, 172 | iovs: number, 173 | iovsLen: number, 174 | nwritten: number, 175 | ) => { 176 | const fdEntry = fdTable[fd]; 177 | if (!fdEntry) return WASIAbi.WASI_ERRNO_BADF; 178 | const view = memoryView(); 179 | const iovsBuffers = abi.iovViews(view, iovs, iovsLen); 180 | const writtenValue = fdEntry.writev(iovsBuffers); 181 | view.setUint32(nwritten, writtenValue, true); 182 | return WASIAbi.WASI_ESUCCESS; 183 | }, 184 | fd_read: (fd: number, iovs: number, iovsLen: number, nread: number) => { 185 | const fdEntry = fdTable[fd]; 186 | if (!fdEntry) return WASIAbi.WASI_ERRNO_BADF; 187 | const view = memoryView(); 188 | const iovsBuffers = abi.iovViews(view, iovs, iovsLen); 189 | const readValue = fdEntry.readv(iovsBuffers); 190 | view.setUint32(nread, readValue, true); 191 | return WASIAbi.WASI_ESUCCESS; 192 | }, 193 | }; 194 | }; 195 | } 196 | 197 | type FileDescriptor = number; 198 | 199 | /** 200 | * Represents a node in the file system that is a directory. 201 | */ 202 | interface DirectoryNode { 203 | readonly type: "dir"; 204 | entries: Record; 205 | } 206 | 207 | /** 208 | * Represents a node in the file system that is a file. 209 | */ 210 | interface FileNode { 211 | readonly type: "file"; 212 | content: Uint8Array; 213 | } 214 | 215 | type CharacterDeviceNode = 216 | | { readonly type: "character"; kind: "stdio"; entry: FdEntry } 217 | | { readonly type: "character"; kind: "devnull" }; 218 | 219 | /** 220 | * Union type representing any node in the file system. 221 | */ 222 | type FSNode = DirectoryNode | FileNode | CharacterDeviceNode; 223 | 224 | /** 225 | * Represents an open file in the file system. 226 | */ 227 | interface OpenFile { 228 | node: FSNode; 229 | position: number; 230 | path: string; 231 | isPreopen?: boolean; 232 | preopenPath?: string; 233 | fd: FileDescriptor; 234 | } 235 | 236 | /** 237 | * Type for file content that can be added to the file system. 238 | */ 239 | type FileContent = string | Uint8Array | Blob; 240 | 241 | /** 242 | * In-memory implementation of a file system. 243 | */ 244 | export class MemoryFileSystem { 245 | private root: DirectoryNode; 246 | private preopenPaths: string[] = []; 247 | 248 | /** 249 | * Creates a new memory file system. 250 | * @param preopens Optional list of directories to pre-open 251 | */ 252 | constructor(preopens?: { [guestPath: string]: string } | undefined) { 253 | this.root = { type: "dir", entries: {} }; 254 | 255 | // Setup essential directories and special files 256 | this.ensureDir("/dev"); 257 | this.setNode("/dev/null", { type: "character", kind: "devnull" }); 258 | 259 | // Setup preopened directories 260 | if (preopens) { 261 | Object.keys(preopens).forEach((guestPath) => { 262 | // there are no 'host' paths in a memory file system, so we just use the guest path. 263 | this.ensureDir(guestPath); 264 | this.preopenPaths.push(guestPath); 265 | }); 266 | } else { 267 | this.preopenPaths.push("/"); 268 | } 269 | } 270 | 271 | addFile(path: string, content: string | Uint8Array): void; 272 | addFile(path: string, content: Blob): Promise; 273 | addFile(path: string, content: FileContent): void | Promise { 274 | if (typeof content === "string") { 275 | const data = new TextEncoder().encode(content); 276 | this.createFile(path, data); 277 | return; 278 | } else if (globalThis.Blob && content instanceof Blob) { 279 | return content.arrayBuffer().then((buffer) => { 280 | const data = new Uint8Array(buffer); 281 | this.createFile(path, data); 282 | }); 283 | } else { 284 | this.createFile(path, content as Uint8Array); 285 | return; 286 | } 287 | } 288 | 289 | /** 290 | * Creates a file with the specified content. 291 | * @param path Path where the file should be created 292 | * @param content Binary content of the file 293 | * @returns The created file node 294 | */ 295 | createFile(path: string, content: Uint8Array): FileNode { 296 | const fileNode: FileNode = { type: "file", content }; 297 | this.setNode(path, fileNode); 298 | return fileNode; 299 | } 300 | 301 | /** 302 | * Sets a node at the specified path. 303 | * @param path Path where the node should be set 304 | * @param node The node to set 305 | */ 306 | setNode(path: string, node: FSNode): void { 307 | const normalizedPath = normalizePath(path); 308 | const parts = normalizedPath.split("/").filter((p) => p.length > 0); 309 | 310 | if (parts.length === 0) { 311 | if (node.type !== "dir") { 312 | throw new Error("Root must be a directory"); 313 | } 314 | this.root = node; 315 | return; 316 | } 317 | 318 | const fileName = parts.pop()!; 319 | const dirPath = "/" + parts.join("/"); 320 | const dir = this.ensureDir(dirPath); 321 | dir.entries[fileName] = node; 322 | } 323 | 324 | /** 325 | * Gets the /dev/null special device. 326 | * @returns The /dev/null node 327 | */ 328 | getDevNull(): FSNode { 329 | const node = this.lookup("/dev/null"); 330 | if (!node) throw new Error("/dev/null not found"); 331 | return node; 332 | } 333 | 334 | /** 335 | * Gets the list of pre-opened paths. 336 | * @returns Array of pre-opened paths 337 | */ 338 | getPreopenPaths(): string[] { 339 | return [...this.preopenPaths]; 340 | } 341 | 342 | /** 343 | * Looks up a node at the specified path. 344 | * @param path Path to look up 345 | * @returns The node at the path, or null if not found 346 | */ 347 | lookup(path: string): FSNode | null { 348 | const normalizedPath = normalizePath(path); 349 | if (normalizedPath === "/") return this.root; 350 | 351 | const parts = normalizedPath.split("/").filter((p) => p.length > 0); 352 | let current: FSNode = this.root; 353 | 354 | for (const part of parts) { 355 | if (current.type !== "dir") return null; 356 | current = current.entries[part]; 357 | if (!current) return null; 358 | } 359 | 360 | return current; 361 | } 362 | 363 | /** 364 | * Resolves a relative path from a directory. 365 | * @param dir Starting directory 366 | * @param relativePath Relative path to resolve 367 | * @returns The resolved node, or null if not found 368 | */ 369 | resolve(dir: DirectoryNode, relativePath: string): FSNode | null { 370 | const normalizedPath = normalizePath(relativePath); 371 | const parts = normalizedPath.split("/").filter((p) => p.length > 0); 372 | let current: FSNode = dir; 373 | 374 | for (const part of parts) { 375 | if (part === ".") continue; 376 | if (part === "..") { 377 | current = this.root; // jump to root 378 | continue; 379 | } 380 | if (current.type !== "dir") return null; 381 | current = current.entries[part]; 382 | if (!current) return null; 383 | } 384 | 385 | return current; 386 | } 387 | 388 | /** 389 | * Ensures a directory exists at the specified path, creating it if necessary. 390 | * @param path Path to the directory 391 | * @returns The directory node 392 | */ 393 | ensureDir(path: string): DirectoryNode { 394 | const normalizedPath = normalizePath(path); 395 | const parts = normalizedPath.split("/").filter((p) => p.length > 0); 396 | let current: DirectoryNode = this.root; 397 | 398 | for (const part of parts) { 399 | if (!current.entries[part]) { 400 | current.entries[part] = { type: "dir", entries: {} }; 401 | } 402 | 403 | const next = current.entries[part]; 404 | if (next.type !== "dir") { 405 | throw new Error(`"${part}" is not a directory`); 406 | } 407 | 408 | current = next; 409 | } 410 | 411 | return current; 412 | } 413 | 414 | /** 415 | * Creates a file in a directory. 416 | * @param dir Parent directory 417 | * @param relativePath Path relative to the directory 418 | * @returns The created file node 419 | */ 420 | createFileIn(dir: DirectoryNode, relativePath: string): FileNode { 421 | const normalizedPath = normalizePath(relativePath); 422 | const parts = normalizedPath.split("/").filter((p) => p.length > 0); 423 | 424 | if (parts.length === 0) { 425 | throw new Error("Cannot create a file with an empty name"); 426 | } 427 | 428 | const fileName = parts.pop()!; 429 | let current = dir; 430 | 431 | for (const part of parts) { 432 | if (!current.entries[part]) { 433 | current.entries[part] = { type: "dir", entries: {} }; 434 | } 435 | 436 | const next = current.entries[part]; 437 | if (next.type !== "dir") { 438 | throw new Error(`"${part}" is not a directory`); 439 | } 440 | 441 | current = next; 442 | } 443 | 444 | const fileNode: FileNode = { type: "file", content: new Uint8Array(0) }; 445 | current.entries[fileName] = fileNode; 446 | return fileNode; 447 | } 448 | 449 | removeEntry(path: string): void { 450 | const normalizedPath = normalizePath(path); 451 | const parts = normalizedPath.split("/").filter((p) => p.length > 0); 452 | let parentDir = this.root; 453 | for (let i = 0; i < parts.length - 1; i++) { 454 | const part = parts[i]; 455 | if (parentDir.type !== "dir") return; 456 | parentDir = parentDir.entries[part] as DirectoryNode; 457 | } 458 | 459 | const fileName = parts[parts.length - 1]; 460 | delete parentDir.entries[fileName]; 461 | } 462 | } 463 | 464 | /** 465 | * Normalizes a path by removing duplicate slashes and trailing slashes. 466 | * @param path Path to normalize 467 | * @returns Normalized path 468 | */ 469 | function normalizePath(path: string): string { 470 | // Handle empty path 471 | if (!path) return "/"; 472 | 473 | const parts = path.split("/").filter((p) => p.length > 0); 474 | const normalizedParts: string[] = []; 475 | 476 | for (const part of parts) { 477 | if (part === ".") continue; 478 | if (part === "..") { 479 | normalizedParts.pop(); 480 | continue; 481 | } 482 | normalizedParts.push(part); 483 | } 484 | if (normalizedParts.length === 0) return "/"; 485 | 486 | const normalized = "/" + normalizedParts.join("/"); 487 | return normalized; 488 | } 489 | 490 | /** 491 | * Creates a feature provider that implements a complete in-memory file system. 492 | * 493 | * This provides implementations for all file descriptor and path-related WASI 494 | * functions, including `fd_read`, `fd_write`, `fd_seek`, `fd_tell`, `fd_close`, 495 | * `path_open`, and more to support a full featured file system environment. 496 | * 497 | * ```js 498 | * const wasi = new WASI({ 499 | * features: [useMemoryFS()], 500 | * }); 501 | * ``` 502 | * 503 | * You can provide a pre-configured file system instance: 504 | * 505 | * ```js 506 | * const fs = new MemoryFileSystem(); 507 | * fs.addFile("/hello.txt", "Hello, world!"); 508 | * 509 | * const wasi = new WASI({ 510 | * features: [useMemoryFS({ withFileSystem: fs })], 511 | * }); 512 | * ``` 513 | * 514 | * You can also combine it with standard IO: 515 | * 516 | * ```js 517 | * const wasi = new WASI({ 518 | * features: [ 519 | * useMemoryFS({ 520 | * withStdio: { 521 | * stdout: (lines) => document.write(lines), 522 | * stderr: (lines) => document.write(lines), 523 | * } 524 | * }) 525 | * ], 526 | * }); 527 | * ``` 528 | * 529 | * @param useOptions - Configuration options for the memory file system 530 | * @param useOptions.withFileSystem - Optional pre-configured file system instance 531 | * @param useOptions.withStdio - Optional standard I/O configuration 532 | * @returns A WASI feature provider implementing file system functionality 533 | */ 534 | export function useMemoryFS( 535 | useOptions: { 536 | withFileSystem?: MemoryFileSystem; 537 | withStdio?: StdioOptions; 538 | } = {}, 539 | ): WASIFeatureProvider { 540 | return ( 541 | wasiOptions: WASIOptions, 542 | abi: WASIAbi, 543 | memoryView: () => DataView, 544 | ) => { 545 | const fileSystem = 546 | useOptions.withFileSystem || new MemoryFileSystem(wasiOptions.preopens); 547 | const files: { [fd: FileDescriptor]: OpenFile } = {}; 548 | 549 | bindStdio(useOptions.withStdio || {}).forEach((entry, fd) => { 550 | files[fd] = { 551 | node: { type: "character", kind: "stdio", entry }, 552 | position: 0, 553 | isPreopen: false, 554 | path: `/dev/fd/${fd}`, 555 | fd, 556 | }; 557 | }); 558 | 559 | let nextFd = 3; 560 | for (const preopenPath of fileSystem.getPreopenPaths()) { 561 | const node = fileSystem.lookup(preopenPath); 562 | if (node && node.type === "dir") { 563 | files[nextFd] = { 564 | node, 565 | position: 0, 566 | isPreopen: true, 567 | preopenPath, 568 | path: preopenPath, 569 | fd: nextFd, 570 | }; 571 | nextFd++; 572 | } 573 | } 574 | 575 | function getFileFromPath(guestPath: string): OpenFile | null { 576 | for (const fd in files) { 577 | const file = files[fd]; 578 | if (file.path === guestPath) return file; 579 | } 580 | return null; 581 | } 582 | 583 | function getFileFromFD(fileDescriptor: FileDescriptor): OpenFile | null { 584 | const file = files[fileDescriptor]; 585 | return file || null; 586 | } 587 | 588 | return { 589 | fd_read: (fd: number, iovs: number, iovsLen: number, nread: number) => { 590 | const view = memoryView(); 591 | 592 | const iovViews = abi.iovViews(view, iovs, iovsLen); 593 | const file = getFileFromFD(fd); 594 | if (!file) { 595 | return WASIAbi.WASI_ERRNO_BADF; 596 | } 597 | 598 | if (file.node.type === "character" && file.node.kind === "stdio") { 599 | const bytesRead = file.node.entry.readv(iovViews); 600 | view.setUint32(nread, bytesRead, true); 601 | return WASIAbi.WASI_ESUCCESS; 602 | } 603 | 604 | if (file.node.type === "dir") { 605 | return WASIAbi.WASI_ERRNO_ISDIR; 606 | } 607 | 608 | if (file.node.type === "character" && file.node.kind === "devnull") { 609 | view.setUint32(nread, 0, true); 610 | return WASIAbi.WASI_ESUCCESS; 611 | } 612 | 613 | const fileNode = file.node; 614 | const data = fileNode.content; 615 | const available = data.byteLength - file.position; 616 | let totalRead = 0; 617 | if (available <= 0) { 618 | view.setUint32(nread, 0, true); 619 | return WASIAbi.WASI_ESUCCESS; 620 | } 621 | 622 | for (const buf of iovViews) { 623 | if (totalRead >= available) break; 624 | 625 | const bytesToRead = Math.min(buf.byteLength, available - totalRead); 626 | if (bytesToRead <= 0) break; 627 | 628 | const sourceStart = file.position + totalRead; 629 | const chunk = data.slice(sourceStart, sourceStart + bytesToRead); 630 | buf.set(chunk); 631 | totalRead += bytesToRead; 632 | } 633 | file.position += totalRead; 634 | view.setUint32(nread, totalRead, true); 635 | return WASIAbi.WASI_ESUCCESS; 636 | }, 637 | 638 | fd_write: ( 639 | fd: number, 640 | iovs: number, 641 | iovsLen: number, 642 | nwritten: number, 643 | ) => { 644 | const view = memoryView(); 645 | const iovViews = abi.iovViews(view, iovs, iovsLen); 646 | const file = getFileFromFD(fd); 647 | if (!file) return WASIAbi.WASI_ERRNO_BADF; 648 | let totalWritten = 0; 649 | 650 | if (file.node.type === "character" && file.node.kind === "stdio") { 651 | const bytesWritten = file.node.entry.writev(iovViews); 652 | view.setUint32(nwritten, bytesWritten, true); 653 | return WASIAbi.WASI_ESUCCESS; 654 | } 655 | 656 | if (file.node.type === "dir") return WASIAbi.WASI_ERRNO_ISDIR; 657 | 658 | if (file.node.type === "character" && file.node.kind === "devnull") { 659 | const total = iovViews.reduce((acc, buf) => acc + buf.byteLength, 0); 660 | view.setUint32(nwritten, total, true); 661 | return WASIAbi.WASI_ESUCCESS; 662 | } 663 | 664 | let pos = file.position; 665 | const dataToWrite = iovViews.reduce( 666 | (acc, buf) => acc + buf.byteLength, 667 | 0, 668 | ); 669 | const requiredLength = pos + dataToWrite; 670 | let newContent: Uint8Array; 671 | 672 | if (requiredLength > file.node.content.byteLength) { 673 | newContent = new Uint8Array(requiredLength); 674 | newContent.set(file.node.content, 0); 675 | } else { 676 | newContent = file.node.content; 677 | } 678 | 679 | for (const buf of iovViews) { 680 | newContent.set(buf, pos); 681 | pos += buf.byteLength; 682 | totalWritten += buf.byteLength; 683 | } 684 | 685 | file.node.content = newContent; 686 | file.position = pos; 687 | view.setUint32(nwritten, totalWritten, true); 688 | return WASIAbi.WASI_ESUCCESS; 689 | }, 690 | 691 | fd_close: (fd: number) => { 692 | const file = getFileFromFD(fd); 693 | if (!file) return WASIAbi.WASI_ERRNO_BADF; 694 | 695 | if (file.node.type === "character" && file.node.kind === "stdio") { 696 | file.node.entry.close(); 697 | return WASIAbi.WASI_ESUCCESS; 698 | } 699 | 700 | delete files[fd]; 701 | return WASIAbi.WASI_ESUCCESS; 702 | }, 703 | 704 | fd_seek: ( 705 | fd: number, 706 | offset: bigint, 707 | whence: number, 708 | newOffset: number, 709 | ) => { 710 | const view = memoryView(); 711 | if (fd < 3) return WASIAbi.WASI_ERRNO_BADF; 712 | 713 | const file = getFileFromFD(fd); 714 | if (!file || file.node.type !== "file") return WASIAbi.WASI_ERRNO_BADF; 715 | 716 | let pos = file.position; 717 | const fileLength = file.node.content.byteLength; 718 | 719 | switch (whence) { 720 | case 0: 721 | pos = Number(offset); 722 | break; 723 | case 1: 724 | pos = pos + Number(offset); 725 | break; 726 | case 2: 727 | pos = fileLength + Number(offset); 728 | break; 729 | default: 730 | return WASIAbi.WASI_ERRNO_INVAL; 731 | } 732 | 733 | if (pos < 0) pos = 0; 734 | file.position = pos; 735 | view.setUint32(newOffset, pos, true); 736 | return WASIAbi.WASI_ESUCCESS; 737 | }, 738 | 739 | fd_tell: (fd: number, offset_ptr: number) => { 740 | const view = memoryView(); 741 | if (fd < 3) return WASIAbi.WASI_ERRNO_BADF; 742 | 743 | const file = getFileFromFD(fd); 744 | if (!file) return WASIAbi.WASI_ERRNO_BADF; 745 | 746 | view.setBigUint64(offset_ptr, BigInt(file.position), true); 747 | return WASIAbi.WASI_ESUCCESS; 748 | }, 749 | 750 | fd_fdstat_get: (fd: number, buf: number) => { 751 | const view = memoryView(); 752 | const file = getFileFromFD(fd); 753 | if (!file) return WASIAbi.WASI_ERRNO_BADF; 754 | 755 | let filetype: number; 756 | switch (file.node.type) { 757 | case "character": 758 | filetype = WASIAbi.WASI_FILETYPE_CHARACTER_DEVICE; 759 | break; 760 | case "dir": 761 | filetype = WASIAbi.WASI_FILETYPE_DIRECTORY; 762 | break; 763 | case "file": 764 | filetype = WASIAbi.WASI_FILETYPE_REGULAR_FILE; 765 | break; 766 | } 767 | 768 | abi.writeFdstat(view, buf, filetype, 0); 769 | return WASIAbi.WASI_ESUCCESS; 770 | }, 771 | 772 | fd_filestat_get: (fd: number, buf: number) => { 773 | const view = memoryView(); 774 | const entry = getFileFromFD(fd); 775 | if (!entry) return WASIAbi.WASI_ERRNO_BADF; 776 | 777 | let filetype: number; 778 | let size = 0; 779 | switch (entry.node.type) { 780 | case "character": 781 | filetype = WASIAbi.WASI_FILETYPE_CHARACTER_DEVICE; 782 | break; 783 | case "dir": 784 | filetype = WASIAbi.WASI_FILETYPE_DIRECTORY; 785 | break; 786 | case "file": 787 | filetype = WASIAbi.WASI_FILETYPE_REGULAR_FILE; 788 | size = entry.node.content.byteLength; 789 | break; 790 | } 791 | 792 | abi.writeFilestat(view, buf, filetype); 793 | view.setBigUint64(buf + 32, BigInt(size), true); 794 | return WASIAbi.WASI_ESUCCESS; 795 | }, 796 | 797 | fd_prestat_get: (fd: number, buf: number) => { 798 | const view = memoryView(); 799 | if (fd < 3) return WASIAbi.WASI_ERRNO_BADF; 800 | 801 | const file = getFileFromFD(fd); 802 | if (!file || !file.isPreopen) return WASIAbi.WASI_ERRNO_BADF; 803 | 804 | view.setUint8(buf, 0); 805 | const pathStr = file.preopenPath || ""; 806 | view.setUint32(buf + 4, pathStr.length, true); 807 | return WASIAbi.WASI_ESUCCESS; 808 | }, 809 | 810 | fd_prestat_dir_name: (fd: number, pathPtr: number, pathLen: number) => { 811 | if (fd < 3) return WASIAbi.WASI_ERRNO_BADF; 812 | 813 | const file = getFileFromFD(fd); 814 | if (!file || !file.isPreopen) return WASIAbi.WASI_ERRNO_BADF; 815 | 816 | const pathStr = file.preopenPath || ""; 817 | if (pathStr.length !== pathLen) return WASIAbi.WASI_ERRNO_INVAL; 818 | 819 | const view = memoryView(); 820 | for (let i = 0; i < pathStr.length; i++) { 821 | view.setUint8(pathPtr + i, pathStr.charCodeAt(i)); 822 | } 823 | 824 | return WASIAbi.WASI_ESUCCESS; 825 | }, 826 | 827 | path_open: ( 828 | dirfd: number, 829 | _dirflags: number, 830 | pathPtr: number, 831 | pathLen: number, 832 | oflags: number, 833 | _fs_rights_base: bigint, 834 | _fs_rights_inheriting: bigint, 835 | _fdflags: number, 836 | opened_fd: number, 837 | ) => { 838 | const view = memoryView(); 839 | 840 | if (dirfd < 3) return WASIAbi.WASI_ERRNO_NOTDIR; 841 | 842 | const dirEntry = getFileFromFD(dirfd); 843 | if (!dirEntry || dirEntry.node.type !== "dir") 844 | return WASIAbi.WASI_ERRNO_NOTDIR; 845 | 846 | const path = abi.readString(view, pathPtr, pathLen); 847 | 848 | const guestPath = normalizePath( 849 | (dirEntry.path.endsWith("/") ? dirEntry.path : dirEntry.path + "/") + 850 | path, 851 | ); 852 | 853 | const existing = getFileFromPath(guestPath); 854 | if (existing) { 855 | view.setUint32(opened_fd, existing.fd, true); 856 | return WASIAbi.WASI_ESUCCESS; 857 | } 858 | 859 | let target = fileSystem.resolve(dirEntry.node as DirectoryNode, path); 860 | 861 | if (target) { 862 | if (oflags & WASIAbi.WASI_OFLAGS_EXCL) 863 | return WASIAbi.WASI_ERRNO_EXIST; 864 | if (oflags & WASIAbi.WASI_OFLAGS_TRUNC) { 865 | if (target.type !== "file") return WASIAbi.WASI_ERRNO_INVAL; 866 | (target as FileNode).content = new Uint8Array(0); 867 | } 868 | } else { 869 | if (!(oflags & WASIAbi.WASI_OFLAGS_CREAT)) 870 | return WASIAbi.WASI_ERRNO_NOENT; 871 | target = fileSystem.createFileIn( 872 | dirEntry.node as DirectoryNode, 873 | path, 874 | ); 875 | } 876 | 877 | files[nextFd] = { 878 | node: target, 879 | position: 0, 880 | isPreopen: false, 881 | path: guestPath, 882 | fd: nextFd, 883 | }; 884 | 885 | view.setUint32(opened_fd, nextFd, true); 886 | nextFd++; 887 | return WASIAbi.WASI_ESUCCESS; 888 | }, 889 | 890 | path_create_directory: (fd: number, pathPtr: number, pathLen: number) => { 891 | const view = memoryView(); 892 | const guestRelPath = abi.readString(view, pathPtr, pathLen); 893 | const dirEntry = getFileFromFD(fd); 894 | if (!dirEntry || dirEntry.node.type !== "dir") 895 | return WASIAbi.WASI_ERRNO_NOTDIR; 896 | 897 | const fullGuestPath = 898 | (dirEntry.path.endsWith("/") ? dirEntry.path : dirEntry.path + "/") + 899 | guestRelPath; 900 | 901 | fileSystem.ensureDir(fullGuestPath); 902 | return WASIAbi.WASI_ESUCCESS; 903 | }, 904 | 905 | path_unlink_file: (fd: number, pathPtr: number, pathLen: number) => { 906 | const view = memoryView(); 907 | const guestRelPath = abi.readString(view, pathPtr, pathLen); 908 | const dirEntry = getFileFromFD(fd); 909 | if (!dirEntry || dirEntry.node.type !== "dir") 910 | return WASIAbi.WASI_ERRNO_NOTDIR; 911 | 912 | const fullGuestPath = 913 | (dirEntry.path.endsWith("/") ? dirEntry.path : dirEntry.path + "/") + 914 | guestRelPath; 915 | 916 | fileSystem.removeEntry(fullGuestPath); 917 | return WASIAbi.WASI_ESUCCESS; 918 | }, 919 | 920 | path_remove_directory: (fd: number, pathPtr: number, pathLen: number) => { 921 | const view = memoryView(); 922 | const guestRelPath = abi.readString(view, pathPtr, pathLen); 923 | const dirEntry = getFileFromFD(fd); 924 | if (!dirEntry || dirEntry.node.type !== "dir") 925 | return WASIAbi.WASI_ERRNO_NOTDIR; 926 | 927 | const fullGuestPath = 928 | (dirEntry.path.endsWith("/") ? dirEntry.path : dirEntry.path + "/") + 929 | guestRelPath; 930 | 931 | fileSystem.removeEntry(fullGuestPath); 932 | return WASIAbi.WASI_ESUCCESS; 933 | }, 934 | 935 | path_filestat_get: ( 936 | fd: number, 937 | flags: number, 938 | pathPtr: number, 939 | pathLen: number, 940 | buf: number, 941 | ) => { 942 | const view = memoryView(); 943 | 944 | // Get the base FD entry; it must be a directory. 945 | const file = getFileFromFD(fd); 946 | if (!file) return WASIAbi.WASI_ERRNO_BADF; 947 | if (file.node.type !== "dir") { 948 | return WASIAbi.WASI_ERRNO_NOTDIR; 949 | } 950 | 951 | const guestRelPath = abi.readString(view, pathPtr, pathLen); 952 | 953 | // Compute the full guest path. 954 | const basePath = file.path; 955 | const fullGuestPath = basePath.endsWith("/") 956 | ? basePath + guestRelPath 957 | : basePath + "/" + guestRelPath; 958 | 959 | // Lookup the node in the MemoryFS. 960 | const node = fileSystem.lookup(fullGuestPath); 961 | if (!node) return WASIAbi.WASI_ERRNO_NOENT; 962 | if (node.type === "character" && node.kind === "stdio") { 963 | return WASIAbi.WASI_ERRNO_INVAL; 964 | } 965 | 966 | // Determine file type and size. 967 | let filetype: number; 968 | let size = 0; 969 | if (node.type === "dir") { 970 | filetype = WASIAbi.WASI_FILETYPE_DIRECTORY; 971 | } else if (node.type === "character" && node.kind === "devnull") { 972 | filetype = WASIAbi.WASI_FILETYPE_CHARACTER_DEVICE; 973 | } else { 974 | filetype = WASIAbi.WASI_FILETYPE_REGULAR_FILE; 975 | size = node.content.byteLength; 976 | } 977 | 978 | abi.writeFilestat(view, buf, filetype); 979 | view.setBigUint64(buf + 32, BigInt(size), true); 980 | return WASIAbi.WASI_ESUCCESS; 981 | }, 982 | }; 983 | }; 984 | } 985 | 986 | export function useFS(useOptions: { fs: any }): WASIFeatureProvider { 987 | return (options: WASIOptions, abi: WASIAbi, memoryView: () => DataView) => { 988 | // TODO: implement fd_* syscalls using `useOptions.fs` 989 | return {}; 990 | }; 991 | } 992 | --------------------------------------------------------------------------------