├── test ├── abort │ ├── CMakeLists.txt │ ├── abort.c │ └── index.js ├── clock │ ├── CMakeLists.txt │ ├── index.js │ └── clock.c ├── stdin │ ├── CMakeLists.txt │ ├── stdin.c │ └── index.js ├── getenv │ ├── CMakeLists.txt │ ├── getenv.c │ └── index.js ├── getentropy │ ├── CMakeLists.txt │ ├── getentropy.c │ └── index.js ├── node.importmap ├── stdout │ ├── CMakeLists.txt │ ├── stderr.c │ ├── stdout.c │ └── index.js ├── exit │ ├── exit_failure.c │ ├── exit_success.c │ ├── CMakeLists.txt │ └── index.js ├── assert │ ├── CMakeLists.txt │ ├── assert_true.c │ ├── assert_false.c │ └── index.js ├── thread │ ├── CMakeLists.txt │ ├── thread.c │ ├── index.js │ └── worker.js ├── jspi │ ├── jspi.c │ ├── build.bat │ ├── CMakeLists.txt │ └── index.js ├── memory │ ├── CMakeLists.txt │ ├── memory.c │ └── index.js ├── asyncify │ ├── asyncify.c │ ├── CMakeLists.txt │ └── index.js ├── src │ ├── base64.h │ ├── a.html │ ├── main.c │ ├── index.js │ └── base64.c ├── directory │ ├── directory.c │ ├── CMakeLists.txt │ └── index.js ├── package.json ├── test.cmake ├── build.sh ├── build.bat ├── ftruncate │ ├── ftruncate.c │ ├── CMakeLists.txt │ └── index.js ├── wasi_snapshot_preview1.txt ├── webpack.config.js ├── CMakeLists.txt └── index.html ├── .eslintignore ├── .vscode ├── settings.json └── c_cpp_properties.json ├── .npmignore ├── .gitignore ├── src ├── index.ts ├── webassembly.ts ├── memory.ts ├── jspi.ts ├── load.ts ├── wasi │ ├── util.ts │ ├── error.ts │ ├── path.ts │ ├── rights.ts │ ├── types.ts │ ├── fs.ts │ ├── index.ts │ └── fd.ts └── asyncify.ts ├── tsconfig.json ├── .eslintrc.js ├── package.json ├── api-extractor.json ├── tsgo.config.js └── README.md /test/abort/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(../test.cmake) 2 | 3 | test("abort" "abort.c" "") 4 | -------------------------------------------------------------------------------- /test/clock/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(../test.cmake) 2 | 3 | test("clock" "clock.c" "") 4 | -------------------------------------------------------------------------------- /test/stdin/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(../test.cmake) 2 | 3 | test("stdin" "stdin.c" "") 4 | -------------------------------------------------------------------------------- /test/getenv/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(../test.cmake) 2 | 3 | test("getenv" "getenv.c" "") 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/dist 2 | node_modules 3 | /lib 4 | .eslintrc.js 5 | /miniprogram_dist 6 | /test 7 | -------------------------------------------------------------------------------- /test/getentropy/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(../test.cmake) 2 | 3 | test("getentropy" "getentropy.c" "") 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "api-extractor.json": "jsonc" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/abort/abort.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(void) { 4 | abort(); 5 | 6 | return 0; 7 | } 8 | -------------------------------------------------------------------------------- /test/node.importmap: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "@tybys/wasm-util": "../lib/mjs/index.mjs" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/stdout/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(../test.cmake) 2 | 3 | test("stdout" "stdout.c" "") 4 | test("stderr" "stderr.c" "") 5 | -------------------------------------------------------------------------------- /test/exit/exit_failure.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main() { 5 | exit(EXIT_FAILURE); 6 | assert(0); 7 | } 8 | -------------------------------------------------------------------------------- /test/exit/exit_success.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main() { 5 | exit(EXIT_SUCCESS); 6 | assert(0); 7 | } 8 | -------------------------------------------------------------------------------- /test/assert/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(../test.cmake) 2 | 3 | test("assert_false" "assert_false.c" "") 4 | test("assert_true" "assert_true.c" "") 5 | -------------------------------------------------------------------------------- /test/assert/assert_true.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(void) { 5 | assert(true); 6 | 7 | return 0; 8 | } 9 | -------------------------------------------------------------------------------- /test/exit/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(../test.cmake) 2 | 3 | test("exit_failure" "exit_failure.c" "") 4 | test("exit_success" "exit_success.c" "") 5 | -------------------------------------------------------------------------------- /test/assert/assert_false.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(void) { 5 | assert(false); 6 | 7 | return 0; 8 | } 9 | -------------------------------------------------------------------------------- /test/thread/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(../test.cmake) 2 | 3 | test("thread" "thread.c" "-mexec-model=reactor;-Wl,--import-memory,--max-memory=2147483648") 4 | -------------------------------------------------------------------------------- /test/jspi/jspi.c: -------------------------------------------------------------------------------- 1 | #define WASI_EXPORT __attribute__((visibility("default"))) 2 | 3 | void async_sleep(int ms); 4 | 5 | int main() { 6 | async_sleep(200); 7 | return 0; 8 | } 9 | -------------------------------------------------------------------------------- /test/memory/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(../test.cmake) 2 | 3 | test("memory_import" "memory.c" "-Wl,--import-memory") 4 | test("memory_export" "memory.c" "-Wl,--initial-memory=16777216") 5 | -------------------------------------------------------------------------------- /test/stdout/stderr.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(void) { 5 | if (fputs("Hello, stderr!\n", stderr) == 0) { 6 | return ferror(stdout); 7 | } 8 | return 0; 9 | } 10 | -------------------------------------------------------------------------------- /test/stdout/stdout.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(void) { 5 | if (fputs("Hello, stdout!\n", stdout) == 0) { 6 | return ferror(stdout); 7 | } 8 | return 0; 9 | } 10 | -------------------------------------------------------------------------------- /test/asyncify/asyncify.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define WASI_EXPORT __attribute__((visibility("default"))) 4 | 5 | void async_sleep(int ms); 6 | 7 | int main() { 8 | async_sleep(200); 9 | return 0; 10 | } 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .eslint* 3 | api-extractor.json 4 | tsconfig* 5 | /src 6 | /scripts 7 | .vscode 8 | /temp 9 | /docs 10 | /coverage 11 | /test 12 | *.map 13 | tsgo.config.js 14 | jest.config* 15 | /lib/esm-bundler 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | .DS_Store 3 | node_modules 4 | /dist 5 | /lib 6 | /temp 7 | /coverage 8 | miniprogram_dist 9 | /docs 10 | /test/build 11 | /test/webpack 12 | /test/**/*.wasm 13 | /test/**/*.wat 14 | /test/**/*.o 15 | -------------------------------------------------------------------------------- /test/getenv/getenv.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int main(void) { 6 | assert(getenv("ABSENT") == NULL); 7 | assert(strcmp(getenv("PRESENT"), "1") == 0); 8 | return 0; 9 | } 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @packageDocumentation 3 | */ 4 | 5 | export * from './asyncify' 6 | export * from './load' 7 | export * from './wasi/index' 8 | export * from './memory' 9 | export * from './jspi' 10 | export * from './wasi/fs' 11 | -------------------------------------------------------------------------------- /test/memory/memory.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | void js_log(void* n, size_t s); 5 | 6 | int main() { 7 | int* p = malloc(sizeof(int)); 8 | *p = 233; 9 | js_log(p, sizeof(int)); 10 | free(p); 11 | return 0; 12 | } 13 | -------------------------------------------------------------------------------- /test/stdin/stdin.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | int main(void) { 7 | char data[32]; 8 | assert(fgets(data, sizeof(data), stdin) != NULL); 9 | assert(strcmp(data, "Hello, stdin!\n") == 0); 10 | 11 | return EXIT_SUCCESS; 12 | } 13 | -------------------------------------------------------------------------------- /test/jspi/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | clang -O3 -o jspi.wasm --target=wasm32-unknown-wasi jspi.c -Wl,--import-undefined,--export-dynamic 4 | wasm-opt -O3 --enable-reference-types --jspi --pass-arg=jspi-imports@env.async_sleep --pass-arg=jspi-exports@_start -o jspi.wasm jspi.wasm 5 | wasm2wat -o jspi.wat jspi.wasm 6 | -------------------------------------------------------------------------------- /test/asyncify/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(../test.cmake) 2 | 3 | find_program(WASM_OPT "wasm-opt") 4 | 5 | test("asyncify" "asyncify.c" "") 6 | 7 | add_custom_command(TARGET asyncify POST_BUILD 8 | COMMAND ${WASM_OPT} "--asyncify" "--pass-arg=asyncify-imports@env.async_sleep" "$" "-o" "$") 9 | -------------------------------------------------------------------------------- /test/getentropy/getentropy.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int main() { 6 | char buf[256] = {0}; 7 | assert(getentropy(buf, 256) == 0); 8 | 9 | for (int i = 0; i < 256; i++) { 10 | if (buf[i] != 0) { 11 | return EXIT_SUCCESS; 12 | } 13 | } 14 | 15 | return EXIT_FAILURE; 16 | } 17 | -------------------------------------------------------------------------------- /test/clock/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('clock', function () { 4 | it('clock', async function () { 5 | const wasi = new wasmUtil.WASI({ 6 | returnOnExit: true 7 | }) 8 | const { instance } = await wasmUtil.load('/test/clock/clock.wasm', wasi.getImportObject()) 9 | 10 | wasi.start(instance) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/webassembly.ts: -------------------------------------------------------------------------------- 1 | declare const WXWebAssembly: typeof WebAssembly 2 | 3 | const _WebAssembly: typeof WebAssembly = typeof WebAssembly !== 'undefined' 4 | ? WebAssembly 5 | : typeof WXWebAssembly !== 'undefined' 6 | ? WXWebAssembly 7 | : undefined! 8 | 9 | if (!_WebAssembly) { 10 | throw new Error('WebAssembly is not supported in this environment') 11 | } 12 | 13 | export { _WebAssembly } 14 | -------------------------------------------------------------------------------- /test/getentropy/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('getentropy', function () { 4 | it('getentropy', async function () { 5 | const wasi = new wasmUtil.WASI({ 6 | returnOnExit: true 7 | }) 8 | const { instance } = await wasmUtil.load('/test/getentropy/getentropy.wasm', wasi.getImportObject()) 9 | 10 | wasi.start(instance) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /test/getenv/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('getenv', function () { 4 | it('getenv', async function () { 5 | const wasi = new wasmUtil.WASI({ 6 | returnOnExit: true, 7 | env: { 8 | PRESENT: '1' 9 | } 10 | }) 11 | const { instance } = await wasmUtil.load('/test/getenv/getenv.wasm', wasi.getImportObject()) 12 | 13 | wasi.start(instance) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/abort/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('abort', function () { 4 | it('should throw RuntimeError', async function () { 5 | const wasi = new wasmUtil.WASI({ 6 | returnOnExit: true 7 | }) 8 | const { instance } = await wasmUtil.load('/test/abort/abort.wasm', wasi.getImportObject()) 9 | 10 | assertThrow(() => { 11 | wasi.start(instance) 12 | }, WebAssembly.RuntimeError, /unreachable/) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /test/src/base64.h: -------------------------------------------------------------------------------- 1 | #ifndef SRC_BASE64_H_ 2 | #define SRC_BASE64_H_ 3 | 4 | #include 5 | 6 | #define WASI_EXPORT __attribute__((visibility("default"))) 7 | 8 | #ifdef __cplusplus 9 | extern "C" { 10 | #endif 11 | 12 | WASI_EXPORT size_t base64_encode(const unsigned char* src, size_t len, char* dst); 13 | WASI_EXPORT size_t base64_decode(const char* src, size_t len, unsigned char* dst); 14 | 15 | #ifdef __cplusplus 16 | } 17 | #endif 18 | 19 | #endif // SRC_BASE64_H_ 20 | -------------------------------------------------------------------------------- /test/directory/directory.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int main() { 6 | FILE* file = fopen("fopen-directory-parent-directory/..", "r"); 7 | assert(file == NULL); 8 | assert(errno == ENOENT); 9 | 10 | file = fopen("..", "r"); 11 | assert(file == NULL); 12 | assert(errno == ENOENT); 13 | 14 | file = fopen("fopen-working-directory.c", "r"); 15 | assert(file == NULL); 16 | assert(errno == ENOENT); 17 | 18 | return 0; 19 | } 20 | -------------------------------------------------------------------------------- /test/jspi/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(../test.cmake) 2 | 3 | find_program(WASM_OPT "wasm-opt") 4 | 5 | test("jspi" "jspi.c" "") 6 | # target_link_options("jspi" PRIVATE "-mexec-model=reactor") 7 | 8 | add_custom_command(TARGET jspi POST_BUILD 9 | COMMAND ${WASM_OPT} 10 | "--enable-reference-types" 11 | "--jspi" 12 | "--pass-arg=jspi-imports@env.async_sleep" 13 | "--pass-arg=jspi-exports@_start" 14 | "$" 15 | "-o" 16 | "$" 17 | ) 18 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "main": "index.js", 4 | "scripts": { 5 | "build": "webpack", 6 | "a": "node --experimental-loader @node-loader/import-maps --experimental-wasi-unstable-preview1 ./src/index.js", 7 | "b": "node --experimental-loader @node-loader/import-maps --experimental-wasi-unstable-preview1 ./src/asyncify/asyncify.js" 8 | }, 9 | "devDependencies": { 10 | "webpack": "^5.74.0", 11 | "webpack-cli": "^4.10.0" 12 | }, 13 | "dependencies": { 14 | "@node-loader/import-maps": "^1.1.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/test.cmake: -------------------------------------------------------------------------------- 1 | function(test TARGET SRCLIST LINKOPTIONS) 2 | add_executable(${TARGET} ${SRCLIST}) 3 | set_target_properties(${TARGET} PROPERTIES SUFFIX ".wasm") 4 | set_target_properties(${TARGET} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") 5 | 6 | target_link_options(${TARGET} PRIVATE 7 | # "-v" 8 | "-Wl,--export-dynamic,--import-undefined" 9 | ${LINKOPTIONS} 10 | ) 11 | 12 | if(CMAKE_BUILD_TYPE STREQUAL "Release") 13 | target_link_options(${TARGET} PRIVATE 14 | "-Wl,--strip-debug" 15 | ) 16 | endif() 17 | endfunction(test) 18 | -------------------------------------------------------------------------------- /test/clock/clock.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | int main() { 7 | struct timespec ts1; 8 | assert(clock_getres(CLOCK_MONOTONIC, &ts1) == 0); 9 | struct timespec ts2; 10 | assert(clock_getres(CLOCK_REALTIME, &ts2) == 0); 11 | struct timespec ts3; 12 | assert(clock_gettime(CLOCK_MONOTONIC, &ts3) == 0); 13 | struct timespec ts4; 14 | assert(clock_gettime(CLOCK_REALTIME, &ts4) == 0); 15 | long long milliseconds = (ts4.tv_sec * 1000) + (ts4.tv_nsec / 1000000); 16 | printf("now: %lld\n", milliseconds); 17 | return EXIT_SUCCESS; 18 | } 19 | -------------------------------------------------------------------------------- /test/stdin/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('stdin', function () { 4 | it('stdin', async function () { 5 | this.timeout = Infinity 6 | const prompt = window.prompt 7 | window.prompt = function (message, defaultValue) { 8 | return prompt.call(window, 'Test stdin', 'Hello, stdin!') 9 | } 10 | const wasi = new wasmUtil.WASI({ 11 | returnOnExit: true 12 | }) 13 | const { instance } = await wasmUtil.load('/test/stdin/stdin.wasm', wasi.getImportObject()) 14 | 15 | assert(0 === wasi.start(instance)) 16 | window.prompt = prompt 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /test/src/a.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | a 8 | 9 | 10 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./lib/cjs", 4 | "allowJs": true, 5 | "target": "ES2020", 6 | "module": "CommonJS", 7 | "strict": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "noImplicitAny": true, 11 | "noImplicitThis": true, 12 | "noImplicitReturns": true, 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "experimentalDecorators": true, 16 | "importsNotUsedAsValues": "error", 17 | "importHelpers": true, 18 | "noEmitHelpers": true, 19 | "types": ["node"] 20 | }, 21 | "include": [ 22 | "./src/**/*" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /test/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf ./build 4 | 5 | mkdir -p ./build 6 | 7 | cmake -DCMAKE_TOOLCHAIN_FILE=$WASI_SDK_PATH/share/cmake/wasi-sdk-pthread.cmake \ 8 | -DWASI_SDK_PREFIX=$WASI_SDK_PATH \ 9 | -DCMAKE_VERBOSE_MAKEFILE=ON \ 10 | -DCMAKE_BUILD_TYPE=Debug \ 11 | -H. -Bbuild -G Ninja 12 | 13 | cmake --build build 14 | 15 | rm -rf ./build 16 | 17 | mkdir -p ./build 18 | 19 | cmake -DCMAKE_TOOLCHAIN_FILE=$WASI_SDK_PATH/share/cmake/wasi-sdk.cmake \ 20 | -DWASI_SDK_PREFIX=$WASI_SDK_PATH \ 21 | -DCMAKE_VERBOSE_MAKEFILE=ON \ 22 | -DCMAKE_BUILD_TYPE=Debug \ 23 | -H. -Bbuild -G Ninja 24 | 25 | cmake --build build 26 | -------------------------------------------------------------------------------- /test/stdout/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('stdout', function () { 4 | it('stdout', async function () { 5 | const wasi = new wasmUtil.WASI({ 6 | returnOnExit: true 7 | }) 8 | const { instance } = await wasmUtil.load('/test/stdout/stdout.wasm', wasi.getImportObject()) 9 | 10 | assert(0 === wasi.start(instance)) 11 | }) 12 | 13 | it('stderr', async function () { 14 | const wasi = new wasmUtil.WASI({ 15 | returnOnExit: true 16 | }) 17 | const { instance } = await wasmUtil.load('/test/stdout/stderr.wasm', wasi.getImportObject()) 18 | 19 | assert(0 === wasi.start(instance)) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set WASI_SDK_PATH=%WASI_SDK_PATH:\=/% 4 | 5 | rd /s /q build 6 | 7 | cmake -DCMAKE_TOOLCHAIN_FILE=%WASI_SDK_PATH%/share/cmake/wasi-sdk-pthread.cmake^ 8 | -DWASI_SDK_PREFIX=%WASI_SDK_PATH%^ 9 | -DCMAKE_VERBOSE_MAKEFILE=ON^ 10 | -DCMAKE_BUILD_TYPE=Debug^ 11 | -H. -Bbuild -G Ninja 12 | 13 | cmake --build build 14 | 15 | rd /s /q build 16 | 17 | set WASI_SDK_PATH=C:/wasi-sdk-19.0 18 | 19 | cmake -DCMAKE_TOOLCHAIN_FILE=%WASI_SDK_PATH%/share/cmake/wasi-sdk.cmake^ 20 | -DWASI_SDK_PREFIX=%WASI_SDK_PATH%^ 21 | -DCMAKE_VERBOSE_MAKEFILE=ON^ 22 | -DCMAKE_BUILD_TYPE=Debug^ 23 | -H. -Bbuild -G Ninja 24 | 25 | cmake --build build 26 | -------------------------------------------------------------------------------- /test/exit/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('exit', function () { 4 | 5 | 6 | it('exit failure', async function () { 7 | const wasi = new wasmUtil.WASI({ 8 | returnOnExit: true 9 | }) 10 | const { instance } = await wasmUtil.load('/test/exit/exit_failure.wasm', wasi.getImportObject()) 11 | 12 | const code = wasi.start(instance) 13 | assert(code === 1) 14 | }) 15 | 16 | it('exit success', async function () { 17 | const wasi = new wasmUtil.WASI({ 18 | returnOnExit: true 19 | }) 20 | const { instance } = await wasmUtil.load('/test/exit/exit_success.wasm', wasi.getImportObject()) 21 | 22 | const code = wasi.start(instance) 23 | assert(code === 0) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /test/ftruncate/ftruncate.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | int main() { 7 | struct stat st; 8 | int fd; 9 | 10 | fd = open("ftruncate.dir/ftruncate.txt", O_CREAT | O_WRONLY, 0666); 11 | assert(fd != -1); 12 | 13 | assert(0 == fstat(fd, &st)); 14 | assert(st.st_size == 0); 15 | assert(0 == lseek(fd, 0, SEEK_CUR)); 16 | 17 | assert(0 == ftruncate(fd, 500)); 18 | assert(0 == fstat(fd, &st)); 19 | assert(st.st_size == 500); 20 | assert(0 == lseek(fd, 0, SEEK_CUR)); 21 | 22 | assert(0 == ftruncate(fd, 300)); 23 | assert(0 == fstat(fd, &st)); 24 | assert(st.st_size == 300); 25 | assert(0 == lseek(fd, 0, SEEK_CUR)); 26 | 27 | assert(0 == close(fd)); 28 | return 0; 29 | } 30 | -------------------------------------------------------------------------------- /test/assert/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('assert', function () { 4 | 5 | 6 | it('assert false', async function () { 7 | const wasi = new wasmUtil.WASI({ 8 | returnOnExit: true 9 | }) 10 | const { instance } = await wasmUtil.load('/test/assert/assert_false.wasm', wasi.getImportObject()) 11 | 12 | assertThrow(() => { 13 | wasi.start(instance) 14 | }, WebAssembly.RuntimeError, /unreachable/) 15 | }) 16 | 17 | it('assert true', async function () { 18 | const wasi = new wasmUtil.WASI({ 19 | returnOnExit: true 20 | }) 21 | const { instance } = await wasmUtil.load('/test/assert/assert_true.wasm', wasi.getImportObject()) 22 | 23 | wasi.start(instance) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /test/ftruncate/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(../test.cmake) 2 | 3 | test("ftruncate" "ftruncate.c" "") 4 | test("ftruncate_asyncify" "ftruncate.c" "") 5 | test("ftruncate_jspi" "ftruncate.c" "") 6 | 7 | add_custom_command(TARGET "ftruncate_asyncify" POST_BUILD 8 | COMMAND ${WASM_OPT} 9 | "--asyncify" 10 | "--pass-arg=asyncify-imports@@${CMAKE_SOURCE_DIR}/wasi_snapshot_preview1.txt" 11 | "$" 12 | "-o" 13 | "$" 14 | ) 15 | 16 | add_custom_command(TARGET "ftruncate_jspi" POST_BUILD 17 | COMMAND ${WASM_OPT} 18 | "--enable-reference-types" 19 | "--jspi" 20 | "--pass-arg=jspi-imports@@${CMAKE_SOURCE_DIR}/wasi_snapshot_preview1.txt" 21 | "--pass-arg=jspi-exports@_start" 22 | "$" 23 | "-o" 24 | "$" 25 | ) 26 | -------------------------------------------------------------------------------- /test/wasi_snapshot_preview1.txt: -------------------------------------------------------------------------------- 1 | wasi_snapshot_preview1.fd_allocate,wasi_snapshot_preview1.fd_close,wasi_snapshot_preview1.fd_datasync,wasi_snapshot_preview1.fd_filestat_get,wasi_snapshot_preview1.fd_filestat_set_size,wasi_snapshot_preview1.fd_filestat_set_times,wasi_snapshot_preview1.fd_pread,wasi_snapshot_preview1.fd_pwrite,wasi_snapshot_preview1.fd_read,wasi_snapshot_preview1.fd_readdir,wasi_snapshot_preview1.fd_renumber,wasi_snapshot_preview1.fd_sync,wasi_snapshot_preview1.fd_write,wasi_snapshot_preview1.path_create_directory,wasi_snapshot_preview1.path_filestat_get,wasi_snapshot_preview1.path_filestat_set_times,wasi_snapshot_preview1.path_link,wasi_snapshot_preview1.path_open,wasi_snapshot_preview1.path_readlink,wasi_snapshot_preview1.path_remove_directory,wasi_snapshot_preview1.path_rename,wasi_snapshot_preview1.path_symlink,wasi_snapshot_preview1.path_unlink_file -------------------------------------------------------------------------------- /test/directory/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(../test.cmake) 2 | 3 | find_program(WASM_OPT "wasm-opt") 4 | 5 | test("directory" "directory.c" "") 6 | test("directory_asyncify" "directory.c" "") 7 | test("directory_jspi" "directory.c" "") 8 | 9 | add_custom_command(TARGET "directory_asyncify" POST_BUILD 10 | COMMAND ${WASM_OPT} 11 | "--asyncify" 12 | "--pass-arg=asyncify-imports@@${CMAKE_SOURCE_DIR}/wasi_snapshot_preview1.txt" 13 | "$" 14 | "-o" 15 | "$" 16 | ) 17 | 18 | add_custom_command(TARGET "directory_jspi" POST_BUILD 19 | COMMAND ${WASM_OPT} 20 | "--enable-reference-types" 21 | "--jspi" 22 | "--pass-arg=jspi-imports@@${CMAKE_SOURCE_DIR}/wasi_snapshot_preview1.txt" 23 | "--pass-arg=jspi-exports@_start" 24 | "$" 25 | "-o" 26 | "$" 27 | ) 28 | -------------------------------------------------------------------------------- /test/webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | 4 | const __filename = fileURLToPath(import.meta.url) 5 | const __dirname = path.dirname(__filename) 6 | 7 | /** @type {import('webpack').Configuration} */ 8 | const config = { 9 | mode: 'production', 10 | entry: { 11 | a: path.join(__dirname, './src/index.js'), 12 | b: path.join(__dirname, './src/asyncify/asyncify.js') 13 | }, 14 | output: { 15 | filename: '[name].js', 16 | path: path.join(__dirname, './webpack') 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.wasm$/, 22 | type: 'asset/resource' 23 | } 24 | ] 25 | }, 26 | resolve: { 27 | alias: { 28 | '@tybys/wasm-util': path.join(__dirname, '..') 29 | } 30 | }, 31 | experiments: { 32 | topLevelAwait: true 33 | } 34 | } 35 | 36 | export default config 37 | -------------------------------------------------------------------------------- /test/jspi/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('jspi', function () { 4 | it('sleep 200ms', async function () { 5 | const wasi = new wasmUtil.WASI({ 6 | returnOnExit: true 7 | }) 8 | 9 | const imports = { 10 | env: { 11 | async_sleep: wasmUtil.wrapAsyncImport(function (ms) { 12 | return new Promise((resolve) => { 13 | setTimeout(resolve, ms) 14 | }) 15 | }, ['i32'], []) 16 | }, 17 | ...wasi.getImportObject() 18 | } 19 | const bytes = await (await fetch('/test/jspi/jspi.wasm')).arrayBuffer() 20 | const { instance } = await WebAssembly.instantiate(bytes, imports) 21 | const promisifiedInstance = Object.create(WebAssembly.Instance.prototype) 22 | Object.defineProperty(promisifiedInstance, 'exports', { value: wasmUtil.wrapExports(instance.exports, ['_start']) }) 23 | const p = wasi.start(promisifiedInstance) 24 | console.log(p) 25 | const now = Date.now() 26 | assert(0 === await p) 27 | assert(Date.now() - now >= 200) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /test/asyncify/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('asyncify', function () { 4 | it('sleep 200ms', async function () { 5 | const wasi = new wasmUtil.WASI({ 6 | returnOnExit: true 7 | }) 8 | 9 | const asyncify = new wasmUtil.Asyncify() 10 | 11 | const imports = { 12 | env: { 13 | async_sleep: asyncify.wrapImportFunction(function (ms) { 14 | return new Promise((resolve) => { 15 | setTimeout(resolve, ms) 16 | }) 17 | }) 18 | }, 19 | ...wasi.getImportObject() 20 | } 21 | const bytes = await (await fetch('/test/asyncify/asyncify.wasm')).arrayBuffer() 22 | const { instance } = await WebAssembly.instantiate(bytes, imports) 23 | const asyncifedInstance = asyncify.init(instance.exports.memory, instance, { 24 | wrapExports: ['_start'] 25 | }) 26 | 27 | const p = wasi.start(asyncifedInstance) 28 | assert(typeof p.then === 'function') 29 | const now = Date.now() 30 | assert(0 === await p) 31 | assert(Date.now() - now >= 200) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/thread/thread.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | int nanosleep(const struct timespec *, struct timespec *); 10 | 11 | void uv_sleep(unsigned int msec) { 12 | struct timespec timeout; 13 | int rc; 14 | 15 | timeout.tv_sec = msec / 1000; 16 | timeout.tv_nsec = (msec % 1000) * 1000 * 1000; 17 | 18 | do 19 | rc = nanosleep(&timeout, &timeout); 20 | while (rc == -1 && errno == EINTR); 21 | 22 | assert(rc == 0); 23 | } 24 | 25 | static int val = 0; 26 | static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 27 | static void* child_thread_execute(void* arg) { 28 | uv_sleep(1000); 29 | printf("sleep: %d\n", 1000); 30 | pthread_mutex_lock(&mutex); 31 | val = 1; 32 | pthread_mutex_unlock(&mutex); 33 | return NULL; 34 | } 35 | 36 | #define WASI_EXPORT __attribute__((visibility("default"))) 37 | 38 | WASI_EXPORT 39 | void sleep_in_child_thread() { 40 | pthread_t t; 41 | pthread_create(&t, NULL, child_thread_execute, NULL); 42 | } 43 | 44 | WASI_EXPORT 45 | int get_value() { 46 | return val; 47 | } 48 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13.0) 2 | 3 | project(wasitest) 4 | 5 | set(TARGET_TEST_EXE a) 6 | 7 | add_executable(${TARGET_TEST_EXE} "src/base64.c" "src/main.c") 8 | set_target_properties(${TARGET_TEST_EXE} PROPERTIES SUFFIX ".wasm") 9 | 10 | target_link_options(${TARGET_TEST_EXE} PRIVATE 11 | "-v" 12 | # "-mexec-model=reactor" 13 | # "-nostartfiles" 14 | # "-Wl,--no-entry" 15 | # "-Wl,--import-memory" 16 | "-Wl,--initial-memory=16777216,--export-dynamic,--export=malloc,--export=free,--import-undefined,--export-table" 17 | ) 18 | 19 | if(CMAKE_BUILD_TYPE STREQUAL "Release") 20 | # https://github.com/WebAssembly/wasi-sdk/issues/254 21 | target_link_options(${TARGET_TEST_EXE} PRIVATE 22 | "-Wl,--strip-debug" 23 | ) 24 | endif() 25 | 26 | add_subdirectory("memory") 27 | add_subdirectory("abort") 28 | add_subdirectory("assert") 29 | add_subdirectory("clock") 30 | add_subdirectory("exit") 31 | add_subdirectory("directory") 32 | add_subdirectory("ftruncate") 33 | add_subdirectory("getentropy") 34 | add_subdirectory("getenv") 35 | add_subdirectory("stdout") 36 | add_subdirectory("stdin") 37 | add_subdirectory("asyncify") 38 | add_subdirectory("jspi") 39 | if(CMAKE_C_COMPILER_TARGET STREQUAL "wasm32-wasi-threads") 40 | add_subdirectory("thread") 41 | endif() -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | browser: true 6 | }, 7 | parser: '@typescript-eslint/parser', 8 | extends: [ 9 | 'standard-with-typescript' 10 | ], 11 | rules: { 12 | '@typescript-eslint/no-unused-vars': 'error', 13 | '@typescript-eslint/no-namespace': 'off', 14 | '@typescript-eslint/no-var-requires': 'off', 15 | '@typescript-eslint/promise-function-async': 'off', 16 | '@typescript-eslint/no-non-null-assertion': 'off', 17 | '@typescript-eslint/consistent-type-definitions': 'off', 18 | '@typescript-eslint/strict-boolean-expressions': 'off', 19 | '@typescript-eslint/naming-convention': 'off', 20 | '@typescript-eslint/no-dynamic-delete': 'off', 21 | '@typescript-eslint/method-signature-style': 'off', 22 | '@typescript-eslint/prefer-includes': 'off', 23 | '@typescript-eslint/member-delimiter-style': ['error', { 24 | multiline: { 25 | delimiter: 'none', 26 | requireLast: true 27 | }, 28 | singleline: { 29 | delimiter: 'semi', 30 | requireLast: false 31 | } 32 | }] 33 | }, 34 | parserOptions: { 35 | ecmaVersion: 2019, 36 | sourceType: 'module', 37 | project: './tsconfig.json', 38 | tsconfigRootDir: __dirname, 39 | createDefaultProgram: true 40 | }, 41 | globals: { 42 | __webpack_public_path__: false 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/memory.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable spaced-comment */ 2 | import { _WebAssembly } from './webassembly' 3 | 4 | /** @public */ 5 | export const WebAssemblyMemory = /*#__PURE__*/ (function () { return _WebAssembly.Memory })() 6 | 7 | /** @public */ 8 | export class Memory extends WebAssemblyMemory { 9 | // eslint-disable-next-line @typescript-eslint/no-useless-constructor 10 | constructor (descriptor: WebAssembly.MemoryDescriptor) { 11 | super(descriptor) 12 | } 13 | 14 | public get HEAP8 (): Int8Array { return new Int8Array(super.buffer) } 15 | public get HEAPU8 (): Uint8Array { return new Uint8Array(super.buffer) } 16 | public get HEAP16 (): Int16Array { return new Int16Array(super.buffer) } 17 | public get HEAPU16 (): Uint16Array { return new Uint16Array(super.buffer) } 18 | public get HEAP32 (): Int32Array { return new Int32Array(super.buffer) } 19 | public get HEAPU32 (): Uint32Array { return new Uint32Array(super.buffer) } 20 | public get HEAP64 (): BigInt64Array { return new BigInt64Array(super.buffer) } 21 | public get HEAPU64 (): BigUint64Array { return new BigUint64Array(super.buffer) } 22 | public get HEAPF32 (): Float32Array { return new Float32Array(super.buffer) } 23 | public get HEAPF64 (): Float64Array { return new Float64Array(super.buffer) } 24 | public get view (): DataView { return new DataView(super.buffer) } 25 | } 26 | 27 | /** @public */ 28 | export function extendMemory (memory: WebAssembly.Memory): Memory { 29 | if (Object.getPrototypeOf(memory) === _WebAssembly.Memory.prototype) { 30 | Object.setPrototypeOf(memory, Memory.prototype) 31 | } 32 | return memory as Memory 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tybys/wasm-util", 3 | "version": "0.10.1", 4 | "description": "WASI polyfill for browser and some wasm util", 5 | "main": "./lib/cjs/index.js", 6 | "module": "./dist/wasm-util.esm-bundler.js", 7 | "types": "./dist/wasm-util.d.ts", 8 | "exports": { 9 | ".": { 10 | "module": "./dist/wasm-util.esm-bundler.js", 11 | "import": "./lib/mjs/index.mjs", 12 | "require": "./lib/cjs/index.js", 13 | "types": "./dist/wasm-util.d.ts" 14 | } 15 | }, 16 | "scripts": { 17 | "build": "tsgo build", 18 | "watch": "tsgo watch", 19 | "test": "jest", 20 | "lint": "eslint ./src/**/*.{ts,js} --fix", 21 | "prepare": "npm run build" 22 | }, 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "keywords": [ 27 | "wasm", 28 | "webassembly", 29 | "wasi", 30 | "polyfill" 31 | ], 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/toyobayashi/wasm-util.git" 35 | }, 36 | "author": "toyobayashi", 37 | "license": "MIT", 38 | "dependencies": { 39 | "tslib": "^2.4.0" 40 | }, 41 | "devDependencies": { 42 | "@tybys/ts-transform-module-specifier": "^0.0.2", 43 | "@tybys/ts-transform-pure-class": "^0.1.1", 44 | "@tybys/tsgo": "^1.1.0", 45 | "@types/node": "^14.14.31", 46 | "@typescript-eslint/eslint-plugin": "^5.40.1", 47 | "@typescript-eslint/parser": "^5.40.1", 48 | "eslint": "^8.25.0", 49 | "eslint-config-standard-with-typescript": "^23.0.0", 50 | "eslint-plugin-import": "^2.26.0", 51 | "eslint-plugin-n": "^15.3.0", 52 | "eslint-plugin-promise": "^6.1.0", 53 | "memfs-browser": "^3.4.13000", 54 | "mocha": "^10.1.0", 55 | "ts-node": "^10.9.1", 56 | "typescript": "~4.8.3" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/src/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | int get_random_values(void* buf, size_t buflen) { 9 | size_t pos; 10 | size_t stride; 11 | 12 | /* getentropy() returns an error for requests > 256 bytes. */ 13 | for (pos = 0, stride = 256; pos + stride < buflen; pos += stride) 14 | if (getentropy((char*) buf + pos, stride)) 15 | return errno; 16 | 17 | if (getentropy((char*) buf + pos, buflen - pos)) 18 | return errno; 19 | 20 | return 0; 21 | } 22 | 23 | extern char **environ; 24 | 25 | void call_js(void (*)(uint64_t data), uint64_t data); 26 | 27 | void print(uint64_t data) { 28 | char buf[128]; 29 | // scanf("%s", buf); 30 | printf("Hello"); 31 | // printf(" %s\n", buf); 32 | 33 | get_random_values(buf, 1); 34 | printf("%d\n", buf[0]); 35 | printf("%llu\n", data); 36 | } 37 | 38 | int main(int argc, char** argv) { 39 | char cwd[256] = { 0 }; 40 | getcwd(cwd, 256); 41 | printf("CWD: %s\n", cwd); 42 | mkdir("./node_modules", 0666); 43 | int r = chdir("./node_modules"); 44 | if (r != 0) { 45 | fprintf(stderr, "chdir: %d\n", errno); 46 | } else { 47 | getcwd(cwd, 256); 48 | printf("CWD: %s\n", cwd); 49 | } 50 | 51 | FILE* f = fopen("./.npmrc", "w"); 52 | if (f == NULL) { 53 | fprintf(stderr, "fopen: %d\n", errno); 54 | } else { 55 | fprintf(f, "file\n"); 56 | fclose(f); 57 | f = fopen("./.npmrc", "r"); 58 | char content[256] = { 0 }; 59 | fread(content, 1, 256, f); 60 | printf(".npmrc: %s\n", content); 61 | fclose(f); 62 | } 63 | 64 | struct stat st; 65 | lstat(cwd, &st); 66 | 67 | for (int i = 0; i < argc; ++i) { 68 | printf("%d: %s\n", i, *(argv + i)); 69 | } 70 | 71 | int i = 0; 72 | while (environ[i]) { 73 | printf("%s\n", environ[i++]); // prints in form of "variable=value" 74 | } 75 | 76 | call_js(print, 18446744073709551615ULL); 77 | return 0; 78 | } 79 | -------------------------------------------------------------------------------- /test/ftruncate/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | describe('ftruncate', function () { 5 | it('ftruncate', async function () { 6 | const vol = memfs.Volume.fromJSON({ 7 | '/ftruncate.dir': null 8 | }) 9 | const wasi = new wasmUtil.WASI({ 10 | returnOnExit: true, 11 | preopens: { 12 | 'ftruncate.dir': 'ftruncate.dir' 13 | }, 14 | fs: memfs.createFsFromVolume(vol) 15 | }) 16 | const { instance } = await wasmUtil.load('/test/ftruncate/ftruncate.wasm', wasi.getImportObject()) 17 | 18 | wasi.start(instance) 19 | }) 20 | 21 | it('ftruncate asyncify', async function () { 22 | const vol = memfs.Volume.fromJSON({ 23 | '/ftruncate.dir': null 24 | }) 25 | const asyncify = new wasmUtil.Asyncify() 26 | const wasi = await wasmUtil.createAsyncWASI({ 27 | returnOnExit: true, 28 | preopens: { 29 | 'ftruncate.dir': 'ftruncate.dir' 30 | }, 31 | fs: memfs.createFsFromVolume(vol), 32 | asyncify: asyncify 33 | }) 34 | const { instance } = await wasmUtil.load('/test/ftruncate/ftruncate_asyncify.wasm', wasi.getImportObject()) 35 | const wrappedInstance = asyncify.init(instance.exports.memory, instance, {}) 36 | 37 | await wasi.start(wrappedInstance) 38 | }) 39 | 40 | it('ftruncate jspi', async function () { 41 | const vol = memfs.Volume.fromJSON({ 42 | '/ftruncate.dir': null 43 | }) 44 | const wasi = await wasmUtil.createAsyncWASI({ 45 | returnOnExit: true, 46 | preopens: { 47 | 'ftruncate.dir': 'ftruncate.dir' 48 | }, 49 | fs: memfs.createFsFromVolume(vol) 50 | }) 51 | const { instance } = await wasmUtil.load('/test/ftruncate/ftruncate_jspi.wasm', wasi.getImportObject()) 52 | const wrappedInstance = Object.create(WebAssembly.Instance.prototype) 53 | Object.defineProperty(wrappedInstance, 'exports', { value: wasmUtil.wrapExports(instance.exports, ['_start']) }) 54 | 55 | await wasi.start(wrappedInstance) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /test/directory/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | describe('directory', function () { 5 | it('directory', async function () { 6 | const vol = memfs.Volume.fromJSON({ 7 | '/fopen-directory-parent-directory.dir': null 8 | }) 9 | const wasi = new wasmUtil.WASI({ 10 | returnOnExit: true, 11 | preopens: { 12 | 'fopen-directory-parent-directory.dir': 'fopen-directory-parent-directory.dir' 13 | }, 14 | fs: memfs.createFsFromVolume(vol) 15 | }) 16 | const { instance } = await wasmUtil.load('/test/directory/directory.wasm', wasi.getImportObject()) 17 | 18 | wasi.start(instance) 19 | }) 20 | 21 | it('directory asyncify', async function () { 22 | const vol = memfs.Volume.fromJSON({ 23 | '/fopen-directory-parent-directory.dir': null 24 | }) 25 | const asyncify = new wasmUtil.Asyncify() 26 | const wasi = await wasmUtil.createAsyncWASI({ 27 | returnOnExit: true, 28 | preopens: { 29 | 'fopen-directory-parent-directory.dir': 'fopen-directory-parent-directory.dir' 30 | }, 31 | fs: memfs.createFsFromVolume(vol), 32 | asyncify: asyncify 33 | }) 34 | const { instance } = await wasmUtil.load('/test/directory/directory_asyncify.wasm', wasi.getImportObject()) 35 | const wrappedInstance = asyncify.init(instance.exports.memory, instance, { 36 | wrapExports: ['_start'] 37 | }) 38 | 39 | await wasi.start(wrappedInstance) 40 | }) 41 | 42 | it('directory jspi', async function () { 43 | const vol = memfs.Volume.fromJSON({ 44 | '/fopen-directory-parent-directory.dir': null 45 | }) 46 | const wasi = await wasmUtil.createAsyncWASI({ 47 | returnOnExit: true, 48 | preopens: { 49 | 'fopen-directory-parent-directory.dir': 'fopen-directory-parent-directory.dir' 50 | }, 51 | fs: memfs.createFsFromVolume(vol) 52 | }) 53 | const { instance } = await wasmUtil.load('/test/directory/directory_jspi.wasm', wasi.getImportObject()) 54 | const wrappedInstance = { exports: wasmUtil.wrapExports(instance.exports, ['_start']) } 55 | 56 | await wasi.start(wrappedInstance) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /test/memory/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('memory', function () { 4 | it('import memory', async function () { 5 | const wasi = new wasmUtil.WASI({ 6 | returnOnExit: true 7 | }) 8 | 9 | const memory = new WebAssembly.Memory({ initial: 256 }) 10 | 11 | wasmUtil.extendMemory(memory) 12 | console.log(memory instanceof wasmUtil.Memory) 13 | console.log(memory instanceof WebAssembly.Memory) 14 | const { HEAP32, view } = memory 15 | 16 | const { instance } = await wasmUtil.load('/test/memory/memory_import.wasm', { 17 | env: { 18 | memory, 19 | js_log (ptr, size) { 20 | console.log(ptr, size) 21 | ptr = Number(ptr) 22 | size = Number(size) 23 | assert(ptr !== 0) 24 | assert(size === 4) 25 | assert(HEAP32[ptr >> 2] === 233) 26 | assert(view.getInt32(ptr, true) === 233) 27 | } 28 | }, 29 | ...wasi.getImportObject() 30 | }) 31 | 32 | const exportsProxy = new Proxy(instance.exports, { 33 | get (target, p, receiver) { 34 | if (p === 'memory') return memory 35 | return Reflect.get(target, p, receiver) 36 | } 37 | }) 38 | 39 | const instanceProxy = new Proxy(instance, { 40 | get (target, p, receiver) { 41 | if (p === 'exports') return exportsProxy 42 | return Reflect.get(target, p, receiver) 43 | } 44 | }) 45 | 46 | wasi.start(instanceProxy) 47 | }) 48 | 49 | it('export memory', async function () { 50 | const wasi = new wasmUtil.WASI({ 51 | returnOnExit: true 52 | }) 53 | 54 | const { instance } = await wasmUtil.load('/test/memory/memory_export.wasm', { 55 | env: { 56 | js_log (ptr, size) { 57 | console.log(ptr, size) 58 | ptr = Number(ptr) 59 | size = Number(size) 60 | assert(ptr !== 0) 61 | assert(size === 4) 62 | assert(HEAP32[ptr >> 2] === 233) 63 | assert(view.getInt32(ptr, true) === 233) 64 | } 65 | }, 66 | ...wasi.getImportObject() 67 | }) 68 | 69 | const memory = instance.exports.memory 70 | 71 | wasmUtil.extendMemory(memory) 72 | console.log(memory instanceof wasmUtil.Memory) 73 | console.log(memory instanceof WebAssembly.Memory) 74 | const { HEAP32, view } = memory 75 | 76 | wasi.start(instance) 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | test 8 | 9 | 10 | 11 |
12 | 49 | 50 | 51 | 52 | 53 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /test/thread/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('thread', function () { 4 | it('thread', async function main() { 5 | this.timeout = Infinity 6 | 7 | let nextTid = 1 8 | const spawnThread = function (startArg, threadId) { 9 | const worker = new Worker('/test/thread/worker.js') 10 | // wasmUtil.WASI.addWorkerListener(worker) 11 | 12 | worker.onmessage = function (e) { 13 | if (e.data.cmd === 'loaded') { 14 | if (typeof worker.unref === 'function') { 15 | worker.unref() 16 | } 17 | if (!e.data.success) { 18 | console.error(e.data.message) 19 | console.error(e.data.stack) 20 | } 21 | } else if (e.data.cmd === 'thread-spawn') { 22 | spawnThread(e.data.startArg, e.data.threadId) 23 | } 24 | } 25 | worker.onerror = (e) => { 26 | console.log(e) 27 | throw e 28 | } 29 | 30 | const tid = nextTid 31 | nextTid++ 32 | const payload = { 33 | cmd: 'load', 34 | request: '/test/thread/thread.wasm', 35 | tid, 36 | arg: startArg, 37 | wasmMemory 38 | } 39 | // console.log(payload) 40 | if (threadId) { 41 | Atomics.store(threadId, 0, tid) 42 | Atomics.notify(threadId, 0) 43 | } 44 | worker.postMessage(payload) 45 | return tid 46 | } 47 | 48 | const wasmMemory = new WebAssembly.Memory({ 49 | initial: 16777216 / 65536, 50 | maximum: 2147483648 / 65536, 51 | shared: true 52 | }) 53 | 54 | const wasi = new wasmUtil.WASI({ 55 | returnOnExit: true 56 | }) 57 | let { instance } = await wasmUtil.load('/test/thread/thread.wasm', { 58 | ...wasi.getImportObject(), 59 | env: { 60 | memory: wasmMemory 61 | }, 62 | wasi: { 63 | 'thread-spawn' (startArg) { 64 | return spawnThread(startArg) 65 | } 66 | } 67 | }) 68 | instance = { 69 | exports: { 70 | ...instance.exports, 71 | memory: wasmMemory 72 | } 73 | } 74 | wasi.initialize(instance) 75 | instance.exports.sleep_in_child_thread() 76 | const start = Date.now() 77 | return new Promise((resolve, reject) => { 78 | const check = () => { 79 | const value = instance.exports.get_value() 80 | console.log('get_value: ' + value) 81 | if (value) { 82 | resolve() 83 | } else { 84 | if (Date.now() - start > 1500) { 85 | reject(new Error('timeout')) 86 | } else { 87 | setTimeout(check, 100) 88 | } 89 | } 90 | } 91 | setTimeout(check, 100) 92 | }) 93 | }) 94 | }) 95 | 96 | // main() 97 | -------------------------------------------------------------------------------- /src/jspi.ts: -------------------------------------------------------------------------------- 1 | import type { AsyncifyExportFunction, Callable } from './asyncify' 2 | import { wrapInstanceExports } from './wasi/util' 3 | import { _WebAssembly } from './webassembly' 4 | 5 | function checkWebAssemblyFunction (): any { 6 | const WebAssemblyFunction = (_WebAssembly as any).Function 7 | if (typeof WebAssemblyFunction !== 'function') { 8 | throw new Error( 9 | 'WebAssembly.Function is not supported in this environment.' + 10 | ' If you are using V8 based browser like Chrome, try to specify' + 11 | ' --js-flags="--wasm-staging --experimental-wasm-stack-switching"' 12 | ) 13 | } 14 | return WebAssemblyFunction 15 | } 16 | 17 | /** @public */ 18 | export function wrapAsyncImport any> ( 19 | f: T, 20 | parameterType: WebAssembly.ValueType[], 21 | returnType: WebAssembly.ValueType[] 22 | ): (...args: [object, ...Parameters]) => ReturnType { 23 | const WebAssemblyFunction = checkWebAssemblyFunction() 24 | if (typeof f !== 'function') { 25 | throw new TypeError('Function required') 26 | } 27 | const parameters = parameterType.slice(0) 28 | parameters.unshift('externref') 29 | return new WebAssemblyFunction( 30 | { parameters, results: returnType }, 31 | f, 32 | { suspending: 'first' } 33 | ) 34 | } 35 | 36 | /** @public */ 37 | export function wrapAsyncExport ( 38 | f: Function 39 | ): T { 40 | const WebAssemblyFunction = checkWebAssemblyFunction() 41 | if (typeof f !== 'function') { 42 | throw new TypeError('Function required') 43 | } 44 | return new WebAssemblyFunction( 45 | { parameters: [...WebAssemblyFunction.type(f).parameters.slice(1)], results: ['externref'] }, 46 | f, 47 | { promising: 'first' } 48 | ) 49 | } 50 | 51 | /** @public */ 52 | export type PromisifyExports = T extends Record 53 | ? { 54 | [P in keyof T]: T[P] extends Callable 55 | ? U extends Array 56 | ? P extends U[number] 57 | ? AsyncifyExportFunction 58 | : T[P] 59 | : AsyncifyExportFunction 60 | : T[P] 61 | } 62 | : T 63 | 64 | /** @public */ 65 | export function wrapExports (exports: T): PromisifyExports 66 | /** @public */ 67 | export function wrapExports> (exports: T, needWrap: U): PromisifyExports 68 | /** @public */ 69 | export function wrapExports> (exports: T, needWrap?: U): PromisifyExports { 70 | return wrapInstanceExports(exports, (exportValue, name) => { 71 | let ignore = typeof exportValue !== 'function' 72 | if (Array.isArray(needWrap)) { 73 | ignore = ignore || (needWrap.indexOf(name as any) === -1) 74 | } 75 | return ignore ? exportValue : wrapAsyncExport(exportValue as any) 76 | }) as PromisifyExports 77 | } 78 | -------------------------------------------------------------------------------- /test/thread/worker.js: -------------------------------------------------------------------------------- 1 | var ENVIRONMENT_IS_NODE = typeof process === 'object' && process !== null && typeof process.versions === 'object' && process.versions !== null && typeof process.versions.node === 'string' 2 | if (ENVIRONMENT_IS_NODE) { 3 | const nodeWorkerThreads = require('worker_threads') 4 | 5 | const parentPort = nodeWorkerThreads.parentPort 6 | 7 | parentPort.on('message', (data) => { 8 | onmessage({ data }) 9 | }) 10 | 11 | const fs = require('fs') 12 | 13 | Object.assign(global, { 14 | self: global, 15 | require, 16 | // Module, 17 | location: { 18 | href: __filename 19 | }, 20 | Worker: nodeWorkerThreads.Worker, 21 | importScripts: function (f) { 22 | // eslint-disable-next-line no-eval 23 | (0, eval)(fs.readFileSync(f, 'utf8') + '//# sourceURL=' + f) 24 | }, 25 | postMessage: function (msg) { 26 | parentPort.postMessage(msg) 27 | }, 28 | performance: global.performance || { 29 | now: function () { 30 | return Date.now() 31 | } 32 | } 33 | }) 34 | } 35 | 36 | /// 37 | 38 | importScripts('../../dist/wasm-util.js') 39 | 40 | async function instantiate (wasmMemory, request, tid, arg) { 41 | const wasi = new wasmUtil.WASI({ returnOnExit: true }) 42 | const buffer = await (await (fetch(request))).arrayBuffer() 43 | let { instance } = await WebAssembly.instantiate(buffer, { 44 | ...wasi.getImportObject(), 45 | env: { 46 | memory: wasmMemory, 47 | }, 48 | wasi: { 49 | 'thread-spawn': function (startArg) { 50 | const threadIdBuffer = new SharedArrayBuffer(4) 51 | const id = new Int32Array(threadIdBuffer) 52 | Atomics.store(id, 0, -1) 53 | postMessage({ cmd: 'thread-spawn', startArg, threadId: id }) 54 | Atomics.wait(id, 0, -1) 55 | const tid = Atomics.load(id, 0) 56 | return tid 57 | } 58 | } 59 | }) 60 | 61 | const noop = () => {} 62 | const exportsProxy = new Proxy({}, { 63 | get (t, p, r) { 64 | if (p === 'memory') { 65 | return wasmMemory 66 | } 67 | if (p === '_initialize') { 68 | return noop 69 | } 70 | return Reflect.get(instance.exports, p, r) 71 | } 72 | }) 73 | const instanceProxy = new Proxy(instance, { 74 | get (target, p, receiver) { 75 | if (p === 'exports') { 76 | return exportsProxy 77 | } 78 | return Reflect.get(target, p, receiver) 79 | } 80 | }) 81 | 82 | wasi.initialize(instanceProxy) 83 | postMessage({ cmd: 'loaded', success: true }) 84 | instance.exports.wasi_thread_start(tid, arg) 85 | } 86 | 87 | self.onmessage = function (e) { 88 | if (e.data.cmd === 'load') { 89 | instantiate(e.data.wasmMemory, e.data.request, e.data.tid, e.data.arg).catch(err => { 90 | postMessage({ cmd: 'loaded', success: false, message: err.message, stack: err.stack }) 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "includePath": [ 4 | "${default}" 5 | ], 6 | "defines": [] 7 | }, 8 | "configurations": [ 9 | { 10 | "name": "Win32", 11 | "defines": ["${defines}", "_DEBUG", "UNICODE", "_UNICODE", "_CRT_SECURE_NO_WARNINGS"], 12 | "compilerPath": "${env:VCToolsInstallDir}bin\\Host${env:VSCMD_ARG_HOST_ARCH}\\${env:VSCMD_ARG_TGT_ARCH}\\cl.exe", 13 | "windowsSdkVersion": "${env:UCRTVersion}", 14 | "intelliSenseMode": "windows-msvc-x64", 15 | "cStandard": "c11", 16 | "cppStandard": "c++14", 17 | "includePath": ["${includePath}"] 18 | }, 19 | { 20 | "name": "Linux", 21 | "defines": ["${defines}"], 22 | "compilerPath": "/usr/bin/gcc", 23 | "cStandard": "c11", 24 | "cppStandard": "c++14", 25 | "intelliSenseMode": "linux-gcc-x64", 26 | "browse": { 27 | "path": [ 28 | "${workspaceFolder}" 29 | ], 30 | "limitSymbolsToIncludedHeaders": true, 31 | "databaseFilename": "" 32 | }, 33 | "includePath": ["${includePath}"] 34 | }, 35 | { 36 | "name": "macOS", 37 | "includePath": ["${includePath}"], 38 | "defines": ["${defines}"], 39 | "macFrameworkPath": ["/System/Library/Frameworks", "/Library/Frameworks"], 40 | "compilerPath": "/usr/bin/clang", 41 | "cStandard": "c11", 42 | "cppStandard": "c++14", 43 | "intelliSenseMode": "macos-clang-x64" 44 | }, 45 | { 46 | "name": "Emscripten", 47 | "defines": ["${defines}"], 48 | "compilerPath": "${env:EMSDK}/upstream/emscripten/emcc", 49 | "intelliSenseMode": "clang-x86", 50 | "cStandard": "c11", 51 | "cppStandard": "c++17", 52 | "includePath": ["${includePath}"] 53 | }, 54 | { 55 | "name": "Emscripten (Win32)", 56 | "defines": ["${defines}"], 57 | "compilerPath": "${env:EMSDK}/upstream/emscripten/emcc.bat", 58 | "intelliSenseMode": "clang-x86", 59 | "cStandard": "c11", 60 | "cppStandard": "c++17", 61 | "includePath": ["${includePath}"] 62 | }, 63 | { 64 | "name": "WASI", 65 | "defines": ["${defines}"], 66 | "compilerPath": "${env:WASI_SDK_PATH}/bin/clang", 67 | "intelliSenseMode": "clang-x86", 68 | "cStandard": "c11", 69 | "cppStandard": "c++14", 70 | "includePath": ["${includePath}"] 71 | }, 72 | { 73 | "name": "WASI-THREADS", 74 | "defines": ["${defines}", "_REENTRANT"], 75 | "compilerPath": "${env:WASI_SDK_PATH}/bin/clang", 76 | "intelliSenseMode": "clang-x86", 77 | "cStandard": "c11", 78 | "cppStandard": "c++14", 79 | "includePath": ["${includePath}"], 80 | "compilerArgs": ["--target=wasm32-wasi-threads"] 81 | }, 82 | { 83 | "name": "WASM32", 84 | "defines": ["${defines}", "PAGESIZE=65536"], 85 | "compilerPath": "${env:WASI_SDK_PATH}/bin/clang", 86 | "intelliSenseMode": "clang-x86", 87 | "cStandard": "c11", 88 | "cppStandard": "c++14", 89 | "includePath": ["${includePath}"], 90 | "compilerArgs": ["--target=wasm32"] 91 | } 92 | ], 93 | "version": 4 94 | } 95 | -------------------------------------------------------------------------------- /api-extractor.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 3 | 4 | // "extends": "./shared/api-extractor-base.json" 5 | // "extends": "my-package/include/api-extractor-base.json" 6 | 7 | // "projectFolder": "..", 8 | 9 | "mainEntryPointFilePath": "/lib/esm-bundler/index.d.ts", 10 | 11 | "bundledPackages": [], 12 | 13 | "compiler": { 14 | "tsconfigFilePath": "/tsconfig.json" 15 | 16 | // "overrideTsconfig": { 17 | // . . . 18 | // } 19 | 20 | // "skipLibCheck": true, 21 | }, 22 | 23 | "apiReport": { 24 | "enabled": false 25 | 26 | // "reportFileName": ".api.md", 27 | 28 | // "reportFolder": "/etc/", 29 | 30 | // "reportTempFolder": "/api/temp/" 31 | }, 32 | 33 | "docModel": { 34 | "enabled": true 35 | 36 | // "apiJsonFilePath": "/temp/.api.json" 37 | }, 38 | 39 | "dtsRollup": { 40 | "enabled": true, 41 | 42 | "untrimmedFilePath": "", 43 | 44 | // "betaTrimmedFilePath": "/dist/-beta.d.ts", 45 | 46 | "publicTrimmedFilePath": "/dist/.d.ts" 47 | 48 | // "omitTrimmingComments": true 49 | }, 50 | 51 | "tsdocMetadata": { 52 | "enabled": true, 53 | 54 | "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" 55 | }, 56 | 57 | // "newlineKind": "crlf", 58 | 59 | "messages": { 60 | /** 61 | * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing 62 | * the input .d.ts files. 63 | * 64 | * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" 65 | * 66 | * DEFAULT VALUE: A single "default" entry with logLevel=warning. 67 | */ 68 | "compilerMessageReporting": { 69 | "default": { 70 | "logLevel": "warning" 71 | 72 | // "addToApiReportFile": false 73 | } 74 | 75 | // "TS2551": { 76 | // "logLevel": "warning", 77 | // "addToApiReportFile": true 78 | // }, 79 | // 80 | // . . . 81 | }, 82 | 83 | "extractorMessageReporting": { 84 | "default": { 85 | "logLevel": "warning" 86 | // "addToApiReportFile": false 87 | } 88 | 89 | // "ae-extra-release-tag": { 90 | // "logLevel": "warning", 91 | // "addToApiReportFile": true 92 | // }, 93 | // 94 | // . . . 95 | }, 96 | 97 | "tsdocMessageReporting": { 98 | "default": { 99 | "logLevel": "warning" 100 | // "addToApiReportFile": false 101 | }, 102 | 103 | "tsdoc-param-tag-with-invalid-type": { 104 | "logLevel": "none" 105 | }, 106 | "tsdoc-escape-right-brace": { 107 | "logLevel": "none" 108 | }, 109 | "tsdoc-malformed-inline-tag": { 110 | "logLevel": "none" 111 | } 112 | 113 | // "tsdoc-link-tag-unescaped-text": { 114 | // "logLevel": "warning", 115 | // "addToApiReportFile": true 116 | // }, 117 | // 118 | // . . . 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/load.ts: -------------------------------------------------------------------------------- 1 | import { _WebAssembly } from './webassembly' 2 | import { Asyncify } from './asyncify' 3 | import type { AsyncifyOptions } from './asyncify' 4 | 5 | declare const wx: any 6 | declare const __wxConfig: any 7 | 8 | function validateImports (imports: unknown): void { 9 | if (imports && typeof imports !== 'object') { 10 | throw new TypeError('imports must be an object or undefined') 11 | } 12 | } 13 | 14 | function fetchWasm (urlOrBuffer: string | URL, imports?: WebAssembly.Imports): Promise { 15 | if (typeof wx !== 'undefined' && typeof __wxConfig !== 'undefined') { 16 | return _WebAssembly.instantiate(urlOrBuffer as any, imports) 17 | } 18 | return fetch(urlOrBuffer) 19 | .then(response => response.arrayBuffer()) 20 | .then(buffer => _WebAssembly.instantiate(buffer, imports)) 21 | } 22 | 23 | /** @public */ 24 | export function load ( 25 | wasmInput: string | URL | BufferSource | WebAssembly.Module, 26 | imports?: WebAssembly.Imports 27 | ): Promise { 28 | validateImports(imports) 29 | imports = imports ?? {} 30 | 31 | let source: Promise 32 | 33 | if (wasmInput instanceof ArrayBuffer || ArrayBuffer.isView(wasmInput)) { 34 | return _WebAssembly.instantiate(wasmInput, imports) 35 | } 36 | 37 | if (wasmInput instanceof _WebAssembly.Module) { 38 | return _WebAssembly.instantiate(wasmInput, imports).then((instance) => { 39 | return { instance, module: wasmInput } 40 | }) 41 | } 42 | 43 | if (typeof wasmInput !== 'string' && !(wasmInput instanceof URL)) { 44 | throw new TypeError('Invalid source') 45 | } 46 | 47 | if (typeof _WebAssembly.instantiateStreaming === 'function') { 48 | let responsePromise: Promise 49 | try { 50 | responsePromise = fetch(wasmInput) 51 | source = _WebAssembly.instantiateStreaming(responsePromise, imports).catch(() => { 52 | return fetchWasm(wasmInput, imports) 53 | }) 54 | } catch (_) { 55 | source = fetchWasm(wasmInput, imports) 56 | } 57 | } else { 58 | source = fetchWasm(wasmInput, imports) 59 | } 60 | return source 61 | } 62 | 63 | /** @public */ 64 | export function asyncifyLoad ( 65 | asyncify: AsyncifyOptions, 66 | urlOrBuffer: string | URL | BufferSource | WebAssembly.Module, 67 | imports?: WebAssembly.Imports 68 | ): Promise { 69 | validateImports(imports) 70 | imports = imports ?? {} 71 | 72 | const asyncifyHelper = new Asyncify() 73 | imports = asyncifyHelper.wrapImports(imports) 74 | 75 | return load(urlOrBuffer, imports).then(source => { 76 | const memory: any = source.instance.exports.memory || imports!.env?.memory 77 | return { module: source.module, instance: asyncifyHelper.init(memory, source.instance, asyncify) } 78 | }) 79 | } 80 | 81 | /** @public */ 82 | export function loadSync ( 83 | wasmInput: BufferSource | WebAssembly.Module, 84 | imports?: WebAssembly.Imports 85 | ): WebAssembly.WebAssemblyInstantiatedSource { 86 | validateImports(imports) 87 | imports = imports ?? {} 88 | 89 | let module: WebAssembly.Module 90 | 91 | if ((wasmInput instanceof ArrayBuffer) || ArrayBuffer.isView(wasmInput)) { 92 | module = new _WebAssembly.Module(wasmInput) 93 | } else if (wasmInput instanceof WebAssembly.Module) { 94 | module = wasmInput 95 | } else { 96 | throw new TypeError('Invalid source') 97 | } 98 | 99 | const instance = new _WebAssembly.Instance(module, imports) 100 | const source = { instance, module } 101 | 102 | return source 103 | } 104 | 105 | /** @public */ 106 | export function asyncifyLoadSync ( 107 | asyncify: AsyncifyOptions, 108 | buffer: BufferSource | WebAssembly.Module, 109 | imports?: WebAssembly.Imports 110 | ): WebAssembly.WebAssemblyInstantiatedSource { 111 | validateImports(imports) 112 | imports = imports ?? {} 113 | 114 | const asyncifyHelper = new Asyncify() 115 | imports = asyncifyHelper.wrapImports(imports) 116 | 117 | const source = loadSync(buffer, imports) 118 | 119 | const memory: any = source.instance.exports.memory || imports.env?.memory 120 | return { module: source.module, instance: asyncifyHelper.init(memory, source.instance, asyncify) } 121 | } 122 | -------------------------------------------------------------------------------- /test/src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import { load } from '@tybys/wasm-util' 3 | import { Volume, createFsFromVolume } from 'memfs-browser' 4 | 5 | /* function wrap (wasm) { 6 | const { 7 | memory, 8 | malloc, 9 | free, 10 | base64_encode, 11 | base64_decode 12 | } = wasm 13 | 14 | function getMemory () { 15 | return { 16 | HEAPU8: new Uint8Array(memory.buffer), 17 | HEAPU16: new Uint16Array(memory.buffer), 18 | HEAP32: new Int32Array(memory.buffer), 19 | HEAPU32: new Uint32Array(memory.buffer) 20 | } 21 | } 22 | 23 | function b64Encode (data) { 24 | let buffer 25 | if (typeof data === 'string') { 26 | buffer = new TextEncoder().encode(data) 27 | } else if (ArrayBuffer.isView(data)) { 28 | buffer = new Uint8Array(data.buffer, data.byteOffset, data.byteLength) 29 | } else { 30 | throw new TypeError('Invalid data') 31 | } 32 | 33 | const buf = malloc(buffer.length) 34 | if (buf === 0) throw new Error('malloc failed') 35 | const { HEAPU8 } = getMemory() 36 | HEAPU8.set(buffer, buf) 37 | let size = base64_encode(buf, buffer.length, 0) 38 | if (size === 0) { 39 | free(buf) 40 | throw new Error('encode failed') 41 | } 42 | const res = malloc(size) 43 | if (res === 0) { 44 | free(buf) 45 | throw new Error('malloc failed') 46 | } 47 | size = base64_encode(buf, buffer.length, res) 48 | free(buf) 49 | const str = new TextDecoder().decode(HEAPU8.subarray(res, res + size)) 50 | free(res) 51 | return str 52 | } 53 | 54 | function b64Decode (str) { 55 | const buffer = new TextEncoder().encode(str) 56 | const buf = malloc(buffer.length) 57 | if (buf === 0) throw new Error('malloc failed') 58 | const { HEAPU8 } = getMemory() 59 | HEAPU8.set(buffer, buf) 60 | let size = base64_decode(buf, buffer.length, 0) 61 | if (size === 0) { 62 | free(buf) 63 | throw new Error('decode failed') 64 | } 65 | const res = malloc(size) 66 | if (res === 0) { 67 | free(buf) 68 | throw new Error('malloc failed') 69 | } 70 | size = base64_decode(buf, buffer.length, res) 71 | free(buf) 72 | const arr = HEAPU8.slice(res, res + size) 73 | free(res) 74 | return arr 75 | } 76 | 77 | return { 78 | b64Encode, 79 | b64Decode 80 | } 81 | } */ 82 | 83 | let wasm 84 | 85 | const imports = { 86 | env: { 87 | call_js (f, data) { 88 | console.log(data) 89 | wasm.__indirect_function_table.get(f)(data) 90 | } 91 | } 92 | } 93 | 94 | const vol = Volume.fromJSON({ 95 | '/home/wasi': null 96 | }, '/') 97 | 98 | const wasiOptions = { 99 | version: 'preview1', 100 | args: ['node', 'a.wasm'], 101 | env: { 102 | NODE_ENV: 'development', 103 | WASI_SDK_PATH: '/tmp/wasi-sdk' 104 | }, 105 | preopens: { 106 | '/': '/' 107 | }, 108 | fs: createFsFromVolume(vol) 109 | } 110 | 111 | if (typeof __webpack_public_path__ !== 'undefined') { 112 | // webpack 113 | const wasmUrl = (await import('../build/a.wasm')).default 114 | const { WASI } = await import('@tybys/wasm-util') 115 | const wasi = new WASI(wasiOptions) 116 | const { instance } = await load(wasmUrl, { ...imports, ...wasi.getImportObject() }) 117 | wasm = instance.exports 118 | await wasi.start(instance) 119 | } else { 120 | const isNodeJs = !!(typeof process === 'object' && process.versions && process.versions.node) 121 | 122 | const url = new URL('../build/a.wasm', import.meta.url) 123 | const { WASI } = isNodeJs ? await import('node:wasi') : await import('@tybys/wasm-util') 124 | const wasi = new WASI(wasiOptions) 125 | const { instance } = await load(isNodeJs ? await (await import('node:fs/promises')).readFile(url) : url, { ...imports, ...wasi.getImportObject() }) 126 | wasm = instance.exports 127 | await wasi.start(instance) 128 | } 129 | 130 | console.log(vol.toJSON()) 131 | 132 | // async function main (wrappedExports) { 133 | // const { 134 | // b64Encode, 135 | // b64Decode 136 | // } = wrappedExports 137 | 138 | // const input = 'Hello wasi\n' 139 | // const b64Str = b64Encode(input) 140 | // console.log(b64Str) 141 | // const origin = b64Decode(b64Str) 142 | // const originStr = new TextDecoder().decode(origin) 143 | // console.log(originStr === input) 144 | // } 145 | 146 | // await main(wrap(wasm)) 147 | -------------------------------------------------------------------------------- /tsgo.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const typescript = require('typescript') 3 | const transformPureClass = require('@tybys/ts-transform-pure-class').default 4 | const transformModuleSpecifier = require('@tybys/ts-transform-module-specifier').default 5 | const { defineConfig } = require('@tybys/tsgo') 6 | 7 | function removeSuffix (str, suffix) { 8 | if (suffix == null) { 9 | const pathList = str.split(/[/\\]/) 10 | const last = pathList[pathList.length - 1] 11 | const dot = last.lastIndexOf('.') 12 | pathList[pathList.length - 1] = dot !== -1 ? last.slice(0, dot) : last 13 | return pathList.join('/') 14 | } 15 | return str.endsWith(suffix) ? str.slice(0, str.length - suffix.length) : str 16 | } 17 | 18 | function createModuleSpecifierTransformer (suffix) { 19 | const rewriteExtensions = [''] 20 | return transformModuleSpecifier({ 21 | targets: [ 22 | { 23 | replacer: (_currentSourceFile, request) => { 24 | if (request.charAt(0) === '.' && (rewriteExtensions.indexOf(path.extname(request)) !== -1)) { 25 | return removeSuffix(request) + suffix 26 | } 27 | return request 28 | } 29 | } 30 | ] 31 | }) 32 | } 33 | 34 | const root = __dirname 35 | const name = path.posix.basename(require('./package.json').name) 36 | const entry = path.resolve(root, 'lib/esm-bundler/index.js') 37 | const dist = path.resolve(root, 'dist') 38 | const output = { 39 | name, 40 | path: dist 41 | } 42 | const terserOptions = { 43 | output: { 44 | beautify: false, 45 | comments: false 46 | } 47 | } 48 | // const mpDist = path.resolve(root, require('./package.json').miniprogram || 'miniprogram_dist') 49 | 50 | module.exports = defineConfig({ 51 | root, 52 | baseTsconfig: 'tsconfig.json', 53 | docOutputPath: 'docs/api', 54 | tscTargets: [ 55 | { 56 | optionsToExtend: { 57 | target: typescript.ScriptTarget.ES2019, 58 | module: typescript.ModuleKind.ESNext, 59 | outDir: path.join(root, 'lib/esm-bundler'), 60 | declaration: true 61 | }, 62 | customTransformersAfter: () => ({ 63 | after: [transformPureClass, createModuleSpecifierTransformer('.js')] 64 | }) 65 | }, 66 | { 67 | transpileOnly: true, 68 | optionsToExtend: { 69 | target: typescript.ScriptTarget.ES2019, 70 | module: typescript.ModuleKind.CommonJS, 71 | sourceMap: true, // for test 72 | outDir: path.join(root, 'lib/cjs') 73 | }, 74 | customTransformersAfter: () => ({ 75 | after: [transformPureClass] 76 | }) 77 | }, 78 | { 79 | transpileOnly: true, 80 | outputSuffix: '.mjs', 81 | optionsToExtend: { 82 | target: typescript.ScriptTarget.ES2019, 83 | module: typescript.ModuleKind.ESNext, 84 | outDir: path.join(root, 'lib/mjs') 85 | }, 86 | customTransformersAfter: () => ({ 87 | after: [transformPureClass, createModuleSpecifierTransformer('.mjs')] 88 | }) 89 | } 90 | ], 91 | libraryName: name, 92 | bundleTargets: [ 93 | { 94 | entry, 95 | output, 96 | define: { 97 | 'process.env.NODE_DEBUG_NATIVE': '"wasi"' 98 | }, 99 | minify: false, 100 | type: 'umd', 101 | resolveOnly: [/^(?!(memfs-browser)).*?$/], 102 | globals: { 'memfs-browser': 'memfs' } 103 | }, 104 | { 105 | entry, 106 | output, 107 | define: { 108 | 'process.env.NODE_DEBUG_NATIVE': 'undefined' 109 | }, 110 | minify: true, 111 | terserOptions, 112 | type: 'umd', 113 | resolveOnly: [/^(?!(memfs-browser)).*?$/], 114 | globals: { 'memfs-browser': 'memfs' } 115 | }, 116 | { 117 | entry, 118 | output, 119 | define: { 120 | 'process.env.NODE_DEBUG_NATIVE': '"wasi"' 121 | }, 122 | minify: false, 123 | type: 'esm', 124 | resolveOnly: [/^(?!(memfs-browser)).*?$/] 125 | }, 126 | { 127 | entry, 128 | output, 129 | define: { 130 | 'process.env.NODE_DEBUG_NATIVE': 'undefined' 131 | }, 132 | minify: true, 133 | terserOptions, 134 | type: 'esm', 135 | resolveOnly: [/^(?!(memfs-browser)).*?$/] 136 | }, 137 | /* { 138 | entry, 139 | output: { 140 | name: 'index', 141 | path: mpDist 142 | }, 143 | minify: true, 144 | type: 'mp' 145 | }, */ 146 | { 147 | entry, 148 | output, 149 | minify: false, 150 | type: 'esm-bundler', 151 | resolveOnly: [/^(?!(memfs-browser)).*?$/] 152 | } 153 | ] 154 | }) 155 | -------------------------------------------------------------------------------- /src/wasi/util.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable spaced-comment */ 2 | export function validateObject (value: unknown, name: string): void { 3 | if (value === null || typeof value !== 'object') { 4 | throw new TypeError(`${name} must be an object. Received ${value === null ? 'null' : typeof value}`) 5 | } 6 | } 7 | 8 | export function validateArray (value: unknown, name: string): void { 9 | if (!Array.isArray(value)) { 10 | throw new TypeError(`${name} must be an array. Received ${value === null ? 'null' : typeof value}`) 11 | } 12 | } 13 | 14 | export function validateBoolean (value: unknown, name: string): void { 15 | if (typeof value !== 'boolean') { 16 | throw new TypeError(`${name} must be a boolean. Received ${value === null ? 'null' : typeof value}`) 17 | } 18 | } 19 | 20 | export function validateString (value: unknown, name: string): void { 21 | if (typeof value !== 'string') { 22 | throw new TypeError(`${name} must be a string. Received ${value === null ? 'null' : typeof value}`) 23 | } 24 | } 25 | 26 | export function validateFunction (value: unknown, name: string): void { 27 | if (typeof value !== 'function') { 28 | throw new TypeError(`${name} must be a function. Received ${value === null ? 'null' : typeof value}`) 29 | } 30 | } 31 | 32 | export function validateUndefined (value: unknown, name: string): void { 33 | if (value !== undefined) { 34 | throw new TypeError(`${name} must be undefined. Received ${value === null ? 'null' : typeof value}`) 35 | } 36 | } 37 | 38 | export function validateInt32 (value: unknown, name: string, min = -2147483648, max = 2147483647): void { 39 | if (typeof value !== 'number') { 40 | throw new TypeError(`${name} must be a number. Received ${value === null ? 'null' : typeof value}`) 41 | } 42 | if (!Number.isInteger(value)) { 43 | throw new RangeError(`${name} must be a integer.`) 44 | } 45 | if (value < min || value > max) { 46 | throw new RangeError(`${name} must be >= ${min} && <= ${max}. Received ${value}`) 47 | } 48 | } 49 | 50 | export function isPromiseLike (obj: any): obj is PromiseLike { 51 | return !!(obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function') 52 | } 53 | 54 | export function wrapInstanceExports (exports: WebAssembly.Exports, mapFn: (value: WebAssembly.ExportValue, key: string) => WebAssembly.ExportValue): WebAssembly.Exports { 55 | const newExports = Object.create(null) 56 | Object.keys(exports).forEach(name => { 57 | const exportValue = exports[name] 58 | Object.defineProperty(newExports, name, { 59 | enumerable: true, 60 | value: mapFn(exportValue, name) 61 | }) 62 | }) 63 | 64 | return newExports 65 | } 66 | 67 | declare const __webpack_public_path__: string 68 | declare const __non_webpack_require__: any 69 | 70 | const _require = /*#__PURE__*/ (function () { 71 | let nativeRequire 72 | 73 | if (typeof __webpack_public_path__ !== 'undefined') { 74 | nativeRequire = (function () { 75 | return typeof __non_webpack_require__ !== 'undefined' ? __non_webpack_require__ : undefined 76 | })() 77 | } else { 78 | nativeRequire = (function () { 79 | return typeof __webpack_public_path__ !== 'undefined' ? (typeof __non_webpack_require__ !== 'undefined' ? __non_webpack_require__ : undefined) : (typeof require !== 'undefined' ? require : undefined) 80 | })() 81 | } 82 | 83 | return nativeRequire 84 | })() 85 | 86 | export const isMainThread: boolean = /*#__PURE__*/ (function () { 87 | let worker_threads 88 | try { 89 | worker_threads = _require('worker_threads') 90 | } catch (_) {} 91 | if (!worker_threads) { 92 | return typeof importScripts === 'undefined' 93 | } 94 | return worker_threads.isMainThread 95 | })() 96 | 97 | export const postMsg = isMainThread 98 | ? () => {} 99 | : /*#__PURE__*/ (function () { 100 | let worker_threads: undefined | typeof import('worker_threads') 101 | try { 102 | worker_threads = _require('worker_threads') 103 | } catch (_) {} 104 | if (!worker_threads) { 105 | return postMessage 106 | } 107 | return function postMessage (data: any) { 108 | worker_threads!.parentPort!.postMessage({ data }) 109 | } 110 | })() 111 | 112 | export function sleepBreakIf (delay: number, breakIf: () => boolean): boolean { 113 | const start = Date.now() 114 | const end = start + delay 115 | let ret = false 116 | while (Date.now() < end) { 117 | if (breakIf()) { 118 | ret = true 119 | break 120 | } 121 | } 122 | return ret 123 | } 124 | 125 | export function unsharedSlice (view: Uint8Array, start: number, end?: number): Uint8Array { 126 | return ((typeof SharedArrayBuffer === 'function' && view.buffer instanceof SharedArrayBuffer) || (Object.prototype.toString.call(view.buffer.constructor) === '[object SharedArrayBuffer]')) 127 | ? view.slice(start, end) 128 | : view.subarray(start, end) 129 | } 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @tybys/wasm-util 2 | 3 | WebAssembly related utils for browser environment 4 | 5 | **The output code is ES2019** 6 | 7 | ## Features 8 | 9 | All example code below need to be bundled by ES module bundlers like `webpack` / `rollup`, or specify import map in browser native ES module runtime. 10 | 11 | ### WASI polyfill for browser 12 | 13 | The API is similar to the `require('wasi').WASI` in Node.js. 14 | 15 | You can use `memfs-browser` to provide filesystem capability. 16 | 17 | - Example: [https://github.com/toyobayashi/wasi-wabt](https://github.com/toyobayashi/wasi-wabt) 18 | - Demo: [https://toyobayashi.github.io/wasi-wabt/](https://toyobayashi.github.io/wasi-wabt/) 19 | 20 | ```js 21 | import { load, WASI } from '@tybys/wasm-util' 22 | import { Volume, createFsFromVolume } from 'memfs-browser' 23 | 24 | const fs = createFsFromVolume(Volume.fromJSON({ 25 | '/home/wasi': null 26 | })) 27 | 28 | const wasi = new WASI({ 29 | args: ['chrome', 'file.wasm'], 30 | env: { 31 | NODE_ENV: 'development', 32 | WASI_SDK_PATH: '/opt/wasi-sdk' 33 | }, 34 | preopens: { 35 | '/': '/' 36 | }, 37 | fs, 38 | 39 | // redirect stdout / stderr 40 | 41 | // print (text) { console.log(text) }, 42 | // printErr (text) { console.error(text) } 43 | }) 44 | 45 | const imports = { 46 | wasi_snapshot_preview1: wasi.wasiImport 47 | } 48 | 49 | const { module, instance } = await load('/path/to/file.wasm', imports) 50 | wasi.start(instance) 51 | // wasi.initialize(instance) 52 | ``` 53 | 54 | Implemented syscalls: [wasi_snapshot_preview1](#wasi_snapshot_preview1) 55 | 56 | ### `load` / `loadSync` 57 | 58 | `loadSync` has 4KB wasm size limit in browser. 59 | 60 | ```js 61 | // bundler 62 | import { load, loadSync } from '@tybys/wasm-util' 63 | 64 | const imports = { /* ... */ } 65 | 66 | // using path 67 | const { module, instance } = await load('/path/to/file.wasm', imports) 68 | const { module, instance } = loadSync('/path/to/file.wasm', imports) 69 | 70 | // using URL 71 | const { module, instance } = await load(new URL('./file.wasm', import.meta.url), imports) 72 | const { module, instance } = loadSync(new URL('./file.wasm', import.meta.url), imports) 73 | 74 | // using Uint8Array 75 | const buffer = new Uint8Array([ 76 | 0x00, 0x61, 0x73, 0x6d, 77 | 0x01, 0x00, 0x00, 0x00 78 | ]) 79 | const { module, instance } = await load(buffer, imports) 80 | const { module, instance } = loadSync(buffer, imports) 81 | 82 | // auto asyncify 83 | const { 84 | module, 85 | instance: asyncifiedInstance 86 | } = await load(buffer, imports, { /* asyncify options */}) 87 | asyncifiedInstance.exports.fn() // => return Promise 88 | ``` 89 | 90 | ### Extend Memory instance 91 | 92 | ```js 93 | import { Memory, extendMemory } from '@tybys/wasm-util' 94 | 95 | const memory = new WebAssembly.Memory({ initial: 256 }) 96 | // const memory = instance.exports.memory 97 | 98 | extendMemory(memory) 99 | console.log(memory instanceof Memory) 100 | console.log(memory instanceof WebAssembly.Memory) 101 | // expose memory view getters like Emscripten 102 | const { HEAPU8, HEAPU32, view } = memory 103 | ``` 104 | 105 | ### Asyncify wrap 106 | 107 | Build the C code using `clang`, `wasm-ld` and `wasm-opt` 108 | 109 | ```c 110 | void async_sleep(int ms); 111 | 112 | int main() { 113 | async_sleep(200); 114 | return 0; 115 | } 116 | ``` 117 | 118 | ```js 119 | import { Asyncify } from '@tybys/wasm-util' 120 | 121 | const asyncify = new Asyncify() 122 | 123 | const imports = { 124 | env: { 125 | async_sleep: asyncify.wrapImportFunction(function (ms) { 126 | return new Promise((resolve) => { 127 | setTimeout(resolve, ms) 128 | }) 129 | }) 130 | } 131 | } 132 | 133 | // async_sleep(200) 134 | const bytes = await (await fetch('/asyncfied_by_wasm-opt.wasm')).arrayBuffer() 135 | const { instance } = await WebAssembly.instantiate(bytes, imports) 136 | const asyncifiedInstance = asyncify.init(instance.exports.memory, instance, { 137 | wrapExports: ['_start'] 138 | }) 139 | 140 | const p = asyncifedInstance._start() 141 | console.log(typeof p.then === 'function') 142 | const now = Date.now() 143 | await p 144 | console.log(Date.now() - now >= 200) 145 | ``` 146 | 147 | ### wasi_snapshot_preview1 148 | 149 | - [x] args_get 150 | - [x] args_sizes_get 151 | - [x] environ_get 152 | - [x] environ_sizes_get 153 | - [x] clock_res_get 154 | - [x] clock_time_get 155 | - [ ] ~~fd_advise~~ 156 | - [x] fd_allocate 157 | - [x] fd_close 158 | - [x] fd_datasync 159 | - [x] fd_fdstat_get 160 | - [ ] ~~fd_fdstat_set_flags~~ 161 | - [x] fd_fdstat_set_rights 162 | - [x] fd_filestat_get 163 | - [x] fd_filestat_set_size 164 | - [x] fd_filestat_set_times 165 | - [x] fd_pread 166 | - [x] fd_prestat_get 167 | - [x] fd_prestat_dir_name 168 | - [x] fd_pwrite 169 | - [x] fd_read 170 | - [x] fd_readdir 171 | - [x] fd_renumber 172 | - [x] fd_seek 173 | - [x] fd_sync 174 | - [x] fd_tell 175 | - [x] fd_write 176 | - [x] path_create_directory 177 | - [x] path_filestat_get 178 | - [x] path_filestat_set_times 179 | - [x] path_link 180 | - [x] path_open 181 | - [x] path_readlink 182 | - [x] path_remove_directory 183 | - [x] path_rename 184 | - [x] path_symlink 185 | - [x] path_unlink_file 186 | - [x] poll_oneoff (timer only) 187 | - [x] proc_exit 188 | - [ ] ~~proc_raise~~ 189 | - [x] sched_yield 190 | - [x] random_get 191 | - [ ] ~~sock_recv~~ 192 | - [ ] ~~sock_send~~ 193 | - [ ] ~~sock_shutdown~~ 194 | -------------------------------------------------------------------------------- /src/wasi/error.ts: -------------------------------------------------------------------------------- 1 | import { WasiErrno } from './types' 2 | 3 | export function strerror (errno: WasiErrno): string { 4 | switch (errno) { 5 | case WasiErrno.ESUCCESS: return 'Success' 6 | case WasiErrno.E2BIG: return 'Argument list too long' 7 | case WasiErrno.EACCES: return 'Permission denied' 8 | case WasiErrno.EADDRINUSE: return 'Address in use' 9 | case WasiErrno.EADDRNOTAVAIL: return 'Address not available' 10 | case WasiErrno.EAFNOSUPPORT: return 'Address family not supported by protocol' 11 | case WasiErrno.EAGAIN: return 'Resource temporarily unavailable' 12 | case WasiErrno.EALREADY: return 'Operation already in progress' 13 | case WasiErrno.EBADF: return 'Bad file descriptor' 14 | case WasiErrno.EBADMSG: return 'Bad message' 15 | case WasiErrno.EBUSY: return 'Resource busy' 16 | case WasiErrno.ECANCELED: return 'Operation canceled' 17 | case WasiErrno.ECHILD: return 'No child process' 18 | case WasiErrno.ECONNABORTED: return 'Connection aborted' 19 | case WasiErrno.ECONNREFUSED: return 'Connection refused' 20 | case WasiErrno.ECONNRESET: return 'Connection reset by peer' 21 | case WasiErrno.EDEADLK: return 'Resource deadlock would occur' 22 | case WasiErrno.EDESTADDRREQ: return 'Destination address required' 23 | case WasiErrno.EDOM: return 'Domain error' 24 | case WasiErrno.EDQUOT: return 'Quota exceeded' 25 | case WasiErrno.EEXIST: return 'File exists' 26 | case WasiErrno.EFAULT: return 'Bad address' 27 | case WasiErrno.EFBIG: return 'File too large' 28 | case WasiErrno.EHOSTUNREACH: return 'Host is unreachable' 29 | case WasiErrno.EIDRM: return 'Identifier removed' 30 | case WasiErrno.EILSEQ: return 'Illegal byte sequence' 31 | case WasiErrno.EINPROGRESS: return 'Operation in progress' 32 | case WasiErrno.EINTR: return 'Interrupted system call' 33 | case WasiErrno.EINVAL: return 'Invalid argument' 34 | case WasiErrno.EIO: return 'I/O error' 35 | case WasiErrno.EISCONN: return 'Socket is connected' 36 | case WasiErrno.EISDIR: return 'Is a directory' 37 | case WasiErrno.ELOOP: return 'Symbolic link loop' 38 | case WasiErrno.EMFILE: return 'No file descriptors available' 39 | case WasiErrno.EMLINK: return 'Too many links' 40 | case WasiErrno.EMSGSIZE: return 'Message too large' 41 | case WasiErrno.EMULTIHOP: return 'Multihop attempted' 42 | case WasiErrno.ENAMETOOLONG: return 'Filename too long' 43 | case WasiErrno.ENETDOWN: return 'Network is down' 44 | case WasiErrno.ENETRESET: return 'Connection reset by network' 45 | case WasiErrno.ENETUNREACH: return 'Network unreachable' 46 | case WasiErrno.ENFILE: return 'Too many files open in system' 47 | case WasiErrno.ENOBUFS: return 'No buffer space available' 48 | case WasiErrno.ENODEV: return 'No such device' 49 | case WasiErrno.ENOENT: return 'No such file or directory' 50 | case WasiErrno.ENOEXEC: return 'Exec format error' 51 | case WasiErrno.ENOLCK: return 'No locks available' 52 | case WasiErrno.ENOLINK: return 'Link has been severed' 53 | case WasiErrno.ENOMEM: return 'Out of memory' 54 | case WasiErrno.ENOMSG: return 'No message of the desired type' 55 | case WasiErrno.ENOPROTOOPT: return 'Protocol not available' 56 | case WasiErrno.ENOSPC: return 'No space left on device' 57 | case WasiErrno.ENOSYS: return 'Function not implemented' 58 | case WasiErrno.ENOTCONN: return 'Socket not connected' 59 | case WasiErrno.ENOTDIR: return 'Not a directory' 60 | case WasiErrno.ENOTEMPTY: return 'Directory not empty' 61 | case WasiErrno.ENOTRECOVERABLE: return 'State not recoverable' 62 | case WasiErrno.ENOTSOCK: return 'Not a socket' 63 | case WasiErrno.ENOTSUP: return 'Not supported' 64 | case WasiErrno.ENOTTY: return 'Not a tty' 65 | case WasiErrno.ENXIO: return 'No such device or address' 66 | case WasiErrno.EOVERFLOW: return 'Value too large for data type' 67 | case WasiErrno.EOWNERDEAD: return 'Previous owner died' 68 | case WasiErrno.EPERM: return 'Operation not permitted' 69 | case WasiErrno.EPIPE: return 'Broken pipe' 70 | case WasiErrno.EPROTO: return 'Protocol error' 71 | case WasiErrno.EPROTONOSUPPORT: return 'Protocol not supported' 72 | case WasiErrno.EPROTOTYPE: return 'Protocol wrong type for socket' 73 | case WasiErrno.ERANGE: return 'Result not representable' 74 | case WasiErrno.EROFS: return 'Read-only file system' 75 | case WasiErrno.ESPIPE: return 'Invalid seek' 76 | case WasiErrno.ESRCH: return 'No such process' 77 | case WasiErrno.ESTALE: return 'Stale file handle' 78 | case WasiErrno.ETIMEDOUT: return 'Operation timed out' 79 | case WasiErrno.ETXTBSY: return 'Text file busy' 80 | case WasiErrno.EXDEV: return 'Cross-device link' 81 | case WasiErrno.ENOTCAPABLE: return 'Capabilities insufficient' 82 | default: return 'Unknown error' 83 | } 84 | } 85 | 86 | export class WasiError extends Error { 87 | constructor (message: string, public errno: WasiErrno) { 88 | super(message) 89 | } 90 | 91 | getErrorMessage (): string { 92 | return strerror(this.errno) 93 | } 94 | } 95 | Object.defineProperty(WasiError.prototype, 'name', { 96 | configurable: true, 97 | writable: true, 98 | value: 'WasiError' 99 | }) 100 | 101 | export type Result = { value: T; errno: WasiErrno.ESUCCESS } | { value: undefined; errno: Exclude } 102 | -------------------------------------------------------------------------------- /src/wasi/path.ts: -------------------------------------------------------------------------------- 1 | import { validateString } from './util' 2 | 3 | const CHAR_DOT = 46 /* . */ 4 | const CHAR_FORWARD_SLASH = 47 /* / */ 5 | 6 | function isPosixPathSeparator (code: number): boolean { 7 | return code === CHAR_FORWARD_SLASH 8 | } 9 | 10 | function normalizeString (path: string, allowAboveRoot: boolean, separator: string, isPathSeparator: (code: number) => boolean): string { 11 | let res = '' 12 | let lastSegmentLength = 0 13 | let lastSlash = -1 14 | let dots = 0 15 | let code = 0 16 | for (let i = 0; i <= path.length; ++i) { 17 | if (i < path.length) { 18 | code = path.charCodeAt(i) 19 | } else if (isPathSeparator(code)) { 20 | break 21 | } else { 22 | code = CHAR_FORWARD_SLASH 23 | } 24 | 25 | if (isPathSeparator(code)) { 26 | if (lastSlash === i - 1 || dots === 1) { 27 | // NOOP 28 | } else if (dots === 2) { 29 | if (res.length < 2 || lastSegmentLength !== 2 || 30 | res.charCodeAt(res.length - 1) !== CHAR_DOT || 31 | res.charCodeAt(res.length - 2) !== CHAR_DOT) { 32 | if (res.length > 2) { 33 | const lastSlashIndex = res.indexOf(separator) 34 | if (lastSlashIndex === -1) { 35 | res = '' 36 | lastSegmentLength = 0 37 | } else { 38 | res = res.slice(0, lastSlashIndex) 39 | lastSegmentLength = 40 | res.length - 1 - res.indexOf(separator) 41 | } 42 | lastSlash = i 43 | dots = 0 44 | continue 45 | } else if (res.length !== 0) { 46 | res = '' 47 | lastSegmentLength = 0 48 | lastSlash = i 49 | dots = 0 50 | continue 51 | } 52 | } 53 | if (allowAboveRoot) { 54 | res += res.length > 0 ? `${separator}..` : '..' 55 | lastSegmentLength = 2 56 | } 57 | } else { 58 | if (res.length > 0) { 59 | res += `${separator}${path.slice(lastSlash + 1, i)}` 60 | } else { 61 | res = path.slice(lastSlash + 1, i) 62 | } 63 | lastSegmentLength = i - lastSlash - 1 64 | } 65 | lastSlash = i 66 | dots = 0 67 | } else if (code === CHAR_DOT && dots !== -1) { 68 | ++dots 69 | } else { 70 | dots = -1 71 | } 72 | } 73 | return res 74 | } 75 | 76 | export function resolve (...args: string[]): string { 77 | let resolvedPath = '' 78 | let resolvedAbsolute = false 79 | 80 | for (let i = args.length - 1; i >= -1 && !resolvedAbsolute; i--) { 81 | const path = i >= 0 ? args[i] : '/' 82 | 83 | validateString(path, 'path') 84 | 85 | // Skip empty entries 86 | if (path.length === 0) { 87 | continue 88 | } 89 | 90 | resolvedPath = `${path}/${resolvedPath}` 91 | resolvedAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH 92 | } 93 | 94 | // At this point the path should be resolved to a full absolute path, but 95 | // handle relative paths to be safe (might happen when process.cwd() fails) 96 | 97 | // Normalize the path 98 | resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute, '/', 99 | isPosixPathSeparator) 100 | 101 | if (resolvedAbsolute) { 102 | return `/${resolvedPath}` 103 | } 104 | return resolvedPath.length > 0 ? resolvedPath : '.' 105 | } 106 | 107 | export function relative (from: string, to: string): string { 108 | validateString(from, 'from') 109 | validateString(to, 'to') 110 | 111 | if (from === to) return '' 112 | 113 | // Trim leading forward slashes. 114 | from = resolve(from) 115 | to = resolve(to) 116 | 117 | if (from === to) return '' 118 | 119 | const fromStart = 1 120 | const fromEnd = from.length 121 | const fromLen = fromEnd - fromStart 122 | const toStart = 1 123 | const toLen = to.length - toStart 124 | 125 | // Compare paths to find the longest common path from root 126 | const length = (fromLen < toLen ? fromLen : toLen) 127 | let lastCommonSep = -1 128 | let i = 0 129 | for (; i < length; i++) { 130 | const fromCode = from.charCodeAt(fromStart + i) 131 | if (fromCode !== to.charCodeAt(toStart + i)) { break } else if (fromCode === CHAR_FORWARD_SLASH) { lastCommonSep = i } 132 | } 133 | if (i === length) { 134 | if (toLen > length) { 135 | if (to.charCodeAt(toStart + i) === CHAR_FORWARD_SLASH) { 136 | // We get here if `from` is the exact base path for `to`. 137 | // For example: from='/foo/bar'; to='/foo/bar/baz' 138 | return to.slice(toStart + i + 1) 139 | } 140 | if (i === 0) { 141 | // We get here if `from` is the root 142 | // For example: from='/'; to='/foo' 143 | return to.slice(toStart + i) 144 | } 145 | } else if (fromLen > length) { 146 | if (from.charCodeAt(fromStart + i) === CHAR_FORWARD_SLASH) { 147 | // We get here if `to` is the exact base path for `from`. 148 | // For example: from='/foo/bar/baz'; to='/foo/bar' 149 | lastCommonSep = i 150 | } else if (i === 0) { 151 | // We get here if `to` is the root. 152 | // For example: from='/foo/bar'; to='/' 153 | lastCommonSep = 0 154 | } 155 | } 156 | } 157 | 158 | let out = '' 159 | // Generate the relative path based on the path difference between `to` 160 | // and `from`. 161 | for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) { 162 | if (i === fromEnd || 163 | from.charCodeAt(i) === CHAR_FORWARD_SLASH) { 164 | out += out.length === 0 ? '..' : '/..' 165 | } 166 | } 167 | 168 | // Lastly, append the rest of the destination (`to`) path that comes after 169 | // the common path parts. 170 | return `${out}${to.slice(toStart + lastCommonSep)}` 171 | } 172 | -------------------------------------------------------------------------------- /test/src/base64.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "base64.h" 4 | 5 | static const char table[] = 6 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 7 | 8 | // supports regular and URL-safe base64 9 | static const int8_t unbase64_table[256] = { 10 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -2, -1, -1, -2, -1, -1, 11 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 12 | -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, 13 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, 14 | -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 | 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, 16 | -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 17 | 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, 18 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 19 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 20 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 21 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 22 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 23 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 25 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 26 | }; 27 | 28 | size_t base64_encode(const unsigned char* src, size_t len, char* dst) { 29 | size_t slen, dlen; 30 | unsigned i, k, n, a, b, c; 31 | if (src == NULL) { 32 | return 0; 33 | } 34 | 35 | if (len == -1) { 36 | slen = strlen((const char*)src); 37 | } else { 38 | slen = len; 39 | } 40 | 41 | dlen = ((slen + 2 - ((slen + 2) % 3)) / 3 * 4); 42 | 43 | if (dst == NULL) { 44 | return dlen; 45 | } 46 | 47 | i = 0; 48 | k = 0; 49 | n = slen / 3 * 3; 50 | 51 | while (i < n) { 52 | a = src[i + 0] & 0xff; 53 | b = src[i + 1] & 0xff; 54 | c = src[i + 2] & 0xff; 55 | 56 | dst[k + 0] = table[a >> 2]; 57 | dst[k + 1] = table[((a & 3) << 4) | (b >> 4)]; 58 | dst[k + 2] = table[((b & 0x0f) << 2) | (c >> 6)]; 59 | dst[k + 3] = table[c & 0x3f]; 60 | 61 | i += 3; 62 | k += 4; 63 | } 64 | 65 | if (n != slen) { 66 | switch (slen - n) { 67 | case 1: 68 | a = src[i + 0] & 0xff; 69 | dst[k + 0] = table[a >> 2]; 70 | dst[k + 1] = table[(a & 3) << 4]; 71 | dst[k + 2] = '='; 72 | dst[k + 3] = '='; 73 | break; 74 | 75 | case 2: 76 | a = src[i + 0] & 0xff; 77 | b = src[i + 1] & 0xff; 78 | dst[k + 0] = table[a >> 2]; 79 | dst[k + 1] = table[((a & 3) << 4) | (b >> 4)]; 80 | dst[k + 2] = table[(b & 0x0f) << 2]; 81 | dst[k + 3] = '='; 82 | break; 83 | } 84 | } 85 | 86 | return dlen; 87 | } 88 | 89 | static int base64_decode_group_slow(char* const dst, const size_t dstlen, 90 | const char* const src, const size_t srclen, 91 | size_t* const i, size_t* const k) { 92 | uint8_t hi; 93 | uint8_t lo; 94 | uint8_t c; 95 | #define V(expr) \ 96 | for (;;) { \ 97 | c = src[*i]; \ 98 | lo = unbase64_table[c]; \ 99 | *i += 1; \ 100 | if (lo < 64) \ 101 | break; /* Legal character. */ \ 102 | if (c == '=' || *i >= srclen) \ 103 | return 0; /* Stop decoding. */ \ 104 | } \ 105 | expr; \ 106 | if (*i >= srclen) \ 107 | return 0; \ 108 | if (*k >= dstlen) \ 109 | return 0; \ 110 | hi = lo; 111 | V((void)0); 112 | V(dst[(*k)++] = ((hi & 0x3F) << 2) | ((lo & 0x30) >> 4)); 113 | V(dst[(*k)++] = ((hi & 0x0F) << 4) | ((lo & 0x3C) >> 2)); 114 | V(dst[(*k)++] = ((hi & 0x03) << 6) | ((lo & 0x3F) >> 0)); 115 | #undef V 116 | return 1; // Continue decoding. 117 | } 118 | 119 | size_t base64_decode(const char* src, size_t len, unsigned char* dst) { 120 | size_t slen, dlen, remainder, size; 121 | size_t available, max_k, max_i, i, k, v; 122 | 123 | if (src == NULL) { 124 | return 0; 125 | } 126 | 127 | if (len == -1) { 128 | slen = strlen(src); 129 | } else { 130 | slen = len; 131 | } 132 | 133 | if (slen == 0) { 134 | dlen = 0; 135 | } else { 136 | if (src[slen - 1] == '=') slen--; 137 | if (slen > 0 && src[slen - 1] == '=') slen--; 138 | 139 | size = slen; 140 | remainder = size % 4; 141 | 142 | size = (size / 4) * 3; 143 | if (remainder) { 144 | if (size == 0 && remainder == 1) { 145 | size = 0; 146 | } else { 147 | size += 1 + (remainder == 3); 148 | } 149 | } 150 | 151 | dlen = size; 152 | } 153 | 154 | if (dst == NULL) { 155 | return dlen; 156 | } 157 | 158 | available = dlen; 159 | max_k = available / 3 * 3; 160 | max_i = slen / 4 * 4; 161 | i = 0; 162 | k = 0; 163 | while (i < max_i && k < max_k) { 164 | v = unbase64_table[src[i + 0]] << 24 | 165 | unbase64_table[src[i + 1]] << 16 | 166 | unbase64_table[src[i + 2]] << 8 | 167 | unbase64_table[src[i + 3]]; 168 | // If MSB is set, input contains whitespace or is not valid base64. 169 | if (v & 0x80808080) { 170 | if (!base64_decode_group_slow((char*)dst, dlen, src, slen, &i, &k)) 171 | return k; 172 | max_i = i + (slen - i) / 4 * 4; // Align max_i again. 173 | } else { 174 | dst[k + 0] = ((v >> 22) & 0xFC) | ((v >> 20) & 0x03); 175 | dst[k + 1] = ((v >> 12) & 0xF0) | ((v >> 10) & 0x0F); 176 | dst[k + 2] = ((v >> 2) & 0xC0) | ((v >> 0) & 0x3F); 177 | i += 4; 178 | k += 3; 179 | } 180 | } 181 | if (i < slen && k < dlen) { 182 | base64_decode_group_slow((char*)dst, dlen, src, slen, &i, &k); 183 | } 184 | return k; 185 | } 186 | -------------------------------------------------------------------------------- /src/wasi/rights.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable spaced-comment */ 2 | import { WasiError } from './error' 3 | import { WasiErrno, WasiRights, WasiFileType } from './types' 4 | 5 | export const RIGHTS_ALL = WasiRights.FD_DATASYNC | 6 | WasiRights.FD_READ | 7 | WasiRights.FD_SEEK | 8 | WasiRights.FD_FDSTAT_SET_FLAGS | 9 | WasiRights.FD_SYNC | 10 | WasiRights.FD_TELL | 11 | WasiRights.FD_WRITE | 12 | WasiRights.FD_ADVISE | 13 | WasiRights.FD_ALLOCATE | 14 | WasiRights.PATH_CREATE_DIRECTORY | 15 | WasiRights.PATH_CREATE_FILE | 16 | WasiRights.PATH_LINK_SOURCE | 17 | WasiRights.PATH_LINK_TARGET | 18 | WasiRights.PATH_OPEN | 19 | WasiRights.FD_READDIR | 20 | WasiRights.PATH_READLINK | 21 | WasiRights.PATH_RENAME_SOURCE | 22 | WasiRights.PATH_RENAME_TARGET | 23 | WasiRights.PATH_FILESTAT_GET | 24 | WasiRights.PATH_FILESTAT_SET_SIZE | 25 | WasiRights.PATH_FILESTAT_SET_TIMES | 26 | WasiRights.FD_FILESTAT_GET | 27 | WasiRights.FD_FILESTAT_SET_TIMES | 28 | WasiRights.FD_FILESTAT_SET_SIZE | 29 | WasiRights.PATH_SYMLINK | 30 | WasiRights.PATH_UNLINK_FILE | 31 | WasiRights.PATH_REMOVE_DIRECTORY | 32 | WasiRights.POLL_FD_READWRITE | 33 | WasiRights.SOCK_SHUTDOWN | 34 | WasiRights.SOCK_ACCEPT 35 | 36 | export const BLOCK_DEVICE_BASE = RIGHTS_ALL 37 | export const BLOCK_DEVICE_INHERITING = RIGHTS_ALL 38 | 39 | export const CHARACTER_DEVICE_BASE = RIGHTS_ALL 40 | export const CHARACTER_DEVICE_INHERITING = RIGHTS_ALL 41 | 42 | export const REGULAR_FILE_BASE = WasiRights.FD_DATASYNC | 43 | WasiRights.FD_READ | 44 | WasiRights.FD_SEEK | 45 | WasiRights.FD_FDSTAT_SET_FLAGS | 46 | WasiRights.FD_SYNC | 47 | WasiRights.FD_TELL | 48 | WasiRights.FD_WRITE | 49 | WasiRights.FD_ADVISE | 50 | WasiRights.FD_ALLOCATE | 51 | WasiRights.FD_FILESTAT_GET | 52 | WasiRights.FD_FILESTAT_SET_SIZE | 53 | WasiRights.FD_FILESTAT_SET_TIMES | 54 | WasiRights.POLL_FD_READWRITE 55 | export const REGULAR_FILE_INHERITING = /*#__PURE__*/ BigInt(0) 56 | 57 | export const DIRECTORY_BASE = WasiRights.FD_FDSTAT_SET_FLAGS | 58 | WasiRights.FD_SYNC | 59 | WasiRights.FD_ADVISE | 60 | WasiRights.PATH_CREATE_DIRECTORY | 61 | WasiRights.PATH_CREATE_FILE | 62 | WasiRights.PATH_LINK_SOURCE | 63 | WasiRights.PATH_LINK_TARGET | 64 | WasiRights.PATH_OPEN | 65 | WasiRights.FD_READDIR | 66 | WasiRights.PATH_READLINK | 67 | WasiRights.PATH_RENAME_SOURCE | 68 | WasiRights.PATH_RENAME_TARGET | 69 | WasiRights.PATH_FILESTAT_GET | 70 | WasiRights.PATH_FILESTAT_SET_SIZE | 71 | WasiRights.PATH_FILESTAT_SET_TIMES | 72 | WasiRights.FD_FILESTAT_GET | 73 | WasiRights.FD_FILESTAT_SET_TIMES | 74 | WasiRights.PATH_SYMLINK | 75 | WasiRights.PATH_UNLINK_FILE | 76 | WasiRights.PATH_REMOVE_DIRECTORY | 77 | WasiRights.POLL_FD_READWRITE 78 | export const DIRECTORY_INHERITING = DIRECTORY_BASE | REGULAR_FILE_BASE 79 | 80 | export const SOCKET_BASE = (WasiRights.FD_READ | 81 | WasiRights.FD_FDSTAT_SET_FLAGS | 82 | WasiRights.FD_WRITE | 83 | WasiRights.FD_FILESTAT_GET | 84 | WasiRights.POLL_FD_READWRITE | 85 | WasiRights.SOCK_SHUTDOWN) 86 | export const SOCKET_INHERITING = RIGHTS_ALL 87 | 88 | export const TTY_BASE = WasiRights.FD_READ | 89 | WasiRights.FD_FDSTAT_SET_FLAGS | 90 | WasiRights.FD_WRITE | 91 | WasiRights.FD_FILESTAT_GET | 92 | WasiRights.POLL_FD_READWRITE 93 | export const TTY_INHERITING = /*#__PURE__*/ BigInt(0) as 0n 94 | 95 | export interface GetRightsResult { 96 | base: bigint 97 | inheriting: bigint 98 | } 99 | 100 | export function getRights (stdio: number[], fd: number, flags: number, type: WasiFileType): GetRightsResult { 101 | const ret: GetRightsResult = { 102 | base: BigInt(0), 103 | inheriting: BigInt(0) 104 | } 105 | 106 | if (type === WasiFileType.UNKNOWN) { 107 | throw new WasiError('Unknown file type', WasiErrno.EINVAL) 108 | } 109 | 110 | switch (type) { 111 | case WasiFileType.REGULAR_FILE: 112 | ret.base = REGULAR_FILE_BASE 113 | ret.inheriting = REGULAR_FILE_INHERITING 114 | break 115 | 116 | case WasiFileType.DIRECTORY: 117 | ret.base = DIRECTORY_BASE 118 | ret.inheriting = DIRECTORY_INHERITING 119 | break 120 | 121 | case WasiFileType.SOCKET_STREAM: 122 | case WasiFileType.SOCKET_DGRAM: 123 | ret.base = SOCKET_BASE 124 | ret.inheriting = SOCKET_INHERITING 125 | break 126 | 127 | case WasiFileType.CHARACTER_DEVICE: 128 | if (stdio.indexOf(fd) !== -1) { 129 | ret.base = TTY_BASE 130 | ret.inheriting = TTY_INHERITING 131 | } else { 132 | ret.base = CHARACTER_DEVICE_BASE 133 | ret.inheriting = CHARACTER_DEVICE_INHERITING 134 | } 135 | break 136 | 137 | case WasiFileType.BLOCK_DEVICE: 138 | ret.base = BLOCK_DEVICE_BASE 139 | ret.inheriting = BLOCK_DEVICE_INHERITING 140 | break 141 | 142 | default: 143 | ret.base = BigInt(0) 144 | ret.inheriting = BigInt(0) 145 | } 146 | 147 | /* Disable read/write bits depending on access mode. */ 148 | const read_or_write_only = flags & (0 | 1 | 2) 149 | 150 | if (read_or_write_only === 0) { 151 | ret.base &= ~WasiRights.FD_WRITE 152 | } else if (read_or_write_only === 1) { 153 | ret.base &= ~WasiRights.FD_READ 154 | } 155 | 156 | return ret 157 | } 158 | -------------------------------------------------------------------------------- /src/wasi/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable spaced-comment */ 2 | export const enum WasiErrno { 3 | ESUCCESS = 0, 4 | E2BIG = 1, 5 | EACCES = 2, 6 | EADDRINUSE = 3, 7 | EADDRNOTAVAIL = 4, 8 | EAFNOSUPPORT = 5, 9 | EAGAIN = 6, 10 | EALREADY = 7, 11 | EBADF = 8, 12 | EBADMSG = 9, 13 | EBUSY = 10, 14 | ECANCELED = 11, 15 | ECHILD = 12, 16 | ECONNABORTED = 13, 17 | ECONNREFUSED = 14, 18 | ECONNRESET = 15, 19 | EDEADLK = 16, 20 | EDESTADDRREQ = 17, 21 | EDOM = 18, 22 | EDQUOT = 19, 23 | EEXIST = 20, 24 | EFAULT = 21, 25 | EFBIG = 22, 26 | EHOSTUNREACH = 23, 27 | EIDRM = 24, 28 | EILSEQ = 25, 29 | EINPROGRESS = 26, 30 | EINTR = 27, 31 | EINVAL = 28, 32 | EIO = 29, 33 | EISCONN = 30, 34 | EISDIR = 31, 35 | ELOOP = 32, 36 | EMFILE = 33, 37 | EMLINK = 34, 38 | EMSGSIZE = 35, 39 | EMULTIHOP = 36, 40 | ENAMETOOLONG = 37, 41 | ENETDOWN = 38, 42 | ENETRESET = 39, 43 | ENETUNREACH = 40, 44 | ENFILE = 41, 45 | ENOBUFS = 42, 46 | ENODEV = 43, 47 | ENOENT = 44, 48 | ENOEXEC = 45, 49 | ENOLCK = 46, 50 | ENOLINK = 47, 51 | ENOMEM = 48, 52 | ENOMSG = 49, 53 | ENOPROTOOPT = 50, 54 | ENOSPC = 51, 55 | ENOSYS = 52, 56 | ENOTCONN = 53, 57 | ENOTDIR = 54, 58 | ENOTEMPTY = 55, 59 | ENOTRECOVERABLE = 56, 60 | ENOTSOCK = 57, 61 | ENOTSUP = 58, 62 | ENOTTY = 59, 63 | ENXIO = 60, 64 | EOVERFLOW = 61, 65 | EOWNERDEAD = 62, 66 | EPERM = 63, 67 | EPIPE = 64, 68 | EPROTO = 65, 69 | EPROTONOSUPPORT = 66, 70 | EPROTOTYPE = 67, 71 | ERANGE = 68, 72 | EROFS = 69, 73 | ESPIPE = 70, 74 | ESRCH = 71, 75 | ESTALE = 72, 76 | ETIMEDOUT = 73, 77 | ETXTBSY = 74, 78 | EXDEV = 75, 79 | ENOTCAPABLE = 76 80 | } 81 | 82 | export const enum WasiFileType { 83 | UNKNOWN = 0, 84 | BLOCK_DEVICE = 1, 85 | CHARACTER_DEVICE = 2, 86 | DIRECTORY = 3, 87 | REGULAR_FILE = 4, 88 | SOCKET_DGRAM = 5, 89 | SOCKET_STREAM = 6, 90 | SYMBOLIC_LINK = 7 91 | } 92 | 93 | const FD_DATASYNC = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(0)) as 1n 94 | const FD_READ = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(1)) as 2n 95 | const FD_SEEK = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(2)) as 4n 96 | const FD_FDSTAT_SET_FLAGS = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(3)) as 8n 97 | const FD_SYNC = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(4)) as 16n 98 | const FD_TELL = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(5)) as 32n 99 | const FD_WRITE = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(6)) as 64n 100 | const FD_ADVISE = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(7)) as 128n 101 | const FD_ALLOCATE = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(8)) as 256n 102 | const PATH_CREATE_DIRECTORY = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(9)) as 512n 103 | const PATH_CREATE_FILE = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(10)) as 1024n 104 | const PATH_LINK_SOURCE = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(11)) as 2048n 105 | const PATH_LINK_TARGET = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(12)) as 4092n 106 | const PATH_OPEN = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(13)) as 8192n 107 | const FD_READDIR = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(14)) as 16384n 108 | const PATH_READLINK = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(15)) as 32768n 109 | const PATH_RENAME_SOURCE = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(16)) as 65536n 110 | const PATH_RENAME_TARGET = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(17)) as 131072n 111 | const PATH_FILESTAT_GET = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(18)) as 262144n 112 | const PATH_FILESTAT_SET_SIZE = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(19)) as 524288n 113 | const PATH_FILESTAT_SET_TIMES = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(20)) as 1048576n 114 | const FD_FILESTAT_GET = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(21)) as 2097152n 115 | const FD_FILESTAT_SET_SIZE = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(22)) as 4194304n 116 | const FD_FILESTAT_SET_TIMES = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(23)) as 8388608n 117 | const PATH_SYMLINK = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(24)) as 16777216n 118 | const PATH_REMOVE_DIRECTORY = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(25)) as 33554432n 119 | const PATH_UNLINK_FILE = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(26)) as 67108864n 120 | const POLL_FD_READWRITE = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(27)) as 134217728n 121 | const SOCK_SHUTDOWN = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(28)) as 268435456n 122 | const SOCK_ACCEPT = (/*#__PURE__*/ BigInt(1) << /*#__PURE__*/ BigInt(29)) as 536870912n 123 | 124 | export const WasiRights = { 125 | FD_DATASYNC, 126 | FD_READ, 127 | FD_SEEK, 128 | FD_FDSTAT_SET_FLAGS, 129 | FD_SYNC, 130 | FD_TELL, 131 | FD_WRITE, 132 | FD_ADVISE, 133 | FD_ALLOCATE, 134 | PATH_CREATE_DIRECTORY, 135 | PATH_CREATE_FILE, 136 | PATH_LINK_SOURCE, 137 | PATH_LINK_TARGET, 138 | PATH_OPEN, 139 | FD_READDIR, 140 | PATH_READLINK, 141 | PATH_RENAME_SOURCE, 142 | PATH_RENAME_TARGET, 143 | PATH_FILESTAT_GET, 144 | PATH_FILESTAT_SET_SIZE, 145 | PATH_FILESTAT_SET_TIMES, 146 | FD_FILESTAT_GET, 147 | FD_FILESTAT_SET_SIZE, 148 | FD_FILESTAT_SET_TIMES, 149 | PATH_SYMLINK, 150 | PATH_REMOVE_DIRECTORY, 151 | PATH_UNLINK_FILE, 152 | POLL_FD_READWRITE, 153 | SOCK_SHUTDOWN, 154 | SOCK_ACCEPT 155 | } 156 | 157 | export const enum WasiWhence { 158 | SET = 0, 159 | CUR = 1, 160 | END = 2 161 | } 162 | 163 | export const enum FileControlFlag { 164 | O_RDONLY = 0, 165 | O_WRONLY = 1, 166 | O_RDWR = 2, 167 | O_CREAT = 64, 168 | O_EXCL = 128, 169 | O_NOCTTY = 256, 170 | O_TRUNC = 512, 171 | O_APPEND = 1024, 172 | O_DIRECTORY = 65536, 173 | O_NOATIME = 262144, 174 | O_NOFOLLOW = 131072, 175 | O_SYNC = 1052672, 176 | O_DIRECT = 16384, 177 | O_NONBLOCK = 2048 178 | } 179 | 180 | export const enum WasiFileControlFlag { 181 | O_CREAT = (1 << 0), 182 | O_DIRECTORY = (1 << 1), 183 | O_EXCL = (1 << 2), 184 | O_TRUNC = (1 << 3) 185 | } 186 | 187 | export const enum WasiFdFlag { 188 | APPEND = (1 << 0), 189 | DSYNC = (1 << 1), 190 | NONBLOCK = (1 << 2), 191 | RSYNC = (1 << 3), 192 | SYNC = (1 << 4) 193 | } 194 | 195 | export const enum WasiClockid { 196 | REALTIME = 0, 197 | MONOTONIC = 1, 198 | PROCESS_CPUTIME_ID = 2, 199 | THREAD_CPUTIME_ID = 3 200 | } 201 | 202 | export const enum WasiFstFlag { 203 | SET_ATIM = (1 << 0), 204 | SET_ATIM_NOW = (1 << 1), 205 | SET_MTIM = (1 << 2), 206 | SET_MTIM_NOW = (1 << 3) 207 | } 208 | 209 | export const enum WasiEventType { 210 | CLOCK = 0, 211 | FD_READ = 1, 212 | FD_WRITE = 2 213 | } 214 | 215 | export const enum WasiSubclockflags { 216 | ABSTIME = (1 << 0) 217 | } 218 | 219 | export interface Subscription { 220 | userdata: bigint 221 | type: T 222 | u: { 223 | clock: { 224 | clock_id: number 225 | timeout: bigint 226 | precision: bigint 227 | flags: WasiSubclockflags 228 | } 229 | fd_readwrite: { 230 | fd: number 231 | } 232 | } 233 | } 234 | 235 | export type FdEventSubscription = Subscription 236 | 237 | export type u8 = number 238 | export type u16 = number 239 | export type u32 = number 240 | export type s64 = bigint 241 | export type u64 = bigint 242 | export type fd = number 243 | export type filedelta = s64 244 | export type filesize = u64 245 | export type size = u32 | u64 246 | export type exitcode = u32 247 | export type Pointer = number | bigint | T 248 | -------------------------------------------------------------------------------- /src/asyncify.ts: -------------------------------------------------------------------------------- 1 | import { _WebAssembly } from './webassembly' 2 | import { isPromiseLike, wrapInstanceExports } from './wasi/util' 3 | 4 | const ignoreNames = [ 5 | 'asyncify_get_state', 6 | 'asyncify_start_rewind', 7 | 'asyncify_start_unwind', 8 | 'asyncify_stop_rewind', 9 | 'asyncify_stop_unwind' 10 | ] 11 | 12 | // const wrappedExports = new WeakMap() 13 | 14 | const enum AsyncifyState { 15 | NONE, 16 | UNWINDING, 17 | REWINDING, 18 | } 19 | 20 | /** @public */ 21 | export type AsyncifyExportName = 22 | 'asyncify_get_state' | 23 | 'asyncify_start_unwind' | 24 | 'asyncify_stop_unwind' | 25 | 'asyncify_start_rewind' | 26 | 'asyncify_stop_rewind' 27 | 28 | type AsyncifiedExports = { 29 | asyncify_get_state: () => AsyncifyState 30 | asyncify_start_unwind: (p: number | bigint) => void 31 | asyncify_stop_unwind: () => void 32 | asyncify_start_rewind: (p: number | bigint) => void 33 | asyncify_stop_rewind: () => void 34 | [x: string]: WebAssembly.ExportValue 35 | } 36 | 37 | /** @public */ 38 | export type Callable = (...args: any[]) => any 39 | 40 | /** @public */ 41 | export type AsyncifyExportFunction = T extends Callable ? (...args: Parameters) => Promise> : T 42 | 43 | /** @public */ 44 | export type AsyncifyExports = T extends Record 45 | ? { 46 | [P in keyof T]: T[P] extends Callable 47 | ? U extends Array> 48 | ? P extends U[number] 49 | ? AsyncifyExportFunction 50 | : T[P] 51 | : AsyncifyExportFunction 52 | : T[P] 53 | } 54 | : T 55 | 56 | /** @public */ 57 | export interface AsyncifyOptions { 58 | wasm64?: boolean 59 | tryAllocate?: boolean | { 60 | size?: number 61 | name?: string 62 | } 63 | wrapExports?: string[] 64 | } 65 | 66 | interface AsyncifyDataAddress { 67 | wasm64: boolean 68 | dataPtr: number 69 | start: number 70 | end: number 71 | } 72 | 73 | function tryAllocate (instance: WebAssembly.Instance, wasm64: boolean, size: number, mallocName: string): AsyncifyDataAddress { 74 | if (typeof instance.exports[mallocName] !== 'function' || size <= 0) { 75 | return { 76 | wasm64, 77 | dataPtr: 16, 78 | start: wasm64 ? 32 : 24, 79 | end: 1024 80 | } 81 | } 82 | const malloc = instance.exports[mallocName] as Function 83 | const dataPtr: number = wasm64 ? Number(malloc(BigInt(16) + BigInt(size))) : malloc(8 + size) 84 | if (dataPtr === 0) { 85 | throw new Error('Allocate asyncify data failed') 86 | } 87 | return wasm64 88 | ? { wasm64, dataPtr, start: dataPtr + 16, end: dataPtr + 16 + size } 89 | : { wasm64, dataPtr, start: dataPtr + 8, end: dataPtr + 8 + size } 90 | } 91 | 92 | /** @public */ 93 | export class Asyncify { 94 | private value: any = undefined 95 | private exports: AsyncifiedExports | undefined = undefined 96 | private dataPtr: number = 0 97 | 98 | public init>> ( 99 | memory: WebAssembly.Memory, 100 | instance: { readonly exports: T }, 101 | options: AsyncifyOptions 102 | ): { readonly exports: AsyncifyExports } { 103 | if (this.exports) { 104 | throw new Error('Asyncify has been initialized') 105 | } 106 | if (!(memory instanceof _WebAssembly.Memory)) { 107 | throw new TypeError('Require WebAssembly.Memory object') 108 | } 109 | const exports = instance.exports 110 | for (let i = 0; i < ignoreNames.length; ++i) { 111 | if (typeof exports[ignoreNames[i]] !== 'function') { 112 | throw new TypeError('Invalid asyncify wasm') 113 | } 114 | } 115 | let address: AsyncifyDataAddress 116 | const wasm64 = Boolean(options.wasm64) 117 | 118 | if (!options.tryAllocate) { 119 | address = { 120 | wasm64, 121 | dataPtr: 16, 122 | start: wasm64 ? 32 : 24, 123 | end: 1024 124 | } 125 | } else { 126 | if (options.tryAllocate === true) { 127 | address = tryAllocate(instance, wasm64, 4096, 'malloc') 128 | } else { 129 | address = tryAllocate(instance, wasm64, options.tryAllocate.size ?? 4096, options.tryAllocate.name ?? 'malloc') 130 | } 131 | } 132 | this.dataPtr = address.dataPtr 133 | if (wasm64) { 134 | new BigInt64Array(memory.buffer, this.dataPtr).set([BigInt(address.start), BigInt(address.end)]) 135 | } else { 136 | new Int32Array(memory.buffer, this.dataPtr).set([address.start, address.end]) 137 | } 138 | this.exports = this.wrapExports(exports, options.wrapExports as any) as any 139 | const asyncifiedInstance = Object.create(_WebAssembly.Instance.prototype) 140 | Object.defineProperty(asyncifiedInstance, 'exports', { value: this.exports }) 141 | // Object.setPrototypeOf(instance, Instance.prototype) 142 | return asyncifiedInstance 143 | } 144 | 145 | private assertState (): void { 146 | if (this.exports!.asyncify_get_state() !== AsyncifyState.NONE) { 147 | throw new Error('Asyncify state error') 148 | } 149 | } 150 | 151 | public wrapImportFunction (f: T): T { 152 | // eslint-disable-next-line @typescript-eslint/no-this-alias 153 | const _this = this 154 | return (function (this: any) { 155 | // eslint-disable-next-line no-unreachable-loop 156 | while (_this.exports!.asyncify_get_state() === AsyncifyState.REWINDING) { 157 | _this.exports!.asyncify_stop_rewind() 158 | return _this.value 159 | } 160 | _this.assertState() 161 | const v = f.apply(this, arguments) 162 | if (!isPromiseLike(v)) return v 163 | _this.exports!.asyncify_start_unwind(_this.dataPtr) 164 | _this.value = v 165 | }) as any 166 | } 167 | 168 | public wrapImports (imports: T): T { 169 | const importObject: any = {} 170 | Object.keys(imports).forEach(k => { 171 | const mod = imports[k] 172 | const newModule: any = {} 173 | Object.keys(mod).forEach(name => { 174 | const importValue = mod[name] 175 | if (typeof importValue === 'function') { 176 | newModule[name] = this.wrapImportFunction(importValue as any) 177 | } else { 178 | newModule[name] = importValue 179 | } 180 | }) 181 | importObject[k] = newModule 182 | }) 183 | return importObject 184 | } 185 | 186 | public wrapExportFunction (f: T): AsyncifyExportFunction { 187 | // eslint-disable-next-line @typescript-eslint/no-this-alias 188 | const _this = this 189 | return (async function (this: any) { 190 | _this.assertState() 191 | let ret = f.apply(this, arguments) 192 | 193 | while (_this.exports!.asyncify_get_state() === AsyncifyState.UNWINDING) { 194 | _this.exports!.asyncify_stop_unwind() 195 | _this.value = await _this.value 196 | _this.assertState() 197 | _this.exports!.asyncify_start_rewind(_this.dataPtr) 198 | ret = f.call(this) 199 | } 200 | 201 | _this.assertState() 202 | return ret 203 | }) as any 204 | } 205 | 206 | public wrapExports (exports: T): AsyncifyExports 207 | public wrapExports>> (exports: T, needWrap: U): AsyncifyExports 208 | public wrapExports>> (exports: T, needWrap?: U): AsyncifyExports { 209 | return wrapInstanceExports(exports, (exportValue, name) => { 210 | let ignore = ignoreNames.indexOf(name) !== -1 || typeof exportValue !== 'function' 211 | if (Array.isArray(needWrap)) { 212 | ignore = ignore || (needWrap.indexOf(name as any) === -1) 213 | } 214 | return ignore ? exportValue : this.wrapExportFunction(exportValue as any) 215 | }) as AsyncifyExports 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/wasi/fs.ts: -------------------------------------------------------------------------------- 1 | /** @public */ 2 | export interface StatsBase { 3 | isFile(): boolean 4 | isDirectory(): boolean 5 | isBlockDevice(): boolean 6 | isCharacterDevice(): boolean 7 | isSymbolicLink(): boolean 8 | isFIFO(): boolean 9 | isSocket(): boolean 10 | 11 | dev: T 12 | ino: T 13 | mode: T 14 | nlink: T 15 | uid: T 16 | gid: T 17 | rdev: T 18 | size: T 19 | blksize: T 20 | blocks: T 21 | atimeMs: T 22 | mtimeMs: T 23 | ctimeMs: T 24 | birthtimeMs: T 25 | atime: Date 26 | mtime: Date 27 | ctime: Date 28 | birthtime: Date 29 | } 30 | 31 | /** @public */ 32 | export interface Stats extends StatsBase {} 33 | /** @public */ 34 | export interface BigIntStats extends StatsBase {} 35 | 36 | /** @public */ 37 | export interface StatOptions { 38 | bigint?: boolean 39 | } 40 | 41 | /** @public */ 42 | export type PathLike = string | Uint8Array | URL 43 | 44 | /** @public */ 45 | export type BufferEncoding = 'ascii' | 'utf8' | 'utf-8' | 'utf16le' | 'ucs2' | 'ucs-2' | 'base64' | 'base64url' | 'latin1' | 'binary' | 'hex' 46 | 47 | /** @public */ 48 | export type BufferEncodingOption = 'buffer' | { encoding: 'buffer' } 49 | 50 | /** @public */ 51 | export interface BaseEncodingOptions { 52 | encoding?: BufferEncoding 53 | } 54 | 55 | /** @public */ 56 | export type OpenMode = number | string 57 | 58 | /** @public */ 59 | export type Mode = number | string 60 | 61 | /** @public */ 62 | export interface ReadSyncOptions { 63 | offset?: number 64 | length?: number 65 | position?: number 66 | } 67 | 68 | /** @public */ 69 | export interface IDirent { 70 | isFile(): boolean 71 | isDirectory(): boolean 72 | isBlockDevice(): boolean 73 | isCharacterDevice(): boolean 74 | isSymbolicLink(): boolean 75 | isFIFO(): boolean 76 | isSocket(): boolean 77 | name: string 78 | } 79 | 80 | /** @public */ 81 | export interface MakeDirectoryOptions { 82 | recursive?: boolean 83 | mode?: Mode 84 | } 85 | 86 | /** @public */ 87 | export interface RmDirOptions { 88 | maxRetries?: number 89 | recursive?: boolean 90 | retryDelay?: number 91 | } 92 | 93 | /** @public */ 94 | export interface FileHandle { 95 | readonly fd: number 96 | datasync (): Promise 97 | sync (): Promise 98 | read (buffer: TBuffer, offset?: number, length?: number, position?: number): Promise<{ bytesRead: number; buffer: TBuffer }> 99 | 100 | stat (opts?: StatOptions & { bigint?: false }): Promise 101 | stat (opts: StatOptions & { bigint: true }): Promise 102 | stat (opts?: StatOptions): Promise 103 | 104 | truncate (len?: number): Promise 105 | 106 | utimes (atime: string | number | Date, mtime: string | number | Date): Promise 107 | 108 | write (buffer: TBuffer, offset?: number, length?: number, position?: number): Promise<{ bytesWritten: number; buffer: TBuffer }> 109 | write (data: string | Uint8Array, position?: number, encoding?: BufferEncoding): Promise<{ bytesWritten: number; buffer: string }> 110 | 111 | close (): Promise 112 | } 113 | 114 | /** @public */ 115 | export interface IFsPromises { 116 | stat (path: PathLike, options?: StatOptions & { bigint?: false }): Promise 117 | stat (path: PathLike, options: StatOptions & { bigint: true }): Promise 118 | stat (path: PathLike, options?: StatOptions): Promise 119 | 120 | lstat (path: PathLike, options?: StatOptions & { bigint?: false }): Promise 121 | lstat (path: PathLike, options: StatOptions & { bigint: true }): Promise 122 | lstat (path: PathLike, options?: StatOptions): Promise 123 | 124 | utimes (path: PathLike, atime: string | number | Date, mtime: string | number | Date): Promise 125 | 126 | open (path: PathLike, flags: OpenMode, mode?: Mode): Promise 127 | 128 | read( 129 | handle: FileHandle, 130 | buffer: TBuffer, 131 | offset?: number, 132 | length?: number, 133 | position?: number, 134 | ): Promise<{ bytesRead: number; buffer: TBuffer }> 135 | 136 | write( 137 | handle: FileHandle, 138 | buffer: TBuffer, 139 | offset?: number, 140 | length?: number, position?: number 141 | ): Promise<{ bytesWritten: number; buffer: TBuffer }> 142 | 143 | readlink (path: PathLike, options?: BaseEncodingOptions | BufferEncoding): Promise 144 | readlink (path: PathLike, options: BufferEncodingOption): Promise 145 | readlink (path: PathLike, options?: BaseEncodingOptions | string): Promise 146 | 147 | realpath (path: PathLike, options?: BaseEncodingOptions | BufferEncoding): Promise 148 | realpath (path: PathLike, options: BufferEncodingOption): Promise 149 | realpath (path: PathLike, options?: BaseEncodingOptions | string): Promise 150 | 151 | truncate (path: PathLike, len?: number): Promise 152 | ftruncate (handle: FileHandle, len?: number): Promise 153 | fdatasync (handle: FileHandle): Promise 154 | futimes (handle: FileHandle, atime: string | number | Date, mtime: string | number | Date): Promise 155 | 156 | readdir (path: PathLike, options?: { encoding: BufferEncoding | null; withFileTypes?: false } | BufferEncoding): Promise 157 | readdir (path: PathLike, options: { encoding: 'buffer'; withFileTypes?: false } | 'buffer'): Promise 158 | readdir (path: PathLike, options?: BaseEncodingOptions & { withFileTypes?: false } | BufferEncoding): Promise 159 | readdir (path: PathLike, options: BaseEncodingOptions & { withFileTypes: true }): Promise 160 | 161 | fsync (handle: FileHandle): Promise 162 | 163 | mkdir (path: PathLike, options: MakeDirectoryOptions & { recursive: true }): Promise 164 | mkdir (path: PathLike, options?: Mode | (MakeDirectoryOptions & { recursive?: false })): Promise 165 | mkdir (path: PathLike, options?: Mode | MakeDirectoryOptions): Promise 166 | 167 | rmdir (path: PathLike, options?: RmDirOptions): Promise 168 | 169 | link (existingPath: PathLike, newPath: PathLike): Promise 170 | unlink (path: PathLike): Promise 171 | rename (oldPath: PathLike, newPath: PathLike): Promise 172 | symlink (target: PathLike, path: PathLike, type?: 'dir' | 'file' | 'junction'): Promise 173 | } 174 | 175 | /** @public */ 176 | export interface IFs { 177 | fstatSync (fd: number, options?: StatOptions & { bigint?: false }): Stats 178 | fstatSync (fd: number, options: StatOptions & { bigint: true }): BigIntStats 179 | fstatSync (fd: number, options?: StatOptions): Stats | BigIntStats 180 | 181 | statSync (path: PathLike, options?: StatOptions & { bigint?: false }): Stats 182 | statSync (path: PathLike, options: StatOptions & { bigint: true }): BigIntStats 183 | statSync (path: PathLike, options?: StatOptions): Stats | BigIntStats 184 | 185 | lstatSync (path: PathLike, options?: StatOptions & { bigint?: false }): Stats 186 | lstatSync (path: PathLike, options: StatOptions & { bigint: true }): BigIntStats 187 | lstatSync (path: PathLike, options?: StatOptions): Stats | BigIntStats 188 | 189 | utimesSync (path: PathLike, atime: string | number | Date, mtime: string | number | Date): void 190 | 191 | openSync (path: PathLike, flags: OpenMode, mode?: Mode): number 192 | 193 | readSync (fd: number, buffer: ArrayBufferView, offset: number, length: number, position: number | null): number 194 | readSync (fd: number, buffer: ArrayBufferView, opts?: ReadSyncOptions): number 195 | 196 | writeSync (fd: number, buffer: ArrayBufferView, offset?: number, length?: number, position?: number): number 197 | writeSync (fd: number, string: string, position?: number, encoding?: BufferEncoding): number 198 | 199 | closeSync (fd: number): void 200 | 201 | readlinkSync (path: PathLike, options?: BaseEncodingOptions | BufferEncoding): string 202 | readlinkSync (path: PathLike, options: BufferEncodingOption): Uint8Array 203 | readlinkSync (path: PathLike, options?: BaseEncodingOptions | string): string | Uint8Array 204 | 205 | realpathSync (path: PathLike, options?: BaseEncodingOptions | BufferEncoding): string 206 | realpathSync (path: PathLike, options: BufferEncodingOption): Uint8Array 207 | realpathSync (path: PathLike, options?: BaseEncodingOptions | string): string | Uint8Array 208 | 209 | truncateSync (path: PathLike, len?: number): void 210 | ftruncateSync (fd: number, len?: number): void 211 | fdatasyncSync (fd: number): void 212 | futimesSync (fd: number, atime: string | number | Date, mtime: string | number | Date): void 213 | 214 | readdirSync (path: PathLike, options?: { encoding: BufferEncoding | null; withFileTypes?: false } | BufferEncoding): string[] 215 | readdirSync (path: PathLike, options: { encoding: 'buffer'; withFileTypes?: false } | 'buffer'): Uint8Array[] 216 | readdirSync (path: PathLike, options?: BaseEncodingOptions & { withFileTypes?: false } | BufferEncoding): string[] | Uint8Array[] 217 | readdirSync (path: PathLike, options: BaseEncodingOptions & { withFileTypes: true }): IDirent[] 218 | 219 | fsyncSync (fd: number): void 220 | 221 | mkdirSync (path: PathLike, options: MakeDirectoryOptions & { recursive: true }): string | undefined 222 | mkdirSync (path: PathLike, options?: Mode | (MakeDirectoryOptions & { recursive?: false })): void 223 | mkdirSync (path: PathLike, options?: Mode | MakeDirectoryOptions): string | undefined 224 | 225 | rmdirSync (path: PathLike, options?: RmDirOptions): void 226 | 227 | linkSync (existingPath: PathLike, newPath: PathLike): void 228 | unlinkSync (path: PathLike): void 229 | renameSync (oldPath: PathLike, newPath: PathLike): void 230 | symlinkSync (target: PathLike, path: PathLike, type?: 'dir' | 'file' | 'junction'): void 231 | } 232 | -------------------------------------------------------------------------------- /src/wasi/index.ts: -------------------------------------------------------------------------------- 1 | import { WASI as WASIPreview1 } from './preview1' 2 | import type { Preopen } from './preview1' 3 | import type { exitcode } from './types' 4 | 5 | import { 6 | validateObject, 7 | validateArray, 8 | validateBoolean, 9 | validateFunction, 10 | validateUndefined, 11 | validateString 12 | } from './util' 13 | import type { IFs, IFsPromises } from './fs' 14 | import type { Asyncify } from '../asyncify' 15 | 16 | // eslint-disable-next-line spaced-comment 17 | const kEmptyObject = /*#__PURE__*/ Object.freeze(/*#__PURE__*/ Object.create(null)) 18 | const kExitCode = Symbol('kExitCode') 19 | const kSetMemory = Symbol('kSetMemory') 20 | const kStarted = Symbol('kStarted') 21 | const kInstance = Symbol('kInstance') 22 | const kBindingName = Symbol('kBindingName') 23 | 24 | /** @public */ 25 | export interface WASIOptions { 26 | version?: 'unstable' | 'preview1' 27 | args?: string[] | undefined 28 | env?: Record | undefined 29 | preopens?: Record | undefined 30 | 31 | /** 32 | * @defaultValue `false` 33 | */ 34 | returnOnExit?: boolean | undefined 35 | 36 | // /** 37 | // * @defaultValue `0` 38 | // */ 39 | // stdin?: number | undefined 40 | 41 | // /** 42 | // * @defaultValue `1` 43 | // */ 44 | // stdout?: number | undefined 45 | 46 | // /** 47 | // * @defaultValue `2` 48 | // */ 49 | // stderr?: number | undefined 50 | 51 | print?: (str: string) => void 52 | printErr?: (str: string) => void 53 | } 54 | 55 | /** @public */ 56 | export interface SyncWASIOptions extends WASIOptions { 57 | fs?: IFs 58 | } 59 | 60 | /** @public */ 61 | export interface AsyncWASIOptions extends WASIOptions { 62 | fs: { promises: IFsPromises } 63 | asyncify?: Asyncify 64 | } 65 | 66 | function validateOptions (this: WASI, options: WASIOptions & { fs?: IFs | { promises: IFsPromises } }): { 67 | args: string[] 68 | env: string[] 69 | preopens: Preopen[] 70 | stdio: readonly [0, 1, 2] 71 | _WASI: any 72 | } { 73 | validateObject(options, 'options') 74 | 75 | let _WASI: any 76 | if (options.version !== undefined) { 77 | validateString(options.version, 'options.version') 78 | switch (options.version) { 79 | case 'unstable': 80 | _WASI = WASIPreview1 81 | this[kBindingName] = 'wasi_unstable' 82 | break 83 | case 'preview1': 84 | _WASI = WASIPreview1 85 | this[kBindingName] = 'wasi_snapshot_preview1' 86 | break 87 | default: 88 | throw new TypeError(`unsupported WASI version "${options.version as string}"`) 89 | } 90 | } else { 91 | _WASI = WASIPreview1 92 | this[kBindingName] = 'wasi_snapshot_preview1' 93 | } 94 | 95 | if (options.args !== undefined) { 96 | validateArray(options.args, 'options.args') 97 | } 98 | const args = (options.args ?? []).map(String) 99 | 100 | const env: string[] = [] 101 | if (options.env !== undefined) { 102 | validateObject(options.env, 'options.env') 103 | Object.entries(options.env).forEach(({ 0: key, 1: value }) => { 104 | if (value !== undefined) { 105 | env.push(`${key}=${value}`) 106 | } 107 | }) 108 | } 109 | 110 | const preopens: Preopen[] = [] 111 | if (options.preopens !== undefined) { 112 | validateObject(options.preopens, 'options.preopens') 113 | Object.entries(options.preopens).forEach( 114 | ({ 0: key, 1: value }) => 115 | preopens.push({ mappedPath: String(key), realPath: String(value) }) 116 | ) 117 | } 118 | 119 | if (preopens.length > 0) { 120 | if (options.fs === undefined) { 121 | throw new Error('filesystem is disabled, can not preopen directory') 122 | } 123 | try { 124 | validateObject(options.fs, 'options.fs') 125 | } catch (_) { 126 | throw new TypeError('Node.js fs like implementation is not provided') 127 | } 128 | } 129 | 130 | // if (options.filesystem !== undefined) { 131 | // validateObject(options.filesystem, 'options.filesystem') 132 | // validateString(options.filesystem.type, 'options.filesystem.type') 133 | // if (options.filesystem.type !== 'memfs' && options.filesystem.type !== 'file-system-access-api') { 134 | // throw new Error(`Filesystem type ${(options.filesystem as any).type as string} is not supported, only "memfs" and "file-system-access-api" is supported currently`) 135 | // } 136 | // try { 137 | // validateObject(options.filesystem.fs, 'options.filesystem.fs') 138 | // } catch (_) { 139 | // throw new Error('Node.js fs like implementation is not provided') 140 | // } 141 | // } 142 | 143 | if (options.print !== undefined) validateFunction(options.print, 'options.print') 144 | if (options.printErr !== undefined) validateFunction(options.printErr, 'options.printErr') 145 | 146 | if (options.returnOnExit !== undefined) { 147 | validateBoolean(options.returnOnExit, 'options.returnOnExit') 148 | } 149 | 150 | // const { stdin = 0, stdout = 1, stderr = 2 } = options 151 | // validateInt32(stdin, 'options.stdin', 0) 152 | // validateInt32(stdout, 'options.stdout', 0) 153 | // validateInt32(stderr, 'options.stderr', 0) 154 | // const stdio = [stdin, stdout, stderr] as const 155 | const stdio = [0, 1, 2] as const 156 | 157 | return { 158 | args, 159 | env, 160 | preopens, 161 | stdio, 162 | _WASI 163 | } 164 | } 165 | 166 | function initWASI (this: WASI, setMemory: (m: WebAssembly.Memory) => void, wrap: any): void { 167 | this[kSetMemory] = setMemory; 168 | (this as any).wasiImport = wrap 169 | this[kStarted] = false 170 | this[kExitCode] = 0 171 | this[kInstance] = undefined 172 | } 173 | 174 | /** @public */ 175 | export interface FinalizeBindingsOptions { 176 | memory?: WebAssembly.Memory 177 | } 178 | 179 | /** @public */ 180 | export class WASI { 181 | /* addWorkerListener (worker: any): void { 182 | if (worker && !worker._tybysWasmUtilWasiListener) { 183 | worker._tybysWasmUtilWasiListener = function _tybysWasmUtilWasiListener (e: any) { 184 | const data = e.data 185 | const msg = data.__tybys_wasm_util_wasi__ 186 | if (msg) { 187 | const type = msg.type 188 | const payload = msg.payload 189 | if (type === 'set-timeout') { 190 | const buffer = payload.buffer 191 | setTimeout(() => { 192 | const arr = new Int32Array(buffer) 193 | Atomics.store(arr, 0, 1) 194 | Atomics.notify(arr, 0) 195 | }, payload.delay) 196 | } 197 | } 198 | } 199 | if (typeof worker.on === 'function') { 200 | worker.on('message', worker._tybysWasmUtilWasiListener) 201 | } else { 202 | worker.addEventListener('message', worker._tybysWasmUtilWasiListener, false) 203 | } 204 | } 205 | } */ 206 | 207 | private [kSetMemory]!: (m: WebAssembly.Memory) => void 208 | private [kStarted]!: boolean 209 | private [kExitCode]!: number 210 | private [kInstance]: WebAssembly.Instance | undefined 211 | private [kBindingName]!: string 212 | 213 | public readonly wasiImport!: Record 214 | 215 | public constructor (options: SyncWASIOptions = kEmptyObject) { 216 | const { 217 | args, 218 | env, 219 | preopens, 220 | stdio, 221 | _WASI 222 | } = validateOptions.call(this, options) 223 | 224 | const wrap = _WASI.createSync( 225 | args, 226 | env, 227 | preopens, 228 | stdio, 229 | options.fs, 230 | options.print, 231 | options.printErr 232 | ) 233 | 234 | const setMemory = wrap._setMemory! 235 | delete wrap._setMemory 236 | initWASI.call(this, setMemory, wrap) 237 | if (options.returnOnExit) { wrap.proc_exit = wasiReturnOnProcExit.bind(this) } 238 | } 239 | 240 | finalizeBindings (instance: WebAssembly.Instance, { 241 | memory = instance?.exports?.memory as WebAssembly.Memory 242 | }: FinalizeBindingsOptions = {}): void { 243 | if (this[kStarted]) { 244 | throw new Error('WASI instance has already started') 245 | } 246 | 247 | validateObject(instance, 'instance') 248 | validateObject(instance.exports, 'instance.exports') 249 | 250 | this[kSetMemory](memory) 251 | 252 | this[kInstance] = instance 253 | this[kStarted] = true 254 | } 255 | 256 | // Must not export _initialize, must export _start 257 | start (instance: WebAssembly.Instance): number | undefined | Promise | Promise { 258 | this.finalizeBindings(instance) 259 | 260 | const { _start, _initialize } = this[kInstance]!.exports 261 | 262 | validateFunction(_start, 'instance.exports._start') 263 | validateUndefined(_initialize, 'instance.exports._initialize') 264 | 265 | let ret 266 | try { 267 | ret = (_start as () => any)() 268 | } catch (err) { 269 | if (err !== kExitCode) { 270 | throw err 271 | } 272 | } 273 | 274 | if (ret instanceof Promise) { 275 | return ret.then( 276 | () => this[kExitCode], 277 | (err) => { 278 | if (err !== kExitCode) { 279 | throw err 280 | } 281 | return this[kExitCode] 282 | } 283 | ) 284 | } 285 | return this[kExitCode] 286 | } 287 | 288 | // Must not export _start, may optionally export _initialize 289 | initialize (instance: WebAssembly.Instance): void | Promise { 290 | this.finalizeBindings(instance) 291 | 292 | const { _start, _initialize } = this[kInstance]!.exports 293 | 294 | validateUndefined(_start, 'instance.exports._start') 295 | if (_initialize !== undefined) { 296 | validateFunction(_initialize, 'instance.exports._initialize') 297 | return (_initialize as () => any)() 298 | } 299 | } 300 | 301 | getImportObject (): Record> { 302 | return { [this[kBindingName]]: this.wasiImport } 303 | } 304 | } 305 | 306 | function wasiReturnOnProcExit (this: WASI, rval: exitcode): exitcode { 307 | this[kExitCode] = rval 308 | // eslint-disable-next-line @typescript-eslint/no-throw-literal 309 | throw kExitCode 310 | } 311 | 312 | /** @public */ 313 | export async function createAsyncWASI (options: AsyncWASIOptions = kEmptyObject): Promise { 314 | const _this = Object.create(WASI.prototype) 315 | const { 316 | args, 317 | env, 318 | preopens, 319 | stdio, 320 | _WASI 321 | } = validateOptions.call(_this, options) 322 | 323 | if (options.asyncify !== undefined) { 324 | validateObject(options.asyncify, 'options.asyncify') 325 | validateFunction(options.asyncify.wrapImportFunction, 'options.asyncify.wrapImportFunction') 326 | } 327 | 328 | const wrap = await _WASI.createAsync( 329 | args, 330 | env, 331 | preopens, 332 | stdio, 333 | options.fs, 334 | options.print, 335 | options.printErr, 336 | options.asyncify 337 | ) 338 | 339 | const setMemory = wrap._setMemory! 340 | delete wrap._setMemory 341 | initWASI.call(_this, 342 | setMemory, 343 | wrap 344 | ) 345 | 346 | if (options.returnOnExit) { wrap.proc_exit = wasiReturnOnProcExit.bind(_this) } 347 | 348 | return _this 349 | } 350 | -------------------------------------------------------------------------------- /src/wasi/fd.ts: -------------------------------------------------------------------------------- 1 | import type { IFs, BigIntStats, FileHandle } from './fs' 2 | import { 3 | WasiErrno, 4 | FileControlFlag, 5 | WasiFileType, 6 | WasiWhence 7 | } from './types' 8 | import { getRights } from './rights' 9 | import { WasiError } from './error' 10 | 11 | export function concatBuffer (buffers: Uint8Array[], size?: number): Uint8Array { 12 | let total = 0 13 | if (typeof size === 'number' && size >= 0) { 14 | total = size 15 | } else { 16 | for (let i = 0; i < buffers.length; i++) { 17 | const buffer = buffers[i] 18 | total += buffer.length 19 | } 20 | } 21 | let pos = 0 22 | const ret = new Uint8Array(total) 23 | for (let i = 0; i < buffers.length; i++) { 24 | const buffer = buffers[i] 25 | ret.set(buffer, pos) 26 | pos += buffer.length 27 | } 28 | return ret 29 | } 30 | 31 | export class FileDescriptor { 32 | public pos = BigInt(0) 33 | public size = BigInt(0) 34 | 35 | constructor ( 36 | public id: number, 37 | public fd: number | FileHandle, 38 | public path: string, 39 | public realPath: string, 40 | public type: WasiFileType, 41 | public rightsBase: bigint, 42 | public rightsInheriting: bigint, 43 | public preopen: number 44 | ) {} 45 | 46 | seek (offset: bigint, whence: WasiWhence): bigint { 47 | if (whence === WasiWhence.SET) { 48 | this.pos = BigInt(offset) 49 | } else if (whence === WasiWhence.CUR) { 50 | this.pos += BigInt(offset) 51 | } else if (whence === WasiWhence.END) { 52 | this.pos = BigInt(this.size) - BigInt(offset) 53 | } else { 54 | throw new WasiError('Unknown whence', WasiErrno.EIO) 55 | } 56 | return this.pos 57 | } 58 | } 59 | 60 | export class StandardOutput extends FileDescriptor { 61 | private readonly _log: (str: string) => void 62 | private _buf: Uint8Array | null 63 | constructor ( 64 | log: (str: string) => void, 65 | id: number, 66 | fd: number, 67 | path: string, 68 | realPath: string, 69 | type: WasiFileType, 70 | rightsBase: bigint, 71 | rightsInheriting: bigint, 72 | preopen: number 73 | ) { 74 | super(id, fd, path, realPath, type, rightsBase, rightsInheriting, preopen) 75 | this._log = log 76 | this._buf = null 77 | } 78 | 79 | write (buffer: Uint8Array): number { 80 | const originalBuffer = buffer 81 | if (this._buf) { 82 | buffer = concatBuffer([this._buf, buffer]) 83 | this._buf = null 84 | } 85 | 86 | if (buffer.indexOf(10) === -1) { 87 | this._buf = buffer 88 | return originalBuffer.byteLength 89 | } 90 | 91 | let written = 0 92 | let lastBegin = 0 93 | let index 94 | while ((index = buffer.indexOf(10, written)) !== -1) { 95 | const str = new TextDecoder().decode(buffer.subarray(lastBegin, index)) 96 | this._log(str) 97 | written += index - lastBegin + 1 98 | lastBegin = index + 1 99 | } 100 | 101 | if (written < buffer.length) { 102 | this._buf = buffer.slice(written) 103 | } 104 | 105 | return originalBuffer.byteLength 106 | } 107 | } 108 | 109 | export function toFileType (stat: BigIntStats): WasiFileType { 110 | if (stat.isBlockDevice()) return WasiFileType.BLOCK_DEVICE 111 | if (stat.isCharacterDevice()) return WasiFileType.CHARACTER_DEVICE 112 | if (stat.isDirectory()) return WasiFileType.DIRECTORY 113 | if (stat.isSocket()) return WasiFileType.SOCKET_STREAM 114 | if (stat.isFile()) return WasiFileType.REGULAR_FILE 115 | if (stat.isSymbolicLink()) return WasiFileType.SYMBOLIC_LINK 116 | return WasiFileType.UNKNOWN 117 | } 118 | 119 | export function toFileStat (view: DataView, buf: number, stat: BigIntStats): void { 120 | view.setBigUint64(buf, stat.dev, true) 121 | view.setBigUint64(buf + 8, stat.ino, true) 122 | view.setBigUint64(buf + 16, BigInt(toFileType(stat)), true) 123 | view.setBigUint64(buf + 24, stat.nlink, true) 124 | view.setBigUint64(buf + 32, stat.size, true) 125 | view.setBigUint64(buf + 40, stat.atimeMs * BigInt(1000000), true) 126 | view.setBigUint64(buf + 48, stat.mtimeMs * BigInt(1000000), true) 127 | view.setBigUint64(buf + 56, stat.ctimeMs * BigInt(1000000), true) 128 | } 129 | 130 | export interface FileDescriptorTableOptions { 131 | size: number 132 | in: number 133 | out: number 134 | err: number 135 | print?: (str: string) => void 136 | printErr?: (str: string) => void 137 | } 138 | 139 | export interface SyncTableOptions extends FileDescriptorTableOptions { 140 | fs?: IFs | undefined 141 | } 142 | 143 | export interface AsyncTableOptions extends FileDescriptorTableOptions { 144 | // fs: { promises: IFsPromises } 145 | } 146 | 147 | export class FileDescriptorTable { 148 | public used: number 149 | public size: number 150 | public fds: FileDescriptor[] 151 | public stdio: [number, number, number] 152 | public print?: (str: string) => void 153 | public printErr?: (str: string) => void 154 | 155 | protected constructor (options: FileDescriptorTableOptions) { 156 | this.used = 0 157 | this.size = options.size 158 | this.fds = Array(options.size) 159 | this.stdio = [options.in, options.out, options.err] 160 | this.print = options.print 161 | this.printErr = options.printErr 162 | 163 | this.insertStdio(options.in, 0, '') 164 | this.insertStdio(options.out, 1, '') 165 | this.insertStdio(options.err, 2, '') 166 | } 167 | 168 | private insertStdio ( 169 | fd: number, 170 | expected: number, 171 | name: string 172 | ): FileDescriptor { 173 | const type = WasiFileType.CHARACTER_DEVICE 174 | const { base, inheriting } = getRights(this.stdio, fd, FileControlFlag.O_RDWR, type) 175 | const wrap = this.insert(fd, name, name, type, base, inheriting, 0) 176 | if (wrap.id !== expected) { 177 | throw new WasiError(`id: ${wrap.id} !== expected: ${expected}`, WasiErrno.EBADF) 178 | } 179 | return wrap 180 | } 181 | 182 | insert ( 183 | fd: number | FileHandle, 184 | mappedPath: string, 185 | realPath: string, 186 | type: WasiFileType, 187 | rightsBase: bigint, 188 | rightsInheriting: bigint, 189 | preopen: number 190 | ): FileDescriptor { 191 | let index = -1 192 | if (this.used >= this.size) { 193 | const newSize = this.size * 2 194 | this.fds.length = newSize 195 | index = this.size 196 | this.size = newSize 197 | } else { 198 | for (let i = 0; i < this.size; ++i) { 199 | if (this.fds[i] == null) { 200 | index = i 201 | break 202 | } 203 | } 204 | } 205 | 206 | let entry: FileDescriptor 207 | if (mappedPath === '') { 208 | entry = new StandardOutput( 209 | this.print ?? console.log, 210 | index, 211 | fd as number, 212 | mappedPath, 213 | realPath, 214 | type, 215 | rightsBase, 216 | rightsInheriting, 217 | preopen 218 | ) 219 | } else if (mappedPath === '') { 220 | entry = new StandardOutput( 221 | this.printErr ?? console.error, 222 | index, 223 | fd as number, 224 | mappedPath, 225 | realPath, 226 | type, 227 | rightsBase, 228 | rightsInheriting, 229 | preopen 230 | ) 231 | } else { 232 | entry = new FileDescriptor( 233 | index, 234 | fd, 235 | mappedPath, 236 | realPath, 237 | type, 238 | rightsBase, 239 | rightsInheriting, 240 | preopen 241 | ) 242 | } 243 | 244 | this.fds[index] = entry 245 | this.used++ 246 | return entry 247 | } 248 | 249 | get (id: number, base: bigint, inheriting: bigint): FileDescriptor { 250 | if (id >= this.size) { 251 | throw new WasiError('Invalid fd', WasiErrno.EBADF) 252 | } 253 | 254 | const entry = this.fds[id] 255 | if (!entry || entry.id !== id) { 256 | throw new WasiError('Bad file descriptor', WasiErrno.EBADF) 257 | } 258 | 259 | /* Validate that the fd has the necessary rights. */ 260 | if ((~entry.rightsBase & base) !== BigInt(0) || (~entry.rightsInheriting & inheriting) !== BigInt(0)) { 261 | throw new WasiError('Capabilities insufficient', WasiErrno.ENOTCAPABLE) 262 | } 263 | return entry 264 | } 265 | 266 | remove (id: number): void { 267 | if (id >= this.size) { 268 | throw new WasiError('Invalid fd', WasiErrno.EBADF) 269 | } 270 | 271 | const entry = this.fds[id] 272 | if (!entry || entry.id !== id) { 273 | throw new WasiError('Bad file descriptor', WasiErrno.EBADF) 274 | } 275 | 276 | this.fds[id] = undefined! 277 | this.used-- 278 | } 279 | } 280 | 281 | export class SyncTable extends FileDescriptorTable { 282 | private readonly fs: IFs | undefined 283 | constructor (options: SyncTableOptions) { 284 | super(options) 285 | this.fs = options.fs 286 | } 287 | 288 | getFileTypeByFd (fd: number): WasiFileType { 289 | const stats = this.fs!.fstatSync(fd, { bigint: true }) 290 | return toFileType(stats) 291 | } 292 | 293 | insertPreopen (fd: number, mappedPath: string, realPath: string): FileDescriptor { 294 | const type = this.getFileTypeByFd(fd) 295 | if (type !== WasiFileType.DIRECTORY) { 296 | throw new WasiError(`Preopen not dir: ["${mappedPath}", "${realPath}"]`, WasiErrno.ENOTDIR) 297 | } 298 | const result = getRights(this.stdio, fd, 0, type) 299 | return this.insert(fd, mappedPath, realPath, type, result.base, result.inheriting, 1) 300 | } 301 | 302 | renumber (dst: number, src: number): void { 303 | if (dst === src) return 304 | if (dst >= this.size || src >= this.size) { 305 | throw new WasiError('Invalid fd', WasiErrno.EBADF) 306 | } 307 | const dstEntry = this.fds[dst] 308 | const srcEntry = this.fds[src] 309 | if (!dstEntry || !srcEntry || dstEntry.id !== dst || srcEntry.id !== src) { 310 | throw new WasiError('Invalid fd', WasiErrno.EBADF) 311 | } 312 | this.fs!.closeSync(dstEntry.fd as number) 313 | this.fds[dst] = this.fds[src] 314 | this.fds[dst].id = dst 315 | this.fds[src] = undefined! 316 | this.used-- 317 | } 318 | } 319 | 320 | export class AsyncTable extends FileDescriptorTable { 321 | // eslint-disable-next-line @typescript-eslint/no-useless-constructor 322 | constructor (options: AsyncTableOptions) { 323 | super(options) 324 | } 325 | 326 | async getFileTypeByFd (fd: FileHandle): Promise { 327 | const stats = await fd.stat({ bigint: true }) 328 | return toFileType(stats) 329 | } 330 | 331 | async insertPreopen (fd: FileHandle, mappedPath: string, realPath: string): Promise { 332 | const type = await this.getFileTypeByFd(fd) 333 | if (type !== WasiFileType.DIRECTORY) { 334 | throw new WasiError(`Preopen not dir: ["${mappedPath}", "${realPath}"]`, WasiErrno.ENOTDIR) 335 | } 336 | const result = getRights(this.stdio, fd.fd, 0, type) 337 | return this.insert(fd, mappedPath, realPath, type, result.base, result.inheriting, 1) 338 | } 339 | 340 | async renumber (dst: number, src: number): Promise { 341 | if (dst === src) return 342 | if (dst >= this.size || src >= this.size) { 343 | throw new WasiError('Invalid fd', WasiErrno.EBADF) 344 | } 345 | const dstEntry = this.fds[dst] 346 | const srcEntry = this.fds[src] 347 | if (!dstEntry || !srcEntry || dstEntry.id !== dst || srcEntry.id !== src) { 348 | throw new WasiError('Invalid fd', WasiErrno.EBADF) 349 | } 350 | await (dstEntry.fd as FileHandle).close() 351 | this.fds[dst] = this.fds[src] 352 | this.fds[dst].id = dst 353 | this.fds[src] = undefined! 354 | this.used-- 355 | } 356 | } 357 | --------------------------------------------------------------------------------