typesafe/node-api-zig
Idiomatic Zig bindings for Node-API.
refsThe node-api module provides Node-API bindings for writing idiomatic Zig addons
for V8-based runtimes like Node.JS or Bun. Thanks to its conventions-based approach it bridges the gap seamlessly,
with almost no Node-API specific code!
init, deinit support & allocator injectionerrorunion supportNodeValue et.al.) for read/writeNodeFunction(fn (u32, u32) !u32)Install the node_api (note the underscore) dependency
> zig fetch --save https://github.com/typesafe/node-api-zig/archive/refs/tags/v0.0.4-beta.tar.gz
Add the node-api module to your library:
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const mod = b.addModule("root", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
});
const node_api = b.dependency("node_api", .{});
mod.addImport("node-api", node_api.module("node-api"));
const lib = b.addLibrary(.{
// if Linux
.use_llvm = true,
.name = "my-native-node-module",
.root_module = mod,
});
b.installArtifact(lib);
}
Initialize your Node-API extension:
const node_api = @import("node-api");
comptime {
// export encrypt function (or types, or values or pointers)
node_api.@"export"(.{
.encrypt = encrypt,
// ...
});
}
fn encrypt(
// serialized from JS string, borrowed memory
value: []const u8,
// serialized from JS object by value (use *Options for wrapped native instances)
options: Options,
// "injected" by convention, any memory allocated with it is freed after returning to JS
allocator: std.mem.Allocator,
) ![]const u8 {
const res = allocator.alloc(u8, 123);
// ...
return res; // freed by node-api
}
Use your library as node module:
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const { encrypt } = require("my-native-node-module.node");
const m = encrypt("secret", { salt: "..." });
There are 2 options to initialize a module:
node_api.@"export": conveniant for exporting comptime values, which is very likelynode_api.init(fn (node: NodeContext) !NodeValue): for exporting runtime valuescomptime {
node_api.register(init);
}
fn init(node: node_api.NodeContext) !?node_api.NodeValue {
// init & return `exports` value/object/function
}
The node_api.init initialization function as well as any
Struct types, functions, fields, parameters and return values are all converted by convention. Unsupported types result in compile errors.
| Native type | Node type | Remarks |
|---|---|---|
type |
Class or Function |
Returning or passing a struct type to JS, turns it into a class.Returning or passing a fn, turns it into a JS-callable, well, function. |
i32,i64,u32 |
number |
|
u64 |
BigInt |
|
[]const u8, []u8 |
string |
UTF-8 |
[]const T, []T |
array |
|
*T |
Object |
Passing struct pointers to JS will wrap & track them. |
NodeValue |
any |
NodeValue can be used to access JS values by reference. |
Function parameters and return types can be
Native values and NodeValue instance can be converted using Convert:
nativeFromNode(comptime T: type, value: NodeValue, allocator. Allocator) TnodeFromNative(value: anytype) NodeValueArguments can be of type:
node.defineClass transforms a Zig struct to a JS-accessible class by convention:
comptime {
node_api.register(init);
}
fn init(node: node_api.NodeContext) !?node_api.NodeValue {
// epo
return try node.serialize(.{
.MyClass = try node.defineClass(MyClass),
});
}
const MyClass = struct {
Self = @This();
field1: i32,
field2: ?[]u8,
pub fn init() Self {
return .{ .field1 = 0, .field2 = null };
}
pub fn method1(self: Self, v: i32) !void {
}
}
init maps to new.
/*
Scenarios:
native (wrapped) instance lifecycle:
external instance memory:
parameters and return values:
setting field values
frees previous value, if any
*/