Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/node-addon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ pnpm build
pnpm test
```

It installs the addon as `zig-out/node/hello.<platform-arch-abi>.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.<platform-arch-abi>.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.

Expand Down Expand Up @@ -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/<platform-arch-abi>` 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 `<binary>.<platform-arch-abi>.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/<platform-arch-abi>` 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 `<binary>.<platform-arch-abi>.node` or `<binary>.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.

Expand All @@ -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

Expand Down
42 changes: 21 additions & 21 deletions benchmark/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
2 changes: 2 additions & 0 deletions examples/node/browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from "zig-napi-node-example-wasm32-wasi";
export * from "zig-napi-node-example-wasm32-wasi";
59 changes: 59 additions & 0 deletions examples/node/hello.wasi-browser.js
Original file line number Diff line number Diff line change
@@ -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;
116 changes: 116 additions & 0 deletions examples/node/hello.wasi.cjs
Original file line number Diff line number Diff line change
@@ -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;
11 changes: 11 additions & 0 deletions examples/node/index.d.ts
Original file line number Diff line number Diff line change
@@ -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<number>;
export declare function countAsyncProgress(
total: number,
onEvent?: (event: CountProgress) => void,
): Promise<number>;
Loading
Loading