Skip to content

Commit bd36012

Browse files
committed
feat: add export method to init module directly from comptime value
1 parent 75f38db commit bd36012

File tree

12 files changed

+193
-43
lines changed

12 files changed

+193
-43
lines changed

README.md

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,39 @@
1-
21
The `node-api` Zig package provides [Node-API](https://nodejs.org/api/n-api.html) bindings for writing idiomatic Zig addons for V8-based runtimes like Node.JS or Bun.
32
Thanks to its conventions-based approach it bridges the gap seamlessly, with almost no Node-API specific code!
43

5-
![build-badge](https://img.shields.io/github/actions/workflow/status/typesafe/node-api-zig/ci.yml
6-
)
4+
![build-badge](https://img.shields.io/github/actions/workflow/status/typesafe/node-api-zig/ci.yml)
75
![test-badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2Ftypesafe%2F26882516c7ac38bf94a81784f966bd86%2Fraw%2Fnode-api-zig-test-badge.json)
86

7+
```zig
8+
const node_api = @import("node-api");
9+
const Options = @import("Options");
10+
11+
comptime {
12+
// or node_api.init(fn (node) NodeValue) for runtime values
13+
node_api.@"export"(encrypt);
14+
}
15+
16+
fn encrypt(value: []const u8, options: Options, allocator: std.mem.Allocator) ![]const u8 {
17+
const res = allocator.alloc(u8, 123);
18+
errdefer allocator.free(res);
19+
20+
// ...
21+
22+
return res; // freed by node-api
23+
}
24+
25+
```
26+
27+
```TypeScript
28+
import { createRequire } from "module";
29+
const require = createRequire(import.meta.url);
30+
31+
const encrypt = require("zig-module.node");
32+
33+
// call zig function
34+
const m = encrypt("secret", { salt: "..."});
35+
36+
```
937

1038
TODO:
1139

@@ -35,7 +63,6 @@ TODO:
3563

3664
# Getting started
3765

38-
3966
TODO
4067

4168
# Features
@@ -64,21 +91,23 @@ import fromZig from(zig-module.node);
6491
Struct types, functions, fields, parameters and return values are all converted by convention.
6592
Unsupported types result in compile errors.
6693

67-
|Native type|Node type|Remarks|
68-
|-|-|-|
69-
|`type`|`Class` or `Function`|Returning or passing a struct `type` to JS, turns it into a class.<br>Returning or passing a `fn`, turns it into a JS-callable, well, function. |
70-
|`i32`,`i64`,`u32`|`number`| |
71-
|`u64`|`BigInt`| |
72-
|`[]const u8`, `[]u8`|`string`|UTF-8|
73-
|`[]const T`, `[]T`|`array`| |
74-
|`*T`|`Object`|Passing struct pointers to JS will wrap & track them.|
75-
|`NodeValue`|`any`|NodeValue can be used to access JS values by reference.|
94+
| Native type | Node type | Remarks |
95+
| -------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
96+
| `type` | `Class` or `Function` | Returning or passing a struct `type` to JS, turns it into a class.<br>Returning or passing a `fn`, turns it into a JS-callable, well, function. |
97+
| `i32`,`i64`,`u32` | `number` | |
98+
| `u64` | `BigInt` | |
99+
| `[]const u8`, `[]u8` | `string` | UTF-8 |
100+
| `[]const T`, `[]T` | `array` | |
101+
| `*T` | `Object` | Passing struct pointers to JS will wrap & track them. |
102+
| `NodeValue` | `any` | NodeValue can be used to access JS values by reference. |
76103

77104
Function parameters and return types can be
105+
78106
- native Zig types (unsupported types will result in compile time errors)
79107
- one of the NodeValue types to access values by reference.
80108

81109
Native values and NodeValue instance can be converted using `Convert`:
110+
82111
- `nativeFromNode(comptime T: type, value: NodeValue, allocator. Allocator) T`
83112
- `nodeFromNative(value: anytype) NodeValue`
84113

@@ -93,16 +122,13 @@ Arguments can be of type:
93122
- optional
94123
- enum
95124
- NodeXxx values for references
96-
## Define async functions
97125

126+
## Define async functions
98127

99128
## Define classes
100129

101130
`node.defineClass` transforms a Zig struct to a JS-accessible class by convention:
102131

103-
104-
105-
106132
```zig
107133
108134
comptime {
@@ -134,7 +160,6 @@ const MyClass = struct {
134160
135161
```
136162

137-
138163
### Contstructors
139164

140165
`init` maps to `new`.
@@ -151,37 +176,35 @@ const MyClass = struct {
151176

152177
## Memory management
153178

154-
155-
156179
- class instances are allocated and freed automatically
157180
- new-ing instance (from JS) will allocate memory (and update V8 stats)
158181
- GC finalizers will automatically free the memory (and update V8 stats)
159182
- Zig type arguments that require allocations are "owned by the instance"
160183
- when the are store as field values the will be freed as part of the finalization process
161184
- existing field values must be freed manually when they are overwritten!
162185

163-
164-
165-
166-
/*
186+
/\*
167187

168188
Scenarios:
169189

170190
native (wrapped) instance lifecycle:
191+
171192
- new in JS -> finalize in Zig
172193
- create in Zig -> finalize in Zig
173194

174195
external instance memory:
196+
175197
- arena per instance?
176198
- managed by instance if instance has allocator field
177199

178200
parameters and return values:
201+
179202
- pointers to structs result in uwrapped values
180203
- parameters and return type memory
181204
- arena per function call
182205

183206
setting field values
184-
- frees previous value, if any
185207

208+
- frees previous value, if any
186209

187-
*/
210+
\*/

src/Convert.zig

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,11 @@ pub fn nativeFromNode(env: c.napi_env, comptime T: type, js_value: c.napi_value,
188188
}
189189
} else {
190190
switch (i.bits) {
191-
32 => try s2e(c.napi_get_value_uint32(env, js_value, &res)),
191+
0...32 => {
192+
var tmp: u32 = undefined;
193+
try s2e(c.napi_get_value_uint32(env, js_value, &tmp));
194+
return @intCast(tmp);
195+
},
192196
64 => {
193197
var b: bool = undefined;
194198
try s2e(c.napi_get_value_bigint_uint64(env, js_value, &res, &b));

src/root.zig

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,48 @@ const std = @import("std");
33
const c = @import("c.zig").c;
44
const Convert = @import("Convert.zig");
55
const NodeValues = @import("node_values.zig");
6-
/// Represents a Node VM Context.
7-
pub const NodeContext = @import("Node.zig").NodeContext;
86

9-
/// Represents a Node value.
7+
pub const NodeContext = @import("Node.zig").NodeContext;
108
pub const NodeValue = NodeValues.NodeValue;
119
pub const NodeObject = NodeValues.NodeObject;
1210
pub const NodeArray = NodeValues.NodeArray;
1311
pub const NodeFunction = NodeValues.NodeFunction;
1412

15-
/// The InitFunction to pass to the `register` method. The `ctx` parameter
16-
/// represents the Node context. The returned value becomes the `exports` value
17-
/// of the JS module.
18-
pub const InitFunction = fn (ctx: NodeContext) anyerror!?NodeValue;
13+
/// Exports the specified comptime value as a native Node-API module.
14+
///
15+
/// Example:
16+
///
17+
/// ```
18+
/// const std = @import("std");
19+
/// const node_api = @import("node-api");
20+
///
21+
/// comptime {
22+
/// node_api.@"export"(.{
23+
/// .fn = function,
24+
/// .Class = Class,
25+
/// });
26+
/// }
27+
/// ```
28+
pub fn @"export"(comptime value: anytype) void {
29+
const module = opaque {
30+
pub fn napi_register_module_v1(env: c.napi_env, _: c.napi_value) callconv(.c) c.napi_value {
31+
const node = NodeContext.init(env);
1932

20-
/// Initializes a native Node-API module. Example:
33+
const exports = node.serialize(value) catch |err| {
34+
node.handleError(err);
35+
return null;
36+
};
37+
38+
return exports.napi_value;
39+
}
40+
};
41+
42+
registerModule(&module.napi_register_module_v1);
43+
}
44+
45+
/// Initializes a native Node-API module by returning a runtime-known value.
46+
///
47+
/// Example:
2148
///
2249
/// ```
2350
/// const std = @import("std");
@@ -32,12 +59,12 @@ pub const InitFunction = fn (ctx: NodeContext) anyerror!?NodeValue;
3259
/// return try node.createString("hello!");
3360
/// }
3461
/// ```
35-
pub fn register(comptime init_fn: InitFunction) void {
62+
pub fn init(comptime f: InitFunction) void {
3663
const module = opaque {
3764
pub fn napi_register_module_v1(env: c.napi_env, exp: c.napi_value) callconv(.c) c.napi_value {
38-
const node = NodeContext{ .napi_env = env };
65+
const node = NodeContext.init(env);
3966

40-
const exports = init_fn(node) catch |err| {
67+
const exports = f(node) catch |err| {
4168
node.handleError(err);
4269
return null;
4370
};
@@ -46,5 +73,14 @@ pub fn register(comptime init_fn: InitFunction) void {
4673
}
4774
};
4875

49-
@export(&module.napi_register_module_v1, .{ .name = "napi_register_module_v1" });
76+
registerModule(&module.napi_register_module_v1);
77+
}
78+
79+
/// The InitFunction to pass to the `register` method. The `ctx` parameter
80+
/// represents the Node context. The returned value becomes the `exports` value
81+
/// of the JS module.
82+
pub const InitFunction = fn (ctx: NodeContext) anyerror!?NodeValue;
83+
84+
inline fn registerModule(comptime ptr: *const anyopaque) void {
85+
@export(ptr, .{ .name = "napi_register_module_v1" });
5086
}

tests/README.md

Lines changed: 0 additions & 3 deletions
This file was deleted.

tests/bun.lock

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"lockfileVersion": 1,
3+
"workspaces": {
4+
"": {
5+
"name": "test",
6+
"dependencies": {
7+
"bun-types": "^1.3.0",
8+
},
9+
},
10+
},
11+
"packages": {
12+
"@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="],
13+
14+
"@types/react": ["@types/[email protected]", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
15+
16+
"bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
17+
18+
"csstype": ["[email protected]", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
19+
20+
"undici-types": ["[email protected]", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
21+
}
22+
}

tests/zig_modules/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
The `zig_modules` folder contains a folder per test zig module.
2+
3+
`/tests/zig_modules/{mod}/src/root.zig`
4+
5+
is compiled to
6+
7+
`/test/zig_modules/{mod}.node`
8+
9+
and can be imported in a test using:
10+
11+
```TypeScript
12+
import requireTestModule from "../";
13+
14+
const addon = requireTestModule("{mod}");
15+
16+
```
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { describe, it, expect } from "bun:test";
2+
3+
import requireTestModule from "../";
4+
const encrypt = requireTestModule("allocators");
5+
6+
describe("allocator", () => {
7+
it("should get allocator", () => {
8+
expect(encrypt("secret", { char: 88 })).toEqual("XXXXXX");
9+
});
10+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
pub fn init() @This() {
2+
return .{};
3+
}
4+
5+
key: u32,
6+
7+
pub fn deinit(_: @This()) !void {}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const std = @import("std");
2+
const node_api = @import("node-api");
3+
4+
comptime {
5+
node_api.@"export"(encrypt);
6+
}
7+
8+
const Options = struct {
9+
char: u8,
10+
};
11+
12+
// allocator is "injected" by convention
13+
fn encrypt(value: []const u8, options: Options, allocator: std.mem.Allocator) ![]const u8 {
14+
const result = try allocator.alloc(u8, value.len);
15+
errdefer allocator.free(result);
16+
17+
@memset(result, options.char);
18+
19+
return result;
20+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { describe, it, expect } from "bun:test";
2+
3+
import requireTestModule from "../";
4+
const addon = requireTestModule("export-object");
5+
6+
describe("node_api.register(.{})", () => {
7+
it("should return serialized value", () => {
8+
expect(addon).toEqual({ foo: "foo", bar: 123 });
9+
});
10+
});

0 commit comments

Comments
 (0)