@chainsafe/zapi
v0.1.1
Published
A tool for managing and publishing Zig NAPI packages.
Readme
zapi
A Zig N-API wrapper library and CLI for building and publishing cross-platform Node.js native addons.
Overview
zapi provides two main components:
- Zig Library (
src/) - Idiomatic Zig bindings for the Node.js N-API, making it easy to write native addons in Zig - CLI Tool (
ts/) - Build tooling for cross-compiling and publishing multi-platform npm packages
Installation
npm install -D @chainsafe/zapiAdd the Zig dependency to your build.zig.zon:
.dependencies = .{
.zapi = .{
.url = "https://github.com/chainsafe/zapi/archive/<commit>.tar.gz",
.hash = "...",
},
},Zig Library
Quick Start
const napi = @import("napi");
comptime {
napi.module.register(initModule);
}
fn initModule(env: napi.Env, module: napi.Value) !void {
// Export a string
try module.setNamedProperty("greeting", try env.createStringUtf8("Hello from Zig!"));
// Export a function
try module.setNamedProperty("add", try env.createFunction("add", 2, napi.createCallback(2, add, .{}), null));
}
fn add(a: i32, b: i32) i32 {
return a + b;
}Core Types
| Type | Description |
|------|-------------|
| Env | The N-API environment, provides methods to create values, throw errors, manage scopes |
| Value | A JavaScript value handle with methods for type checking, property access, conversions |
| CallbackInfo | Provides access to function arguments and this binding |
| HandleScope | Prevents garbage collection of values within a scope |
| EscapableHandleScope | Like HandleScope but allows one value to escape |
| Ref | A persistent reference to a value that survives garbage collection |
| Deferred | Resolver/rejecter for promises |
| AsyncWork | Run work on a thread pool with completion callback on main thread |
| ThreadSafeFunction | Call JavaScript from any thread safely |
| AsyncContext | Context for async resource tracking |
Creating Functions
Manual Style
Full control using raw Env and Value:
fn add_manual(env: napi.Env, info: napi.CallbackInfo(2)) !napi.Value {
const a = try info.arg(0).getValueInt32();
const b = try info.arg(1).getValueInt32();
return try env.createInt32(a + b);
}Automatic Conversion with createCallback
Let zapi handle argument/return conversion:
// Arguments and return value are automatically converted
fn add(a: i32, b: i32) i32 {
return a + b;
}
// Register with automatic wrapping
try env.createFunction("add", 2, napi.createCallback(2, add, .{}), null);Argument Hints
Control how arguments are converted:
napi.createCallback(2, myFunc, .{
.args = .{ .env, .auto, .value, .data, .string, .buffer },
.returns = .value, // or .string, .buffer, .auto
});| Hint | Description |
|------|-------------|
| .auto | Automatic type conversion |
| .env | Inject napi.Env |
| .value | Pass raw napi.Value |
| .data | User data pointer passed to createFunction |
| .string | Convert to/from []const u8 |
| .buffer | Convert to/from byte slice |
Creating Classes
const Timer = struct {
start: i64,
pub fn read(self: *Timer) i64 {
return std.time.milliTimestamp() - self.start;
}
};
try env.defineClass(
"Timer",
0,
timerConstructor,
null,
&[_]napi.c.napi_property_descriptor{
.{ .utf8name = "read", .method = napi.wrapCallback(0, Timer.read) },
},
);Async Work (Thread Pool)
Run CPU-intensive work off the main thread:
const Work = struct {
a: i32,
b: i32,
result: i32,
deferred: napi.Deferred,
};
fn execute(env: napi.Env, data: *Work) void {
// Runs on thread pool - don't call JS here!
data.result = data.a + data.b;
}
fn complete(env: napi.Env, status: napi.status.Status, data: *Work) void {
// Back on main thread - resolve the promise
const result = env.createInt32(data.result) catch return;
data.deferred.resolve(result) catch return;
}
// Create async work
const work = try napi.AsyncWork(Work).create(env, null, name, execute, complete, &data);
try work.queue();Thread-Safe Functions
Call JavaScript from any thread:
const tsfn = try env.createThreadsafeFunction(
jsCallback, // JS function to call
context, // User context
"name",
0, // Max queue size (0 = unlimited)
1, // Initial thread count
null, // Finalize data
null, // Finalize callback
myCallJsCallback, // Called on main thread
);
// From any thread:
try tsfn.call(&data, .blocking);Error Handling
All N-API calls return NapiError on failure:
fn myFunction(env: napi.Env) !void {
// Errors propagate naturally
const value = try env.createStringUtf8("hello");
// Throw JavaScript errors
try env.throwError("ERR_CODE", "Something went wrong");
try env.throwTypeError("ERR_TYPE", "Expected a number");
}CLI Tool
Configuration
Add a zapi field to your package.json:
{
"name": "my-addon",
"zapi": {
"binaryName": "my-addon",
"step": "my-lib",
"targets": [
"x86_64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
"aarch64-unknown-linux-gnu",
"x86_64-apple-darwin",
"aarch64-apple-darwin",
"x86_64-pc-windows-msvc"
]
}
}Supported Targets
| Target | Platform | Arch | ABI |
|--------|----------|------|-----|
| aarch64-apple-darwin | macOS | arm64 | - |
| x86_64-apple-darwin | macOS | x64 | - |
| aarch64-unknown-linux-gnu | Linux | arm64 | glibc |
| x86_64-unknown-linux-gnu | Linux | x64 | glibc |
| x86_64-unknown-linux-musl | Linux | x64 | musl |
| x86_64-pc-windows-msvc | Windows | x64 | msvc |
Global Options
| Option | Description |
|--------|-------------|
| --help, -h | Show help message |
| --version, -v | Show version number |
Commands
zapi build
Build for a single target platform.
zapi build [options]| Option | Description | Default |
|--------|-------------|---------|
| --step | Zig build step | zapi.step from package.json |
| --target | Target triple | Current platform |
| --optimize | Debug, ReleaseSafe, ReleaseFast, ReleaseSmall | - |
| --zig-cwd | Working directory for zig build | . |
zapi build-artifacts
Build for all configured targets and collect artifacts.
zapi build-artifacts [options]| Option | Description | Default |
|--------|-------------|---------|
| --step | Zig build step | zapi.step from package.json |
| --optimize | Optimization level | - |
| --zig-cwd | Working directory for zig build | . |
| --artifacts-dir | Output directory for artifacts | artifacts |
Example output:
▶ Building my-addon for 6 target(s)...
[1/6] Building for x86_64-unknown-linux-gnu...
→ Moving artifact to artifacts/x86_64-unknown-linux-gnu
[2/6] Building for aarch64-apple-darwin...
→ Moving artifact to artifacts/aarch64-apple-darwin
...
✓ Built 6 artifact(s) to artifacts/zapi prepublish
Prepare npm packages for publishing:
- Creates
npm/<target>/directories for each target - Moves compiled
.nodebinaries from artifacts into target packages - Generates
package.jsonfor each target package (with correctos,cpu,libc) - Updates the main
package.jsonwithoptionalDependencies
zapi prepublish [options]| Option | Description | Default |
|--------|-------------|---------|
| --artifacts-dir | Directory containing built artifacts | artifacts |
| --npm-dir | Directory for npm packages | npm |
Example output:
▶ Preparing [email protected] for publishing...
▶ Moving artifacts to npm packages...
→ x86_64-unknown-linux-gnu → npm/x86_64-unknown-linux-gnu/my-addon.node
▶ Generating target package.json files...
→ Created npm/x86_64-unknown-linux-gnu/package.json
▶ Updating package.json with optionalDependencies...
✓ Prepared 6 target package(s) in npm/zapi publish
Publish all target-specific packages and the main package to npm.
zapi publish [options] [-- <npm-args>]| Option | Description | Default |
|--------|-------------|---------|
| --npm-dir | Directory containing npm packages | npm |
| --dry-run | Preview what would be published without publishing | false |
Any arguments after -- are passed directly to npm publish (e.g., --access public, --tag beta).
Example dry-run:
zapi publish --dry-run▶ [DRY RUN] Would publish 6 target package(s) + main package
→ Extra npm args: (none)
[1/7] Would publish x86_64-unknown-linux-gnu
→ Directory: /path/to/npm/x86_64-unknown-linux-gnu
...
✓ [DRY RUN] 7 package(s) would be publishedRelease Workflow
# 1. Build for all targets
zapi build-artifacts --optimize ReleaseFast
# 2. Prepare npm packages
zapi prepublish
# 3. Preview what will be published
zapi publish --dry-run
# 4. Publish to npm
zapi publish -- --access publicError Handling
Set DEBUG=1 for full stack traces on errors.
Runtime Loading
requireNapiLibrary(packageDir)
Load the native addon, automatically selecting the correct binary for the current platform:
import { requireNapiLibrary } from "@chainsafe/zapi";
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
const __dirname = dirname(fileURLToPath(import.meta.url));
const addon = requireNapiLibrary(__dirname);Resolution order:
- Local build:
zig-out/lib/<binaryName>.node - Published package:
<pkg-name>-<target>
Example
See the example/ directory for a comprehensive example including:
- String properties
- Functions with manual and automatic argument handling
- Classes with methods
- Async work with promises
- Thread-safe functions
# Build the example
zig build
# Test it
node example/test.jsLicense
MIT
