diff --git a/.github/workflows/node-addon.yml b/.github/workflows/node-addon.yml index 305d7c0..fdea022 100644 --- a/.github/workflows/node-addon.yml +++ b/.github/workflows/node-addon.yml @@ -78,3 +78,47 @@ jobs: shell: pwsh working-directory: node-test run: npm run test:run + + wasm-addon-tests: + runs-on: ubuntu-latest + name: Node.js wasm32-wasip1-threads tests + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + cache-key: node-wasm + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.24.0 + + - name: Install workspace dependencies + run: pnpm install --frozen-lockfile + + - name: Build wasm Node example + run: node packages/zig-napi/bin/zig-napi.js build --cwd examples/node --target wasm32-wasip1-threads -- --summary all + + - name: Test wasm Node example + env: + NAPI_RS_FORCE_WASI: 'error' + run: node examples/node/test.js + + - name: Build wasm node-test matrix + run: node packages/zig-napi/bin/zig-napi.js build --cwd node-test --target wasm32-wasip1-threads -- --summary all + + - name: Test wasm node-test matrix + working-directory: node-test + env: + NAPI_RS_FORCE_WASI: 'error' + run: npm run test:run diff --git a/README.md b/README.md index 9dd9255..90d805b 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ pnpm build pnpm test ``` -It installs the addon as `zig-out/node/hello..node`, for example `hello.darwin-arm64.node`, `hello.linux-x64-gnu.node`, or `hello.win32-x64-msvc.node`. +It installs the addon as `zig-out/node/hello..node`, for example `hello.darwin-arm64.node`, `hello.linux-x64-gnu.node`, or `hello.win32-x64-msvc.node`. For WASI threads, use `zig-napi build --target wasm32-wasip1-threads`; the CLI maps that to Zig's `wasm32-wasi` target with atomics/shared-memory features, and the output follows napi-rs naming as `hello.wasm32-wasi.wasm`. The package also provides a `zig-napi` CLI for Node.js addons. Zig-specific commands such as `new` and `build` are implemented by this project. Packaging commands reuse the community `@napi-rs/cli` API for npm package directory creation, artifact collection, and pre-publish processing. @@ -179,7 +179,7 @@ pnpm run node-example:package pnpm --filter zig-napi-node-example run test ``` -`zig-napi create-npm-dirs` calls `@napi-rs/cli`'s `createNpmDirs` API and creates `npm/` packages from the `napi` field in `package.json`. `zig-napi artifacts --output-dir zig-out/node` calls the community `artifacts` API and copies Zig's `..node` outputs into those packages and into the root package. `zig-napi pre-publish` calls the community `prePublish` API to update optional dependencies and handle publish preparation. +`zig-napi create-npm-dirs` calls `@napi-rs/cli`'s `createNpmDirs` API and creates `npm/` packages from the `napi` field in `package.json`. `zig-napi artifacts --output-dir zig-out/node` calls the community `artifacts` API and copies Zig's `..node` or `.wasm32-wasi.wasm` outputs into those packages and into the root package. When `wasm32-wasip1-threads` is configured, `zig-napi build`, `artifacts`, and `package` also generate the napi-rs compatible `.wasi.cjs` and worker files used by `@napi-rs/wasm-runtime`. `zig-napi pre-publish` calls the community `prePublish` API to update optional dependencies and handle publish preparation. Upstream `napi build` and `napi new` are not used directly for Zig addons because they currently expect Cargo projects and napi-rs' Rust templates. @@ -188,7 +188,7 @@ Node.js matrix tests live in `node-test`. It mirrors the NAPI-RS example split w - `node-test/napi-compat-mode` covers compat-mode style APIs and runtime-gated N-API v4/v5/v6/v7/v8 scenarios. - `node-test/napi` covers the non compat-mode example surface such as values, strict validation, async, ThreadSafeFunction, and worker-thread loading. -The Node addon CI runs those tests on Linux, macOS, and Windows for Node.js 12, 14, 16, 18, 20, and 22. +The Node addon CI runs those tests on Linux, macOS, and Windows for Node.js 12, 14, 16, 18, 20, 22, and 24. It also builds `wasm32-wasip1-threads` addons and runs `node-test` with `NAPI_RS_FORCE_WASI=error` to verify the napi-rs compatible wasm runtime path. ## Website diff --git a/benchmark/README.md b/benchmark/README.md index 05cd771..f7ddb39 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -35,24 +35,24 @@ Environment: - Zig addon: `zig build -Darkvm-test=true -Doptimize=ReleaseFast` - Native addon: C source compiled in Docker with `gcc` -| module | api content | iterations | native C N-API avg (us) | zig-napi avg (us) | diff (us) | ratio | -| --- | --- | ---: | ---: | ---: | ---: | ---: | -| global function | void(*)() | 100000 | 0.159 | 0.159 | 0 | 1.001x | -| primitive | i32(i32, i32) | 100000 | 0.183 | 0.169 | -0.014 | 0.924x | -| primitive | bool(bool) | 100000 | 0.169 | 0.169 | 0 | 1.003x | -| string | len(string) | 100000 | 0.213 | 0.204 | -0.008 | 0.961x | -| object | read properties | 100000 | 0.368 | 0.37 | 0.001 | 1.003x | -| array | sum(number[]) | 100000 | 1.065 | 0.792 | -0.274 | 0.743x | -| function | call callback | 100000 | 0.333 | 0.339 | 0.006 | 1.017x | -| class | constructor | 20000 | 1.517 | 3.091 | 1.574 | 2.037x | -| class | getter | 100000 | 0.264 | 0.266 | 0.002 | 1.009x | -| class | setter | 100000 | 0.458 | 0.472 | 0.014 | 1.03x | -| class | method | 100000 | 0.269 | 0.27 | 0.001 | 1.004x | -| ArrayBuffer | constructor | 20000 | 0.339 | 0.424 | 0.085 | 1.251x | -| ArrayBuffer | byteLength | 100000 | 0.2 | 0.198 | -0.002 | 0.989x | -| Buffer | constructor | 20000 | 0.691 | 0.72 | 0.029 | 1.042x | -| Buffer | length | 100000 | 0.207 | 0.204 | -0.004 | 0.983x | -| TypedArray | Uint8Array constructor | 20000 | 0.548 | 0.647 | 0.099 | 1.18x | -| TypedArray | Uint8Array sum | 100000 | 0.24 | 0.275 | 0.035 | 1.147x | -| DataView | constructor | 20000 | 0.885 | 0.44 | -0.445 | 0.498x | -| DataView | byteLength | 100000 | 0.201 | 0.235 | 0.034 | 1.169x | +| module | api content | iterations | native C N-API avg (us) | zig-napi avg (us) | diff (us) | ratio | +| --------------- | ---------------------- | ---------: | ----------------------: | ----------------: | --------: | -----: | +| global function | void(*)() | 100000 | 0.159 | 0.159 | 0 | 1.001x | +| primitive | i32(i32, i32) | 100000 | 0.183 | 0.169 | -0.014 | 0.924x | +| primitive | bool(bool) | 100000 | 0.169 | 0.169 | 0 | 1.003x | +| string | len(string) | 100000 | 0.213 | 0.204 | -0.008 | 0.961x | +| object | read properties | 100000 | 0.368 | 0.37 | 0.001 | 1.003x | +| array | sum(number[]) | 100000 | 1.065 | 0.792 | -0.274 | 0.743x | +| function | call callback | 100000 | 0.333 | 0.339 | 0.006 | 1.017x | +| class | constructor | 20000 | 1.517 | 3.091 | 1.574 | 2.037x | +| class | getter | 100000 | 0.264 | 0.266 | 0.002 | 1.009x | +| class | setter | 100000 | 0.458 | 0.472 | 0.014 | 1.03x | +| class | method | 100000 | 0.269 | 0.27 | 0.001 | 1.004x | +| ArrayBuffer | constructor | 20000 | 0.339 | 0.424 | 0.085 | 1.251x | +| ArrayBuffer | byteLength | 100000 | 0.2 | 0.198 | -0.002 | 0.989x | +| Buffer | constructor | 20000 | 0.691 | 0.72 | 0.029 | 1.042x | +| Buffer | length | 100000 | 0.207 | 0.204 | -0.004 | 0.983x | +| TypedArray | Uint8Array constructor | 20000 | 0.548 | 0.647 | 0.099 | 1.18x | +| TypedArray | Uint8Array sum | 100000 | 0.24 | 0.275 | 0.035 | 1.147x | +| DataView | constructor | 20000 | 0.885 | 0.44 | -0.445 | 0.498x | +| DataView | byteLength | 100000 | 0.201 | 0.235 | 0.034 | 1.169x | diff --git a/examples/node/browser.js b/examples/node/browser.js new file mode 100644 index 0000000..72a7254 --- /dev/null +++ b/examples/node/browser.js @@ -0,0 +1,2 @@ +export { default } from "zig-napi-node-example-wasm32-wasi"; +export * from "zig-napi-node-example-wasm32-wasi"; diff --git a/examples/node/hello.wasi-browser.js b/examples/node/hello.wasi-browser.js new file mode 100644 index 0000000..86d6b43 --- /dev/null +++ b/examples/node/hello.wasi-browser.js @@ -0,0 +1,59 @@ +/* auto-generated by zig-napi */ +import { + getDefaultContext as __emnapiGetDefaultContext, + instantiateNapiModuleSync as __emnapiInstantiateNapiModuleSync, + WASI as __WASI, +} from "@napi-rs/wasm-runtime"; + +const __wasi = new __WASI({ + version: "preview1", +}); + +const __wasmUrl = new URL("./hello.wasm32-wasi.wasm", import.meta.url).href; +const __emnapiContext = __emnapiGetDefaultContext(); + +const __sharedMemory = new WebAssembly.Memory({ + initial: 4000, + maximum: 65536, + shared: true, +}); + +const __wasmFile = await fetch(__wasmUrl).then((res) => res.arrayBuffer()); + +const { + instance: __napiInstance, + module: __wasiModule, + napiModule: __napiModule, +} = __emnapiInstantiateNapiModuleSync(__wasmFile, { + context: __emnapiContext, + asyncWorkPoolSize: 4, + wasi: __wasi, + onCreateWorker() { + return new Worker(new URL("./wasi-worker-browser.mjs", import.meta.url), { + type: "module", + }); + }, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __sharedMemory, + }; + return importObject; + }, + beforeInit({ instance }) { + for (const name of Object.keys(instance.exports)) { + if (name.startsWith("__napi_register__")) { + instance.exports[name](); + } + } + }, +}); + +export default __napiModule.exports; +export const add = __napiModule.exports.add; +export const hello = __napiModule.exports.hello; +export const requestedNapiVersion = __napiModule.exports.requestedNapiVersion; +export const fibonacciAsync = __napiModule.exports.fibonacciAsync; +export const countAsyncProgress = __napiModule.exports.countAsyncProgress; diff --git a/examples/node/hello.wasi.cjs b/examples/node/hello.wasi.cjs new file mode 100644 index 0000000..0d740bd --- /dev/null +++ b/examples/node/hello.wasi.cjs @@ -0,0 +1,116 @@ +/* eslint-disable */ +/* auto-generated by zig-napi */ + +const __nodeFs = require("node:fs"); +const __nodePath = require("node:path"); +const { WASI: __nodeWASI } = require("node:wasi"); +const { Worker } = require("node:worker_threads"); + +const { + createOnMessage: __wasmCreateOnMessageForFsProxy, + getDefaultContext: __emnapiGetDefaultContext, + instantiateNapiModuleSync: __emnapiInstantiateNapiModuleSync, +} = require("@napi-rs/wasm-runtime"); + +const __rootDir = __nodePath.parse(process.cwd()).root; + +const __wasi = new __nodeWASI({ + version: "preview1", + env: process.env, + preopens: { + [__rootDir]: __rootDir, + }, +}); + +const __emnapiContext = __emnapiGetDefaultContext(); + +const __sharedMemory = new WebAssembly.Memory({ + initial: 4000, + maximum: 65536, + shared: true, +}); + +const __wasmCandidates = [ + __nodePath.join(__dirname, "hello.wasm32-wasi.debug.wasm"), + __nodePath.join(__dirname, "hello.wasm32-wasi.wasm"), + __nodePath.join(__dirname, "zig-out", "node", "hello.wasm32-wasi.debug.wasm"), + __nodePath.join(__dirname, "zig-out", "node", "hello.wasm32-wasi.wasm"), +]; + +let __wasmFilePath = __wasmCandidates.find((candidate) => __nodeFs.existsSync(candidate)); + +if (!__wasmFilePath) { + try { + __wasmFilePath = require.resolve("zig-napi-node-example-wasm32-wasi/hello.wasm32-wasi.wasm"); + } catch { + throw new Error( + "Cannot find hello.wasm32-wasi.wasm file, and zig-napi-node-example-wasm32-wasi package is not installed.", + ); + } +} + +const { + instance: __napiInstance, + module: __wasiModule, + napiModule: __napiModule, +} = __emnapiInstantiateNapiModuleSync(__nodeFs.readFileSync(__wasmFilePath), { + context: __emnapiContext, + asyncWorkPoolSize: (function () { + const threadsSizeFromEnv = Number( + process.env.NAPI_RS_ASYNC_WORK_POOL_SIZE ?? process.env.UV_THREADPOOL_SIZE, + ); + return threadsSizeFromEnv > 0 ? threadsSizeFromEnv : 4; + })(), + reuseWorker: true, + wasi: __wasi, + onCreateWorker() { + const worker = new Worker(__nodePath.join(__dirname, "wasi-worker.mjs"), { + env: process.env, + }); + worker.onmessage = ({ data }) => { + __wasmCreateOnMessageForFsProxy(__nodeFs)(data); + }; + + { + const kPublicPort = Object.getOwnPropertySymbols(worker).find((symbol) => + symbol.toString().includes("kPublicPort"), + ); + if (kPublicPort) { + worker[kPublicPort].ref = () => {}; + } + + const kHandle = Object.getOwnPropertySymbols(worker).find((symbol) => + symbol.toString().includes("kHandle"), + ); + if (kHandle) { + worker[kHandle].ref = () => {}; + } + + worker.unref(); + } + return worker; + }, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __sharedMemory, + }; + return importObject; + }, + beforeInit({ instance }) { + for (const name of Object.keys(instance.exports)) { + if (name.startsWith("__napi_register__")) { + instance.exports[name](); + } + } + }, +}); + +module.exports = __napiModule.exports; +module.exports.add = __napiModule.exports.add; +module.exports.hello = __napiModule.exports.hello; +module.exports.requestedNapiVersion = __napiModule.exports.requestedNapiVersion; +module.exports.fibonacciAsync = __napiModule.exports.fibonacciAsync; +module.exports.countAsyncProgress = __napiModule.exports.countAsyncProgress; diff --git a/examples/node/index.d.ts b/examples/node/index.d.ts index e8ba35b..a1f6659 100644 --- a/examples/node/index.d.ts +++ b/examples/node/index.d.ts @@ -1,5 +1,16 @@ /* auto-generated by zig-addon */ /* eslint-disable */ + +export interface CountProgress { + current: number; + total: number; +} + export declare function add(left: number, right: number): number; export declare function hello(): string; export declare function requestedNapiVersion(): number; +export declare function fibonacciAsync(n: number): Promise; +export declare function countAsyncProgress( + total: number, + onEvent?: (event: CountProgress) => void, +): Promise; diff --git a/examples/node/index.js b/examples/node/index.js index b1cbd2c..ff2cf9f 100644 --- a/examples/node/index.js +++ b/examples/node/index.js @@ -1,41 +1,154 @@ -const fs = require("fs"); +// prettier-ignore +/* eslint-disable */ +// @ts-nocheck +/* auto-generated by zig-napi */ + +const fs = require('fs') const path = require("path"); -const packageName = "zig-napi-node-example"; -const binaryName = "hello"; +let nativeBinding = null; +const loadErrors = []; -function loadAddon() { - const platformArchABI = detectPlatformArchABI(); - const optionalPackage = `${packageName}-${platformArchABI}`; - const candidates = [ - () => require(optionalPackage), - () => require(path.join(__dirname, `${binaryName}.${platformArchABI}.node`)), - () => require(path.join(__dirname, "zig-out", "node", `${binaryName}.${platformArchABI}.node`)), - ]; +function requireNative() { + if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) { + try { + return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH); + } catch (error) { + loadErrors.push(error); + } + } - const errors = []; - for (const candidate of candidates) { + for (const platformArchABI of detectPlatformArchABIs()) { + const binding = requireTuple(platformArchABI); + if (binding) { + return binding; + } + } +} + +function requireTuple(platformArchABI) { + try { + return require(path.join(__dirname, "zig-out", "node", `hello.${platformArchABI}.node`)); + } catch (error) { + loadErrors.push(error); + } + try { + return require(path.join(__dirname, `hello.${platformArchABI}.node`)); + } catch (error) { + loadErrors.push(error); + } + try { + return require(optionalPackageName("zig-napi-node-example", platformArchABI)); + } catch (error) { + loadErrors.push(error); + } +} + +nativeBinding = requireNative(); + +// NAPI_RS_FORCE_WASI is a tri-state flag: +// unset / any other value -> native binding preferred, WASI is only a fallback +// 'true' -> force WASI fallback even if native loaded +// 'error' -> force WASI and throw if no WASI binding is found +const forceWasi = + process.env.NAPI_RS_FORCE_WASI === "true" || process.env.NAPI_RS_FORCE_WASI === "error"; + +if (!nativeBinding || forceWasi) { + let wasiBinding = null; + let wasiBindingError = null; + try { + wasiBinding = require(path.join(__dirname, "zig-out", "node", "hello.wasi.cjs")); + nativeBinding = wasiBinding; + } catch (error) { + if (forceWasi) { + wasiBindingError = error; + } + } + if (!nativeBinding || forceWasi) { + try { + wasiBinding = require("./hello.wasi.cjs"); + nativeBinding = wasiBinding; + } catch (error) { + if (forceWasi) { + if (!wasiBindingError) { + wasiBindingError = error; + } else { + wasiBindingError.cause = error; + } + loadErrors.push(error); + } + } + } + if (!nativeBinding || forceWasi) { try { - return candidate(); + wasiBinding = require("zig-napi-node-example-wasm32-wasi"); + nativeBinding = wasiBinding; } catch (error) { - errors.push(error); + if (forceWasi) { + if (!wasiBindingError) { + wasiBindingError = error; + } else { + wasiBindingError.cause = error; + } + loadErrors.push(error); + } } } + if (process.env.NAPI_RS_FORCE_WASI === "error" && !wasiBinding) { + const error = new Error("WASI binding not found and NAPI_RS_FORCE_WASI is set to error"); + error.cause = wasiBindingError; + throw error; + } +} - throw new Error( - `Unable to load ${binaryName}.${platformArchABI}.node\n` + - errors.map((error) => `- ${error.message}`).join("\n"), - ); +function optionalPackageName(name, platformArchABI) { + if (name.startsWith("@")) { + const slash = name.indexOf("/"); + return `${name.slice(0, slash + 1)}${name.slice(slash + 1)}-${platformArchABI}`; + } + return `${name}-${platformArchABI}`; } -function detectPlatformArchABI() { +function detectPlatformArchABIs() { + if (process.platform === "android") { + if (process.arch === "arm64") return ["android-arm64"]; + if (process.arch === "arm") return ["android-arm-eabi"]; + } + + if (process.platform === "darwin") { + if (process.arch === "x64") return ["darwin-universal", "darwin-x64"]; + if (process.arch === "arm64") return ["darwin-universal", "darwin-arm64"]; + } + + if (process.platform === "freebsd") { + if (process.arch === "x64") return ["freebsd-x64"]; + if (process.arch === "arm64") return ["freebsd-arm64"]; + } + if (process.platform === "linux") { - return `${process.platform}-${process.arch}-${isMusl() ? "musl" : "gnu"}`; + const abi = isMusl() ? "musl" : "gnu"; + if (process.arch === "x64") return [`linux-x64-${abi}`]; + if (process.arch === "arm64") return [`linux-arm64-${abi}`]; + if (process.arch === "arm") return [`linux-arm-${abi === "musl" ? "musleabihf" : "gnueabihf"}`]; + if (process.arch === "loong64") return [`linux-loong64-${abi}`]; + if (process.arch === "riscv64") return [`linux-riscv64-${abi}`]; + if (process.arch === "ppc64") return ["linux-ppc64-gnu"]; + if (process.arch === "s390x") return ["linux-s390x-gnu"]; + } + + if (process.platform === "openharmony") { + if (process.arch === "arm64") return ["openharmony-arm64"]; + if (process.arch === "x64") return ["openharmony-x64"]; + if (process.arch === "arm") return ["openharmony-arm"]; } + if (process.platform === "win32") { - return `${process.platform}-${process.arch}-msvc`; + if (process.arch === "x64") return ["win32-x64-msvc"]; + if (process.arch === "ia32") return ["win32-ia32-msvc"]; + if (process.arch === "arm64") return ["win32-arm64-msvc"]; } - return `${process.platform}-${process.arch}`; + + return []; } function isMusl() { @@ -49,4 +162,23 @@ function isMusl() { } } -module.exports = loadAddon(); +if (!nativeBinding) { + if (loadErrors.length > 0) { + const error = new Error( + "Cannot find native binding. npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). Please try npm i again after removing both package-lock.json and node_modules directory.", + ); + error.cause = loadErrors.reduce((err, cur) => { + cur.cause = err; + return cur; + }); + throw error; + } + throw new Error("Failed to load native binding"); +} + +module.exports = nativeBinding; +module.exports.add = nativeBinding.add; +module.exports.hello = nativeBinding.hello; +module.exports.requestedNapiVersion = nativeBinding.requestedNapiVersion; +module.exports.fibonacciAsync = nativeBinding.fibonacciAsync; +module.exports.countAsyncProgress = nativeBinding.countAsyncProgress; diff --git a/examples/node/npm/darwin-arm64/package.json b/examples/node/npm/darwin-arm64/package.json index 9561f44..8b4bff4 100644 --- a/examples/node/npm/darwin-arm64/package.json +++ b/examples/node/npm/darwin-arm64/package.json @@ -1,16 +1,16 @@ { "name": "zig-napi-node-example-darwin-arm64", "version": "0.1.0", - "cpu": [ - "arm64" - ], - "main": "hello.darwin-arm64.node", + "description": "Node.js addon example built with zig-napi.", + "license": "MIT", "files": [ "hello.darwin-arm64.node" ], - "description": "Node.js addon example built with zig-napi.", - "license": "MIT", "os": [ "darwin" - ] + ], + "cpu": [ + "arm64" + ], + "main": "hello.darwin-arm64.node" } diff --git a/examples/node/npm/darwin-x64/package.json b/examples/node/npm/darwin-x64/package.json index 683d945..cd3a876 100644 --- a/examples/node/npm/darwin-x64/package.json +++ b/examples/node/npm/darwin-x64/package.json @@ -1,16 +1,16 @@ { "name": "zig-napi-node-example-darwin-x64", "version": "0.1.0", - "cpu": [ - "x64" - ], - "main": "hello.darwin-x64.node", + "description": "Node.js addon example built with zig-napi.", + "license": "MIT", "files": [ "hello.darwin-x64.node" ], - "description": "Node.js addon example built with zig-napi.", - "license": "MIT", "os": [ "darwin" - ] + ], + "cpu": [ + "x64" + ], + "main": "hello.darwin-x64.node" } diff --git a/examples/node/npm/linux-arm64-gnu/package.json b/examples/node/npm/linux-arm64-gnu/package.json index 20acceb..5206bd4 100644 --- a/examples/node/npm/linux-arm64-gnu/package.json +++ b/examples/node/npm/linux-arm64-gnu/package.json @@ -1,19 +1,19 @@ { "name": "zig-napi-node-example-linux-arm64-gnu", "version": "0.1.0", - "cpu": [ - "arm64" - ], - "main": "hello.linux-arm64-gnu.node", + "description": "Node.js addon example built with zig-napi.", + "license": "MIT", "files": [ "hello.linux-arm64-gnu.node" ], - "description": "Node.js addon example built with zig-napi.", - "license": "MIT", "os": [ "linux" ], + "cpu": [ + "arm64" + ], "libc": [ "glibc" - ] + ], + "main": "hello.linux-arm64-gnu.node" } diff --git a/examples/node/npm/linux-arm64-musl/package.json b/examples/node/npm/linux-arm64-musl/package.json index 113ba9c..914a83c 100644 --- a/examples/node/npm/linux-arm64-musl/package.json +++ b/examples/node/npm/linux-arm64-musl/package.json @@ -1,19 +1,19 @@ { "name": "zig-napi-node-example-linux-arm64-musl", "version": "0.1.0", - "cpu": [ - "arm64" - ], - "main": "hello.linux-arm64-musl.node", + "description": "Node.js addon example built with zig-napi.", + "license": "MIT", "files": [ "hello.linux-arm64-musl.node" ], - "description": "Node.js addon example built with zig-napi.", - "license": "MIT", "os": [ "linux" ], + "cpu": [ + "arm64" + ], "libc": [ "musl" - ] + ], + "main": "hello.linux-arm64-musl.node" } diff --git a/examples/node/npm/linux-x64-gnu/package.json b/examples/node/npm/linux-x64-gnu/package.json index f6b0022..51de166 100644 --- a/examples/node/npm/linux-x64-gnu/package.json +++ b/examples/node/npm/linux-x64-gnu/package.json @@ -1,19 +1,19 @@ { "name": "zig-napi-node-example-linux-x64-gnu", "version": "0.1.0", - "cpu": [ - "x64" - ], - "main": "hello.linux-x64-gnu.node", + "description": "Node.js addon example built with zig-napi.", + "license": "MIT", "files": [ "hello.linux-x64-gnu.node" ], - "description": "Node.js addon example built with zig-napi.", - "license": "MIT", "os": [ "linux" ], + "cpu": [ + "x64" + ], "libc": [ "glibc" - ] + ], + "main": "hello.linux-x64-gnu.node" } diff --git a/examples/node/npm/linux-x64-musl/package.json b/examples/node/npm/linux-x64-musl/package.json index 28d38cd..60b0d07 100644 --- a/examples/node/npm/linux-x64-musl/package.json +++ b/examples/node/npm/linux-x64-musl/package.json @@ -1,19 +1,19 @@ { "name": "zig-napi-node-example-linux-x64-musl", "version": "0.1.0", - "cpu": [ - "x64" - ], - "main": "hello.linux-x64-musl.node", + "description": "Node.js addon example built with zig-napi.", + "license": "MIT", "files": [ "hello.linux-x64-musl.node" ], - "description": "Node.js addon example built with zig-napi.", - "license": "MIT", "os": [ "linux" ], + "cpu": [ + "x64" + ], "libc": [ "musl" - ] + ], + "main": "hello.linux-x64-musl.node" } diff --git a/examples/node/npm/win32-arm64-msvc/package.json b/examples/node/npm/win32-arm64-msvc/package.json index 062f58e..522de4a 100644 --- a/examples/node/npm/win32-arm64-msvc/package.json +++ b/examples/node/npm/win32-arm64-msvc/package.json @@ -1,16 +1,16 @@ { "name": "zig-napi-node-example-win32-arm64-msvc", "version": "0.1.0", - "cpu": [ - "arm64" - ], - "main": "hello.win32-arm64-msvc.node", + "description": "Node.js addon example built with zig-napi.", + "license": "MIT", "files": [ "hello.win32-arm64-msvc.node" ], - "description": "Node.js addon example built with zig-napi.", - "license": "MIT", "os": [ "win32" - ] + ], + "cpu": [ + "arm64" + ], + "main": "hello.win32-arm64-msvc.node" } diff --git a/examples/node/npm/win32-x64-msvc/package.json b/examples/node/npm/win32-x64-msvc/package.json index aff185d..6a8dce2 100644 --- a/examples/node/npm/win32-x64-msvc/package.json +++ b/examples/node/npm/win32-x64-msvc/package.json @@ -1,16 +1,16 @@ { "name": "zig-napi-node-example-win32-x64-msvc", "version": "0.1.0", - "cpu": [ - "x64" - ], - "main": "hello.win32-x64-msvc.node", + "description": "Node.js addon example built with zig-napi.", + "license": "MIT", "files": [ "hello.win32-x64-msvc.node" ], - "description": "Node.js addon example built with zig-napi.", - "license": "MIT", "os": [ "win32" - ] + ], + "cpu": [ + "x64" + ], + "main": "hello.win32-x64-msvc.node" } diff --git a/examples/node/package.json b/examples/node/package.json index bca5862..0b678eb 100644 --- a/examples/node/package.json +++ b/examples/node/package.json @@ -3,12 +3,20 @@ "version": "0.1.0", "private": true, "description": "Node.js addon example built with zig-napi.", - "main": "index.js", + "license": "MIT", "files": [ "index.js", "index.d.ts", - "*.node" + "browser.js", + "*.node", + "*.wasm", + "*.wasi.cjs", + "*.wasi-browser.js", + "wasi-worker.mjs", + "wasi-worker-browser.mjs" ], + "main": "index.js", + "browser": "browser.js", "scripts": { "build": "zig-napi build -- --summary all", "create-npm-dirs": "zig-napi create-npm-dirs", @@ -20,6 +28,12 @@ "test:run": "node test.js", "clean": "rm -rf .zig-cache zig-out *.node npm/*/*.node" }, + "dependencies": { + "@emnapi/core": "^1.11.0", + "@emnapi/runtime": "^1.11.0", + "@napi-rs/wasm-runtime": "^1.1.6", + "emnapi": "^1.11.1" + }, "devDependencies": { "zig-napi": "workspace:*" }, @@ -34,8 +48,8 @@ "aarch64-unknown-linux-gnu", "aarch64-unknown-linux-musl", "x86_64-pc-windows-msvc", - "aarch64-pc-windows-msvc" + "aarch64-pc-windows-msvc", + "wasm32-wasip1-threads" ] - }, - "license": "MIT" + } } diff --git a/examples/node/src/hello.zig b/examples/node/src/hello.zig index 8ea2d37..4a0ce17 100644 --- a/examples/node/src/hello.zig +++ b/examples/node/src/hello.zig @@ -1,5 +1,10 @@ const napi = @import("napi"); +const CountProgress = struct { + current: u32, + total: u32, +}; + pub fn add(left: i32, right: i32) i32 { return left + right; } @@ -12,6 +17,29 @@ pub fn requestedNapiVersion() i32 { return @intFromEnum(napi.selectedNapiVersion()); } +fn fibonacci(n: u32) u32 { + if (n <= 1) return n; + return fibonacci(n - 1) + fibonacci(n - 2); +} + +fn fibonacciExecute(n: u32) u32 { + return fibonacci(n); +} + +fn countWithProgressExecute(ctx: napi.AsyncContext(CountProgress), total: u32) !u32 { + try ctx.emit(.{ .current = 0, .total = total }); + try ctx.emit(.{ .current = total, .total = total }); + return total; +} + +pub fn fibonacciAsync(n: u32) napi.Async(u32, .thread) { + return napi.Async(u32, .thread).from(n, fibonacciExecute); +} + +pub fn countAsyncProgress(total: u32) napi.AsyncWithEvents(u32, CountProgress, .thread) { + return napi.AsyncWithEvents(u32, CountProgress, .thread).from(total, countWithProgressExecute); +} + comptime { napi.NODE_API_MODULE("hello", @This()); } diff --git a/examples/node/test.js b/examples/node/test.js index cfc91c4..f95a5b1 100644 --- a/examples/node/test.js +++ b/examples/node/test.js @@ -1,6 +1,26 @@ const assert = require("assert").strict; const addon = require("./index"); -assert.equal(addon.add(20, 22), 42); -assert.equal(addon.hello(), "hello from node"); -assert.equal(addon.requestedNapiVersion(), 8); +async function main() { + assert.equal(addon.add(20, 22), 42); + assert.equal(addon.hello(), "hello from node"); + assert.equal(addon.requestedNapiVersion(), 8); + assert.equal(await addon.fibonacciAsync(10), 55); + + const events = []; + assert.equal( + await addon.countAsyncProgress(3, (event) => { + events.push(event); + }), + 3, + ); + assert.deepEqual(events, [ + { current: 0, total: 3 }, + { current: 3, total: 3 }, + ]); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/examples/node/wasi-worker-browser.mjs b/examples/node/wasi-worker-browser.mjs new file mode 100644 index 0000000..15b2616 --- /dev/null +++ b/examples/node/wasi-worker-browser.mjs @@ -0,0 +1,23 @@ +import { instantiateNapiModuleSync, MessageHandler, WASI } from "@napi-rs/wasm-runtime"; + +const handler = new MessageHandler({ + onLoad({ wasmModule, wasmMemory }) { + const wasi = new WASI({}); + return instantiateNapiModuleSync(wasmModule, { + childThread: true, + wasi, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: wasmMemory, + }; + }, + }); + }, +}); + +globalThis.onmessage = function (event) { + handler.handle(event); +}; diff --git a/examples/node/wasi-worker.mjs b/examples/node/wasi-worker.mjs new file mode 100644 index 0000000..e536b74 --- /dev/null +++ b/examples/node/wasi-worker.mjs @@ -0,0 +1,66 @@ +import fs from "node:fs"; +import { createRequire } from "node:module"; +import { parse } from "node:path"; +import { WASI } from "node:wasi"; +import { parentPort, Worker } from "node:worker_threads"; + +const require = createRequire(import.meta.url); + +const { + instantiateNapiModuleSync, + MessageHandler, + getDefaultContext, +} = require("@napi-rs/wasm-runtime"); + +if (parentPort) { + parentPort.on("message", (data) => { + globalThis.onmessage({ data }); + }); +} + +Object.assign(globalThis, { + self: globalThis, + require, + Worker, + importScripts(f) { + (0, eval)(fs.readFileSync(f, "utf8") + "//# sourceURL=" + f); + }, + postMessage(msg) { + if (parentPort) { + parentPort.postMessage(msg); + } + }, +}); + +const emnapiContext = getDefaultContext(); +const __rootDir = parse(process.cwd()).root; + +const handler = new MessageHandler({ + onLoad({ wasmModule, wasmMemory }) { + const wasi = new WASI({ + version: "preview1", + env: process.env, + preopens: { + [__rootDir]: __rootDir, + }, + }); + + return instantiateNapiModuleSync(wasmModule, { + childThread: true, + wasi, + context: emnapiContext, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: wasmMemory, + }; + }, + }); + }, +}); + +globalThis.onmessage = function (event) { + handler.handle(event); +}; diff --git a/node-test/browser.js b/node-test/browser.js new file mode 100644 index 0000000..4170535 --- /dev/null +++ b/node-test/browser.js @@ -0,0 +1,2 @@ +export { default } from "zig-napi-node-test-wasm32-wasi"; +export * from "zig-napi-node-test-wasm32-wasi"; diff --git a/node-test/compat_mode.wasi-browser.js b/node-test/compat_mode.wasi-browser.js new file mode 100644 index 0000000..2f58aba --- /dev/null +++ b/node-test/compat_mode.wasi-browser.js @@ -0,0 +1,54 @@ +/* auto-generated by zig-napi */ +import { + getDefaultContext as __emnapiGetDefaultContext, + instantiateNapiModuleSync as __emnapiInstantiateNapiModuleSync, + WASI as __WASI, +} from "@napi-rs/wasm-runtime"; + +const __wasi = new __WASI({ + version: "preview1", +}); + +const __wasmUrl = new URL("./compat_mode.wasm32-wasi.wasm", import.meta.url).href; +const __emnapiContext = __emnapiGetDefaultContext(); + +const __sharedMemory = new WebAssembly.Memory({ + initial: 4000, + maximum: 65536, + shared: true, +}); + +const __wasmFile = await fetch(__wasmUrl).then((res) => res.arrayBuffer()); + +const { + instance: __napiInstance, + module: __wasiModule, + napiModule: __napiModule, +} = __emnapiInstantiateNapiModuleSync(__wasmFile, { + context: __emnapiContext, + asyncWorkPoolSize: 4, + wasi: __wasi, + onCreateWorker() { + return new Worker(new URL("./wasi-worker-browser.mjs", import.meta.url), { + type: "module", + }); + }, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __sharedMemory, + }; + return importObject; + }, + beforeInit({ instance }) { + for (const name of Object.keys(instance.exports)) { + if (name.startsWith("__napi_register__")) { + instance.exports[name](); + } + } + }, +}); + +export default __napiModule.exports; diff --git a/node-test/compat_mode.wasi.cjs b/node-test/compat_mode.wasi.cjs new file mode 100644 index 0000000..7b9eb8a --- /dev/null +++ b/node-test/compat_mode.wasi.cjs @@ -0,0 +1,111 @@ +/* eslint-disable */ +/* auto-generated by zig-napi */ + +const __nodeFs = require("node:fs"); +const __nodePath = require("node:path"); +const { WASI: __nodeWASI } = require("node:wasi"); +const { Worker } = require("node:worker_threads"); + +const { + createOnMessage: __wasmCreateOnMessageForFsProxy, + getDefaultContext: __emnapiGetDefaultContext, + instantiateNapiModuleSync: __emnapiInstantiateNapiModuleSync, +} = require("@napi-rs/wasm-runtime"); + +const __rootDir = __nodePath.parse(process.cwd()).root; + +const __wasi = new __nodeWASI({ + version: "preview1", + env: process.env, + preopens: { + [__rootDir]: __rootDir, + }, +}); + +const __emnapiContext = __emnapiGetDefaultContext(); + +const __sharedMemory = new WebAssembly.Memory({ + initial: 4000, + maximum: 65536, + shared: true, +}); + +const __wasmCandidates = [ + __nodePath.join(__dirname, "compat_mode.wasm32-wasi.debug.wasm"), + __nodePath.join(__dirname, "compat_mode.wasm32-wasi.wasm"), + __nodePath.join(__dirname, "zig-out", "node", "compat_mode.wasm32-wasi.debug.wasm"), + __nodePath.join(__dirname, "zig-out", "node", "compat_mode.wasm32-wasi.wasm"), +]; + +let __wasmFilePath = __wasmCandidates.find((candidate) => __nodeFs.existsSync(candidate)); + +if (!__wasmFilePath) { + try { + __wasmFilePath = require.resolve("zig-napi-node-test-wasm32-wasi/compat_mode.wasm32-wasi.wasm"); + } catch { + throw new Error( + "Cannot find compat_mode.wasm32-wasi.wasm file, and zig-napi-node-test-wasm32-wasi package is not installed.", + ); + } +} + +const { + instance: __napiInstance, + module: __wasiModule, + napiModule: __napiModule, +} = __emnapiInstantiateNapiModuleSync(__nodeFs.readFileSync(__wasmFilePath), { + context: __emnapiContext, + asyncWorkPoolSize: (function () { + const threadsSizeFromEnv = Number( + process.env.NAPI_RS_ASYNC_WORK_POOL_SIZE ?? process.env.UV_THREADPOOL_SIZE, + ); + return threadsSizeFromEnv > 0 ? threadsSizeFromEnv : 4; + })(), + reuseWorker: true, + wasi: __wasi, + onCreateWorker() { + const worker = new Worker(__nodePath.join(__dirname, "wasi-worker.mjs"), { + env: process.env, + }); + worker.onmessage = ({ data }) => { + __wasmCreateOnMessageForFsProxy(__nodeFs)(data); + }; + + { + const kPublicPort = Object.getOwnPropertySymbols(worker).find((symbol) => + symbol.toString().includes("kPublicPort"), + ); + if (kPublicPort) { + worker[kPublicPort].ref = () => {}; + } + + const kHandle = Object.getOwnPropertySymbols(worker).find((symbol) => + symbol.toString().includes("kHandle"), + ); + if (kHandle) { + worker[kHandle].ref = () => {}; + } + + worker.unref(); + } + return worker; + }, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __sharedMemory, + }; + return importObject; + }, + beforeInit({ instance }) { + for (const name of Object.keys(instance.exports)) { + if (name.startsWith("__napi_register__")) { + instance.exports[name](); + } + } + }, +}); + +module.exports = __napiModule.exports; diff --git a/node-test/example.wasi-browser.js b/node-test/example.wasi-browser.js new file mode 100644 index 0000000..02d2806 --- /dev/null +++ b/node-test/example.wasi-browser.js @@ -0,0 +1,54 @@ +/* auto-generated by zig-napi */ +import { + getDefaultContext as __emnapiGetDefaultContext, + instantiateNapiModuleSync as __emnapiInstantiateNapiModuleSync, + WASI as __WASI, +} from "@napi-rs/wasm-runtime"; + +const __wasi = new __WASI({ + version: "preview1", +}); + +const __wasmUrl = new URL("./example.wasm32-wasi.wasm", import.meta.url).href; +const __emnapiContext = __emnapiGetDefaultContext(); + +const __sharedMemory = new WebAssembly.Memory({ + initial: 4000, + maximum: 65536, + shared: true, +}); + +const __wasmFile = await fetch(__wasmUrl).then((res) => res.arrayBuffer()); + +const { + instance: __napiInstance, + module: __wasiModule, + napiModule: __napiModule, +} = __emnapiInstantiateNapiModuleSync(__wasmFile, { + context: __emnapiContext, + asyncWorkPoolSize: 4, + wasi: __wasi, + onCreateWorker() { + return new Worker(new URL("./wasi-worker-browser.mjs", import.meta.url), { + type: "module", + }); + }, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __sharedMemory, + }; + return importObject; + }, + beforeInit({ instance }) { + for (const name of Object.keys(instance.exports)) { + if (name.startsWith("__napi_register__")) { + instance.exports[name](); + } + } + }, +}); + +export default __napiModule.exports; diff --git a/node-test/example.wasi.cjs b/node-test/example.wasi.cjs new file mode 100644 index 0000000..731ed42 --- /dev/null +++ b/node-test/example.wasi.cjs @@ -0,0 +1,111 @@ +/* eslint-disable */ +/* auto-generated by zig-napi */ + +const __nodeFs = require("node:fs"); +const __nodePath = require("node:path"); +const { WASI: __nodeWASI } = require("node:wasi"); +const { Worker } = require("node:worker_threads"); + +const { + createOnMessage: __wasmCreateOnMessageForFsProxy, + getDefaultContext: __emnapiGetDefaultContext, + instantiateNapiModuleSync: __emnapiInstantiateNapiModuleSync, +} = require("@napi-rs/wasm-runtime"); + +const __rootDir = __nodePath.parse(process.cwd()).root; + +const __wasi = new __nodeWASI({ + version: "preview1", + env: process.env, + preopens: { + [__rootDir]: __rootDir, + }, +}); + +const __emnapiContext = __emnapiGetDefaultContext(); + +const __sharedMemory = new WebAssembly.Memory({ + initial: 4000, + maximum: 65536, + shared: true, +}); + +const __wasmCandidates = [ + __nodePath.join(__dirname, "example.wasm32-wasi.debug.wasm"), + __nodePath.join(__dirname, "example.wasm32-wasi.wasm"), + __nodePath.join(__dirname, "zig-out", "node", "example.wasm32-wasi.debug.wasm"), + __nodePath.join(__dirname, "zig-out", "node", "example.wasm32-wasi.wasm"), +]; + +let __wasmFilePath = __wasmCandidates.find((candidate) => __nodeFs.existsSync(candidate)); + +if (!__wasmFilePath) { + try { + __wasmFilePath = require.resolve("zig-napi-node-test-wasm32-wasi/example.wasm32-wasi.wasm"); + } catch { + throw new Error( + "Cannot find example.wasm32-wasi.wasm file, and zig-napi-node-test-wasm32-wasi package is not installed.", + ); + } +} + +const { + instance: __napiInstance, + module: __wasiModule, + napiModule: __napiModule, +} = __emnapiInstantiateNapiModuleSync(__nodeFs.readFileSync(__wasmFilePath), { + context: __emnapiContext, + asyncWorkPoolSize: (function () { + const threadsSizeFromEnv = Number( + process.env.NAPI_RS_ASYNC_WORK_POOL_SIZE ?? process.env.UV_THREADPOOL_SIZE, + ); + return threadsSizeFromEnv > 0 ? threadsSizeFromEnv : 4; + })(), + reuseWorker: true, + wasi: __wasi, + onCreateWorker() { + const worker = new Worker(__nodePath.join(__dirname, "wasi-worker.mjs"), { + env: process.env, + }); + worker.onmessage = ({ data }) => { + __wasmCreateOnMessageForFsProxy(__nodeFs)(data); + }; + + { + const kPublicPort = Object.getOwnPropertySymbols(worker).find((symbol) => + symbol.toString().includes("kPublicPort"), + ); + if (kPublicPort) { + worker[kPublicPort].ref = () => {}; + } + + const kHandle = Object.getOwnPropertySymbols(worker).find((symbol) => + symbol.toString().includes("kHandle"), + ); + if (kHandle) { + worker[kHandle].ref = () => {}; + } + + worker.unref(); + } + return worker; + }, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __sharedMemory, + }; + return importObject; + }, + beforeInit({ instance }) { + for (const name of Object.keys(instance.exports)) { + if (name.startsWith("__napi_register__")) { + instance.exports[name](); + } + } + }, +}); + +module.exports = __napiModule.exports; diff --git a/node-test/load-addon.js b/node-test/load-addon.js index ce1c748..46fafb0 100644 --- a/node-test/load-addon.js +++ b/node-test/load-addon.js @@ -22,15 +22,25 @@ function platformArchABIs() { } } +function forceWasi() { + return process.env.NAPI_RS_FORCE_WASI === "true" || process.env.NAPI_RS_FORCE_WASI === "error"; +} + module.exports = function loadAddon(name) { - const candidates = platformArchABIs().map((platformArchABI) => + const nativeCandidates = platformArchABIs().flatMap((platformArchABI) => [ + path.join(__dirname, "zig-out", "node", `${name}.${platformArchABI}.node`), path.join(__dirname, `${name}.${platformArchABI}.node`), - ); + ]); + const wasiCandidates = [ + path.join(__dirname, `${name}.wasi.cjs`), + path.join(__dirname, "zig-out", "node", `${name}.wasi.cjs`), + ]; + const candidates = forceWasi() ? wasiCandidates : nativeCandidates.concat(wasiCandidates); const loadErrors = []; for (const candidate of candidates) { if (!fs.existsSync(candidate)) { - loadErrors.push(new Error(`Missing native binding ${candidate}`)); + loadErrors.push(new Error(`Missing binding ${candidate}`)); continue; } @@ -43,7 +53,7 @@ module.exports = function loadAddon(name) { throw new Error( [ - `Unable to load ${name}.node from: ${candidates.join(", ")}`, + `Unable to load ${name}.node or ${name}.wasi.cjs`, ...loadErrors.map((error) => `- ${error && error.message ? error.message : error}`), ].join("\n"), ); diff --git a/node-test/napi-compat-mode/src/arraybuffer.zig b/node-test/napi-compat-mode/src/arraybuffer.zig index df85b76..2b9d28e 100644 --- a/node-test/napi-compat-mode/src/arraybuffer.zig +++ b/node-test/napi-compat-mode/src/arraybuffer.zig @@ -28,22 +28,27 @@ pub fn createExternalArraybuffer(env: napi.Env) !napi.ArrayBuffer { return try napi.ArrayBuffer.from(env, bytes); } -pub fn mutateUint8Array(values: napi.Uint8Array) void { +pub fn mutateUint8Array(values: napi.Uint8Array) !void { if (values.length() > 0) values.asSlice()[0] = 42; + try values.flush(); } -pub fn mutateUint16Array(values: napi.Uint16Array) void { +pub fn mutateUint16Array(values: napi.Uint16Array) !void { if (values.length() > 0) values.asSlice()[0] = 65535; + try values.flush(); } -pub fn mutateInt16Array(values: napi.Int16Array) void { +pub fn mutateInt16Array(values: napi.Int16Array) !void { if (values.length() > 0) values.asSlice()[0] = 32767; + try values.flush(); } -pub fn mutateFloat32Array(values: napi.Float32Array) void { +pub fn mutateFloat32Array(values: napi.Float32Array) !void { if (values.length() > 0) values.asSlice()[0] = 3.33; + try values.flush(); } -pub fn mutateFloat64Array(values: napi.Float64Array) void { +pub fn mutateFloat64Array(values: napi.Float64Array) !void { if (values.length() > 0) values.asSlice()[0] = std.math.pi; + try values.flush(); } diff --git a/node-test/napi-compat-mode/src/buffer.zig b/node-test/napi-compat-mode/src/buffer.zig index 1a1dc0c..40cd9ce 100644 --- a/node-test/napi-compat-mode/src/buffer.zig +++ b/node-test/napi-compat-mode/src/buffer.zig @@ -48,8 +48,9 @@ pub fn createEmptyExternalBuffer(env: napi.Env) !napi.Buffer { return try napi.Buffer.from(env, bytes); } -pub fn mutateBuffer(buffer: napi.Buffer) void { +pub fn mutateBuffer(buffer: napi.Buffer) !void { if (buffer.length() > 1) { buffer.asSlice()[1] = 42; } + try buffer.flush(); } diff --git a/node-test/napi-compat-mode/src/napi4/threadsafe_function.zig b/node-test/napi-compat-mode/src/napi4/threadsafe_function.zig index c6e1442..9e89255 100644 --- a/node-test/napi-compat-mode/src/napi4/threadsafe_function.zig +++ b/node-test/napi-compat-mode/src/napi4/threadsafe_function.zig @@ -1,18 +1,33 @@ const std = @import("std"); +const builtin = @import("builtin"); const napi = @import("napi"); const TsfnArgs = struct { i32, i32 }; const TsfnReturn = i32; +const Tsfn = napi.ThreadSafeFunction(TsfnArgs, TsfnReturn, true, 0); +const use_wasm_async_work = builtin.cpu.arch == .wasm32 and builtin.os.tag == .wasi; -fn executeThreadSafeFunction(tsfn: *napi.ThreadSafeFunction(TsfnArgs, TsfnReturn, true, 0)) void { +fn executeThreadSafeFunction(tsfn: *Tsfn) void { defer tsfn.release(.Release) catch {}; tsfn.Ok(.{ 1, 2 }, .NonBlocking) catch {}; } -pub fn callThreadsafeFunction(tsfn: *napi.ThreadSafeFunction(TsfnArgs, TsfnReturn, true, 0)) !void { +fn queueThreadSafeFunction(tsfn: *Tsfn) void { + const worker = napi.Worker(napi.Env.from_raw(tsfn.env), .{ + .data = tsfn, + .Execute = executeThreadSafeFunction, + }); + worker.Queue(); +} + +pub fn callThreadsafeFunction(tsfn: *Tsfn) !void { try tsfn.acquire(); + errdefer tsfn.release(.Release) catch {}; + if (comptime use_wasm_async_work) { + queueThreadSafeFunction(tsfn); + return; + } + const worker = try std.Thread.spawn(.{}, executeThreadSafeFunction, .{tsfn}); worker.detach(); - - try tsfn.release(.Release); } diff --git a/node-test/napi-compat-mode/src/napi6/bigint.zig b/node-test/napi-compat-mode/src/napi6/bigint.zig index 3a122bb..eb47804 100644 --- a/node-test/napi-compat-mode/src/napi6/bigint.zig +++ b/node-test/napi-compat-mode/src/napi6/bigint.zig @@ -19,8 +19,9 @@ pub fn bigintAdd(env: napi.Env, left: napi.BigInt, right: napi.BigInt) napi.BigI return napi.BigInt.New(env, @as(i128, left_value + right_value)); } -pub fn mutateI64Array(values: napi.BigInt64Array) void { +pub fn mutateI64Array(values: napi.BigInt64Array) !void { if (values.length() > 0) { values.asSlice()[0] = std.math.maxInt(i64); } + try values.flush(); } diff --git a/node-test/napi/__tests__/values.spec.js b/node-test/napi/__tests__/values.spec.js index ff376e8..30bd041 100644 --- a/node-test/napi/__tests__/values.spec.js +++ b/node-test/napi/__tests__/values.spec.js @@ -216,6 +216,11 @@ test("DataView", (t) => { const view = new DataView(new ArrayBuffer(4)); bindings.mutateDataView(view); t.is(view.getUint16(0, true), 0x1234); + + const fixture = new Uint8Array([0, 1, 2, 3]); + const offsetView = new DataView(fixture.buffer, 1, 2); + bindings.mutateDataView(offsetView); + t.deepEqual(Array.from(fixture), [0, 0x34, 0x12, 3]); }); test("async", async (t) => { diff --git a/node-test/napi/src/threadsafe_function.zig b/node-test/napi/src/threadsafe_function.zig index d8838a2..368110c 100644 --- a/node-test/napi/src/threadsafe_function.zig +++ b/node-test/napi/src/threadsafe_function.zig @@ -1,18 +1,33 @@ const std = @import("std"); +const builtin = @import("builtin"); const napi = @import("napi"); const TsfnArgs = struct { i32, i32 }; const TsfnReturn = i32; +const Tsfn = napi.ThreadSafeFunction(TsfnArgs, TsfnReturn, true, 0); +const use_wasm_async_work = builtin.cpu.arch == .wasm32 and builtin.os.tag == .wasi; -fn executeThreadSafeFunction(tsfn: *napi.ThreadSafeFunction(TsfnArgs, TsfnReturn, true, 0)) void { +fn executeThreadSafeFunction(tsfn: *Tsfn) void { defer tsfn.release(.Release) catch {}; tsfn.Ok(.{ 20, 22 }, .NonBlocking) catch {}; } -pub fn callThreadsafeFunction(tsfn: *napi.ThreadSafeFunction(TsfnArgs, TsfnReturn, true, 0)) !void { +fn queueThreadSafeFunction(tsfn: *Tsfn) void { + const worker = napi.Worker(napi.Env.from_raw(tsfn.env), .{ + .data = tsfn, + .Execute = executeThreadSafeFunction, + }); + worker.Queue(); +} + +pub fn callThreadsafeFunction(tsfn: *Tsfn) !void { try tsfn.acquire(); + errdefer tsfn.release(.Release) catch {}; + if (comptime use_wasm_async_work) { + queueThreadSafeFunction(tsfn); + return; + } + const worker = try std.Thread.spawn(.{}, executeThreadSafeFunction, .{tsfn}); worker.detach(); - - try tsfn.release(.Release); } diff --git a/node-test/napi/src/values.zig b/node-test/napi/src/values.zig index 4d5f209..b6ed307 100644 --- a/node-test/napi/src/values.zig +++ b/node-test/napi/src/values.zig @@ -473,16 +473,18 @@ pub fn createExternalTypedArray(env: napi.Env) !napi.Uint32Array { return try napi.Uint32Array.copy(env, &[_]u32{ 1, 2, 3 }); } -pub fn mutateTypedArray(input: napi.Uint8Array) void { +pub fn mutateTypedArray(input: napi.Uint8Array) !void { for (input.asSlice()) |*value| { value.* +%= 1; } + try input.flush(); } -pub fn mutateArraybuffer(input: napi.ArrayBuffer) void { +pub fn mutateArraybuffer(input: napi.ArrayBuffer) !void { for (input.asSlice()) |*value| { value.* +%= 1; } + try input.flush(); } pub fn createUint8ClampedArrayFromData(env: napi.Env) !napi.Uint8ClampedArray { @@ -528,6 +530,7 @@ pub fn readDataView(input: napi.DataView) !i32 { pub fn mutateDataView(input: napi.DataView) !void { try input.setUint16(0, 0x1234, true); + try input.flush(); } pub fn asyncPlus100(value: i32) napi.Async(i32, .single) { diff --git a/node-test/package.json b/node-test/package.json index 55caf78..09b786a 100644 --- a/node-test/package.json +++ b/node-test/package.json @@ -12,14 +12,28 @@ "test:napi": "ava --serial \"napi/__tests__/**/*.spec.js\"" }, "devDependencies": { - "ava": "3.15.0" + "@emnapi/core": "^1.11.0", + "@emnapi/runtime": "^1.11.0", + "@napi-rs/wasm-runtime": "^1.1.6", + "ava": "3.15.0", + "emnapi": "^1.11.1" + }, + "napi": { + "binaryNames": [ + "compat_mode", + "example" + ], + "packageName": "zig-napi-node-test", + "targets": [ + "wasm32-wasip1-threads" + ] }, "ava": { "files": [ "napi-compat-mode/__tests__/**/*.spec.js", "napi/__tests__/**/*.spec.js" ], - "workerThreads": false, - "timeout": "30s" + "timeout": "30s", + "workerThreads": false } } diff --git a/node-test/wasi-worker-browser.mjs b/node-test/wasi-worker-browser.mjs new file mode 100644 index 0000000..15b2616 --- /dev/null +++ b/node-test/wasi-worker-browser.mjs @@ -0,0 +1,23 @@ +import { instantiateNapiModuleSync, MessageHandler, WASI } from "@napi-rs/wasm-runtime"; + +const handler = new MessageHandler({ + onLoad({ wasmModule, wasmMemory }) { + const wasi = new WASI({}); + return instantiateNapiModuleSync(wasmModule, { + childThread: true, + wasi, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: wasmMemory, + }; + }, + }); + }, +}); + +globalThis.onmessage = function (event) { + handler.handle(event); +}; diff --git a/node-test/wasi-worker.mjs b/node-test/wasi-worker.mjs new file mode 100644 index 0000000..e536b74 --- /dev/null +++ b/node-test/wasi-worker.mjs @@ -0,0 +1,66 @@ +import fs from "node:fs"; +import { createRequire } from "node:module"; +import { parse } from "node:path"; +import { WASI } from "node:wasi"; +import { parentPort, Worker } from "node:worker_threads"; + +const require = createRequire(import.meta.url); + +const { + instantiateNapiModuleSync, + MessageHandler, + getDefaultContext, +} = require("@napi-rs/wasm-runtime"); + +if (parentPort) { + parentPort.on("message", (data) => { + globalThis.onmessage({ data }); + }); +} + +Object.assign(globalThis, { + self: globalThis, + require, + Worker, + importScripts(f) { + (0, eval)(fs.readFileSync(f, "utf8") + "//# sourceURL=" + f); + }, + postMessage(msg) { + if (parentPort) { + parentPort.postMessage(msg); + } + }, +}); + +const emnapiContext = getDefaultContext(); +const __rootDir = parse(process.cwd()).root; + +const handler = new MessageHandler({ + onLoad({ wasmModule, wasmMemory }) { + const wasi = new WASI({ + version: "preview1", + env: process.env, + preopens: { + [__rootDir]: __rootDir, + }, + }); + + return instantiateNapiModuleSync(wasmModule, { + childThread: true, + wasi, + context: emnapiContext, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: wasmMemory, + }; + }, + }); + }, +}); + +globalThis.onmessage = function (event) { + handler.handle(event); +}; diff --git a/package.json b/package.json index 7457b47..09429fe 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,8 @@ }, "devDependencies": { "@j178/prek": "^0.4.5", - "@ohos-rs/oxk": "^0.6.0", + "@ohos-rs/oxk": "^0.7.2", + "emnapi": "^1.11.1", "oxlint": "^1.71.0" }, "license": "MIT" diff --git a/packages/zig-napi/bin/zig-napi.js b/packages/zig-napi/bin/zig-napi.js index d210ac8..b147cca 100755 --- a/packages/zig-napi/bin/zig-napi.js +++ b/packages/zig-napi/bin/zig-napi.js @@ -3,7 +3,7 @@ const childProcess = require("node:child_process"); const fs = require("node:fs"); const path = require("node:path"); -const { NapiCli } = require("@napi-rs/cli"); +const { NapiCli, parseTriple } = require("@napi-rs/cli"); const { Command } = require("commander"); const packageDir = path.resolve(__dirname, ".."); @@ -34,7 +34,6 @@ const availableTargets = [ "riscv64gc-unknown-linux-gnu", "powerpc64le-unknown-linux-gnu", "s390x-unknown-linux-gnu", - "wasm32-wasi-preview1-threads", "wasm32-wasip1-threads", ]; @@ -90,7 +89,7 @@ function collectTargets(value, previous) { } function resolveNewTargets(flags) { - const targets = flags.targets; + const targets = flags.targets.map(normalizeTargetName); if (!targets.length) { fail("at least one target must be enabled"); @@ -130,6 +129,10 @@ function validateTargets(targets) { } } +function normalizeTargetName(target) { + return target === "wasm32-wasi-preview1-threads" ? "wasm32-wasip1-threads" : target; +} + function formatJsonStringArrayItems(values, indent) { return values.map((value) => `${indent}${JSON.stringify(value)}`).join(",\n"); } @@ -297,6 +300,448 @@ function cleanOptions(options) { return Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined)); } +function readJson(file) { + return JSON.parse(fs.readFileSync(file, "utf8")); +} + +function writeJson(file, value) { + fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`); +} + +function createBrowserEntry(packageName) { + return `export { default } from '${packageName}-wasm32-wasi'\nexport * from '${packageName}-wasm32-wasi'\n`; +} + +function readDtsExportIdents(cwd) { + const dtsPath = path.join(cwd, "index.d.ts"); + if (!fs.existsSync(dtsPath)) return []; + + const dts = fs.readFileSync(dtsPath, "utf8"); + const idents = new Set(); + const exportDeclaration = + /^export\s+(?:declare\s+)?(?:function|const|let|var|class|enum)\s+([A-Za-z_$][\w$]*)/gm; + let match = exportDeclaration.exec(dts); + while (match) { + idents.add(match[1]); + match = exportDeclaration.exec(dts); + } + return [...idents]; +} + +function removeWasiBrowserTemplateFiles(projectDir) { + for (const file of [ + "browser.js", + "wasi-worker.mjs", + "wasi-worker-browser.mjs", + ...fs + .readdirSync(projectDir) + .filter((entry) => entry.endsWith(".wasi.cjs") || entry.endsWith(".wasi-browser.js")), + ]) { + fs.rmSync(path.join(projectDir, file), { force: true }); + } +} + +function updateTemplatePackageForTargets(projectDir, targets) { + if (targets.some((target) => isWasiTargetName(target))) return; + + removeWasiBrowserTemplateFiles(projectDir); + + const packageJsonPath = path.join(projectDir, "package.json"); + const packageJson = readJson(packageJsonPath); + if (packageJson.browser === "browser.js" || packageJson.browser === "./browser.js") { + delete packageJson.browser; + } + if (Array.isArray(packageJson.files)) { + packageJson.files = packageJson.files.filter( + (file) => + file !== "browser.js" && + file !== "./browser.js" && + file !== "*.wasm" && + file !== "*.wasi.cjs" && + file !== "*.wasi-browser.js" && + file !== "wasi-worker.mjs" && + file !== "wasi-worker-browser.mjs", + ); + } + writeJson(packageJsonPath, packageJson); +} + +function resolveZigNapiPath(targetDir, value) { + const source = value ? path.resolve(process.cwd(), value) : workspaceRoot; + return path.relative(targetDir, source) || "."; +} + +function readZigNapiConfig(cwd, flags) { + const packageJsonPath = path.resolve(cwd, flags.packageJsonPath || "package.json"); + const packageJson = readJson(packageJsonPath); + const configPath = flags.configPath ? path.resolve(cwd, flags.configPath) : null; + const config = configPath ? readJson(configPath) : packageJson.napi || {}; + return { + binaryName: config.binaryName, + binaryNames: Array.isArray(config.binaryNames) ? config.binaryNames : [], + packageName: config.packageName || packageJson.name, + targets: Array.isArray(config.targets) ? config.targets : [], + wasm: config.wasm || {}, + }; +} + +function readBinaryNames(config) { + const binaryNames = []; + if (typeof config.binaryName === "string" && config.binaryName) { + binaryNames.push(config.binaryName); + } + for (const binaryName of config.binaryNames) { + if (typeof binaryName === "string" && binaryName) { + binaryNames.push(binaryName); + } + } + return [...new Set(binaryNames)]; +} + +function isWasiTargetName(target) { + if (!target) return false; + try { + const parsed = parseTriple(target); + return parsed.platformArchABI === "wasm32-wasi"; + } catch { + return target === "wasm32-wasi" || target.startsWith("wasm32-wasip"); + } +} + +function isWasiThreadsTargetName(target) { + return target === "wasm32-wasi-preview1-threads" || target === "wasm32-wasip1-threads"; +} + +function appendWasiThreadsBuildFlags(args, target, passthrough) { + if (!isWasiThreadsTargetName(target)) return; + + const hasCpuOption = + passthrough.some((arg) => arg === "-Dcpu" || arg.startsWith("-Dcpu=")) || + args.some((arg) => arg === "-Dcpu" || arg.startsWith("-Dcpu=")); + if (!hasCpuOption) { + args.push("-Dcpu=baseline+atomics+bulk_memory+mutable_globals"); + } +} + +function createWasiBinding( + wasmFileName, + packageName, + initialMemory = 4000, + maximumMemory = 65536, + exportIdents = [], +) { + const namedExports = exportIdents + .map((ident) => `module.exports.${ident} = __napiModule.exports.${ident}`) + .join("\n"); + + return `/* eslint-disable */ +/* auto-generated by zig-napi */ + +const __nodeFs = require('node:fs') +const __nodePath = require('node:path') +const { WASI: __nodeWASI } = require('node:wasi') +const { Worker } = require('node:worker_threads') + +const { + createOnMessage: __wasmCreateOnMessageForFsProxy, + getDefaultContext: __emnapiGetDefaultContext, + instantiateNapiModuleSync: __emnapiInstantiateNapiModuleSync, +} = require('@napi-rs/wasm-runtime') + +const __rootDir = __nodePath.parse(process.cwd()).root + +const __wasi = new __nodeWASI({ + version: 'preview1', + env: process.env, + preopens: { + [__rootDir]: __rootDir, + }, +}) + +const __emnapiContext = __emnapiGetDefaultContext() + +const __sharedMemory = new WebAssembly.Memory({ + initial: ${initialMemory}, + maximum: ${maximumMemory}, + shared: true, +}) + +const __wasmCandidates = [ + __nodePath.join(__dirname, '${wasmFileName}.debug.wasm'), + __nodePath.join(__dirname, '${wasmFileName}.wasm'), + __nodePath.join(__dirname, 'zig-out', 'node', '${wasmFileName}.debug.wasm'), + __nodePath.join(__dirname, 'zig-out', 'node', '${wasmFileName}.wasm'), +] + +let __wasmFilePath = __wasmCandidates.find((candidate) => __nodeFs.existsSync(candidate)) + +if (!__wasmFilePath) { + try { + __wasmFilePath = require.resolve('${packageName}-wasm32-wasi/${wasmFileName}.wasm') + } catch { + throw new Error('Cannot find ${wasmFileName}.wasm file, and ${packageName}-wasm32-wasi package is not installed.') + } +} + +const { + instance: __napiInstance, + module: __wasiModule, + napiModule: __napiModule, +} = __emnapiInstantiateNapiModuleSync(__nodeFs.readFileSync(__wasmFilePath), { + context: __emnapiContext, + asyncWorkPoolSize: (function () { + const threadsSizeFromEnv = Number(process.env.NAPI_RS_ASYNC_WORK_POOL_SIZE ?? process.env.UV_THREADPOOL_SIZE) + return threadsSizeFromEnv > 0 ? threadsSizeFromEnv : 4 + })(), + reuseWorker: true, + wasi: __wasi, + onCreateWorker() { + const worker = new Worker(__nodePath.join(__dirname, 'wasi-worker.mjs'), { + env: process.env, + }) + worker.onmessage = ({ data }) => { + __wasmCreateOnMessageForFsProxy(__nodeFs)(data) + } + + { + const kPublicPort = Object.getOwnPropertySymbols(worker).find((symbol) => + symbol.toString().includes('kPublicPort') + ) + if (kPublicPort) { + worker[kPublicPort].ref = () => {} + } + + const kHandle = Object.getOwnPropertySymbols(worker).find((symbol) => + symbol.toString().includes('kHandle') + ) + if (kHandle) { + worker[kHandle].ref = () => {} + } + + worker.unref() + } + return worker + }, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __sharedMemory, + } + return importObject + }, + beforeInit({ instance }) { + for (const name of Object.keys(instance.exports)) { + if (name.startsWith('__napi_register__')) { + instance.exports[name]() + } + } + }, +}) + +module.exports = __napiModule.exports${namedExports ? `\n${namedExports}` : ""} +`; +} + +function createWasiBrowserBinding( + wasmFileName, + initialMemory = 4000, + maximumMemory = 65536, + exportIdents = [], +) { + const namedExports = exportIdents + .map((ident) => `export const ${ident} = __napiModule.exports.${ident}`) + .join("\n"); + + return `/* auto-generated by zig-napi */ +import { + getDefaultContext as __emnapiGetDefaultContext, + instantiateNapiModuleSync as __emnapiInstantiateNapiModuleSync, + WASI as __WASI, +} from '@napi-rs/wasm-runtime' + +const __wasi = new __WASI({ + version: 'preview1', +}) + +const __wasmUrl = new URL('./${wasmFileName}.wasm', import.meta.url).href +const __emnapiContext = __emnapiGetDefaultContext() + +const __sharedMemory = new WebAssembly.Memory({ + initial: ${initialMemory}, + maximum: ${maximumMemory}, + shared: true, +}) + +const __wasmFile = await fetch(__wasmUrl).then((res) => res.arrayBuffer()) + +const { + instance: __napiInstance, + module: __wasiModule, + napiModule: __napiModule, +} = __emnapiInstantiateNapiModuleSync(__wasmFile, { + context: __emnapiContext, + asyncWorkPoolSize: 4, + wasi: __wasi, + onCreateWorker() { + return new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), { + type: 'module', + }) + }, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __sharedMemory, + } + return importObject + }, + beforeInit({ instance }) { + for (const name of Object.keys(instance.exports)) { + if (name.startsWith('__napi_register__')) { + instance.exports[name]() + } + } + }, +}) + +export default __napiModule.exports${namedExports ? `\n${namedExports}` : ""} +`; +} + +const WASI_WORKER_TEMPLATE = `import fs from "node:fs"; +import { createRequire } from "node:module"; +import { parse } from "node:path"; +import { WASI } from "node:wasi"; +import { parentPort, Worker } from "node:worker_threads"; + +const require = createRequire(import.meta.url); + +const { instantiateNapiModuleSync, MessageHandler, getDefaultContext } = require("@napi-rs/wasm-runtime"); + +if (parentPort) { + parentPort.on("message", (data) => { + globalThis.onmessage({ data }); + }); +} + +Object.assign(globalThis, { + self: globalThis, + require, + Worker, + importScripts(f) { + ;(0, eval)(fs.readFileSync(f, "utf8") + "//# sourceURL=" + f); + }, + postMessage(msg) { + if (parentPort) { + parentPort.postMessage(msg); + } + }, +}); + +const emnapiContext = getDefaultContext(); +const __rootDir = parse(process.cwd()).root; + +const handler = new MessageHandler({ + onLoad({ wasmModule, wasmMemory }) { + const wasi = new WASI({ + version: "preview1", + env: process.env, + preopens: { + [__rootDir]: __rootDir, + }, + }); + + return instantiateNapiModuleSync(wasmModule, { + childThread: true, + wasi, + context: emnapiContext, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: wasmMemory, + }; + }, + }); + }, +}); + +globalThis.onmessage = function (event) { + handler.handle(event); +}; +`; + +const WASI_BROWSER_WORKER_TEMPLATE = `import { instantiateNapiModuleSync, MessageHandler, WASI } from '@napi-rs/wasm-runtime' + +const handler = new MessageHandler({ + onLoad({ wasmModule, wasmMemory }) { + const wasi = new WASI({}) + return instantiateNapiModuleSync(wasmModule, { + childThread: true, + wasi, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: wasmMemory, + } + }, + }) + }, +}) + +globalThis.onmessage = function (event) { + handler.handle(event) +} +`; + +async function generateWasiBindings(cwd, flags) { + const config = readZigNapiConfig(cwd, flags); + return generateWasiBindingsWithConfig(cwd, flags, config, readDtsExportIdents(cwd)); +} + +async function generateWasiBindingsWithConfig(cwd, flags, config, exportIdents) { + const shouldGenerate = + isWasiTargetName(flags.target) || config.targets.some((target) => isWasiTargetName(target)); + + if (!shouldGenerate) return; + const binaryNames = readBinaryNames(config); + if (binaryNames.length === 0) fail("missing napi.binaryName; required to generate wasm bindings"); + if (!config.packageName) fail("missing package name; required to generate wasm bindings"); + + const outputDir = path.resolve(cwd, flags.buildOutputDir || "."); + fs.mkdirSync(outputDir, { recursive: true }); + + const initialMemory = config.wasm.initialMemory || 4000; + const maximumMemory = config.wasm.maximumMemory || 65536; + + for (const binaryName of binaryNames) { + const wasmFileName = `${binaryName}.wasm32-wasi`; + fs.writeFileSync( + path.join(outputDir, `${binaryName}.wasi.cjs`), + createWasiBinding( + wasmFileName, + config.packageName, + initialMemory, + maximumMemory, + exportIdents, + ), + ); + fs.writeFileSync( + path.join(outputDir, `${binaryName}.wasi-browser.js`), + createWasiBrowserBinding(wasmFileName, initialMemory, maximumMemory, exportIdents), + ); + } + fs.writeFileSync(path.join(outputDir, "browser.js"), createBrowserEntry(config.packageName)); + fs.writeFileSync(path.join(outputDir, "wasi-worker.mjs"), WASI_WORKER_TEMPLATE); + fs.writeFileSync(path.join(outputDir, "wasi-worker-browser.mjs"), WASI_BROWSER_WORKER_TEMPLATE); +} + async function commandNew(projectDir, flags) { const options = await promptNewOptions(projectDir, flags); const targetDir = path.resolve(process.cwd(), options.projectDir); @@ -306,7 +751,7 @@ async function commandNew(projectDir, flags) { const packageName = options.packageName; const addonName = options.addonName; - const zigNapiZigPath = flags.zigNapi || path.relative(targetDir, workspaceRoot) || "."; + const zigNapiZigPath = resolveZigNapiPath(targetDir, flags.zigNapi); fs.mkdirSync(targetDir, { recursive: true }); copyTemplate(templateDir, targetDir, { @@ -317,18 +762,24 @@ async function commandNew(projectDir, flags) { ' "__NAPI_TARGETS__"': formatJsonStringArrayItems(options.targets, " "), __FINGERPRINT__: "0x0", }); + updateTemplatePackageForTargets(targetDir, options.targets); repairZigFingerprint(targetDir); console.log(`Created ${packageName} in ${targetDir}`); } -function commandBuild(flags, passthrough = []) { +async function commandBuild(flags, passthrough = []) { const cwd = path.resolve(process.cwd(), flags.cwd || "."); + const config = readZigNapiConfig(cwd, flags); const args = ["build"]; if (flags.release) args.push("-Doptimize=ReleaseFast"); - if (flags.target) args.push(`-Dtarget=${flags.target}`); + if (flags.target) { + args.push(`-Dtarget=${isWasiThreadsTargetName(flags.target) ? "wasm32-wasi" : flags.target}`); + } + appendWasiThreadsBuildFlags(args, flags.target, passthrough); args.push(...passthrough); run("zig", args, { cwd }); + await generateWasiBindingsWithConfig(cwd, flags, config, readDtsExportIdents(cwd)); } function commandDts(flags, passthrough = []) { @@ -349,6 +800,7 @@ async function commandCreateNpmDirs(flags) { } async function commandArtifacts(flags) { + await generateWasiBindings(path.resolve(process.cwd(), flags.cwd || "."), flags); await napiCli.artifacts(cleanOptions(napiOptions(flags))); } @@ -367,7 +819,7 @@ async function commandPackage(flags) { dryRun: flags.dryRun, }), ); - commandBuild({ cwd, release: flags.release, target: flags.target }); + await commandBuild({ ...flags, cwd, release: flags.release, target: flags.target }); await napiCli.artifacts( cleanOptions({ cwd, diff --git a/packages/zig-napi/package.json b/packages/zig-napi/package.json index b7a704b..e6c3951 100644 --- a/packages/zig-napi/package.json +++ b/packages/zig-napi/package.json @@ -3,25 +3,25 @@ "version": "0.1.0", "private": true, "description": "CLI tools for building Node.js addons with zig-napi.", + "license": "MIT", "bin": { "zig-napi": "bin/zig-napi.js" }, - "engines": { - "node": ">=20.17" - }, - "scripts": { - "cli": "node bin/zig-napi.js" - }, "files": [ "bin", "templates", "README.md", "LICENSE" ], + "scripts": { + "cli": "node bin/zig-napi.js" + }, "dependencies": { "@inquirer/prompts": "^8.5.2", "@napi-rs/cli": "^3.7.2", "commander": "^9.5.0" }, - "license": "MIT" + "engines": { + "node": ">=20.17" + } } diff --git a/packages/zig-napi/templates/node-addon/__ADDON_NAME__.wasi-browser.js b/packages/zig-napi/templates/node-addon/__ADDON_NAME__.wasi-browser.js new file mode 100644 index 0000000..00531ab --- /dev/null +++ b/packages/zig-napi/templates/node-addon/__ADDON_NAME__.wasi-browser.js @@ -0,0 +1,56 @@ +/* auto-generated by zig-napi */ +import { + getDefaultContext as __emnapiGetDefaultContext, + instantiateNapiModuleSync as __emnapiInstantiateNapiModuleSync, + WASI as __WASI, +} from "@napi-rs/wasm-runtime"; + +const __wasi = new __WASI({ + version: "preview1", +}); + +const __wasmUrl = new URL("./__ADDON_NAME__.wasm32-wasi.wasm", import.meta.url).href; +const __emnapiContext = __emnapiGetDefaultContext(); + +const __sharedMemory = new WebAssembly.Memory({ + initial: 4000, + maximum: 65536, + shared: true, +}); + +const __wasmFile = await fetch(__wasmUrl).then((res) => res.arrayBuffer()); + +const { + instance: __napiInstance, + module: __wasiModule, + napiModule: __napiModule, +} = __emnapiInstantiateNapiModuleSync(__wasmFile, { + context: __emnapiContext, + asyncWorkPoolSize: 4, + wasi: __wasi, + onCreateWorker() { + return new Worker(new URL("./wasi-worker-browser.mjs", import.meta.url), { + type: "module", + }); + }, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __sharedMemory, + }; + return importObject; + }, + beforeInit({ instance }) { + for (const name of Object.keys(instance.exports)) { + if (name.startsWith("__napi_register__")) { + instance.exports[name](); + } + } + }, +}); + +export default __napiModule.exports; +export const add = __napiModule.exports.add; +export const hello = __napiModule.exports.hello; diff --git a/packages/zig-napi/templates/node-addon/__ADDON_NAME__.wasi.cjs b/packages/zig-napi/templates/node-addon/__ADDON_NAME__.wasi.cjs new file mode 100644 index 0000000..78491c7 --- /dev/null +++ b/packages/zig-napi/templates/node-addon/__ADDON_NAME__.wasi.cjs @@ -0,0 +1,114 @@ +/* eslint-disable */ +/* auto-generated by zig-napi */ + +const __nodeFs = require("node:fs"); +const __nodePath = require("node:path"); +const { WASI: __nodeWASI } = require("node:wasi"); +const { Worker } = require("node:worker_threads"); + +const { + createOnMessage: __wasmCreateOnMessageForFsProxy, + getDefaultContext: __emnapiGetDefaultContext, + instantiateNapiModuleSync: __emnapiInstantiateNapiModuleSync, +} = require("@napi-rs/wasm-runtime"); + +const __rootDir = __nodePath.parse(process.cwd()).root; + +const __wasi = new __nodeWASI({ + version: "preview1", + env: process.env, + preopens: { + [__rootDir]: __rootDir, + }, +}); + +const __emnapiContext = __emnapiGetDefaultContext(); + +const __sharedMemory = new WebAssembly.Memory({ + initial: 4000, + maximum: 65536, + shared: true, +}); + +const __wasmCandidates = [ + __nodePath.join(__dirname, "__ADDON_NAME__.wasm32-wasi.debug.wasm"), + __nodePath.join(__dirname, "__ADDON_NAME__.wasm32-wasi.wasm"), + __nodePath.join(__dirname, "zig-out", "node", "__ADDON_NAME__.wasm32-wasi.debug.wasm"), + __nodePath.join(__dirname, "zig-out", "node", "__ADDON_NAME__.wasm32-wasi.wasm"), +]; + +let __wasmFilePath = __wasmCandidates.find((candidate) => __nodeFs.existsSync(candidate)); + +if (!__wasmFilePath) { + try { + __wasmFilePath = + require.resolve("__PACKAGE_NAME__-wasm32-wasi/__ADDON_NAME__.wasm32-wasi.wasm"); + } catch { + throw new Error( + "Cannot find __ADDON_NAME__.wasm32-wasi.wasm file, and __PACKAGE_NAME__-wasm32-wasi package is not installed.", + ); + } +} + +const { + instance: __napiInstance, + module: __wasiModule, + napiModule: __napiModule, +} = __emnapiInstantiateNapiModuleSync(__nodeFs.readFileSync(__wasmFilePath), { + context: __emnapiContext, + asyncWorkPoolSize: (function () { + const threadsSizeFromEnv = Number( + process.env.NAPI_RS_ASYNC_WORK_POOL_SIZE ?? process.env.UV_THREADPOOL_SIZE, + ); + return threadsSizeFromEnv > 0 ? threadsSizeFromEnv : 4; + })(), + reuseWorker: true, + wasi: __wasi, + onCreateWorker() { + const worker = new Worker(__nodePath.join(__dirname, "wasi-worker.mjs"), { + env: process.env, + }); + worker.onmessage = ({ data }) => { + __wasmCreateOnMessageForFsProxy(__nodeFs)(data); + }; + + { + const kPublicPort = Object.getOwnPropertySymbols(worker).find((symbol) => + symbol.toString().includes("kPublicPort"), + ); + if (kPublicPort) { + worker[kPublicPort].ref = () => {}; + } + + const kHandle = Object.getOwnPropertySymbols(worker).find((symbol) => + symbol.toString().includes("kHandle"), + ); + if (kHandle) { + worker[kHandle].ref = () => {}; + } + + worker.unref(); + } + return worker; + }, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __sharedMemory, + }; + return importObject; + }, + beforeInit({ instance }) { + for (const name of Object.keys(instance.exports)) { + if (name.startsWith("__napi_register__")) { + instance.exports[name](); + } + } + }, +}); + +module.exports = __napiModule.exports; +module.exports.add = __napiModule.exports.add; +module.exports.hello = __napiModule.exports.hello; diff --git a/packages/zig-napi/templates/node-addon/__test__/index.spec.js b/packages/zig-napi/templates/node-addon/__test__/index.spec.js index 7a47315..391b71a 100644 --- a/packages/zig-napi/templates/node-addon/__test__/index.spec.js +++ b/packages/zig-napi/templates/node-addon/__test__/index.spec.js @@ -4,5 +4,4 @@ const addon = require("../index"); test("exports addon functions", (t) => { t.is(addon.add(20, 22), 42); t.is(addon.hello(), "hello from __PACKAGE_NAME__"); - t.is(addon.requestedNapiVersion(), 8); }); diff --git a/packages/zig-napi/templates/node-addon/browser.js b/packages/zig-napi/templates/node-addon/browser.js new file mode 100644 index 0000000..42e8030 --- /dev/null +++ b/packages/zig-napi/templates/node-addon/browser.js @@ -0,0 +1,2 @@ +export { default } from "__PACKAGE_NAME__-wasm32-wasi"; +export * from "__PACKAGE_NAME__-wasm32-wasi"; diff --git a/packages/zig-napi/templates/node-addon/index.d.ts b/packages/zig-napi/templates/node-addon/index.d.ts new file mode 100644 index 0000000..c12a4d3 --- /dev/null +++ b/packages/zig-napi/templates/node-addon/index.d.ts @@ -0,0 +1,5 @@ +/* auto-generated by zig-napi */ +/* eslint-disable */ + +export declare function add(left: number, right: number): number; +export declare function hello(): string; diff --git a/packages/zig-napi/templates/node-addon/index.js b/packages/zig-napi/templates/node-addon/index.js index ca0a389..d1de6a6 100644 --- a/packages/zig-napi/templates/node-addon/index.js +++ b/packages/zig-napi/templates/node-addon/index.js @@ -3,39 +3,101 @@ const path = require("path"); const packageName = "__PACKAGE_NAME__"; const binaryName = "__ADDON_NAME__"; +let nativeBinding = null; +const loadErrors = []; -function loadAddon() { - const errors = []; +function requireNative() { if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) { try { return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH); } catch (error) { - errors.push(error); + loadErrors.push(error); } } for (const platformArchABI of detectPlatformArchABIs()) { - const optionalPackage = optionalPackageName(packageName, platformArchABI); - const candidates = [ - () => require(path.join(__dirname, `${binaryName}.${platformArchABI}.node`)), - () => require(optionalPackage), - () => - require(path.join(__dirname, "zig-out", "node", `${binaryName}.${platformArchABI}.node`)), - ]; - - for (const candidate of candidates) { - try { - return candidate(); - } catch (error) { - errors.push(error); - } + const binding = requireTuple(platformArchABI); + if (binding) { + return binding; } } +} - throw new Error( - `Unable to load ${binaryName} native binding\n` + - errors.map((error) => `- ${error.message}`).join("\n"), - ); +function requireTuple(platformArchABI) { + try { + return require( + path.join(__dirname, "zig-out", "node", `${binaryName}.${platformArchABI}.node`), + ); + } catch (error) { + loadErrors.push(error); + } + try { + return require(path.join(__dirname, `${binaryName}.${platformArchABI}.node`)); + } catch (error) { + loadErrors.push(error); + } + try { + return require(optionalPackageName(packageName, platformArchABI)); + } catch (error) { + loadErrors.push(error); + } +} + +nativeBinding = requireNative(); + +// NAPI_RS_FORCE_WASI is a tri-state flag: +// unset / any other value -> native binding preferred, WASI is only a fallback +// 'true' -> force WASI fallback even if native loaded +// 'error' -> force WASI and throw if no WASI binding is found +const forceWasi = + process.env.NAPI_RS_FORCE_WASI === "true" || process.env.NAPI_RS_FORCE_WASI === "error"; + +if (!nativeBinding || forceWasi) { + let wasiBinding = null; + let wasiBindingError = null; + try { + wasiBinding = require(path.join(__dirname, "zig-out", "node", `${binaryName}.wasi.cjs`)); + nativeBinding = wasiBinding; + } catch (error) { + if (forceWasi) { + wasiBindingError = error; + } + } + if (!nativeBinding || forceWasi) { + try { + wasiBinding = require(path.join(__dirname, `${binaryName}.wasi.cjs`)); + nativeBinding = wasiBinding; + } catch (error) { + if (forceWasi) { + if (!wasiBindingError) { + wasiBindingError = error; + } else { + wasiBindingError.cause = error; + } + loadErrors.push(error); + } + } + } + if (!nativeBinding || forceWasi) { + try { + wasiBinding = require(optionalPackageName(packageName, "wasm32-wasi")); + nativeBinding = wasiBinding; + } catch (error) { + if (forceWasi) { + if (!wasiBindingError) { + wasiBindingError = error; + } else { + wasiBindingError.cause = error; + } + loadErrors.push(error); + } + } + } + if (process.env.NAPI_RS_FORCE_WASI === "error" && !wasiBinding) { + const error = new Error("WASI binding not found and NAPI_RS_FORCE_WASI is set to error"); + error.cause = wasiBindingError; + throw error; + } } function optionalPackageName(name, platformArchABI) { @@ -99,4 +161,20 @@ function isMusl() { } } -module.exports = loadAddon(); +if (!nativeBinding) { + if (loadErrors.length > 0) { + const error = new Error( + "Cannot find native binding. npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). Please try `npm i` again after removing both package-lock.json and node_modules directory.", + ); + error.cause = loadErrors.reduce((err, cur) => { + cur.cause = err; + return cur; + }); + throw error; + } + throw new Error("Failed to load native binding"); +} + +module.exports = nativeBinding; +module.exports.add = nativeBinding.add; +module.exports.hello = nativeBinding.hello; diff --git a/packages/zig-napi/templates/node-addon/package.json b/packages/zig-napi/templates/node-addon/package.json index 11c1b27..5182d7c 100644 --- a/packages/zig-napi/templates/node-addon/package.json +++ b/packages/zig-napi/templates/node-addon/package.json @@ -2,20 +2,33 @@ "name": "__PACKAGE_NAME__", "version": "0.1.0", "description": "A Node.js native addon built with zig-napi.", - "main": "index.js", - "types": "index.d.ts", "keywords": [ - "zig-napi", - "NAPI", "N-API", + "NAPI", "Zig", "node-addon", - "node-addon-api" + "node-addon-api", + "zig-napi" ], + "license": "MIT", "files": [ "index.js", - "index.d.ts" + "index.d.ts", + "browser.js", + "*.node", + "*.wasm", + "*.wasi.cjs", + "*.wasi-browser.js", + "wasi-worker.mjs", + "wasi-worker-browser.mjs" ], + "main": "index.js", + "browser": "browser.js", + "types": "index.d.ts", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, "scripts": { "artifacts": "zig-napi artifacts", "build": "zig-napi build --release", @@ -25,6 +38,12 @@ "build:test": "npm run build", "test:run": "ava" }, + "dependencies": { + "@emnapi/core": "^1.11.0", + "@emnapi/runtime": "^1.11.0", + "@napi-rs/wasm-runtime": "^1.1.6", + "emnapi": "^1.11.1" + }, "devDependencies": { "ava": "^8.0.0", "zig-napi": "^0.1.0" @@ -37,10 +56,5 @@ }, "engines": { "node": ">=20.17" - }, - "publishConfig": { - "registry": "https://registry.npmjs.org/", - "access": "public" - }, - "license": "MIT" + } } diff --git a/packages/zig-napi/templates/node-addon/src/lib.zig b/packages/zig-napi/templates/node-addon/src/lib.zig index dd593fb..17fb17b 100644 --- a/packages/zig-napi/templates/node-addon/src/lib.zig +++ b/packages/zig-napi/templates/node-addon/src/lib.zig @@ -8,10 +8,6 @@ pub fn hello() []const u8 { return "hello from __PACKAGE_NAME__"; } -pub fn requestedNapiVersion() i32 { - return @intFromEnum(napi.selectedNapiVersion()); -} - comptime { napi.NODE_API_MODULE("__ADDON_NAME__", @This()); } diff --git a/packages/zig-napi/templates/node-addon/wasi-worker-browser.mjs b/packages/zig-napi/templates/node-addon/wasi-worker-browser.mjs new file mode 100644 index 0000000..15b2616 --- /dev/null +++ b/packages/zig-napi/templates/node-addon/wasi-worker-browser.mjs @@ -0,0 +1,23 @@ +import { instantiateNapiModuleSync, MessageHandler, WASI } from "@napi-rs/wasm-runtime"; + +const handler = new MessageHandler({ + onLoad({ wasmModule, wasmMemory }) { + const wasi = new WASI({}); + return instantiateNapiModuleSync(wasmModule, { + childThread: true, + wasi, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: wasmMemory, + }; + }, + }); + }, +}); + +globalThis.onmessage = function (event) { + handler.handle(event); +}; diff --git a/packages/zig-napi/templates/node-addon/wasi-worker.mjs b/packages/zig-napi/templates/node-addon/wasi-worker.mjs new file mode 100644 index 0000000..e536b74 --- /dev/null +++ b/packages/zig-napi/templates/node-addon/wasi-worker.mjs @@ -0,0 +1,66 @@ +import fs from "node:fs"; +import { createRequire } from "node:module"; +import { parse } from "node:path"; +import { WASI } from "node:wasi"; +import { parentPort, Worker } from "node:worker_threads"; + +const require = createRequire(import.meta.url); + +const { + instantiateNapiModuleSync, + MessageHandler, + getDefaultContext, +} = require("@napi-rs/wasm-runtime"); + +if (parentPort) { + parentPort.on("message", (data) => { + globalThis.onmessage({ data }); + }); +} + +Object.assign(globalThis, { + self: globalThis, + require, + Worker, + importScripts(f) { + (0, eval)(fs.readFileSync(f, "utf8") + "//# sourceURL=" + f); + }, + postMessage(msg) { + if (parentPort) { + parentPort.postMessage(msg); + } + }, +}); + +const emnapiContext = getDefaultContext(); +const __rootDir = parse(process.cwd()).root; + +const handler = new MessageHandler({ + onLoad({ wasmModule, wasmMemory }) { + const wasi = new WASI({ + version: "preview1", + env: process.env, + preopens: { + [__rootDir]: __rootDir, + }, + }); + + return instantiateNapiModuleSync(wasmModule, { + childThread: true, + wasi, + context: emnapiContext, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: wasmMemory, + }; + }, + }); + }, +}); + +globalThis.onmessage = function (event) { + handler.handle(event); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d2f732..2ed358e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,13 +12,29 @@ importers: specifier: ^0.4.5 version: 0.4.5 '@ohos-rs/oxk': - specifier: ^0.6.0 - version: 0.6.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + specifier: ^0.7.2 + version: 0.7.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + emnapi: + specifier: ^1.11.1 + version: 1.11.1 oxlint: specifier: ^1.71.0 version: 1.71.0 examples/node: + dependencies: + '@emnapi/core': + specifier: ^1.11.0 + version: 1.11.1 + '@emnapi/runtime': + specifier: ^1.11.0 + version: 1.11.1 + '@napi-rs/wasm-runtime': + specifier: ^1.1.6 + version: 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + emnapi: + specifier: ^1.11.1 + version: 1.11.1 devDependencies: zig-napi: specifier: workspace:* @@ -26,9 +42,21 @@ importers: node-test: devDependencies: + '@emnapi/core': + specifier: ^1.11.0 + version: 1.11.1 + '@emnapi/runtime': + specifier: ^1.11.0 + version: 1.11.1 + '@napi-rs/wasm-runtime': + specifier: ^1.1.6 + version: 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) ava: specifier: 3.15.0 version: 3.15.0 + emnapi: + specifier: ^1.11.1 + version: 1.11.1 packages/zig-napi: dependencies: @@ -1002,10 +1030,6 @@ packages: '@types/node': optional: true - '@isaacs/cliui@9.0.0': - resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} - engines: {node: '>=18'} - '@j178/prek-android-arm64@0.4.5': resolution: {integrity: sha512-tuFxbPp9kuwRf2FReWNPwsOLB986KaYvwxs2NZLjbU7AERK7R+n4kmRQ0yWCabdSlX+5ZXAfqj9sA3uoAw7zGw==} engines: {node: '>=18'} @@ -1616,93 +1640,98 @@ packages: '@octokit/types@16.0.0': resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} - '@ohos-rs/oxk-android-arm-eabi@0.6.0': - resolution: {integrity: sha512-iBCNWSMoBM30XZjEK4ZR3cq9pbRTy1FgDIF++OQuMRBC0ge6MbfQcETIWdq8Tki6z15W+QDHVYCLUm6dtpJwLw==} + '@ohos-rs/oxk-android-arm-eabi@0.7.2': + resolution: {integrity: sha512-WJfgxU8KVt+6sZlrkTbJQ/FMHqpgoHwXFZ0wo4rDTCRpUsS7NwQOlnPCD9FbGCrm/Vmfm6ROF+jMK6sAkTyotA==} engines: {node: ^20.19.0 || >=22.18.0} cpu: [arm] os: [android] - '@ohos-rs/oxk-android-arm64@0.6.0': - resolution: {integrity: sha512-HBkgsyO5RUf4obd2wSvrKAq3U6ON9yvnDiiz72bPGV9Kx4VqBwzMvOyK9BdQzQl6mY51mZwaapdVk8GNx+1uSA==} + '@ohos-rs/oxk-android-arm64@0.7.2': + resolution: {integrity: sha512-mrZ+Zoi6qO/njqy1jWzIe0NakDjp4VbiBBSGYI/P00QY+EPAbF+E1I4LjEMYt5yGRWKDOiZgmXoM+mFJnEFBtg==} engines: {node: ^20.19.0 || >=22.18.0} cpu: [arm64] os: [android] - '@ohos-rs/oxk-darwin-arm64@0.6.0': - resolution: {integrity: sha512-x/Azc/OtL/OT4yrcEdRojWYxO7FubfHtYke4Z0cauKJREh41ojpLArtpuYflhFZ4w4CkmmeyJef8jpn0Gogneg==} + '@ohos-rs/oxk-darwin-arm64@0.7.2': + resolution: {integrity: sha512-y/qIo66L60Q8pgfq4eHaukdwpwimDU9BfxoF5N2apTAoClf3qiO9pMKIIcTU6FjUCnW4j9eQLs3ARN1asJ+fIg==} engines: {node: ^20.19.0 || >=22.18.0} cpu: [arm64] os: [darwin] - '@ohos-rs/oxk-darwin-x64@0.6.0': - resolution: {integrity: sha512-TaVqNXjI8mKWGDW+NmlKXpCZFLNNYHjkBJK95RnRc77lZ0DTW5YJ+KIKpUs4JRDpRVV2iqhUpXjt4rwBp/bomw==} + '@ohos-rs/oxk-darwin-x64@0.7.2': + resolution: {integrity: sha512-wRsChAcoB4rbYOdsUe5rcwksIj8PyxseSE5sy34GzmkyPwo4sV0JYUlG+jM9ovXhyuCfLKJtzBqrcC3nTDi1Hg==} engines: {node: ^20.19.0 || >=22.18.0} cpu: [x64] os: [darwin] - '@ohos-rs/oxk-freebsd-x64@0.6.0': - resolution: {integrity: sha512-BZ6aFz7Vq9/Sy6/FTytTr8ZQwoPVHzvULWjsHDrS+FfxzyzJRAzMO+RlirfkDwNf6chat5XJRFjv0wqUmY068w==} + '@ohos-rs/oxk-freebsd-x64@0.7.2': + resolution: {integrity: sha512-xIa0OkZKTMkYqZ1V7noYYGAj5qDIYr4y22v56qNk0gCRlbSvKn0NdEiBcj4nvZAEtGlaVYbUYQguCfl5zXNwJg==} engines: {node: ^20.19.0 || >=22.18.0} cpu: [x64] os: [freebsd] - '@ohos-rs/oxk-linux-arm-gnueabihf@0.6.0': - resolution: {integrity: sha512-t8OT9kaa6b0Ljhh8+EGeajflnOlpdkJyU76iOsi8tLrPpSv5psXO9Dd+42OLMUyFrBE8c3jPsREhz5z+lHRPSA==} + '@ohos-rs/oxk-linux-arm-gnueabihf@0.7.2': + resolution: {integrity: sha512-dHbjcw42868poAVNCDOUJSCAhe4icYWXbdURKNzGQuFobmGQFWpDgSkRjY2NdnQVKjW2SeeMBitsApArEXhjOw==} engines: {node: ^20.19.0 || >=22.18.0} cpu: [arm] os: [linux] - '@ohos-rs/oxk-linux-arm64-gnu@0.6.0': - resolution: {integrity: sha512-gJboMAURyqjmMyzZnyhIjrhjebeunSJGkTu2KgX9Q5sqIbY5nZKKseLUCu8SOTe5WwSxHJLPFN28JHSBiJuyBA==} + '@ohos-rs/oxk-linux-arm64-gnu@0.7.2': + resolution: {integrity: sha512-zm/m/6er9ls0100vbGfHG/Ye2nb9SXV90EqJprtKCEgmzqNI6Xs9ED7/qjoLcq9gRI+TVUgTeXKaEcTAyjcymg==} engines: {node: ^20.19.0 || >=22.18.0} cpu: [arm64] os: [linux] - '@ohos-rs/oxk-linux-arm64-musl@0.6.0': - resolution: {integrity: sha512-paJ9W8YZf/n+l3yDZ6Rl5FnwJZsUYEAD4r/vU3mDB48Jm1Nh196RZT3UkY9uzQ77LJhaz0NH6EKkUwJ/n0mbLA==} + '@ohos-rs/oxk-linux-arm64-musl@0.7.2': + resolution: {integrity: sha512-XaNKi2z37E3u/1rP7Fk4TboM4M7If9qQz6bZC0+i4ZOVIHyXxbrpIHouYDyWHbqGn4lJtKkIwsglBj8UFqVouw==} engines: {node: ^20.19.0 || >=22.18.0} cpu: [arm64] os: [linux] - '@ohos-rs/oxk-linux-x64-gnu@0.6.0': - resolution: {integrity: sha512-q2pVzDLcHJFB24/aqChGj/2IZ3mO5Q4eUw7LVUhmyLTmBdtkkiqFKWJbWBSwma69XowGb1b57Oi0ed758Hzx5Q==} + '@ohos-rs/oxk-linux-x64-gnu@0.7.2': + resolution: {integrity: sha512-w8MZ664iHJtMycUQSVnnXCI7FC7SyKUuP0FrwPvnjcRNV0pWpvfwxhxxr35jQE41xG85M4KFLsQ+HN0873xWLw==} engines: {node: ^20.19.0 || >=22.18.0} cpu: [x64] os: [linux] - '@ohos-rs/oxk-linux-x64-musl@0.6.0': - resolution: {integrity: sha512-bgI4LCQtLldcJKsbZhYFhmn05TNEj88mN6Mx+t9CqpAx98Oyq5E1WXqLASkx/nhKOaJazEVlJK3Y3fb+kcwFeg==} + '@ohos-rs/oxk-linux-x64-musl@0.7.2': + resolution: {integrity: sha512-Vzqs/nvBt3Bd5VUfdAtm6/1dqcWL+2pp87XCEuuOQbJihK0rzSP5eVCYMH8ZOCPoiS7CRJDs+lX3G99RFKH2Tw==} engines: {node: ^20.19.0 || >=22.18.0} cpu: [x64] os: [linux] - '@ohos-rs/oxk-wasm32-wasi@0.6.0': - resolution: {integrity: sha512-meFC63dZJ7nOpMqSRwpetglKu6EFXSL5cJKOF/lOhvom4tE3Gqvnacr8YiGhLNjphHY3zHWEU4yxSWsvzjY3gw==} + '@ohos-rs/oxk-wasm32-wasi@0.7.2': + resolution: {integrity: sha512-vHz0Bs+Quphe8Gxr7UAwfFc6a7iI/KdqGEUrAS0cDHQRBbpyLlofOP24Fc9f0jZrcyNmjxurW23ANL5xwLbbng==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@ohos-rs/oxk-win32-arm64-msvc@0.6.0': - resolution: {integrity: sha512-vozQyY4f7zN0VXM50uXO2mKWakOFtm1K4FzegCYsekcPMwP2UH4ABpPPR8hBsR3bujku01WHxjNboeExQlrzDw==} + '@ohos-rs/oxk-win32-arm64-msvc@0.7.2': + resolution: {integrity: sha512-VxpVTAuhbC4wmoGRMyiSHhqboO6E5Uu8f2fh2OnXYWYZq9g5aePI8a2+bVPTe8I4YdMeY1YMEN2q2BAzsoMr7Q==} engines: {node: ^20.19.0 || >=22.18.0} cpu: [arm64] os: [win32] - '@ohos-rs/oxk-win32-ia32-msvc@0.6.0': - resolution: {integrity: sha512-19R57d+GvPFYVm6JReDlnoW3VimF8jI6Lm1kFzPbJ/eAVjaN6x/PNJVbx6/lUfM3mdYANzgI7dQis3SKzKJ07Q==} + '@ohos-rs/oxk-win32-ia32-msvc@0.7.2': + resolution: {integrity: sha512-AdiyR4SdDyNKJkS+Y+w1kiaHbAo5bNYwpI4olETEl6/5ccafLG7+ulkA2TiEBBsHfDjdLNDaav4/AjKAbdh2kQ==} engines: {node: ^20.19.0 || >=22.18.0} cpu: [ia32] os: [win32] - '@ohos-rs/oxk-win32-x64-msvc@0.6.0': - resolution: {integrity: sha512-u7GoAgBafROoDKspiCHt+13quzzgdNN7gzMD1dfJZsYKvAYLAxZmue41HaIhiyiIU6yo91QiLqd6EKbRiqYjTw==} + '@ohos-rs/oxk-win32-x64-msvc@0.7.2': + resolution: {integrity: sha512-pPuYRDL1zvZhHv+ySZaKbeF4dB4WSw5w8CNXujofJ98E4R735ndpS2+34L8umUR+7DF48Xmp62nlAytMd7FNEA==} engines: {node: ^20.19.0 || >=22.18.0} cpu: [x64] os: [win32] - '@ohos-rs/oxk@0.6.0': - resolution: {integrity: sha512-ERDNZru+i5f43frQxtc1tpbcZiP/hAij2zr4XSzlg1MwAKn5c+8GolHqjNHROIojI3rGLKGFI+evPzDbsdfiBA==} + '@ohos-rs/oxk@0.7.2': + resolution: {integrity: sha512-sUqnEwvYRSYlAZUxcxOxQ9TFz48DLCPvhrsJYck/o//jfjy8xMrrJ8nVXg0VAAwFqwrnlUJnEM/DD6sTi5uHtw==} engines: {node: ^20.19.0 || >=22.18.0} hasBin: true + peerDependencies: + vite-plus: '*' + peerDependenciesMeta: + vite-plus: + optional: true '@opentelemetry/semantic-conventions@1.41.1': resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} @@ -2219,10 +2248,6 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} - base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -2329,10 +2354,6 @@ packages: brace-expansion@1.1.15: resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} - brace-expansion@5.0.6: - resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} - engines: {node: 18 || 20 || >=22} - braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -2483,10 +2504,6 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - crypto-random-string@2.0.0: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} @@ -2821,10 +2838,6 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -2861,12 +2874,6 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} - glob@11.1.0: - resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} - engines: {node: 20 || >=22} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - hasBin: true - glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -3054,13 +3061,6 @@ packages: is-yarn-global@0.3.0: resolution: {integrity: sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==} - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - jackspeak@4.2.3: - resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} - engines: {node: 20 || >=22} - jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -3220,10 +3220,6 @@ packages: resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} engines: {node: '>=8'} - lru-cache@11.5.1: - resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} - engines: {node: 20 || >=22} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -3322,20 +3318,12 @@ packages: engines: {node: '>=22.0.0'} hasBin: true - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} - engines: {node: 18 || 20 || >=22} - minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass@7.1.3: - resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} - engines: {node: '>=16 || 14 >=14.17'} - mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} @@ -3447,9 +3435,6 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - package-json@6.5.0: resolution: {integrity: sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==} engines: {node: '>=8'} @@ -3478,17 +3463,9 @@ packages: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-scurry@2.0.2: - resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} - engines: {node: 18 || 20 || >=22} - path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -3746,14 +3723,6 @@ packages: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - shiki@4.3.0: resolution: {integrity: sha512-NKKjWzR6LIGL3sXBrWDw9sDS9cxx42/DkysaNqJEeOWE8Kix5gpak0bc00OfDVEO4oyXSyz8+aRaqKoBD1yo7A==} engines: {node: '>=20'} @@ -4069,11 +4038,6 @@ packages: resolution: {integrity: sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==} engines: {node: '>=6'} - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - widest-line@3.1.0: resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} engines: {node: '>=8'} @@ -4279,17 +4243,14 @@ snapshots: dependencies: '@emnapi/wasi-threads': 1.2.2 tslib: 2.8.1 - optional: true '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 - optional: true '@emnapi/wasi-threads@1.2.2': dependencies: tslib: 2.8.1 - optional: true '@esbuild-kit/core-utils@3.3.2': dependencies: @@ -4744,8 +4705,6 @@ snapshots: optionalDependencies: '@types/node': 26.0.1 - '@isaacs/cliui@9.0.0': {} - '@j178/prek-android-arm64@0.4.5': optional: true @@ -5093,7 +5052,6 @@ snapshots: '@emnapi/core': 1.11.1 '@emnapi/runtime': 1.11.1 '@tybys/wasm-util': 0.10.3 - optional: true '@napi-rs/wasm-tools-android-arm-eabi@1.0.1': optional: true @@ -5237,37 +5195,37 @@ snapshots: dependencies: '@octokit/openapi-types': 27.0.0 - '@ohos-rs/oxk-android-arm-eabi@0.6.0': + '@ohos-rs/oxk-android-arm-eabi@0.7.2': optional: true - '@ohos-rs/oxk-android-arm64@0.6.0': + '@ohos-rs/oxk-android-arm64@0.7.2': optional: true - '@ohos-rs/oxk-darwin-arm64@0.6.0': + '@ohos-rs/oxk-darwin-arm64@0.7.2': optional: true - '@ohos-rs/oxk-darwin-x64@0.6.0': + '@ohos-rs/oxk-darwin-x64@0.7.2': optional: true - '@ohos-rs/oxk-freebsd-x64@0.6.0': + '@ohos-rs/oxk-freebsd-x64@0.7.2': optional: true - '@ohos-rs/oxk-linux-arm-gnueabihf@0.6.0': + '@ohos-rs/oxk-linux-arm-gnueabihf@0.7.2': optional: true - '@ohos-rs/oxk-linux-arm64-gnu@0.6.0': + '@ohos-rs/oxk-linux-arm64-gnu@0.7.2': optional: true - '@ohos-rs/oxk-linux-arm64-musl@0.6.0': + '@ohos-rs/oxk-linux-arm64-musl@0.7.2': optional: true - '@ohos-rs/oxk-linux-x64-gnu@0.6.0': + '@ohos-rs/oxk-linux-x64-gnu@0.7.2': optional: true - '@ohos-rs/oxk-linux-x64-musl@0.6.0': + '@ohos-rs/oxk-linux-x64-musl@0.7.2': optional: true - '@ohos-rs/oxk-wasm32-wasi@0.6.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + '@ohos-rs/oxk-wasm32-wasi@0.7.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) transitivePeerDependencies: @@ -5275,34 +5233,33 @@ snapshots: - '@emnapi/runtime' optional: true - '@ohos-rs/oxk-win32-arm64-msvc@0.6.0': + '@ohos-rs/oxk-win32-arm64-msvc@0.7.2': optional: true - '@ohos-rs/oxk-win32-ia32-msvc@0.6.0': + '@ohos-rs/oxk-win32-ia32-msvc@0.7.2': optional: true - '@ohos-rs/oxk-win32-x64-msvc@0.6.0': + '@ohos-rs/oxk-win32-x64-msvc@0.7.2': optional: true - '@ohos-rs/oxk@0.6.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + '@ohos-rs/oxk@0.7.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: - glob: 11.1.0 prettier: 3.9.1 optionalDependencies: - '@ohos-rs/oxk-android-arm-eabi': 0.6.0 - '@ohos-rs/oxk-android-arm64': 0.6.0 - '@ohos-rs/oxk-darwin-arm64': 0.6.0 - '@ohos-rs/oxk-darwin-x64': 0.6.0 - '@ohos-rs/oxk-freebsd-x64': 0.6.0 - '@ohos-rs/oxk-linux-arm-gnueabihf': 0.6.0 - '@ohos-rs/oxk-linux-arm64-gnu': 0.6.0 - '@ohos-rs/oxk-linux-arm64-musl': 0.6.0 - '@ohos-rs/oxk-linux-x64-gnu': 0.6.0 - '@ohos-rs/oxk-linux-x64-musl': 0.6.0 - '@ohos-rs/oxk-wasm32-wasi': 0.6.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) - '@ohos-rs/oxk-win32-arm64-msvc': 0.6.0 - '@ohos-rs/oxk-win32-ia32-msvc': 0.6.0 - '@ohos-rs/oxk-win32-x64-msvc': 0.6.0 + '@ohos-rs/oxk-android-arm-eabi': 0.7.2 + '@ohos-rs/oxk-android-arm64': 0.7.2 + '@ohos-rs/oxk-darwin-arm64': 0.7.2 + '@ohos-rs/oxk-darwin-x64': 0.7.2 + '@ohos-rs/oxk-freebsd-x64': 0.7.2 + '@ohos-rs/oxk-linux-arm-gnueabihf': 0.7.2 + '@ohos-rs/oxk-linux-arm64-gnu': 0.7.2 + '@ohos-rs/oxk-linux-arm64-musl': 0.7.2 + '@ohos-rs/oxk-linux-x64-gnu': 0.7.2 + '@ohos-rs/oxk-linux-x64-musl': 0.7.2 + '@ohos-rs/oxk-wasm32-wasi': 0.7.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@ohos-rs/oxk-win32-arm64-msvc': 0.7.2 + '@ohos-rs/oxk-win32-ia32-msvc': 0.7.2 + '@ohos-rs/oxk-win32-x64-msvc': 0.7.2 transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -5559,7 +5516,6 @@ snapshots: '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 - optional: true '@types/hast@3.0.4': dependencies: @@ -5760,8 +5716,6 @@ snapshots: balanced-match@1.0.2: {} - balanced-match@4.0.4: {} - base64-js@1.5.1: {} before-after-hook@4.0.0: {} @@ -5844,10 +5798,6 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@5.0.6: - dependencies: - balanced-match: 4.0.4 - braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -5991,12 +5941,6 @@ snapshots: cookie@1.1.1: {} - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - crypto-random-string@2.0.0: {} csstype@3.2.3: {} @@ -6272,11 +6216,6 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - fs-constants@1.0.0: {} fs.realpath@1.0.0: {} @@ -6306,15 +6245,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@11.1.0: - dependencies: - foreground-child: 3.3.1 - jackspeak: 4.2.3 - minimatch: 10.2.5 - minipass: 7.1.3 - package-json-from-dist: 1.0.1 - path-scurry: 2.0.2 - glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -6485,12 +6415,6 @@ snapshots: is-yarn-global@0.3.0: {} - isexe@2.0.0: {} - - jackspeak@4.2.3: - dependencies: - '@isaacs/cliui': 9.0.0 - jiti@2.7.0: {} jose@6.2.3: {} @@ -6615,8 +6539,6 @@ snapshots: lowercase-keys@2.0.0: {} - lru-cache@11.5.1: {} - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -6732,18 +6654,12 @@ snapshots: - bufferutil - utf-8-validate - minimatch@10.2.5: - dependencies: - brace-expansion: 5.0.6 - minimatch@3.1.5: dependencies: brace-expansion: 1.1.15 minimist@1.2.8: {} - minipass@7.1.3: {} - mkdirp-classic@0.5.3: {} ms@2.1.3: {} @@ -6855,8 +6771,6 @@ snapshots: p-try@2.2.0: {} - package-json-from-dist@1.0.1: {} - package-json@6.5.0: dependencies: got: 9.6.0 @@ -6884,15 +6798,8 @@ snapshots: path-is-absolute@1.0.1: {} - path-key@3.1.1: {} - path-parse@1.0.7: {} - path-scurry@2.0.2: - dependencies: - lru-cache: 11.5.1 - minipass: 7.1.3 - path-to-regexp@6.3.0: {} path-type@4.0.0: {} @@ -7175,12 +7082,6 @@ snapshots: '@img/sharp-win32-ia32': 0.34.5 '@img/sharp-win32-x64': 0.34.5 - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - shiki@4.3.0: dependencies: '@shikijs/core': 4.3.0 @@ -7324,8 +7225,7 @@ snapshots: trim-off-newlines@1.0.3: {} - tslib@2.8.1: - optional: true + tslib@2.8.1: {} tsx@4.22.4: dependencies: @@ -7528,10 +7428,6 @@ snapshots: well-known-symbols@2.0.0: {} - which@2.0.2: - dependencies: - isexe: 2.0.0 - widest-line@3.1.0: dependencies: string-width: 4.2.3 diff --git a/src/build/napi-build.zig b/src/build/napi-build.zig index 3ebf64c..5e0fc26 100644 --- a/src/build/napi-build.zig +++ b/src/build/napi-build.zig @@ -206,7 +206,31 @@ fn nodeAbi(target: std.Target) ?[]const u8 { }; } +fn isWasiNodeAddonTarget(target: std.Target) bool { + return target.cpu.arch == .wasm32 and target.os.tag == .wasi; +} + +fn linkWasiEmnapi(compile: *std.Build.Step.Compile) void { + const package = compile.root_module.owner; + const lib_path = package.path("node_modules/emnapi/lib/wasm32-wasip1-threads/libemnapi-basic-napi-rs-mt.a"); + + compile.root_module.addObjectFile(lib_path); + compile.root_module.export_symbol_names = &.{ + "malloc", + "free", + "napi_register_wasm_v1", + "node_api_module_get_api_version_v1", + "emnapi_thread_crashed", + "emnapi_async_worker_create", + "emnapi_async_worker_init", + }; +} + pub fn nodePlatformArchAbi(build: *std.Build, target: std.Build.ResolvedTarget) []const u8 { + if (isWasiNodeAddonTarget(target.result)) { + return "wasm32-wasi"; + } + const platform = nodePlatform(target.result); const arch = nodeArch(target.result); if (nodeAbi(target.result)) |abi| { @@ -215,8 +239,12 @@ pub fn nodePlatformArchAbi(build: *std.Build, target: std.Build.ResolvedTarget) return build.fmt("{s}-{s}", .{ platform, arch }); } +pub fn nodeAddonExtension(target: std.Build.ResolvedTarget) []const u8 { + return if (isWasiNodeAddonTarget(target.result)) "wasm" else "node"; +} + pub fn nodeAddonFilename(build: *std.Build, name: []const u8, target: std.Build.ResolvedTarget) []const u8 { - return build.fmt("{s}.{s}.node", .{ name, nodePlatformArchAbi(build, target) }); + return build.fmt("{s}.{s}.{s}", .{ name, nodePlatformArchAbi(build, target), nodeAddonExtension(target) }); } pub const NativeAddonBuildOptionsWithModule = struct { @@ -357,14 +385,39 @@ pub fn nodeAddonBuild(build: *std.Build, option: NodeAddonBuildOptionsWithModule .node_api = option.node_api, }); const target = option.root_module_options.target orelse build.graph.host; + const is_wasi = isWasiNodeAddonTarget(target.result); var nodeOption = cloneLibraryOptionsInternal(build, option, target); nodeOption.linkage = .dynamic; - const compile = build.addLibrary(nodeOption); + const compile = if (is_wasi) compile: { + const wasm = build.addExecutable(.{ + .name = nodeOption.name, + .root_module = nodeOption.root_module, + .version = nodeOption.version, + .max_rss = nodeOption.max_rss, + .use_llvm = nodeOption.use_llvm, + .use_lld = nodeOption.use_lld, + .zig_lib_dir = nodeOption.zig_lib_dir, + .win32_manifest = nodeOption.win32_manifest, + }); + wasm.entry = .disabled; + wasm.import_symbols = true; + wasm.import_memory = true; + wasm.export_table = true; + wasm.shared_memory = true; + wasm.initial_memory = 4000 * 65536; + wasm.max_memory = 65536 * 65536; + break :compile wasm; + } else build.addLibrary(nodeOption); const build_options_module = addon_build_options.createModule(); addConfiguredNapiImport(build, compile.root_module, option.napi_module, build_options_module, true); compile.linker_allow_shlib_undefined = true; + if (is_wasi) { + compile.rdynamic = true; + compile.root_module.link_libc = true; + linkWasiEmnapi(compile); + } if (target.result.os.tag == .windows) { if (option.node_import_lib) |node_import_lib| { compile.root_module.addObjectFile(node_import_lib); diff --git a/src/napi/async.zig b/src/napi/async.zig index b82e6db..4c47806 100644 --- a/src/napi/async.zig +++ b/src/napi/async.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const napi = @import("napi-sys").napi_sys; const Env = @import("./env.zig").Env; const Promise = @import("./value/promise.zig").Promise; @@ -19,6 +20,8 @@ var threaded_runtime_cleanup_registered = false; var threaded_runtime_cleanup_requested = false; var threaded_runtime: std.Io.Threaded = undefined; +const use_wasm_emnapi_async_work = builtin.cpu.arch == .wasm32 and builtin.os.tag == .wasi; + pub const RuntimeModel = enum { single, thread, @@ -402,6 +405,7 @@ fn AsyncTaskOperation( cancel_token: CancelToken = .{}, future: ?std.Io.Future(void) = null, controller_future: ?std.Io.Future(void) = null, + async_work: napi.napi_async_work = null, tsfn_raw: napi.napi_threadsafe_function = null, state_mutex: std.Io.Mutex = .init, state_cond: std.Io.Condition = .init, @@ -457,6 +461,11 @@ fn AsyncTaskOperation( switch (effectiveRuntime(runtime)) { .single => self.runSingle(), .thread => { + if (comptime use_wasm_emnapi_async_work) { + try self.runWasmAsyncWork(); + return promise; + } + const io = try acquireThreadedRuntime(self.env); self.uses_threaded_runtime = true; try self.initThreadDispatcher(); @@ -504,7 +513,13 @@ fn AsyncTaskOperation( defer self.markTaskDone(); const task_runtime = effectiveRuntime(runtime); - const io = ioForRuntime(task_runtime); + self.runTaskWithIo(ioForRuntime(task_runtime), switch (task_runtime) { + .single => .single, + .thread => .thread, + }); + } + + fn runTaskWithIo(self: *Self, io: std.Io, effective_runtime: RuntimeModel) void { var group: std.Io.Group = .init; defer group.cancel(io); @@ -513,10 +528,7 @@ fn AsyncTaskOperation( .io = io, .group = &group, .runtime = runtime, - .effective_runtime = switch (task_runtime) { - .single => .single, - .thread => .thread, - }, + .effective_runtime = effective_runtime, .cancel_token = &self.cancel_token, .emitter_ptr = if (Event == void) null else @ptrCast(self), .emit_fn = if (Event == void) null else emitFromContext, @@ -532,6 +544,52 @@ fn AsyncTaskOperation( }; } + fn runWasmAsyncWork(self: *Self) !void { + try self.initThreadDispatcher(); + + const resource_name = String.New(Env.from_raw(self.env), "ZigAsyncTask"); + var async_work: napi.napi_async_work = null; + const create_status = napi.napi_create_async_work( + self.env, + null, + resource_name.raw, + wasmAsyncWorkExecute, + wasmAsyncWorkComplete, + @ptrCast(self), + &async_work, + ); + if (create_status != napi.napi_ok) { + return NapiError.Error.fromStatus(NapiError.Status.New(create_status)); + } + self.async_work = async_work; + + const queue_status = napi.napi_queue_async_work(self.env, async_work); + if (queue_status != napi.napi_ok) { + _ = napi.napi_delete_async_work(self.env, async_work); + self.async_work = null; + return NapiError.Error.fromStatus(NapiError.Status.New(queue_status)); + } + } + + fn wasmAsyncWorkExecute(_: napi.napi_env, data: ?*anyopaque) callconv(.c) void { + const self: *Self = @ptrCast(@alignCast(data)); + self.runTaskWithIo(singleIo(), .thread); + } + + fn wasmAsyncWorkComplete(inner_env: napi.napi_env, status: napi.napi_status, data: ?*anyopaque) callconv(.c) void { + const self: *Self = @ptrCast(@alignCast(data)); + if (status == napi.napi_cancelled) { + self.cancel_dispatched = true; + } + if (self.async_work != null) { + _ = napi.napi_delete_async_work(inner_env, self.async_work); + self.async_work = null; + } + self.queueCompletion() catch { + self.dispatchCompletion(inner_env); + }; + } + fn isAbortRequestedFromSignal(self: *Self) bool { if (self.abort_registration) |registration| { var signal_value: napi.napi_value = undefined; @@ -554,6 +612,11 @@ fn AsyncTaskOperation( defer self.state_mutex.unlock(io); if (self.task_done) return; self.cancel_requested = true; + if (comptime use_wasm_emnapi_async_work) { + if (self.async_work != null) { + _ = napi.napi_cancel_async_work(self.env, self.async_work); + } + } self.state_cond.signal(io); } @@ -779,6 +842,10 @@ fn AsyncTaskOperation( _ = napi.napi_release_threadsafe_function(self.tsfn_raw, napi.napi_tsfn_release); self.tsfn_raw = null; } + if (self.async_work != null) { + _ = napi.napi_delete_async_work(env_raw, self.async_work); + self.async_work = null; + } var deinit_state = Napi.DeinitState{}; Napi.deinit_napi_value_with_state(Input, self.input, &deinit_state); if (comptime Result != void) { diff --git a/src/napi/options.zig b/src/napi/options.zig index 37d7db8..85b7075 100644 --- a/src/napi/options.zig +++ b/src/napi/options.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const build_options = @import("build_options"); pub const NapiVersion = enum(i32) { @@ -31,6 +32,10 @@ pub fn isNodeAddon() bool { return build_options.node_addon; } +pub fn isWasmNodeAddon() bool { + return build_options.node_addon and builtin.cpu.arch == .wasm32 and builtin.os.tag == .wasi; +} + pub fn isOhosAddon() bool { return !build_options.node_addon; } diff --git a/src/napi/wrapper/arraybuffer.zig b/src/napi/wrapper/arraybuffer.zig index 1de6050..d396843 100644 --- a/src/napi/wrapper/arraybuffer.zig +++ b/src/napi/wrapper/arraybuffer.zig @@ -199,12 +199,14 @@ pub const ArrayBuffer = struct { @memcpy(dest[0..data.len], data); } - return ArrayBuffer{ + const result_buffer = ArrayBuffer{ .env = env.raw, .raw = create_status.result, .data = if (data.len == 0 or create_status.data == null) &[_]u8{} else @ptrCast(create_status.data), .len = data.len, }; + try result_buffer.flush(); + return result_buffer; } /// Create a new uninitialized ArrayBuffer with the specified length @@ -246,6 +248,17 @@ pub const ArrayBuffer = struct { return self.len; } + /// Sync wasm-side mutations back to the JavaScript ArrayBuffer when running on emnapi. + pub fn flush(self: ArrayBuffer) !void { + if (comptime !options.isWasmNodeAddon()) return; + if (self.len == 0) return; + var raw = self.raw; + const status = napi.emnapi_sync_memory(self.env, false, &raw, 0, self.len); + if (status != napi.napi_ok) { + return NapiError.Error.fromStatus(NapiError.Status.New(status)); + } + } + /// Detach the ArrayBuffer. pub fn detach(self: *ArrayBuffer) !void { comptime options.requireNapiVersion(.v7); diff --git a/src/napi/wrapper/buffer.zig b/src/napi/wrapper/buffer.zig index 169df05..bb41f09 100644 --- a/src/napi/wrapper/buffer.zig +++ b/src/napi/wrapper/buffer.zig @@ -245,6 +245,17 @@ pub const Buffer = struct { pub fn length(self: Buffer) usize { return self.len; } + + /// Sync wasm-side mutations back to the JavaScript Buffer when running on emnapi. + pub fn flush(self: Buffer) !void { + if (comptime !options.isWasmNodeAddon()) return; + if (self.len == 0) return; + var raw = self.raw; + const status = napi.emnapi_sync_memory(self.env, false, &raw, 0, self.len); + if (status != napi.napi_ok) { + return NapiError.Error.fromStatus(NapiError.Status.New(status)); + } + } }; const BufferCopyStatus = struct { diff --git a/src/napi/wrapper/dataview.zig b/src/napi/wrapper/dataview.zig index 52adac1..0a3741e 100644 --- a/src/napi/wrapper/dataview.zig +++ b/src/napi/wrapper/dataview.zig @@ -3,6 +3,7 @@ const napi = @import("napi-sys").napi_sys; const Env = @import("../env.zig").Env; const ArrayBuffer = @import("./arraybuffer.zig").ArrayBuffer; const NapiError = @import("./error.zig"); +const options = @import("../options.zig"); const Endian = std.builtin.Endian; pub const DataView = struct { @@ -75,12 +76,16 @@ pub const DataView = struct { pub fn copy(env: Env, data: []const u8) !DataView { const arraybuffer = try ArrayBuffer.copy(env, data); - return DataView.fromArrayBuffer(env, arraybuffer, 0, data.len); + const result = try DataView.fromArrayBuffer(env, arraybuffer, 0, data.len); + try result.flush(); + return result; } pub fn from(env: Env, data: []u8) !DataView { const arraybuffer = try ArrayBuffer.from(env, data); - return DataView.fromArrayBuffer(env, arraybuffer, 0, data.len); + const result = try DataView.fromArrayBuffer(env, arraybuffer, 0, data.len); + try result.flush(); + return result; } pub fn asSlice(self: DataView) []u8 { @@ -95,6 +100,23 @@ pub const DataView = struct { return self.byte_length; } + /// Sync wasm-side mutations back to the JavaScript DataView when running on emnapi. + pub fn flush(self: DataView) !void { + try self.flushRange(0, self.byte_length); + } + + /// Sync wasm-side mutations for a byte range relative to this DataView. + pub fn flushRange(self: DataView, byte_offset: usize, byte_length: usize) !void { + if (comptime !options.isWasmNodeAddon()) return; + try self.ensureRange(byte_offset, byte_length); + if (byte_length == 0) return; + var raw = self.raw; + const status = napi.emnapi_sync_memory(self.env, false, &raw, byte_offset, byte_length); + if (status != napi.napi_ok) { + return NapiError.Error.fromStatus(NapiError.Status.New(status)); + } + } + fn endianOf(little_endian: bool) Endian { return if (little_endian) .little else .big; } diff --git a/src/napi/wrapper/typedarray.zig b/src/napi/wrapper/typedarray.zig index bed73d8..737f166 100644 --- a/src/napi/wrapper/typedarray.zig +++ b/src/napi/wrapper/typedarray.zig @@ -201,6 +201,7 @@ fn TypedArrayWithRawType(comptime T: type, comptime raw_type: napi.napi_typedarr pub fn copy(env: Env, data: []const T) !Self { var result = try Self.New(env, data.len); @memcpy(result.asSlice(), data); + try result.flush(); return result; } @@ -224,6 +225,25 @@ fn TypedArrayWithRawType(comptime T: type, comptime raw_type: napi.napi_typedarr pub fn byteLength(self: Self) usize { return self.len * @sizeOf(T); } + + /// Sync wasm-side mutations back to the JavaScript TypedArray when running on emnapi. + pub fn flush(self: Self) !void { + try self.flushRange(0, self.byteLength()); + } + + /// Sync wasm-side mutations for a byte range relative to this TypedArray view. + pub fn flushRange(self: Self, byte_offset: usize, byte_length: usize) !void { + if (comptime !options.isWasmNodeAddon()) return; + if (byte_offset > self.byteLength() or byte_length > self.byteLength() - byte_offset) { + return NapiError.Error.fromStatus(NapiError.Status.InvalidArg); + } + if (byte_length == 0) return; + var raw = self.raw; + const status = napi.emnapi_sync_memory(self.env, false, &raw, byte_offset, byte_length); + if (status != napi.napi_ok) { + return NapiError.Error.fromStatus(NapiError.Status.New(status)); + } + } }; } diff --git a/src/prelude/module.zig b/src/prelude/module.zig index 6d5770a..fdb0ef3 100644 --- a/src/prelude/module.zig +++ b/src/prelude/module.zig @@ -118,7 +118,11 @@ pub fn NODE_API_MODULE_WITH_INIT( comptime { if (build_options.node_addon) { - @export(&ModuleImpl.node_init, .{ .linkage = .strong, .name = "napi_register_module_v1" }); + if (options.isWasmNodeAddon()) { + @export(&ModuleImpl.node_init, .{ .linkage = .strong, .name = "napi_register_wasm_v1" }); + } else { + @export(&ModuleImpl.node_init, .{ .linkage = .strong, .name = "napi_register_module_v1" }); + } @export(&ModuleImpl.node_api_version, .{ .linkage = .strong, .name = "node_api_module_get_api_version_v1" }); } else if (builtin.object_format == .elf) { const InitFnPtr = *const fn () callconv(.c) void; diff --git a/src/sys/node.zig b/src/sys/node.zig index f2cfe5d..cfbe6ed 100644 --- a/src/sys/node.zig +++ b/src/sys/node.zig @@ -1,130 +1,101 @@ const std = @import("std"); const builtin = @import("builtin"); - -pub const napi_env__ = opaque {}; -pub const napi_value__ = opaque {}; -pub const napi_ref__ = opaque {}; -pub const napi_handle_scope__ = opaque {}; -pub const napi_escapable_handle_scope__ = opaque {}; -pub const napi_callback_info__ = opaque {}; -pub const napi_deferred__ = opaque {}; -pub const napi_callback_scope__ = opaque {}; -pub const napi_async_context__ = opaque {}; -pub const napi_async_work__ = opaque {}; - -pub const napi_env = ?*napi_env__; -pub const node_api_basic_env = napi_env; -pub const napi_value = ?*napi_value__; -pub const napi_ref = ?*napi_ref__; -pub const napi_handle_scope = ?*napi_handle_scope__; -pub const napi_escapable_handle_scope = ?*napi_escapable_handle_scope__; -pub const napi_callback_info = ?*napi_callback_info__; -pub const napi_deferred = ?*napi_deferred__; -pub const napi_callback_scope = ?*napi_callback_scope__; -pub const napi_async_context = ?*napi_async_context__; -pub const napi_async_work = ?*napi_async_work__; - -pub const napi_property_attributes = c_int; -pub const napi_default: napi_property_attributes = 0; -pub const napi_writable: napi_property_attributes = 1 << 0; -pub const napi_enumerable: napi_property_attributes = 1 << 1; -pub const napi_configurable: napi_property_attributes = 1 << 2; -pub const napi_static: napi_property_attributes = 1 << 10; -pub const napi_default_method: napi_property_attributes = napi_writable | napi_configurable; -pub const napi_default_jsproperty: napi_property_attributes = napi_writable | napi_enumerable | napi_configurable; - -pub const napi_valuetype = c_int; -pub const napi_undefined: napi_valuetype = 0; -pub const napi_null: napi_valuetype = 1; -pub const napi_boolean: napi_valuetype = 2; -pub const napi_number: napi_valuetype = 3; -pub const napi_string: napi_valuetype = 4; -pub const napi_symbol: napi_valuetype = 5; -pub const napi_object: napi_valuetype = 6; -pub const napi_function: napi_valuetype = 7; -pub const napi_external: napi_valuetype = 8; - -pub const napi_typedarray_type = c_int; -pub const napi_int8_array: napi_typedarray_type = 0; -pub const napi_uint8_array: napi_typedarray_type = 1; -pub const napi_uint8_clamped_array: napi_typedarray_type = 2; -pub const napi_int16_array: napi_typedarray_type = 3; -pub const napi_uint16_array: napi_typedarray_type = 4; -pub const napi_int32_array: napi_typedarray_type = 5; -pub const napi_uint32_array: napi_typedarray_type = 6; -pub const napi_float32_array: napi_typedarray_type = 7; -pub const napi_float64_array: napi_typedarray_type = 8; - -pub const napi_status = c_int; -pub const napi_ok: napi_status = 0; -pub const napi_invalid_arg: napi_status = 1; -pub const napi_object_expected: napi_status = 2; -pub const napi_string_expected: napi_status = 3; -pub const napi_name_expected: napi_status = 4; -pub const napi_function_expected: napi_status = 5; -pub const napi_number_expected: napi_status = 6; -pub const napi_boolean_expected: napi_status = 7; -pub const napi_array_expected: napi_status = 8; -pub const napi_generic_failure: napi_status = 9; -pub const napi_pending_exception: napi_status = 10; -pub const napi_cancelled: napi_status = 11; -pub const napi_escape_called_twice: napi_status = 12; -pub const napi_handle_scope_mismatch: napi_status = 13; -pub const napi_callback_scope_mismatch: napi_status = 14; -pub const napi_queue_full: napi_status = 15; -pub const napi_closing: napi_status = 16; -pub const napi_bigint_expected: napi_status = 17; -pub const napi_date_expected: napi_status = 18; -pub const napi_arraybuffer_expected: napi_status = 19; -pub const napi_detachable_arraybuffer_expected: napi_status = 20; -pub const napi_would_deadlock: napi_status = 21; -pub const napi_no_external_buffers_allowed: napi_status = 22; -pub const napi_cannot_run_js: napi_status = 23; - -pub const napi_callback = ?*const fn (env: napi_env, info: napi_callback_info) callconv(.c) napi_value; -pub const napi_finalize = ?*const fn (env: napi_env, finalize_data: ?*anyopaque, finalize_hint: ?*anyopaque) callconv(.c) void; -pub const node_api_basic_finalize = napi_finalize; -pub const napi_cleanup_hook = ?*const fn (arg: ?*anyopaque) callconv(.c) void; -pub const napi_async_execute_callback = ?*const fn (env: napi_env, data: ?*anyopaque) callconv(.c) void; -pub const napi_async_complete_callback = ?*const fn (env: napi_env, status: napi_status, data: ?*anyopaque) callconv(.c) void; -pub const napi_addon_register_func = ?*const fn (env: napi_env, exports: napi_value) callconv(.c) napi_value; - -pub const napi_property_descriptor = extern struct { - utf8name: [*c]const u8, - name: napi_value, - method: napi_callback, - getter: napi_callback, - setter: napi_callback, - value: napi_value, - attributes: napi_property_attributes, - data: ?*anyopaque, -}; - -pub const napi_extended_error_info = extern struct { - error_message: [*c]const u8, - engine_reserved: ?*anyopaque, - engine_error_code: u32, - error_code: napi_status, -}; - -pub const napi_node_version = extern struct { - major: u32, - minor: u32, - patch: u32, - release: [*c]const u8, -}; - -pub const napi_module = extern struct { - nm_version: c_int, - nm_flags: c_uint, - nm_filename: [*c]const u8, - nm_register_func: napi_addon_register_func, - nm_modname: [*c]const u8, - nm_priv: ?*anyopaque, - reserved: [4]?*anyopaque, -}; +const types = @import("types.zig"); + +pub const napi_env__ = types.napi_env__; +pub const napi_value__ = types.napi_value__; +pub const napi_ref__ = types.napi_ref__; +pub const napi_handle_scope__ = types.napi_handle_scope__; +pub const napi_escapable_handle_scope__ = types.napi_escapable_handle_scope__; +pub const napi_callback_info__ = types.napi_callback_info__; +pub const napi_deferred__ = types.napi_deferred__; +pub const napi_callback_scope__ = types.napi_callback_scope__; +pub const napi_async_context__ = types.napi_async_context__; +pub const napi_async_work__ = types.napi_async_work__; + +pub const napi_env = types.napi_env; +pub const node_api_basic_env = types.node_api_basic_env; +pub const napi_value = types.napi_value; +pub const napi_ref = types.napi_ref; +pub const napi_handle_scope = types.napi_handle_scope; +pub const napi_escapable_handle_scope = types.napi_escapable_handle_scope; +pub const napi_callback_info = types.napi_callback_info; +pub const napi_deferred = types.napi_deferred; +pub const napi_callback_scope = types.napi_callback_scope; +pub const napi_async_context = types.napi_async_context; +pub const napi_async_work = types.napi_async_work; + +pub const napi_property_attributes = types.napi_property_attributes; +pub const napi_default = types.napi_default; +pub const napi_writable = types.napi_writable; +pub const napi_enumerable = types.napi_enumerable; +pub const napi_configurable = types.napi_configurable; +pub const napi_static = types.napi_static; +pub const napi_default_method = types.napi_default_method; +pub const napi_default_jsproperty = types.napi_default_jsproperty; + +pub const napi_valuetype = types.napi_valuetype; +pub const napi_undefined = types.napi_undefined; +pub const napi_null = types.napi_null; +pub const napi_boolean = types.napi_boolean; +pub const napi_number = types.napi_number; +pub const napi_string = types.napi_string; +pub const napi_symbol = types.napi_symbol; +pub const napi_object = types.napi_object; +pub const napi_function = types.napi_function; +pub const napi_external = types.napi_external; + +pub const napi_typedarray_type = types.napi_typedarray_type; +pub const napi_int8_array = types.napi_int8_array; +pub const napi_uint8_array = types.napi_uint8_array; +pub const napi_uint8_clamped_array = types.napi_uint8_clamped_array; +pub const napi_int16_array = types.napi_int16_array; +pub const napi_uint16_array = types.napi_uint16_array; +pub const napi_int32_array = types.napi_int32_array; +pub const napi_uint32_array = types.napi_uint32_array; +pub const napi_float32_array = types.napi_float32_array; +pub const napi_float64_array = types.napi_float64_array; + +pub const napi_status = types.napi_status; +pub const napi_ok = types.napi_ok; +pub const napi_invalid_arg = types.napi_invalid_arg; +pub const napi_object_expected = types.napi_object_expected; +pub const napi_string_expected = types.napi_string_expected; +pub const napi_name_expected = types.napi_name_expected; +pub const napi_function_expected = types.napi_function_expected; +pub const napi_number_expected = types.napi_number_expected; +pub const napi_boolean_expected = types.napi_boolean_expected; +pub const napi_array_expected = types.napi_array_expected; +pub const napi_generic_failure = types.napi_generic_failure; +pub const napi_pending_exception = types.napi_pending_exception; +pub const napi_cancelled = types.napi_cancelled; +pub const napi_escape_called_twice = types.napi_escape_called_twice; +pub const napi_handle_scope_mismatch = types.napi_handle_scope_mismatch; +pub const napi_callback_scope_mismatch = types.napi_callback_scope_mismatch; +pub const napi_queue_full = types.napi_queue_full; +pub const napi_closing = types.napi_closing; +pub const napi_bigint_expected = types.napi_bigint_expected; +pub const napi_date_expected = types.napi_date_expected; +pub const napi_arraybuffer_expected = types.napi_arraybuffer_expected; +pub const napi_detachable_arraybuffer_expected = types.napi_detachable_arraybuffer_expected; +pub const napi_would_deadlock = types.napi_would_deadlock; +pub const napi_no_external_buffers_allowed = types.napi_no_external_buffers_allowed; +pub const napi_cannot_run_js = types.napi_cannot_run_js; + +pub const napi_callback = types.napi_callback; +pub const napi_finalize = types.napi_finalize; +pub const node_api_basic_finalize = types.node_api_basic_finalize; +pub const napi_cleanup_hook = types.napi_cleanup_hook; +pub const napi_async_execute_callback = types.napi_async_execute_callback; +pub const napi_async_complete_callback = types.napi_async_complete_callback; +pub const napi_addon_register_func = types.napi_addon_register_func; +pub const napi_property_descriptor = types.napi_property_descriptor; +pub const napi_extended_error_info = types.napi_extended_error_info; +pub const napi_node_version = types.napi_node_version; +pub const napi_module = types.napi_module; const use_windows_msvc_dynamic_symbols = builtin.os.tag == .windows and builtin.abi == .msvc; +const wasm = @import("wasm.zig"); const WindowsMsvcLoader = struct { const windows = std.os.windows; @@ -170,6 +141,9 @@ const WindowsMsvcLoader = struct { }; pub fn setup() void { + if (wasm.enabled) { + wasm.setup(); + } if (use_windows_msvc_dynamic_symbols) { WindowsMsvcLoader.setup(); loadAllNodeApiSymbols(); @@ -364,6 +338,7 @@ fn loadAllNodeApiSymbols() void { loadNodeApi("napi_create_object_with_properties", napi_create_object_with_properties); } pub fn napi_get_last_error_info(arg0: node_api_basic_env, arg1: [*c][*c]const napi_extended_error_info) callconv(.c) napi_status { + if (wasm.enabled) return wasm.getLastErrorInfo(arg0, arg1); const Fn = *const fn (node_api_basic_env, [*c][*c]const napi_extended_error_info) callconv(.c) napi_status; return callNodeApi("napi_get_last_error_info", Fn, .{ arg0, arg1 }); } @@ -748,6 +723,7 @@ pub fn napi_adjust_external_memory(arg0: node_api_basic_env, arg1: i64, arg2: [* return callNodeApi("napi_adjust_external_memory", Fn, .{ arg0, arg1, arg2 }); } pub fn napi_module_register(arg0: [*c]napi_module) callconv(.c) void { + if (wasm.enabled) return; const Fn = *const fn ([*c]napi_module) callconv(.c) void; return callNodeApi("napi_module_register", Fn, .{arg0}); } @@ -756,10 +732,12 @@ pub fn napi_fatal_error(arg0: [*c]const u8, arg1: usize, arg2: [*c]const u8, arg return callNodeApi("napi_fatal_error", Fn, .{ arg0, arg1, arg2, arg3 }); } pub fn napi_async_init(arg0: napi_env, arg1: napi_value, arg2: napi_value, arg3: [*c]napi_async_context) callconv(.c) napi_status { + if (wasm.enabled) return wasm.asyncInit(arg0, arg1, arg2, arg3); const Fn = *const fn (napi_env, napi_value, napi_value, [*c]napi_async_context) callconv(.c) napi_status; return callNodeApi("napi_async_init", Fn, .{ arg0, arg1, arg2, arg3 }); } pub fn napi_async_destroy(arg0: napi_env, arg1: napi_async_context) callconv(.c) napi_status { + if (wasm.enabled) return wasm.asyncDestroy(arg0, arg1); const Fn = *const fn (napi_env, napi_async_context) callconv(.c) napi_status; return callNodeApi("napi_async_destroy", Fn, .{ arg0, arg1 }); } @@ -787,6 +765,9 @@ pub fn napi_get_buffer_info(arg0: napi_env, arg1: napi_value, arg2: [*c]?*anyopa const Fn = *const fn (napi_env, napi_value, [*c]?*anyopaque, [*c]usize) callconv(.c) napi_status; return callNodeApi("napi_get_buffer_info", Fn, .{ arg0, arg1, arg2, arg3 }); } +pub fn emnapi_sync_memory(arg0: napi_env, arg1: bool, arg2: [*c]napi_value, arg3: usize, arg4: usize) callconv(.c) napi_status { + return wasm.syncMemory(arg0, arg1, arg2, arg3, arg4); +} pub fn napi_create_async_work(arg0: napi_env, arg1: napi_value, arg2: napi_value, arg3: napi_async_execute_callback, arg4: napi_async_complete_callback, arg5: ?*anyopaque, arg6: [*c]napi_async_work) callconv(.c) napi_status { const Fn = *const fn (napi_env, napi_value, napi_value, napi_async_execute_callback, napi_async_complete_callback, ?*anyopaque, [*c]napi_async_work) callconv(.c) napi_status; return callNodeApi("napi_create_async_work", Fn, .{ arg0, arg1, arg2, arg3, arg4, arg5, arg6 }); @@ -804,6 +785,7 @@ pub fn napi_cancel_async_work(arg0: node_api_basic_env, arg1: napi_async_work) c return callNodeApi("napi_cancel_async_work", Fn, .{ arg0, arg1 }); } pub fn napi_get_node_version(arg0: node_api_basic_env, arg1: [*c][*c]const napi_node_version) callconv(.c) napi_status { + if (wasm.enabled) return wasm.getNodeVersion(arg0, arg1); const Fn = *const fn (node_api_basic_env, [*c][*c]const napi_node_version) callconv(.c) napi_status; return callNodeApi("napi_get_node_version", Fn, .{ arg0, arg1 }); } @@ -973,10 +955,12 @@ pub fn napi_object_seal(arg0: napi_env, arg1: napi_value) callconv(.c) napi_stat return callNodeApi("napi_object_seal", Fn, .{ arg0, arg1 }); } pub fn napi_add_async_cleanup_hook(arg0: node_api_basic_env, arg1: napi_async_cleanup_hook, arg2: ?*anyopaque, arg3: [*c]napi_async_cleanup_hook_handle) callconv(.c) napi_status { + if (wasm.enabled) return wasm.addAsyncCleanupHook(arg0, arg1, arg2, arg3); const Fn = *const fn (node_api_basic_env, napi_async_cleanup_hook, ?*anyopaque, [*c]napi_async_cleanup_hook_handle) callconv(.c) napi_status; return callNodeApi("napi_add_async_cleanup_hook", Fn, .{ arg0, arg1, arg2, arg3 }); } pub fn napi_remove_async_cleanup_hook(arg0: napi_async_cleanup_hook_handle) callconv(.c) napi_status { + if (wasm.enabled) return wasm.removeAsyncCleanupHook(arg0); const Fn = *const fn (napi_async_cleanup_hook_handle) callconv(.c) napi_status; return callNodeApi("napi_remove_async_cleanup_hook", Fn, .{arg0}); } @@ -994,6 +978,7 @@ pub fn node_api_throw_syntax_error(arg0: napi_env, arg1: [*c]const u8, arg2: [*c return callNodeApi("node_api_throw_syntax_error", Fn, .{ arg0, arg1, arg2 }); } pub fn node_api_get_module_file_name(arg0: node_api_basic_env, arg1: [*c][*c]const u8) callconv(.c) napi_status { + if (wasm.enabled) return wasm.getModuleFileName(arg0, arg1); const Fn = *const fn (node_api_basic_env, [*c][*c]const u8) callconv(.c) napi_status; return callNodeApi("node_api_get_module_file_name", Fn, .{ arg0, arg1 }); } @@ -1029,5 +1014,6 @@ pub fn node_api_post_finalizer(arg0: node_api_basic_env, arg1: napi_finalize, ar } pub fn napi_create_object_with_properties(arg0: napi_env, arg1: napi_value, arg2: [*c]const napi_value, arg3: [*c]const napi_value, arg4: usize, arg5: [*c]napi_value) callconv(.c) napi_status { const Fn = *const fn (napi_env, napi_value, [*c]const napi_value, [*c]const napi_value, usize, [*c]napi_value) callconv(.c) napi_status; + if (wasm.enabled) return wasm.callEmnapiApi("napi_create_object_with_properties", Fn, .{ arg0, arg1, arg2, arg3, arg4, arg5 }); return callNodeApi("napi_create_object_with_properties", Fn, .{ arg0, arg1, arg2, arg3, arg4, arg5 }); } diff --git a/src/sys/types.zig b/src/sys/types.zig new file mode 100644 index 0000000..57fa7bd --- /dev/null +++ b/src/sys/types.zig @@ -0,0 +1,128 @@ +pub const napi_env__ = opaque {}; +pub const napi_value__ = opaque {}; +pub const napi_ref__ = opaque {}; +pub const napi_handle_scope__ = opaque {}; +pub const napi_escapable_handle_scope__ = opaque {}; +pub const napi_callback_info__ = opaque {}; +pub const napi_deferred__ = opaque {}; +pub const napi_callback_scope__ = opaque {}; +pub const napi_async_context__ = opaque {}; +pub const napi_async_work__ = opaque {}; + +pub const napi_env = ?*napi_env__; +pub const node_api_basic_env = napi_env; +pub const napi_value = ?*napi_value__; +pub const napi_ref = ?*napi_ref__; +pub const napi_handle_scope = ?*napi_handle_scope__; +pub const napi_escapable_handle_scope = ?*napi_escapable_handle_scope__; +pub const napi_callback_info = ?*napi_callback_info__; +pub const napi_deferred = ?*napi_deferred__; +pub const napi_callback_scope = ?*napi_callback_scope__; +pub const napi_async_context = ?*napi_async_context__; +pub const napi_async_work = ?*napi_async_work__; + +pub const napi_property_attributes = c_int; +pub const napi_default: napi_property_attributes = 0; +pub const napi_writable: napi_property_attributes = 1 << 0; +pub const napi_enumerable: napi_property_attributes = 1 << 1; +pub const napi_configurable: napi_property_attributes = 1 << 2; +pub const napi_static: napi_property_attributes = 1 << 10; +pub const napi_default_method: napi_property_attributes = napi_writable | napi_configurable; +pub const napi_default_jsproperty: napi_property_attributes = napi_writable | napi_enumerable | napi_configurable; + +pub const napi_valuetype = c_int; +pub const napi_undefined: napi_valuetype = 0; +pub const napi_null: napi_valuetype = 1; +pub const napi_boolean: napi_valuetype = 2; +pub const napi_number: napi_valuetype = 3; +pub const napi_string: napi_valuetype = 4; +pub const napi_symbol: napi_valuetype = 5; +pub const napi_object: napi_valuetype = 6; +pub const napi_function: napi_valuetype = 7; +pub const napi_external: napi_valuetype = 8; + +pub const napi_typedarray_type = c_int; +pub const napi_int8_array: napi_typedarray_type = 0; +pub const napi_uint8_array: napi_typedarray_type = 1; +pub const napi_uint8_clamped_array: napi_typedarray_type = 2; +pub const napi_int16_array: napi_typedarray_type = 3; +pub const napi_uint16_array: napi_typedarray_type = 4; +pub const napi_int32_array: napi_typedarray_type = 5; +pub const napi_uint32_array: napi_typedarray_type = 6; +pub const napi_float32_array: napi_typedarray_type = 7; +pub const napi_float64_array: napi_typedarray_type = 8; + +pub const napi_status = c_int; +pub const napi_ok: napi_status = 0; +pub const napi_invalid_arg: napi_status = 1; +pub const napi_object_expected: napi_status = 2; +pub const napi_string_expected: napi_status = 3; +pub const napi_name_expected: napi_status = 4; +pub const napi_function_expected: napi_status = 5; +pub const napi_number_expected: napi_status = 6; +pub const napi_boolean_expected: napi_status = 7; +pub const napi_array_expected: napi_status = 8; +pub const napi_generic_failure: napi_status = 9; +pub const napi_pending_exception: napi_status = 10; +pub const napi_cancelled: napi_status = 11; +pub const napi_escape_called_twice: napi_status = 12; +pub const napi_handle_scope_mismatch: napi_status = 13; +pub const napi_callback_scope_mismatch: napi_status = 14; +pub const napi_queue_full: napi_status = 15; +pub const napi_closing: napi_status = 16; +pub const napi_bigint_expected: napi_status = 17; +pub const napi_date_expected: napi_status = 18; +pub const napi_arraybuffer_expected: napi_status = 19; +pub const napi_detachable_arraybuffer_expected: napi_status = 20; +pub const napi_would_deadlock: napi_status = 21; +pub const napi_no_external_buffers_allowed: napi_status = 22; +pub const napi_cannot_run_js: napi_status = 23; + +pub const napi_callback = ?*const fn (env: napi_env, info: napi_callback_info) callconv(.c) napi_value; +pub const napi_finalize = ?*const fn (env: napi_env, finalize_data: ?*anyopaque, finalize_hint: ?*anyopaque) callconv(.c) void; +pub const node_api_basic_finalize = napi_finalize; +pub const napi_cleanup_hook = ?*const fn (arg: ?*anyopaque) callconv(.c) void; +pub const napi_async_execute_callback = ?*const fn (env: napi_env, data: ?*anyopaque) callconv(.c) void; +pub const napi_async_complete_callback = ?*const fn (env: napi_env, status: napi_status, data: ?*anyopaque) callconv(.c) void; +pub const napi_addon_register_func = ?*const fn (env: napi_env, exports: napi_value) callconv(.c) napi_value; +pub const napi_async_cleanup_hook_handle__ = opaque {}; +pub const napi_async_cleanup_hook_handle = ?*napi_async_cleanup_hook_handle__; +pub const napi_async_cleanup_hook = ?*const fn (handle: napi_async_cleanup_hook_handle, data: ?*anyopaque) callconv(.c) void; + +pub extern fn napi_add_env_cleanup_hook(env: node_api_basic_env, hook: napi_cleanup_hook, data: ?*anyopaque) callconv(.c) napi_status; +pub extern fn napi_remove_env_cleanup_hook(env: node_api_basic_env, hook: napi_cleanup_hook, data: ?*anyopaque) callconv(.c) napi_status; + +pub const napi_property_descriptor = extern struct { + utf8name: [*c]const u8, + name: napi_value, + method: napi_callback, + getter: napi_callback, + setter: napi_callback, + value: napi_value, + attributes: napi_property_attributes, + data: ?*anyopaque, +}; + +pub const napi_extended_error_info = extern struct { + error_message: [*c]const u8, + engine_reserved: ?*anyopaque, + engine_error_code: u32, + error_code: napi_status, +}; + +pub const napi_node_version = extern struct { + major: u32, + minor: u32, + patch: u32, + release: [*c]const u8, +}; + +pub const napi_module = extern struct { + nm_version: c_int, + nm_flags: c_uint, + nm_filename: [*c]const u8, + nm_register_func: napi_addon_register_func, + nm_modname: [*c]const u8, + nm_priv: ?*anyopaque, + reserved: [4]?*anyopaque, +}; diff --git a/src/sys/wasm.zig b/src/sys/wasm.zig new file mode 100644 index 0000000..a5abf4b --- /dev/null +++ b/src/sys/wasm.zig @@ -0,0 +1,381 @@ +const builtin = @import("builtin"); + +const node = @import("types.zig"); + +const is_enabled = builtin.cpu.arch == .wasm32 and builtin.os.tag == .wasi; + +pub const enabled = enabled: { + _ = AsyncWorkerExports; + break :enabled is_enabled; +}; + +pub fn setup() void { + _ = AsyncWorkerExports; +} + +const node_release = "node"; + +const error_messages = [_][*c]const u8{ + null, + "Invalid argument", + "An object was expected", + "A string was expected", + "A string or symbol was expected", + "A function was expected", + "A number was expected", + "A boolean was expected", + "An array was expected", + "Unknown failure", + "An exception is pending", + "The async work item was cancelled", + "napi_escape_handle already called on scope", + "Invalid handle scope usage", + "Invalid callback scope usage", + "Thread-safe function queue is full", + "Thread-safe function handle is closing", + "A bigint was expected", + "A date was expected", + "An arraybuffer was expected", + "A detachable arraybuffer was expected", + "Main thread would deadlock", + "External buffers are not allowed", + "Cannot run JavaScript", +}; + +const unknown_error_message = "Unknown Node-API error"; + +var last_error_info = node.napi_extended_error_info{ + .error_message = null, + .engine_reserved = null, + .engine_error_code = 0, + .error_code = node.napi_ok, +}; + +var node_version = node.napi_node_version{ + .major = 0, + .minor = 0, + .patch = 0, + .release = node_release, +}; + +var module_filename: ?[*c]u8 = null; + +const AsyncContext = extern struct { + low: i32, + high: i32, +}; + +const AsyncCleanupHook = ?*const fn (?*anyopaque, AsyncCleanupDone, ?*anyopaque) callconv(.c) void; +const AsyncCleanupDone = ?*const fn (?*anyopaque) callconv(.c) void; + +const AsyncCleanupHookInfo = extern struct { + env: node.napi_env, + fun: AsyncCleanupHook, + arg: ?*anyopaque, + started: bool, +}; + +const AsyncCleanupHookHandle = extern struct { + handle: ?*AsyncCleanupHookInfo, + env: node.napi_env, + user_hook: node.napi_async_cleanup_hook, + user_data: ?*anyopaque, + done_cb: AsyncCleanupDone, + done_data: ?*anyopaque, +}; + +const AsyncWorkerArgs = extern struct { + stack_base: ?*anyopaque, + tls_base: ?*anyopaque, +}; + +const async_worker_stack_size = 2 * 1024 * 1024; + +extern fn malloc(size: usize) callconv(.c) ?*anyopaque; +extern fn calloc(count: usize, size: usize) callconv(.c) ?*anyopaque; +extern fn free(ptr: ?*anyopaque) callconv(.c) void; + +extern fn _emnapi_async_worker(arg: ?*anyopaque) callconv(.c) ?*anyopaque; +extern fn _emnapi_spawn_worker(worker: *const fn (?*anyopaque) callconv(.c) ?*anyopaque, arg: ?*anyopaque) callconv(.c) c_int; + +fn apiReturnType(comptime Fn: type) type { + return @typeInfo(@typeInfo(Fn).pointer.child).@"fn".return_type.?; +} + +pub fn callEmnapiApi(comptime name: [:0]const u8, comptime Fn: type, args: anytype) apiReturnType(Fn) { + const function = @extern(Fn, .{ .name = name }); + return @call(.auto, function, args); +} + +fn asyncWorkerCreate(directly_spawn: c_int, global_address: ?*anyopaque) callconv(.c) c_int { + // Delegate actual worker creation to @napi-rs/wasm-runtime; this only matches emnapi's C ABI. + if (directly_spawn != 0) { + const index = _emnapi_spawn_worker(_emnapi_async_worker, global_address); + if (index < 0) return 0; + return -(index + 1); + } + + const args_size = @sizeOf(AsyncWorkerArgs); + const total_size = args_size + async_worker_stack_size; + const block_ptr = calloc(1, total_size) orelse return 0; + const block_addr = @intFromPtr(block_ptr); + const args: *AsyncWorkerArgs = @ptrCast(@alignCast(block_ptr)); + args.* = .{ + .stack_base = @ptrFromInt(block_addr + total_size), + .tls_base = null, + }; + return @intCast(block_addr); +} + +const AsyncWorkerExports = if (is_enabled) struct { + export fn emnapi_async_worker_create(directly_spawn: c_int, global_address: ?*anyopaque) callconv(.c) c_int { + return asyncWorkerCreate(directly_spawn, global_address); + } +} else struct {}; + +fn setLastError(env: node.node_api_basic_env, status: node.napi_status) node.napi_status { + const Fn = *const fn (node.node_api_basic_env, node.napi_status, u32, ?*anyopaque) callconv(.c) node.napi_status; + return callEmnapiApi("napi_set_last_error", Fn, .{ env, status, 0, null }); +} + +fn clearLastError(env: node.node_api_basic_env) node.napi_status { + const Fn = *const fn (node.node_api_basic_env) callconv(.c) node.napi_status; + return callEmnapiApi("napi_clear_last_error", Fn, .{env}); +} + +fn envCheckGcAccess(env: node.napi_env) void { + const Fn = *const fn (node.napi_env) callconv(.c) void; + callEmnapiApi("_emnapi_env_check_gc_access", Fn, .{env}); +} + +pub fn getLastErrorInfo(env: node.node_api_basic_env, result: [*c][*c]const node.napi_extended_error_info) node.napi_status { + if (env == null) return node.napi_invalid_arg; + if (result == null) return setLastError(env, node.napi_invalid_arg); + + const Fn = *const fn (node.napi_env, [*c]node.napi_status, [*c]u32, [*c]?*anyopaque) callconv(.c) void; + callEmnapiApi("_emnapi_get_last_error_info", Fn, .{ env, &last_error_info.error_code, &last_error_info.engine_error_code, &last_error_info.engine_reserved }); + + if (last_error_info.error_code < error_messages.len) { + last_error_info.error_message = error_messages[@intCast(last_error_info.error_code)]; + } else { + last_error_info.error_message = unknown_error_message; + } + + if (last_error_info.error_code == node.napi_ok) { + _ = clearLastError(env); + last_error_info.engine_error_code = 0; + last_error_info.engine_reserved = null; + } + + result.* = &last_error_info; + return node.napi_ok; +} + +pub fn getNodeVersion(env: node.node_api_basic_env, version: [*c][*c]const node.napi_node_version) node.napi_status { + if (env == null) return node.napi_invalid_arg; + if (version == null) return setLastError(env, node.napi_invalid_arg); + + const Fn = *const fn ([*c]u32, [*c]u32, [*c]u32) callconv(.c) void; + callEmnapiApi("_emnapi_get_node_version", Fn, .{ &node_version.major, &node_version.minor, &node_version.patch }); + + version.* = &node_version; + return clearLastError(env); +} + +pub fn asyncInit(env: node.napi_env, async_resource: node.napi_value, async_resource_name: node.napi_value, result: [*c]node.napi_async_context) node.napi_status { + if (env == null) return node.napi_invalid_arg; + envCheckGcAccess(env); + if (async_resource_name == null) return setLastError(env, node.napi_invalid_arg); + if (result == null) return setLastError(env, node.napi_invalid_arg); + + const context_ptr = malloc(@sizeOf(AsyncContext)) orelse return setLastError(env, node.napi_generic_failure); + const context: *AsyncContext = @ptrCast(@alignCast(context_ptr)); + const async_context: node.napi_async_context = @ptrCast(context); + + const Fn = *const fn (node.napi_value, node.napi_value, node.napi_async_context) callconv(.c) node.napi_status; + const status = callEmnapiApi("_emnapi_async_init_js", Fn, .{ async_resource, async_resource_name, async_context }); + if (status != node.napi_ok) { + free(context_ptr); + return setLastError(env, status); + } + + result.* = async_context; + return clearLastError(env); +} + +pub fn asyncDestroy(env: node.napi_env, async_context: node.napi_async_context) node.napi_status { + if (env == null) return node.napi_invalid_arg; + envCheckGcAccess(env); + if (async_context == null) return setLastError(env, node.napi_invalid_arg); + + const Fn = *const fn (node.napi_async_context) callconv(.c) node.napi_status; + const status = callEmnapiApi("_emnapi_async_destroy_js", Fn, .{async_context}); + if (status != node.napi_ok) { + return setLastError(env, status); + } + + free(async_context); + return clearLastError(env); +} + +fn runtimeKeepalivePush() void { + const Fn = *const fn () callconv(.c) void; + callEmnapiApi("_emnapi_runtime_keepalive_push", Fn, .{}); +} + +fn runtimeKeepalivePop() void { + const Fn = *const fn () callconv(.c) void; + callEmnapiApi("_emnapi_runtime_keepalive_pop", Fn, .{}); +} + +fn ctxIncreaseWaitingRequestCounter() void { + const Fn = *const fn () callconv(.c) void; + callEmnapiApi("_emnapi_ctx_increase_waiting_request_counter", Fn, .{}); +} + +fn ctxDecreaseWaitingRequestCounter() void { + const Fn = *const fn () callconv(.c) void; + callEmnapiApi("_emnapi_ctx_decrease_waiting_request_counter", Fn, .{}); +} + +fn envRef(env: node.napi_env) void { + const Fn = *const fn (node.napi_env) callconv(.c) void; + callEmnapiApi("_emnapi_env_ref", Fn, .{env}); +} + +fn envUnref(env: node.napi_env) void { + const Fn = *const fn (node.napi_env) callconv(.c) void; + callEmnapiApi("_emnapi_env_unref", Fn, .{env}); +} + +fn setImmediate(callback: AsyncCleanupDone, data: ?*anyopaque) void { + const Fn = *const fn (AsyncCleanupDone, ?*anyopaque) callconv(.c) void; + callEmnapiApi("_emnapi_set_immediate", Fn, .{ callback, data }); +} + +fn finishAsyncCleanupHook(arg: ?*anyopaque) callconv(.c) void { + const info: *AsyncCleanupHookInfo = @ptrCast(@alignCast(arg.?)); + runtimeKeepalivePop(); + ctxDecreaseWaitingRequestCounter(); + free(info); +} + +fn runAsyncCleanupHook(arg: ?*anyopaque) callconv(.c) void { + const info: *AsyncCleanupHookInfo = @ptrCast(@alignCast(arg.?)); + runtimeKeepalivePush(); + ctxIncreaseWaitingRequestCounter(); + info.started = true; + info.fun.?(info.arg, finishAsyncCleanupHook, info); +} + +fn achHandleHook(data: ?*anyopaque, done_cb: AsyncCleanupDone, done_data: ?*anyopaque) callconv(.c) void { + const handle: *AsyncCleanupHookHandle = @ptrCast(@alignCast(data.?)); + handle.done_cb = done_cb; + handle.done_data = done_data; + handle.user_hook.?(@ptrCast(handle), handle.user_data); +} + +fn addAsyncEnvironmentCleanupHook(env: node.napi_env, fun: AsyncCleanupHook, arg: ?*anyopaque) ?*AsyncCleanupHookInfo { + const info_ptr = malloc(@sizeOf(AsyncCleanupHookInfo)) orelse return null; + const info: *AsyncCleanupHookInfo = @ptrCast(@alignCast(info_ptr)); + info.* = .{ + .env = env, + .fun = fun, + .arg = arg, + .started = false, + }; + + const status = node.napi_add_env_cleanup_hook(env, runAsyncCleanupHook, info); + if (status != node.napi_ok) { + free(info_ptr); + return null; + } + + return info; +} + +fn removeAsyncEnvironmentCleanupHook(info: *AsyncCleanupHookInfo) void { + if (info.started) return; + _ = node.napi_remove_env_cleanup_hook(info.env, runAsyncCleanupHook, info); +} + +fn achHandleCreate(env: node.napi_env, user_hook: node.napi_async_cleanup_hook, user_data: ?*anyopaque) ?*AsyncCleanupHookHandle { + const handle_ptr = calloc(1, @sizeOf(AsyncCleanupHookHandle)) orelse return null; + const handle: *AsyncCleanupHookHandle = @ptrCast(@alignCast(handle_ptr)); + handle.env = env; + handle.user_hook = user_hook; + handle.user_data = user_data; + handle.handle = addAsyncEnvironmentCleanupHook(env, achHandleHook, handle) orelse { + free(handle_ptr); + return null; + }; + envRef(env); + + return handle; +} + +fn achHandleEnvUnref(arg: ?*anyopaque) callconv(.c) void { + envUnref(@ptrCast(arg)); +} + +fn achHandleDelete(handle: *AsyncCleanupHookHandle) void { + if (handle.handle) |info| { + removeAsyncEnvironmentCleanupHook(info); + if (!info.started) free(info); + } + if (handle.done_cb) |done_cb| done_cb(handle.done_data); + + setImmediate(achHandleEnvUnref, handle.env); + free(handle); +} + +pub fn addAsyncCleanupHook(env: node.node_api_basic_env, hook: node.napi_async_cleanup_hook, data: ?*anyopaque, remove_handle: [*c]node.napi_async_cleanup_hook_handle) node.napi_status { + if (env == null) return node.napi_invalid_arg; + if (hook == null) return setLastError(env, node.napi_invalid_arg); + + const handle = achHandleCreate(env, hook, data) orelse return setLastError(env, node.napi_generic_failure); + if (remove_handle != null) { + remove_handle.* = @ptrCast(handle); + } + + return clearLastError(env); +} + +pub fn removeAsyncCleanupHook(remove_handle: node.napi_async_cleanup_hook_handle) node.napi_status { + const handle = remove_handle orelse return node.napi_invalid_arg; + achHandleDelete(@ptrCast(@alignCast(handle))); + return node.napi_ok; +} + +pub fn getModuleFileName(env: node.node_api_basic_env, result: [*c][*c]const u8) node.napi_status { + if (env == null) return node.napi_invalid_arg; + if (result == null) return setLastError(env, node.napi_invalid_arg); + + if (module_filename) |filename| { + free(filename); + module_filename = null; + } + + const Fn = *const fn (node.napi_env, [*c]u8, c_int) callconv(.c) c_int; + var len = callEmnapiApi("_emnapi_get_filename", Fn, .{ env, null, 0 }); + if (len == 0) { + result.* = ""; + } else { + const filename_ptr = malloc(@intCast(len + 1)) orelse return setLastError(env, node.napi_generic_failure); + const filename: [*c]u8 = @ptrCast(@alignCast(filename_ptr)); + len = callEmnapiApi("_emnapi_get_filename", Fn, .{ env, filename, len + 1 }); + filename[@intCast(len)] = 0; + module_filename = filename; + result.* = filename; + } + + return clearLastError(env); +} + +pub fn syncMemory(env: node.napi_env, js_to_wasm: bool, array: [*c]node.napi_value, byte_offset: usize, byte_length: usize) node.napi_status { + if (enabled) { + const Fn = *const fn (node.napi_env, bool, [*c]node.napi_value, usize, usize) callconv(.c) node.napi_status; + return callEmnapiApi("emnapi_sync_memory", Fn, .{ env, js_to_wasm, array, byte_offset, byte_length }); + } + return node.napi_ok; +} diff --git a/website/src/content/api/async-runtime.md b/website/src/content/api/async-runtime.md index b05ad9d..821c730 100644 --- a/website/src/content/api/async-runtime.md +++ b/website/src/content/api/async-runtime.md @@ -14,12 +14,12 @@ napi.AsyncRuntime Runtime values: -| Value | Behavior | -| --- | --- | -| `.single` | Run on the single-threaded IO runtime. | -| `.thread` | Run on the shared threaded IO runtime. | -| `.event` | Use evented IO when available, otherwise fall back through the runtime resolver. | -| `.serial` / `.threaded` / `.evented` | Backward-compatible spellings. | +| Value | Behavior | +| ------------------------------------ | -------------------------------------------------------------------------------- | +| `.single` | Run on the single-threaded IO runtime. | +| `.thread` | Run on the shared threaded IO runtime. | +| `.event` | Use evented IO when available, otherwise fall back through the runtime resolver. | +| `.serial` / `.threaded` / `.evented` | Backward-compatible spellings. | `resolveRequestedRuntime(runtime)` normalizes the backward-compatible spellings. @@ -73,14 +73,14 @@ When an exported function returns `AsyncWithEvents`, declaration generation adds Async descriptors expose: -| Method | Use | -| --- | --- | -| `from(input, run_fn)` | Create a descriptor from input data and a runner function. | -| `schedule(env)` | Schedule without listener or abort signal. | -| `scheduleWithListener(env, listener)` | Schedule with a JavaScript event listener. | -| `scheduleWithSignal(env, signal)` | Schedule with cancellation. | -| `scheduleWithListenerAndSignal(env, listener, signal)` | Schedule with both. | -| `deinit()` | Destroy an unscheduled descriptor. | +| Method | Use | +| ------------------------------------------------------ | ---------------------------------------------------------- | +| `from(input, run_fn)` | Create a descriptor from input data and a runner function. | +| `schedule(env)` | Schedule without listener or abort signal. | +| `scheduleWithListener(env, listener)` | Schedule with a JavaScript event listener. | +| `scheduleWithSignal(env, signal)` | Schedule with cancellation. | +| `scheduleWithListenerAndSignal(env, listener, signal)` | Schedule with both. | +| `deinit()` | Destroy an unscheduled descriptor. | Exported functions usually return the descriptor instead of calling `schedule` manually. The function wrapper schedules it and returns the Promise. @@ -92,13 +92,13 @@ napi.AsyncContext(comptime Event: type) Context helpers: -| Method | Use | -| --- | --- | -| `emit(event)` | Emit one event. Invalid for `AsyncContext(void)`. | -| `isCancelled()` | Read cancellation state. | -| `checkCancelled()` | Return `error.Cancelled` when cancelled. | -| `awaitGroup()` | Await the IO group. | -| `cancelGroup()` | Cancel the IO group. | +| Method | Use | +| ------------------ | ------------------------------------------------- | +| `emit(event)` | Emit one event. Invalid for `AsyncContext(void)`. | +| `isCancelled()` | Read cancellation state. | +| `checkCancelled()` | Return `error.Cancelled` when cancelled. | +| `awaitGroup()` | Await the IO group. | +| `cancelGroup()` | Cancel the IO group. | ## `CancelToken` @@ -108,11 +108,11 @@ napi.CancelToken Small cancellation primitive: -| Method | Use | -| --- | --- | -| `cancel()` | Mark cancelled. | -| `isCancelled()` | Read state. | -| `check()` | Return `error.Cancelled` when cancelled. | +| Method | Use | +| --------------- | ---------------------------------------- | +| `cancel()` | Mark cancelled. | +| `isCancelled()` | Read state. | +| `check()` | Return `error.Cancelled` when cancelled. | ## `AbortSignal` @@ -122,21 +122,21 @@ napi.AbortSignal `AbortSignal` binds JavaScript cancellation to native callbacks. -| Method | Use | -| --- | --- | -| `from_raw(env, raw)` | Wrap an existing signal value. | -| `from_napi_value(env, raw)` | Conversion hook. | -| `isAborted()` | Read the signal's `aborted` property. | -| `bind(context, callback)` | Register a native abort callback. | +| Method | Use | +| --------------------------- | ------------------------------------- | +| `from_raw(env, raw)` | Wrap an existing signal value. | +| `from_napi_value(env, raw)` | Conversion hook. | +| `isAborted()` | Read the signal's `aborted` property. | +| `bind(context, callback)` | Register a native abort callback. | `bind` returns `*AbortRegistration`. ## `AbortRegistration` -| Method | Use | -| --- | --- | -| `requestAbort()` | Invoke the registered native callback. | -| `release()` | Remove the registration and delete the signal reference. | +| Method | Use | +| ---------------- | -------------------------------------------------------- | +| `requestAbort()` | Invoke the registered native callback. | +| `release()` | Remove the registration and delete the signal reference. | `Promise.RejectAbortError()` and async cancellation use the same `AbortError` shape. @@ -150,11 +150,11 @@ napi.Worker(env: napi.Env, data: anytype) `Execute` accepts `(data)` or `(napi.Env, data)` and may return `T`, `!T`, `napi.Result(T)`, or `!napi.Result(T)`. `OnComplete` accepts `(data)` or `(napi.Env, data)`. -| Method | Use | -| --- | --- | -| `Queue()` | Queue work without returning a Promise. | +| Method | Use | +| -------------- | --------------------------------------- | +| `Queue()` | Queue work without returning a Promise. | | `AsyncQueue()` | Queue work and return a `napi.Promise`. | -| `Cancel()` | Cancel the async work. | -| `deinit()` | Delete work and destroy the wrapper. | +| `Cancel()` | Cancel the async work. | +| `deinit()` | Delete work and destroy the wrapper. | Async wrappers, workers, and `ThreadSafeFunction` require Node-API v4 or newer. diff --git a/website/src/content/api/binary-data.md b/website/src/content/api/binary-data.md index 7a93f3b..9059674 100644 --- a/website/src/content/api/binary-data.md +++ b/website/src/content/api/binary-data.md @@ -14,21 +14,21 @@ napi.Buffer Use `Buffer` for Node-compatible binary data. -| Constructor | Behavior | -| --- | --- | -| `Buffer.New(env, len)` | Allocate a new mutable buffer. | -| `Buffer.copy(env, data)` | Copy bytes into a new buffer. | -| `Buffer.from(env, data)` | Wrap mutable data and transfer ownership to JavaScript. | +| Constructor | Behavior | +| -------------------------------------------------- | ----------------------------------------------------------------- | +| `Buffer.New(env, len)` | Allocate a new mutable buffer. | +| `Buffer.copy(env, data)` | Copy bytes into a new buffer. | +| `Buffer.from(env, data)` | Wrap mutable data and transfer ownership to JavaScript. | | `Buffer.fromWithFinalizer(env, data, on_finalize)` | Wrap mutable data and run a callback when JavaScript releases it. | -| `Buffer.from_raw(env, raw)` | Wrap an existing `napi_value`. | +| `Buffer.from_raw(env, raw)` | Wrap an existing `napi_value`. | Read the memory with: -| Method | Use | -| --- | --- | -| `asSlice()` | Mutable `[]u8`. | +| Method | Use | +| ---------------- | ----------------------- | +| `asSlice()` | Mutable `[]u8`. | | `asConstSlice()` | Immutable `[]const u8`. | -| `length()` | Byte length. | +| `length()` | Byte length. | When external buffers are not allowed by the runtime, creation falls back to copied buffers where the implementation can safely do so. @@ -40,20 +40,20 @@ napi.ArrayBuffer `ArrayBuffer` mirrors the buffer API for JavaScript `ArrayBuffer` values. -| Constructor | Behavior | -| --- | --- | -| `ArrayBuffer.New(env, len)` | Allocate a new ArrayBuffer. | -| `ArrayBuffer.copy(env, data)` | Copy bytes into a new ArrayBuffer. | -| `ArrayBuffer.from(env, data)` | Wrap mutable data and transfer ownership to JavaScript. | -| `ArrayBuffer.fromWithFinalizer(env, data, on_finalize)` | Wrap mutable data and run a callback when released. | -| `ArrayBuffer.from_raw(env, raw)` | Wrap an existing `napi_value`. | - -| Method | Use | -| --- | --- | -| `asSlice()` / `asConstSlice()` | Access bytes. | -| `length()` | Byte length. | -| `detach()` | Detach the ArrayBuffer. Requires Node-API v7. | -| `isDetached()` | Check whether it is detached. Requires Node-API v7. | +| Constructor | Behavior | +| ------------------------------------------------------- | ------------------------------------------------------- | +| `ArrayBuffer.New(env, len)` | Allocate a new ArrayBuffer. | +| `ArrayBuffer.copy(env, data)` | Copy bytes into a new ArrayBuffer. | +| `ArrayBuffer.from(env, data)` | Wrap mutable data and transfer ownership to JavaScript. | +| `ArrayBuffer.fromWithFinalizer(env, data, on_finalize)` | Wrap mutable data and run a callback when released. | +| `ArrayBuffer.from_raw(env, raw)` | Wrap an existing `napi_value`. | + +| Method | Use | +| ------------------------------ | --------------------------------------------------- | +| `asSlice()` / `asConstSlice()` | Access bytes. | +| `length()` | Byte length. | +| `detach()` | Detach the ArrayBuffer. Requires Node-API v7. | +| `isDetached()` | Check whether it is detached. Requires Node-API v7. | ## `TypedArray` @@ -63,31 +63,31 @@ napi.TypedArray(T) Typed arrays can be created from new memory, copied memory, external memory, or a view into an existing `ArrayBuffer`. -| Constructor | Behavior | -| --- | --- | -| `TypedArray(T).New(env, len)` | Allocate an ArrayBuffer and create the view. | -| `TypedArray(T).copy(env, data)` | Copy numeric data into a new view. | -| `TypedArray(T).from(env, data)` | Wrap mutable numeric data through an external ArrayBuffer. | -| `TypedArray(T).fromArrayBuffer(env, arraybuffer, len, byte_offset)` | Create a view over an existing ArrayBuffer. | -| `TypedArray(T).from_raw(env, raw)` | Wrap an existing TypedArray. | +| Constructor | Behavior | +| ------------------------------------------------------------------- | ---------------------------------------------------------- | +| `TypedArray(T).New(env, len)` | Allocate an ArrayBuffer and create the view. | +| `TypedArray(T).copy(env, data)` | Copy numeric data into a new view. | +| `TypedArray(T).from(env, data)` | Wrap mutable numeric data through an external ArrayBuffer. | +| `TypedArray(T).fromArrayBuffer(env, arraybuffer, len, byte_offset)` | Create a view over an existing ArrayBuffer. | +| `TypedArray(T).from_raw(env, raw)` | Wrap an existing TypedArray. | -| Method | Use | -| --- | --- | +| Method | Use | +| ------------------------------ | ---------------------- | | `asSlice()` / `asConstSlice()` | Access typed elements. | -| `length()` | Element length. | -| `byteLength()` | Byte length. | +| `length()` | Element length. | +| `byteLength()` | Byte length. | Aliases are exported for common element types: -| Alias | Element | -| --- | --- | -| `Int8Array` | `i8` | -| `Uint8Array` | `u8` | -| `Uint8ClampedArray` | `u8` with clamped raw type | -| `Int16Array` / `Uint16Array` | `i16` / `u16` | -| `Int32Array` / `Uint32Array` | `i32` / `u32` | -| `Float32Array` / `Float64Array` | `f32` / `f64` | -| `BigInt64Array` / `BigUint64Array` | `i64` / `u64` | +| Alias | Element | +| ---------------------------------- | -------------------------- | +| `Int8Array` | `i8` | +| `Uint8Array` | `u8` | +| `Uint8ClampedArray` | `u8` with clamped raw type | +| `Int16Array` / `Uint16Array` | `i16` / `u16` | +| `Int32Array` / `Uint32Array` | `i32` / `u32` | +| `Float32Array` / `Float64Array` | `f32` / `f64` | +| `BigInt64Array` / `BigUint64Array` | `i64` / `u64` | BigInt typed arrays require Node-API v6 or newer. @@ -99,25 +99,25 @@ napi.DataView `DataView` supports byte-level access and explicit endianness. -| Constructor | Behavior | -| --- | --- | -| `DataView.New(env, byte_length)` | Allocate a new ArrayBuffer and view. | -| `DataView.copy(env, data)` | Copy bytes into a new view. | -| `DataView.from(env, data)` | Wrap mutable bytes through an external ArrayBuffer. | -| `DataView.fromArrayBuffer(env, arraybuffer, byte_offset, byte_length)` | Create a view over an existing ArrayBuffer. | -| `DataView.from_raw(env, raw)` | Wrap an existing DataView. | - -| Method | Use | -| --- | --- | -| `asSlice()` / `asConstSlice()` | Access bytes. | -| `byteLength()` | View byte length. | -| `readInt(T, offset, little_endian)` / `writeInt(T, offset, value, little_endian)` | Generic integer access. | +| Constructor | Behavior | +| ---------------------------------------------------------------------- | --------------------------------------------------- | +| `DataView.New(env, byte_length)` | Allocate a new ArrayBuffer and view. | +| `DataView.copy(env, data)` | Copy bytes into a new view. | +| `DataView.from(env, data)` | Wrap mutable bytes through an external ArrayBuffer. | +| `DataView.fromArrayBuffer(env, arraybuffer, byte_offset, byte_length)` | Create a view over an existing ArrayBuffer. | +| `DataView.from_raw(env, raw)` | Wrap an existing DataView. | + +| Method | Use | +| ------------------------------------------------------------------------------------- | ------------------------------ | +| `asSlice()` / `asConstSlice()` | Access bytes. | +| `byteLength()` | View byte length. | +| `readInt(T, offset, little_endian)` / `writeInt(T, offset, value, little_endian)` | Generic integer access. | | `readFloat(T, offset, little_endian)` / `writeFloat(T, offset, value, little_endian)` | Generic floating-point access. | -| `getInt8` / `getUint8` | 8-bit reads. | -| `getInt16` / `getUint16` / `getInt32` / `getUint32` | Endian-aware integer reads. | -| `getBigInt64` / `getBigUint64` | 64-bit integer reads. | -| `getFloat32` / `getFloat64` | Endian-aware float reads. | -| `setInt8` / `setUint8` | 8-bit writes. | -| `setInt16` / `setUint16` / `setInt32` / `setUint32` | Endian-aware integer writes. | -| `setBigInt64` / `setBigUint64` | 64-bit integer writes. | -| `setFloat32` / `setFloat64` | Endian-aware float writes. | +| `getInt8` / `getUint8` | 8-bit reads. | +| `getInt16` / `getUint16` / `getInt32` / `getUint32` | Endian-aware integer reads. | +| `getBigInt64` / `getBigUint64` | 64-bit integer reads. | +| `getFloat32` / `getFloat64` | Endian-aware float reads. | +| `setInt8` / `setUint8` | 8-bit writes. | +| `setInt16` / `setUint16` / `setInt32` / `setUint32` | Endian-aware integer writes. | +| `setBigInt64` / `setBigUint64` | 64-bit integer writes. | +| `setFloat32` / `setFloat64` | Endian-aware float writes. | diff --git a/website/src/content/api/build-node.md b/website/src/content/api/build-node.md index 3d0a2d6..8bf88cb 100644 --- a/website/src/content/api/build-node.md +++ b/website/src/content/api/build-node.md @@ -42,19 +42,29 @@ Node addon builds use platform-specific filenames: hello.darwin-arm64.node hello.linux-x64-gnu.node hello.win32-x64-msvc.node +hello.wasm32-wasi.wasm ``` The helper exposes these utilities: -| Helper | Description | -| --- | --- | -| `nodePlatformArchAbi(build, target)` | Returns the platform-arch-abi suffix. | -| `nodeAddonFilename(build, name, target)` | Returns the final `.node` filename. | +| Helper | Description | +| ---------------------------------------- | ------------------------------------------------------ | +| `nodePlatformArchAbi(build, target)` | Returns the platform-arch-abi suffix. | +| `nodeAddonExtension(target)` | Returns `node` for native targets and `wasm` for WASI. | +| `nodeAddonFilename(build, name, target)` | Returns the final `.node` or `.wasm` filename. | ## Target Selection If `root_module_options.target` is omitted, the Node addon target defaults to the host target. Provide a target explicitly for cross builds. +For WASI threads builds, use the napi-rs target name through the CLI: + +```bash +zig-napi build --target wasm32-wasip1-threads +``` + +The CLI maps that target to Zig's `wasm32-wasi` spelling with atomics/shared-memory CPU features. The helper normalizes the output to napi-rs' `wasm32-wasi` platform package name and installs `.wasm32-wasi.wasm`. + ## Windows Linking On Windows MSVC, Node-API symbols are resolved from the current Node.js process at runtime by default. If a build needs an import library, pass `.node_import_lib`, set `NODE_LIB_FILE`, or set `NODE_LIB_DIR`. @@ -65,18 +75,18 @@ Windows GNU builds search `LIBNODE_PATH`, then `LIBPATH`, then `PATH` for `libno `NodeAddonBuildOptionsWithModule` accepts: -| Field | Use | -| --- | --- | -| `name` | Base addon name. | -| `napi_module` | `zig-napi` module imported into the addon root. | -| `root_module_options` | Source file, target, optimize mode, imports, and Zig module options. | -| `node_api` | Node-API version and experimental mode. | -| `node_import_lib` | Optional Windows import library override. | -| `version` | Optional semantic version. | -| `max_rss` | Build step memory limit. | -| `use_llvm` / `use_lld` | Override Zig backend/linker selection. | -| `zig_lib_dir` | Optional Zig lib directory. | -| `win32_manifest` | Optional Windows manifest. | +| Field | Use | +| ---------------------- | -------------------------------------------------------------------- | +| `name` | Base addon name. | +| `napi_module` | `zig-napi` module imported into the addon root. | +| `root_module_options` | Source file, target, optimize mode, imports, and Zig module options. | +| `node_api` | Node-API version and experimental mode. | +| `node_import_lib` | Optional Windows import library override. | +| `version` | Optional semantic version. | +| `max_rss` | Build step memory limit. | +| `use_llvm` / `use_lld` | Override Zig backend/linker selection. | +| `zig_lib_dir` | Optional Zig lib directory. | +| `win32_manifest` | Optional Windows manifest. | The helper injects `build_options` into the addon root and configures `@import("napi")` with the selected Node-API version. diff --git a/website/src/content/api/build-openharmony.md b/website/src/content/api/build-openharmony.md index 205a497..3e1943e 100644 --- a/website/src/content/api/build-openharmony.md +++ b/website/src/content/api/build-openharmony.md @@ -44,11 +44,11 @@ pub fn build(b: *std.Build) !void { The helper knows the supported OpenHarmony target triples: -| Output slot | Target | -| --- | --- | -| `arm64` | `aarch64-linux-ohos` | -| `arm` | `arm-linux-ohoseabi` | -| `x64` | `x86_64-linux-ohos` | +| Output slot | Target | +| ----------- | -------------------- | +| `arm64` | `aarch64-linux-ohos` | +| `arm` | `arm-linux-ohoseabi` | +| `x64` | `x86_64-linux-ohos` | `NativeAddonBuildResult` stores the compile step for each enabled target. @@ -58,17 +58,17 @@ If `root_module_options.target` is an OpenHarmony target, the helper builds only `NativeAddonBuildOptionsWithModule` accepts: -| Field | Use | -| --- | --- | -| `name` | Shared library name. | -| `napi_module` | Optional `zig-napi` module import. Required when `.node_api` is customized. | -| `node_api` | Node-API version and experimental mode passed into the wrapper module. | -| `root_module_options` | Source file, target, optimize mode, imports, libc/cpp flags, and other Zig module options. | -| `version` | Optional semantic version for the shared library. | -| `max_rss` | Build step memory limit. | -| `use_llvm` / `use_lld` | Override Zig backend/linker selection. | -| `zig_lib_dir` | Optional Zig lib directory. | -| `win32_manifest` | Passed through for API parity with library options. | +| Field | Use | +| ---------------------- | ------------------------------------------------------------------------------------------ | +| `name` | Shared library name. | +| `napi_module` | Optional `zig-napi` module import. Required when `.node_api` is customized. | +| `node_api` | Node-API version and experimental mode passed into the wrapper module. | +| `root_module_options` | Source file, target, optimize mode, imports, libc/cpp flags, and other Zig module options. | +| `version` | Optional semantic version for the shared library. | +| `max_rss` | Build step memory limit. | +| `use_llvm` / `use_lld` | Override Zig backend/linker selection. | +| `zig_lib_dir` | Optional Zig lib directory. | +| `win32_manifest` | Passed through for API parity with library options. | The helper clones the provided root module options for each target and injects configured `build_options` plus the configured `napi` import. @@ -100,7 +100,7 @@ Passing `-Darkvm-test=true` builds a host Linux x64 artifact under `zig-out/arkv ## Helper Functions -| Helper | Use | -| --- | --- | -| `resolveNdkPath(build)` | Returns `$OHOS_NDK_HOME/native`, `ohos_sdk_native`, or an empty string. | -| `cloneLibraryOptions(build, option, target)` | Reuses addon library options for a resolved target. | +| Helper | Use | +| -------------------------------------------- | ----------------------------------------------------------------------- | +| `resolveNdkPath(build)` | Returns `$OHOS_NDK_HOME/native`, `ohos_sdk_native`, or an empty string. | +| `cloneLibraryOptions(build, option, target)` | Reuses addon library options for a resolved target. | diff --git a/website/src/content/api/callback-functions.md b/website/src/content/api/callback-functions.md index b198cb0..8e1cdc3 100644 --- a/website/src/content/api/callback-functions.md +++ b/website/src/content/api/callback-functions.md @@ -22,12 +22,12 @@ pub fn call(callback: Callback) !i32 { } ``` -| Method | Use | -| --- | --- | -| `from_raw(env, raw)` | Wrap an existing JavaScript function. | -| `New(env, name, value)` | Create a JavaScript function from a Zig function. | -| `Call(args)` | Call the JavaScript function and convert the result. | -| `CreateRef()` | Create `Reference(Function(Args, Return))`. | +| Method | Use | +| ----------------------- | ---------------------------------------------------- | +| `from_raw(env, raw)` | Wrap an existing JavaScript function. | +| `New(env, name, value)` | Create a JavaScript function from a Zig function. | +| `Call(args)` | Call the JavaScript function and convert the result. | +| `CreateRef()` | Create `Reference(Function(Args, Return))`. | `Args` may be a tuple type for multiple arguments, a non-tuple type for one argument, or an empty struct for no arguments. @@ -61,16 +61,16 @@ napi.CallbackInfo `CallbackInfo` is the low-level callback context. It reads callback arguments with an inline buffer for up to eight arguments and heap storage for larger calls. -| Method | Use | -| --- | --- | -| `from_raw(env, info)` | Read callback information. | -| `deinit()` | Free heap-backed argument storage. | -| `Env()` | Return `napi.Env`. | -| `Get(index)` | Return argument as `NapiValue`. | -| `Len()` | Argument count. | -| `ArgsRaw()` | Raw `napi_value` slice. | -| `ArgRaw(index)` | One raw argument. | -| `This()` | Raw `this` value. | +| Method | Use | +| --------------------- | ---------------------------------- | +| `from_raw(env, info)` | Read callback information. | +| `deinit()` | Free heap-backed argument storage. | +| `Env()` | Return `napi.Env`. | +| `Get(index)` | Return argument as `NapiValue`. | +| `Len()` | Argument count. | +| `ArgsRaw()` | Raw `napi_value` slice. | +| `ArgRaw(index)` | One raw argument. | +| `This()` | Raw `this` value. | Most exported functions should use typed Zig parameters instead. `CallbackInfo` is useful for variadic or dynamic APIs. @@ -89,16 +89,16 @@ Use `ThreadSafeFunction` to call a JavaScript function from native threads. It r `ThreadSafeFunctionCalleeHandled = true` makes the JavaScript callback receive an error-first argument: `(err, ...args) => void`. -| Method | Use | -| --- | --- | +| Method | Use | +| -------------------- | ----------------------------------------- | | `from_raw(env, raw)` | Create a TSFN from a JavaScript function. | -| `acquire()` | Increment active thread usage. | -| `release(mode)` | Release usage or abort release. | -| `abort()` | Stop future calls. | -| `ref()` / `unref()` | Control event-loop lifetime. | -| `Ok(args, mode)` | Send a successful call. | -| `Err(error, mode)` | Send an error call. | -| `deinit()` | Destroy the wrapper allocation. | +| `acquire()` | Increment active thread usage. | +| `release(mode)` | Release usage or abort release. | +| `abort()` | Stop future calls. | +| `ref()` / `unref()` | Control event-loop lifetime. | +| `Ok(args, mode)` | Send a successful call. | +| `Err(error, mode)` | Send an error call. | +| `deinit()` | Destroy the wrapper allocation. | ## TSFN Modes diff --git a/website/src/content/api/classes-ownership.md b/website/src/content/api/classes-ownership.md index eb452e5..ba37992 100644 --- a/website/src/content/api/classes-ownership.md +++ b/website/src/content/api/classes-ownership.md @@ -16,17 +16,17 @@ Exports a Zig struct type as a JavaScript class with constructor initialization `Class(T)` supports: -| Zig declaration | JavaScript class member | -| --- | --- | -| struct fields | instance properties with getter and setter | -| `pub fn init(...) T` or `!T` | constructor body | -| no `init` | constructor parameters are the struct fields | -| `pub fn method(self: *T, ...)` | instance method | -| `pub fn method(self: T, ...)` | instance method with value receiver | -| `pub fn staticMethod(...)` | static method | -| static factory returning `T` or `*T` | static factory returning a class instance | -| `pub const value = ...` | static readonly value | -| `pub fn deinit(self: *T)` | called when the wrapped instance is finalized | +| Zig declaration | JavaScript class member | +| ------------------------------------ | --------------------------------------------- | +| struct fields | instance properties with getter and setter | +| `pub fn init(...) T` or `!T` | constructor body | +| no `init` | constructor parameters are the struct fields | +| `pub fn method(self: *T, ...)` | instance method | +| `pub fn method(self: T, ...)` | instance method with value receiver | +| `pub fn staticMethod(...)` | static method | +| static factory returning `T` or `*T` | static factory returning a class instance | +| `pub const value = ...` | static readonly value | +| `pub fn deinit(self: *T)` | called when the wrapped instance is finalized | ```zig const Counter = struct { @@ -64,15 +64,15 @@ napi.Ref(comptime T: type) References keep JavaScript values alive across calls. `Ref` is an alias for `Reference`. -| Method | Use | -| --- | --- | -| `New(env, value)` | Create a reference. | -| `from_napi_value(env, raw)` | Convert a JavaScript value into a reference. | -| `to_napi_value(env)` | Get the referenced value as raw `napi_value`. | -| `get_value(env)` / `GetValue(env)` | Get the referenced wrapper value. | -| `Ref(env)` | Increase the reference count and return the count. | -| `Unref(env)` | Unref and delete the reference. | -| `Delete(env)` | Alias for `Unref`. | +| Method | Use | +| ---------------------------------- | -------------------------------------------------- | +| `New(env, value)` | Create a reference. | +| `from_napi_value(env, raw)` | Convert a JavaScript value into a reference. | +| `to_napi_value(env)` | Get the referenced value as raw `napi_value`. | +| `get_value(env)` / `GetValue(env)` | Get the referenced wrapper value. | +| `Ref(env)` | Increase the reference count and return the count. | +| `Unref(env)` | Unref and delete the reference. | +| `Delete(env)` | Alias for `Unref`. | ## `FunctionRef` And `ObjectRef` @@ -93,19 +93,19 @@ napi.External(comptime T: type) Wraps a native payload in a JavaScript external value. The wrapper tags the external with the Zig type name, so `External(A)` does not match `External(B)`. -| Method | Use | -| --- | --- | -| `New(payload)` / `new(payload)` | Create a detached external wrapper. | -| `NewWithSizeHint(payload, size_hint)` | Create with memory pressure accounting. | -| `newWithSizeHint(payload, size_hint)` | Lowercase alias. | -| `from_raw(env, raw)` / `from_napi_value(env, raw)` | Read and validate an external value. | -| `matches_napi_value(env, raw)` | Check whether a raw value is a zig-napi external of type `T`. | -| `to_napi_value(env)` | Materialize the JavaScript external. | -| `value()` / `asConstPtr()` | Immutable payload pointer. | -| `valueMut()` / `asPtr()` | Mutable payload pointer. | -| `sizeHint()` | Declared size hint. | -| `adjustedSize()` | Last adjusted external memory value. | -| `deinit()` / `Deinit()` | Destroy a detached external before it is materialized. | +| Method | Use | +| -------------------------------------------------- | ------------------------------------------------------------- | +| `New(payload)` / `new(payload)` | Create a detached external wrapper. | +| `NewWithSizeHint(payload, size_hint)` | Create with memory pressure accounting. | +| `newWithSizeHint(payload, size_hint)` | Lowercase alias. | +| `from_raw(env, raw)` / `from_napi_value(env, raw)` | Read and validate an external value. | +| `matches_napi_value(env, raw)` | Check whether a raw value is a zig-napi external of type `T`. | +| `to_napi_value(env)` | Materialize the JavaScript external. | +| `value()` / `asConstPtr()` | Immutable payload pointer. | +| `valueMut()` / `asPtr()` | Mutable payload pointer. | +| `sizeHint()` | Declared size hint. | +| `adjustedSize()` | Last adjusted external memory value. | +| `deinit()` / `Deinit()` | Destroy a detached external before it is materialized. | If `T` has a `deinit` method, the conversion layer calls it when the external payload is destroyed. diff --git a/website/src/content/api/conversion-model.md b/website/src/content/api/conversion-model.md index d6740ae..195100f 100644 --- a/website/src/content/api/conversion-model.md +++ b/website/src/content/api/conversion-model.md @@ -21,29 +21,29 @@ All other parameters are read from JavaScript arguments in order. Missing argume ## Supported Input Shapes -| Zig type | JavaScript input | -| --- | --- | -| `bool` | boolean | -| integer and float types | number | -| `napi.BigInt` | bigint | -| `[]u8`, `[]const u8`, `[N]u8` | UTF-8 string input | -| `[]u16`, `[]const u16`, `[N]u16` | UTF-16 string input | -| `?T` | `T`, `null`, or `undefined` | -| struct | object with matching fields | -| tuple struct | array with positional entries | -| array, slice, `std.ArrayList(T)` | JavaScript Array, or numeric TypedArray when `T` is supported | -| enum | numeric enum value | -| enum with `pub const napi_string_enum = true` | string enum name | -| union(enum) | first matching union field | -| `napi.NapiValue` | raw JavaScript value | -| `napi.Function`, `*napi.Function`, `napi.FunctionRef` | JavaScript function | -| `napi.ThreadSafeFunction` pointer | JavaScript function promoted to a TSFN | -| `napi.Buffer` | Node Buffer | -| `napi.ArrayBuffer` | ArrayBuffer | -| `napi.TypedArray(T)` and aliases | matching TypedArray | -| `napi.DataView` | DataView | -| `napi.External(T)` | zig-napi external value tagged with `T` | -| `napi.AbortSignal` | AbortSignal-like object | +| Zig type | JavaScript input | +| ----------------------------------------------------- | ------------------------------------------------------------- | +| `bool` | boolean | +| integer and float types | number | +| `napi.BigInt` | bigint | +| `[]u8`, `[]const u8`, `[N]u8` | UTF-8 string input | +| `[]u16`, `[]const u16`, `[N]u16` | UTF-16 string input | +| `?T` | `T`, `null`, or `undefined` | +| struct | object with matching fields | +| tuple struct | array with positional entries | +| array, slice, `std.ArrayList(T)` | JavaScript Array, or numeric TypedArray when `T` is supported | +| enum | numeric enum value | +| enum with `pub const napi_string_enum = true` | string enum name | +| union(enum) | first matching union field | +| `napi.NapiValue` | raw JavaScript value | +| `napi.Function`, `*napi.Function`, `napi.FunctionRef` | JavaScript function | +| `napi.ThreadSafeFunction` pointer | JavaScript function promoted to a TSFN | +| `napi.Buffer` | Node Buffer | +| `napi.ArrayBuffer` | ArrayBuffer | +| `napi.TypedArray(T)` and aliases | matching TypedArray | +| `napi.DataView` | DataView | +| `napi.External(T)` | zig-napi external value tagged with `T` | +| `napi.AbortSignal` | AbortSignal-like object | Use `napi.Buffer` or `napi.ArrayBuffer` when the JavaScript input should be treated as binary data instead of a string. @@ -63,24 +63,24 @@ stay in the generated conversion layer. The return conversion supports ordinary Zig values and wrapper values. -| Zig return | JavaScript output | -| --- | --- | -| `void` | `undefined` | -| `bool`, numbers, strings | primitive JavaScript values | -| `i128` / `u128` | bigint | -| `?T` | `T` or `undefined` | -| struct | object | -| tuple, array, slice, `std.ArrayList(T)` | array | -| enum | numeric or string enum value | -| union(enum) | payload of the active field | -| `napi.Result(T)` | payload or thrown JavaScript error | -| Zig error union `!T` | payload or thrown JavaScript error | -| `napi.Async(T, runtime)` | `Promise` | -| `napi.AsyncWithEvents(T, Event, runtime)` | `Promise` plus optional event callback | -| `napi.Promise` | Promise | -| `napi.Function` | JavaScript function | -| `napi.Class(T)` / `napi.ClassWithoutInit(T)` | JavaScript class constructor | -| `napi.External(T)` | branded external object | +| Zig return | JavaScript output | +| -------------------------------------------- | ----------------------------------------- | +| `void` | `undefined` | +| `bool`, numbers, strings | primitive JavaScript values | +| `i128` / `u128` | bigint | +| `?T` | `T` or `undefined` | +| struct | object | +| tuple, array, slice, `std.ArrayList(T)` | array | +| enum | numeric or string enum value | +| union(enum) | payload of the active field | +| `napi.Result(T)` | payload or thrown JavaScript error | +| Zig error union `!T` | payload or thrown JavaScript error | +| `napi.Async(T, runtime)` | `Promise` | +| `napi.AsyncWithEvents(T, Event, runtime)` | `Promise` plus optional event callback | +| `napi.Promise` | Promise | +| `napi.Function` | JavaScript function | +| `napi.Class(T)` / `napi.ClassWithoutInit(T)` | JavaScript class constructor | +| `napi.External(T)` | branded external object | ## Allocation And Cleanup diff --git a/website/src/content/api/declaration-generation.md b/website/src/content/api/declaration-generation.md index 90ed3d2..a249ae7 100644 --- a/website/src/content/api/declaration-generation.md +++ b/website/src/content/api/declaration-generation.md @@ -33,44 +33,44 @@ Use the same `.node_api` options for runtime builds and declaration generation. `TypeDefinitionBuildOptions` accepts: -| Field | Use | -| --- | --- | -| `root_source_file` | Addon root source file to compile for reflection. | -| `output` | Destination `.d.ts` path. | -| `napi_module` | `zig-napi` module used to create the configured reflection imports. | -| `node_api` | Node-API version and experimental mode used by version-gated wrappers. | -| `header` | Optional text inserted after the generated banner comments. | -| `options` | Optional extra `std.Build.Step.Options` module for addon-specific build options. | +| Field | Use | +| ------------------ | -------------------------------------------------------------------------------- | +| `root_source_file` | Addon root source file to compile for reflection. | +| `output` | Destination `.d.ts` path. | +| `napi_module` | `zig-napi` module used to create the configured reflection imports. | +| `node_api` | Node-API version and experimental mode used by version-gated wrappers. | +| `header` | Optional text inserted after the generated banner comments. | +| `options` | Optional extra `std.Build.Step.Options` module for addon-specific build options. | ## Supported Shapes The generator maps the public export surface into TypeScript: -| Zig shape | TypeScript shape | -| --- | --- | -| `pub fn` | `export function` | -| primitives | `number`, `string`, `boolean`, `bigint`, `null`, `undefined` | -| object-like structs | `export interface` | -| tuples | tuple types | -| arrays, slices, and `std.ArrayList(T)` | `Array` | -| optionals | `T | undefined | null` | -| enums | `export declare const enum` | -| string enums with `napi_string_enum = true` | string-valued `const enum` | -| `union(enum)` | union of payload types | -| `napi.Buffer` | `Buffer` | -| `napi.ArrayBuffer` | `ArrayBuffer` | -| `napi.DataView` | `DataView` | -| `napi.TypedArray(T)` aliases | matching TypedArray names | -| `napi.Promise` | `Promise` | -| async descriptors | `Promise` | -| `napi.AsyncWithEvents` return | `Promise` plus trailing `onEvent?: (event: Event) => void` | -| `napi.Function` | function signatures | -| `napi.ThreadSafeFunction` | callback signature returning `void` | -| `napi.Reference(T)` | declaration of `T` | -| `napi.External(T)` | `ExternalObject` branded interface | -| `napi.AbortSignal` | local `AbortSignal` interface | -| classes | constructor, fields, instance methods, static methods, static values | -| returned functions | function signature with parameter names when source can be resolved | +| Zig shape | TypeScript shape | +| ------------------------------------------- | -------------------------------------------------------------------- | +| `pub fn` | `export function` | +| primitives | `number`, `string`, `boolean`, `bigint`, `null`, `undefined` | +| object-like structs | `export interface` | +| tuples | tuple types | +| arrays, slices, and `std.ArrayList(T)` | `Array` | +| optionals | T | undefined | null | +| enums | `export declare const enum` | +| string enums with `napi_string_enum = true` | string-valued `const enum` | +| `union(enum)` | union of payload types | +| `napi.Buffer` | `Buffer` | +| `napi.ArrayBuffer` | `ArrayBuffer` | +| `napi.DataView` | `DataView` | +| `napi.TypedArray(T)` aliases | matching TypedArray names | +| `napi.Promise` | `Promise` | +| async descriptors | `Promise` | +| `napi.AsyncWithEvents` return | `Promise` plus trailing `onEvent?: (event: Event) => void` | +| `napi.Function` | function signatures | +| `napi.ThreadSafeFunction` | callback signature returning `void` | +| `napi.Reference(T)` | declaration of `T` | +| `napi.External(T)` | `ExternalObject` branded interface | +| `napi.AbortSignal` | local `AbortSignal` interface | +| classes | constructor, fields, instance methods, static methods, static values | +| returned functions | function signature with parameter names when source can be resolved | When the inferred public shape is not the desired public contract, use `napi.dts` or `napi.Dts`. diff --git a/website/src/content/api/dts-overrides.md b/website/src/content/api/dts-overrides.md index 7056b05..ea9c4b7 100644 --- a/website/src/content/api/dts-overrides.md +++ b/website/src/content/api/dts-overrides.md @@ -43,12 +43,12 @@ pub fn label() napi.Dts([]const u8, "String") { In normal runtime builds, `Dts(Value, TypeScriptType)` is exactly `Value`. In declaration-generation builds, it becomes a wrapper carrying: -| Field | Use | -| --- | --- | -| `is_napi_dts` | Marker consumed by the conversion and tsgen layers. | -| `wrapped_type` | Original Zig type. | -| `ts_type` | TypeScript text to emit. | -| `value` | Runtime value, when the wrapped value is not comptime-only. | +| Field | Use | +| -------------- | ----------------------------------------------------------- | +| `is_napi_dts` | Marker consumed by the conversion and tsgen layers. | +| `wrapped_type` | Original Zig type. | +| `ts_type` | TypeScript text to emit. | +| `value` | Runtime value, when the wrapped value is not comptime-only. | The wrapper exposes `unwrap()` during declaration-generation builds. Most addon code should not need it because conversion unwraps `Dts` automatically. @@ -60,11 +60,11 @@ The wrapper exposes `unwrap()` during declaration-generation builds. Most addon The type string is emitted verbatim. -| Input | Generated declaration intent | -| --- | --- | -| `"String"` | Use the boxed or project-specific `String` name exactly as written. | -| `"string"` | Use the TypeScript primitive. | -| `"(left: Number, right: Number) => Number"` | Replace a function declaration with an explicit callable type. | -| `"ReadonlyArray"` | Use a richer TypeScript utility type. | +| Input | Generated declaration intent | +| ------------------------------------------- | ------------------------------------------------------------------- | +| `"String"` | Use the boxed or project-specific `String` name exactly as written. | +| `"string"` | Use the TypeScript primitive. | +| `"(left: Number, right: Number) => Number"` | Replace a function declaration with an explicit callable type. | +| `"ReadonlyArray"` | Use a richer TypeScript utility type. | Because the string is not parsed by Zig, keep it valid TypeScript. diff --git a/website/src/content/api/errors-results.md b/website/src/content/api/errors-results.md index f25d2ed..a43a6dc 100644 --- a/website/src/content/api/errors-results.md +++ b/website/src/content/api/errors-results.md @@ -24,18 +24,18 @@ pub const Error = union(enum) { Create errors with: -| Constructor | Use | -| --- | --- | -| `withReason(reason)` | Generic JavaScript `Error`. | -| `withStatus(status)` | Error from `napi.Status` or custom status string. | -| `withCodeAndMessage(code, message)` | Error with explicit code and message. | -| `fromAnyError(err)` | Map a Zig error into `napi.Error`. | -| `withTypeError(reason)` | JavaScript `TypeError`. | -| `withRangeError(reason)` | JavaScript `RangeError`. | -| `fromReason(reason)` | Store a pending error and return `error.GenericFailure`. | -| `fromStatus(status)` | Store a pending status error and return `error.GenericFailure`. | -| `typeError(message)` | Store a pending `TypeError`. | -| `rangeError(message)` | Store a pending `RangeError`. | +| Constructor | Use | +| ----------------------------------- | --------------------------------------------------------------- | +| `withReason(reason)` | Generic JavaScript `Error`. | +| `withStatus(status)` | Error from `napi.Status` or custom status string. | +| `withCodeAndMessage(code, message)` | Error with explicit code and message. | +| `fromAnyError(err)` | Map a Zig error into `napi.Error`. | +| `withTypeError(reason)` | JavaScript `TypeError`. | +| `withRangeError(reason)` | JavaScript `RangeError`. | +| `fromReason(reason)` | Store a pending error and return `error.GenericFailure`. | +| `fromStatus(status)` | Store a pending status error and return `error.GenericFailure`. | +| `typeError(message)` | Store a pending `TypeError`. | +| `rangeError(message)` | Store a pending `RangeError`. | Use `throwInto(env)` when manual code needs to throw into a JavaScript environment. @@ -49,12 +49,12 @@ napi.JsRangeError These typed wrappers map directly to JavaScript `Error`, `TypeError`, and `RangeError`. -| Method | Use | -| --- | --- | +| Method | Use | +| ---------------------- | ----------------------------------------- | | `fromMessage(message)` | Create with a message and generic status. | -| `fromStatus(status)` | Create from status. | -| `to_napi_error(env)` | Create a JavaScript error value. | -| `throwInto(env)` | Throw into the environment. | +| `fromStatus(status)` | Create from status. | +| `to_napi_error(env)` | Create a JavaScript error value. | +| `throwInto(env)` | Throw into the environment. | ## Error Mapping @@ -77,10 +77,10 @@ pub fn maybeRead(ok: bool) napi.Result([]const u8) { Use `Result(T)` when the public API should model a handled result without throwing through Zig error unions. -| Method | Use | -| --- | --- | -| `Result(T).Ok(value)` | Return a payload. | -| `Result(T).Err(err)` | Return a JavaScript error. | +| Method | Use | +| --------------------- | -------------------------- | +| `Result(T).Ok(value)` | Return a payload. | +| `Result(T).Err(err)` | Return a JavaScript error. | `Result(void).Ok({})` converts to JavaScript `undefined`. @@ -98,13 +98,13 @@ napi.Status `Status` wraps `napi_status`. -| Method | Use | -| --- | --- | -| `from_raw(raw)` | Convert raw N-API status. | -| `New(status)` | Convenience constructor. | -| `isOk()` | Check for `Ok`. | -| `code()` | Numeric status code. | -| `toString()` / `ToString()` | Stable status text. | +| Method | Use | +| --------------------------- | ------------------------- | +| `from_raw(raw)` | Convert raw N-API status. | +| `New(status)` | Convenience constructor. | +| `isOk()` | Check for `Ok`. | +| `code()` | Numeric status code. | +| `toString()` / `ToString()` | Stable status text. | ## Status Values diff --git a/website/src/content/api/module-registration.md b/website/src/content/api/module-registration.md index 8b08799..ce7be54 100644 --- a/website/src/content/api/module-registration.md +++ b/website/src/content/api/module-registration.md @@ -57,14 +57,14 @@ Returning `null` keeps the generated export object. Returning an object replaces ## Export Rules -| Root member | Export behavior | -| --- | --- | -| `pub fn` declaration | Converted into a JavaScript function. | -| `pub const` declaration | Converted through the `Napi.to_napi_value` conversion layer. | -| public struct field on the root type | Converted and attached as an export. | -| `pub const` class wrapper | Exported as a JavaScript class constructor. | -| `pub const` enum type | Exported as a TypeScript enum during declaration generation. | -| `pub const napi_allocator` | Reserved for allocator configuration and not exported. | +| Root member | Export behavior | +| ------------------------------------ | ------------------------------------------------------------ | +| `pub fn` declaration | Converted into a JavaScript function. | +| `pub const` declaration | Converted through the `Napi.to_napi_value` conversion layer. | +| public struct field on the root type | Converted and attached as an export. | +| `pub const` class wrapper | Exported as a JavaScript class constructor. | +| `pub const` enum type | Exported as a TypeScript enum during declaration generation. | +| `pub const napi_allocator` | Reserved for allocator configuration and not exported. | If conversion fails during module initialization, the wrapper throws the pending JavaScript error into the current `Env`. diff --git a/website/src/content/api/overview.md b/website/src/content/api/overview.md index 925c197..c2463db 100644 --- a/website/src/content/api/overview.md +++ b/website/src/content/api/overview.md @@ -6,10 +6,10 @@ title: API Overview `zig-napi` exposes two public import surfaces: -| Import | Use | -| --- | --- | -| `@import("napi")` | Runtime wrappers, module registration, conversion helpers, async descriptors, and TypeScript override wrappers. | -| `@import("zig-napi").napi_build` | `build.zig` helpers for OpenHarmony shared libraries, Node.js `.node` addons, and generated declaration files. | +| Import | Use | +| -------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `@import("napi")` | Runtime wrappers, module registration, conversion helpers, async descriptors, and TypeScript override wrappers. | +| `@import("zig-napi").napi_build` | `build.zig` helpers for OpenHarmony shared libraries, Node.js `.node` addons, and generated declaration files. | The normal flow is: @@ -44,41 +44,41 @@ Use this when the Zig type is useful internally but the public TypeScript contra The `napi` module re-exports the runtime-facing API: -| Category | Exports | -| --- | --- | -| Environment and raw values | `Env`, `NapiValue`, `napi_sys` raw Node-API bindings | -| Primitive values | `Number`, `String`, `Bool`, `BigInt`, `Null`, `Undefined` | -| Object values | `Object`, `Array`, `Promise` | -| Binary values | `Buffer`, `ArrayBuffer`, `TypedArray`, typed array aliases, `DataView` | -| Functions | `Function`, `FunctionRef`, `CallbackInfo`, `ThreadSafeFunction` | -| Async | `AsyncRuntime`, `Async`, `AsyncWithEvents`, `AsyncContext`, `CancelToken`, `AbortSignal`, `Worker` | -| Native state | `Class`, `ClassWithoutInit`, `Reference`, `Ref`, `ObjectRef`, `External`, `NativeWrap` | -| Errors | `Error`, `Status`, `Result`, `JsError`, `JsTypeError`, `JsRangeError` | -| Build-time behavior | `NapiVersion`, `selectedNapiVersion`, `experimentalEnabled`, `resolveRequestedRuntime` | -| TypeScript overrides | `dts`, `Dts` | -| Allocators | `globalAllocator`, `setOperationAllocator`, `resetOperationAllocator` | -| Registration | `NODE_API_MODULE`, `NODE_API_MODULE_WITH_INIT` | +| Category | Exports | +| -------------------------- | -------------------------------------------------------------------------------------------------- | +| Environment and raw values | `Env`, `NapiValue`, `napi_sys` raw Node-API bindings | +| Primitive values | `Number`, `String`, `Bool`, `BigInt`, `Null`, `Undefined` | +| Object values | `Object`, `Array`, `Promise` | +| Binary values | `Buffer`, `ArrayBuffer`, `TypedArray`, typed array aliases, `DataView` | +| Functions | `Function`, `FunctionRef`, `CallbackInfo`, `ThreadSafeFunction` | +| Async | `AsyncRuntime`, `Async`, `AsyncWithEvents`, `AsyncContext`, `CancelToken`, `AbortSignal`, `Worker` | +| Native state | `Class`, `ClassWithoutInit`, `Reference`, `Ref`, `ObjectRef`, `External`, `NativeWrap` | +| Errors | `Error`, `Status`, `Result`, `JsError`, `JsTypeError`, `JsRangeError` | +| Build-time behavior | `NapiVersion`, `selectedNapiVersion`, `experimentalEnabled`, `resolveRequestedRuntime` | +| TypeScript overrides | `dts`, `Dts` | +| Allocators | `globalAllocator`, `setOperationAllocator`, `resetOperationAllocator` | +| Registration | `NODE_API_MODULE`, `NODE_API_MODULE_WITH_INIT` | ## Build Surface `@import("zig-napi").napi_build` exports the build helpers: -| Export | Use | -| --- | --- | -| `nativeAddonBuild` | Build OpenHarmony shared libraries. | -| `nodeAddonBuild` | Build platform-specific Node.js `.node` addons. | -| `generateTypeDefinition` | Generate `index.d.ts` from the addon root. | -| `NativeAddonBuildOptionsWithModule` | Options for OpenHarmony output. | -| `NodeAddonBuildOptionsWithModule` | Options for Node addon output. | -| `TypeDefinitionBuildOptions` | Options for declaration generation. | -| `NativeAddonBuildResult` | Compile steps for `arm64`, `arm`, and `x64`. | -| `NodeAddonBuildResult` | Node addon compile step. | -| `NodeApiOptions` | Node-API version and experimental mode. | -| `NapiVersion` | Build-time Node-API enum. | -| `resolveNdkPath` | Resolve the OpenHarmony native SDK path. | -| `cloneLibraryOptions` | Clone root module build options for another target. | -| `nodePlatformArchAbi` | Format Node platform/arch/ABI suffix. | -| `nodeAddonFilename` | Format the installed `.node` filename. | +| Export | Use | +| ----------------------------------- | --------------------------------------------------- | +| `nativeAddonBuild` | Build OpenHarmony shared libraries. | +| `nodeAddonBuild` | Build platform-specific Node.js `.node` addons. | +| `generateTypeDefinition` | Generate `index.d.ts` from the addon root. | +| `NativeAddonBuildOptionsWithModule` | Options for OpenHarmony output. | +| `NodeAddonBuildOptionsWithModule` | Options for Node addon output. | +| `TypeDefinitionBuildOptions` | Options for declaration generation. | +| `NativeAddonBuildResult` | Compile steps for `arm64`, `arm`, and `x64`. | +| `NodeAddonBuildResult` | Node addon compile step. | +| `NodeApiOptions` | Node-API version and experimental mode. | +| `NapiVersion` | Build-time Node-API enum. | +| `resolveNdkPath` | Resolve the OpenHarmony native SDK path. | +| `cloneLibraryOptions` | Clone root module build options for another target. | +| `nodePlatformArchAbi` | Format Node platform/arch/ABI suffix. | +| `nodeAddonFilename` | Format the installed `.node` filename. | ## Read Order diff --git a/website/src/content/api/values-objects.md b/website/src/content/api/values-objects.md index 2ab5ee7..ad6c9b1 100644 --- a/website/src/content/api/values-objects.md +++ b/website/src/content/api/values-objects.md @@ -14,20 +14,20 @@ napi.Env `Env` wraps `napi_env`. It is injected automatically when an exported function uses `napi.Env` as its first parameter. -| Method | Use | -| --- | --- | -| `from_raw(raw)` | Wrap an existing `napi_env`. | -| `getUndefined()` / `getNull()` | Get singleton JavaScript values. | -| `getNapiVersion()` | Read the host runtime Node-API version. | -| `getGlobal()` | Get the global object. | -| `createSymbol(description)` | Create a JavaScript symbol. | -| `createDate(value)` | Create a JavaScript Date. Requires Node-API v5. | -| `isExceptionPending()` | Check whether an exception is pending. | -| `getAndClearLastException()` | Consume the pending exception as `NapiValue`. | -| `wrap` / `wrapWithSizeHint` | Attach native payload to an object. | -| `unwrap` / `unwrapConst` | Read native payload from an object. | -| `dropWrapped` | Remove and destroy a wrapped payload. | -| `matchesWrapped` | Check whether an object carries a payload of type `T`. | +| Method | Use | +| ------------------------------ | ------------------------------------------------------ | +| `from_raw(raw)` | Wrap an existing `napi_env`. | +| `getUndefined()` / `getNull()` | Get singleton JavaScript values. | +| `getNapiVersion()` | Read the host runtime Node-API version. | +| `getGlobal()` | Get the global object. | +| `createSymbol(description)` | Create a JavaScript symbol. | +| `createDate(value)` | Create a JavaScript Date. Requires Node-API v5. | +| `isExceptionPending()` | Check whether an exception is pending. | +| `getAndClearLastException()` | Consume the pending exception as `NapiValue`. | +| `wrap` / `wrapWithSizeHint` | Attach native payload to an object. | +| `unwrap` / `unwrapConst` | Read native payload from an object. | +| `dropWrapped` | Remove and destroy a wrapped payload. | +| `matchesWrapped` | Check whether an object carries a payload of type `T`. | ## `Object` @@ -37,21 +37,21 @@ napi.Object Common operations: -| Method | Use | -| --- | --- | -| `Object.Create(env)` | Create an empty JavaScript object. | -| `Object.New(env, value)` | Convert a Zig object-like value. | -| `Set(key, value)` | Set a named string property. | -| `SetProperty(key, value)` | Set a property with a dynamic key. | -| `setProperty(key, value)` | Lowercase alias for `SetProperty`. | -| `Get(key, T)` | Read a property and convert it to `T`. | -| `GetNamed(key, T)` | Read a comptime-known named property. | -| `Has(key)` | Check for a named property. | -| `propertyNames()` | Return property names as `napi.Array`. | -| `isDate()` / `dateValue()` | Inspect Date values. Requires Node-API v5. | -| `freeze()` / `seal()` | Apply JavaScript object immutability operations. Requires Node-API v8. | -| `CreateRef()` | Create `Reference(Object)`. | -| `Wrap` / `Unwrap` / `DropWrapped` | Convenience native wrap operations. | +| Method | Use | +| --------------------------------- | ---------------------------------------------------------------------- | +| `Object.Create(env)` | Create an empty JavaScript object. | +| `Object.New(env, value)` | Convert a Zig object-like value. | +| `Set(key, value)` | Set a named string property. | +| `SetProperty(key, value)` | Set a property with a dynamic key. | +| `setProperty(key, value)` | Lowercase alias for `SetProperty`. | +| `Get(key, T)` | Read a property and convert it to `T`. | +| `GetNamed(key, T)` | Read a comptime-known named property. | +| `Has(key)` | Check for a named property. | +| `propertyNames()` | Return property names as `napi.Array`. | +| `isDate()` / `dateValue()` | Inspect Date values. Requires Node-API v5. | +| `freeze()` / `seal()` | Apply JavaScript object immutability operations. Requires Node-API v8. | +| `CreateRef()` | Create `Reference(Object)`. | +| `Wrap` / `Unwrap` / `DropWrapped` | Convenience native wrap operations. | Automatic object conversion maps object-like Zig structs to JavaScript objects. Optional struct fields become optional TypeScript properties during declaration generation. @@ -69,18 +69,18 @@ try array.Push("first"); try array.Push(42); ``` -| Method | Use | -| --- | --- | -| `Array.New(env, value)` | Create an array from a Zig array, slice, tuple, or `std.ArrayList(T)`. | -| `Array.Create(env)` | Create an empty array. | -| `Array.CreateWithLength(env, len)` | Create an array with length. | -| `createWithLength(env, len)` | Lowercase alias for `CreateWithLength`. | -| `length()` | Return cached length. | -| `Get(index, T)` | Read and convert one element. | -| `Set(index, value)` | Write one element. | -| `HasElement(index)` / `hasElement(index)` | Check whether an index exists. | -| `DeleteElement(index)` / `deleteElement(index)` | Delete one element. | -| `Push(value)` | Append one element. | +| Method | Use | +| ----------------------------------------------- | ---------------------------------------------------------------------- | +| `Array.New(env, value)` | Create an array from a Zig array, slice, tuple, or `std.ArrayList(T)`. | +| `Array.Create(env)` | Create an empty array. | +| `Array.CreateWithLength(env, len)` | Create an array with length. | +| `createWithLength(env, len)` | Lowercase alias for `CreateWithLength`. | +| `length()` | Return cached length. | +| `Get(index, T)` | Read and convert one element. | +| `Set(index, value)` | Write one element. | +| `HasElement(index)` / `hasElement(index)` | Check whether an index exists. | +| `DeleteElement(index)` / `deleteElement(index)` | Delete one element. | +| `Push(value)` | Append one element. | When reading JavaScript values into Zig arrays, slices, or `std.ArrayList(T)`, numeric TypedArray inputs are accepted for supported numeric element types. @@ -92,10 +92,10 @@ napi.Promise `Promise.New(env)` creates a deferred promise wrapper. -| Method | Use | -| --- | --- | -| `Resolve(value)` | Resolve with any value supported by return conversion. | -| `Reject(error)` | Reject with `napi.Error`. | -| `RejectAbortError()` | Reject with an `AbortError` JavaScript error. | +| Method | Use | +| -------------------- | ------------------------------------------------------ | +| `Resolve(value)` | Resolve with any value supported by return conversion. | +| `Reject(error)` | Reject with `napi.Error`. | +| `RejectAbortError()` | Reject with an `AbortError` JavaScript error. | For most async exports, prefer `napi.Async` or `napi.AsyncWithEvents`; they produce promises and handle scheduling. diff --git a/website/src/content/api/values-primitives.md b/website/src/content/api/values-primitives.md index 97bfd2e..2daf93d 100644 --- a/website/src/content/api/values-primitives.md +++ b/website/src/content/api/values-primitives.md @@ -24,11 +24,11 @@ pub fn read(value: napi.NapiValue) i32 { ## Number, Bool, String, BigInt -| Wrapper | Typical Zig values | -| --- | --- | -| `napi.Number` | integers, floats, comptime numeric values | -| `napi.Bool` | `bool` | -| `napi.String` | UTF-8 or UTF-16 string-like values | +| Wrapper | Typical Zig values | +| ------------- | ------------------------------------------------------- | +| `napi.Number` | integers, floats, comptime numeric values | +| `napi.Bool` | `bool` | +| `napi.String` | UTF-8 or UTF-16 string-like values | | `napi.BigInt` | integer values that should cross as JavaScript `bigint` | `BigInt` requires Node-API v6 or newer. @@ -43,12 +43,12 @@ Use `BigInt` for `i128`, `u128`, and values that should not be represented as Ja `String.New(env, bytes)` creates a UTF-8 JavaScript string. -| Method | Use | -| --- | --- | -| `utf8Len()` | Byte length of the UTF-8 representation. | -| `utf16Len()` | Code-unit length of the UTF-16 representation. | -| `copyUtf8()` | Allocate and return `[]u8`. | -| `copyUtf16()` | Allocate and return `[]u16`. | +| Method | Use | +| ------------- | ---------------------------------------------- | +| `utf8Len()` | Byte length of the UTF-8 representation. | +| `utf16Len()` | Code-unit length of the UTF-16 representation. | +| `copyUtf8()` | Allocate and return `[]u8`. | +| `copyUtf16()` | Allocate and return `[]u16`. | Automatic conversion supports UTF-8 and UTF-16 string-like Zig targets. diff --git a/website/src/content/api/versioning.md b/website/src/content/api/versioning.md index 0ccc0f0..f709b81 100644 --- a/website/src/content/api/versioning.md +++ b/website/src/content/api/versioning.md @@ -44,17 +44,17 @@ When `.experimental = true`, the addon requests the experimental Node-API versio ## Version-Gated Wrappers -| Wrapper | Minimum | -| --- | --- | -| `Async` | Node-API v4 | -| `AsyncWithEvents` | Node-API v4 | -| `ThreadSafeFunction` | Node-API v4 | -| `Env.createDate` | Node-API v5 | -| `Object.isDate` / `Object.dateValue` | Node-API v5 | -| `BigInt` | Node-API v6 | -| `BigInt64Array` / `BigUint64Array` | Node-API v6 | +| Wrapper | Minimum | +| ----------------------------------------------- | ----------- | +| `Async` | Node-API v4 | +| `AsyncWithEvents` | Node-API v4 | +| `ThreadSafeFunction` | Node-API v4 | +| `Env.createDate` | Node-API v5 | +| `Object.isDate` / `Object.dateValue` | Node-API v5 | +| `BigInt` | Node-API v6 | +| `BigInt64Array` / `BigUint64Array` | Node-API v6 | | `ArrayBuffer.detach` / `ArrayBuffer.isDetached` | Node-API v7 | -| `Object.freeze` / `Object.seal` | Node-API v8 | +| `Object.freeze` / `Object.seal` | Node-API v8 | If the selected version is too low, wrappers fail at compile time with a message that points back to `.node_api.version`. diff --git a/website/src/styles.css b/website/src/styles.css index 3fc8325..28989ad 100644 --- a/website/src/styles.css +++ b/website/src/styles.css @@ -1,5 +1,5 @@ -@import 'tailwindcss'; -@import '@void/md/theme-content.css'; +@import "tailwindcss"; +@import "@void/md/theme-content.css"; @theme { --color-bg: #0b0a09; @@ -15,7 +15,7 @@ --color-accent-muted: oklch(68% 0.16 52 / 0.16); --color-accent-glow: oklch(70% 0.18 52 / 0.22); --color-accent-fg: #1a1208; - --font-mono: 'Maple Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace; + --font-mono: "Maple Mono", ui-monospace, "SFMono-Regular", Menlo, monospace; --text-display-xl: 3rem; --text-h2: 2rem; } @@ -54,12 +54,11 @@ body { min-width: 320px; overflow-x: clip; background: - radial-gradient(70% 48% at 50% -12%, var(--color-accent-glow), transparent 70%), - var(--color-bg); + radial-gradient(70% 48% at 50% -12%, var(--color-accent-glow), transparent 70%), var(--color-bg); } body::after { - content: ''; + content: ""; position: fixed; inset: 0; z-index: 60; @@ -296,8 +295,8 @@ code { font-size: 0.95rem; } - .api-markdown div[class*='language-'], - .snippet-markdown div[class*='language-'] { + .api-markdown div[class*="language-"], + .snippet-markdown div[class*="language-"] { border: 1px solid var(--color-border); border-radius: 0.5rem; background: var(--color-surface-2) !important; @@ -314,8 +313,8 @@ code { color: var(--shiki-dark, var(--color-fg)) !important; } - .api-markdown pre.shiki.shiki-themes span[style*='--shiki-dark'], - .snippet-markdown pre.shiki.shiki-themes span[style*='--shiki-dark'] { + .api-markdown pre.shiki.shiki-themes span[style*="--shiki-dark"], + .snippet-markdown pre.shiki.shiki-themes span[style*="--shiki-dark"] { color: var(--shiki-dark) !important; } @@ -342,13 +341,13 @@ code { margin: 0; } - .snippet-markdown div[class*='language-'] { + .snippet-markdown div[class*="language-"] { margin: 0; border: 0; border-radius: 0; } - .snippet-markdown div[class*='language-'] pre { + .snippet-markdown div[class*="language-"] pre { padding: 1.4rem 1.5rem; }