zignapi 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build.js +40 -9
- package/dist/new.js +1 -4
- package/dist/zig.js +4 -0
- package/native/build.zig +13 -12
- package/native/build.zig.zon +1 -1
- package/native/src/_check.zig +21 -0
- package/native/src/async.zig +204 -10
- package/native/src/napi.zig +22 -0
- package/native/src/register.zig +43 -11
- package/native/src/typedefs.zig +89 -0
- package/native/src/zignapi.zig +7 -0
- package/package.json +1 -1
- package/templates/dot-gitignore +4 -0
- package/templates/package.json +2 -0
- package/templates/src/main.zig +7 -0
- package/templates/test.js +15 -1
package/dist/build.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
-
import { existsSync, copyFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { existsSync, copyFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join, basename, extname } from "node:path";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
4
5
|
import process from "node:process";
|
|
5
|
-
import { runZig } from "./zig.js";
|
|
6
|
+
import { runZig, errMessage } from "./zig.js";
|
|
6
7
|
const BUILD_HELP = `zignapi build — build the addon in the current directory
|
|
7
8
|
|
|
8
9
|
Usage:
|
|
@@ -12,8 +13,9 @@ Options:
|
|
|
12
13
|
--release Optimize the build (-Doptimize=ReleaseFast)
|
|
13
14
|
-h, --help Show this help
|
|
14
15
|
|
|
15
|
-
Runs "zig build" in the current directory,
|
|
16
|
-
|
|
16
|
+
Runs "zig build" in the current directory, copies the resulting shared library
|
|
17
|
+
to ./<name>.node (renaming a plain .dylib/.so/.dll if needed), then generates
|
|
18
|
+
index.js and index.d.ts from the addon's embedded type declarations.
|
|
17
19
|
`;
|
|
18
20
|
export async function runBuild(argv) {
|
|
19
21
|
const { values } = parseArgs({
|
|
@@ -36,13 +38,42 @@ export async function runBuild(argv) {
|
|
|
36
38
|
zigArgs.push("-Doptimize=ReleaseFast");
|
|
37
39
|
runZig(cwd, zigArgs, { repairZon: join(cwd, "build.zig.zon") });
|
|
38
40
|
// Locate the freshly built addon and copy it to ./<name>.node.
|
|
39
|
-
const
|
|
40
|
-
if (!
|
|
41
|
+
const addonPath = findAddon(join(cwd, "zig-out"));
|
|
42
|
+
if (!addonPath) {
|
|
41
43
|
throw new Error("build succeeded but no addon (.node or shared library) was found under zig-out");
|
|
42
44
|
}
|
|
43
|
-
const
|
|
44
|
-
copyFileSync(
|
|
45
|
-
process.stdout.write(`✔ built ${
|
|
45
|
+
const nodeFile = addonName(addonPath);
|
|
46
|
+
copyFileSync(addonPath, join(cwd, nodeFile));
|
|
47
|
+
process.stdout.write(`✔ built ${nodeFile}\n`);
|
|
48
|
+
generateBindings(cwd, nodeFile);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Emit `index.js` (a loader that re-exports the addon) and `index.d.ts` (the
|
|
52
|
+
* TypeScript declarations) from the type info the addon embeds as
|
|
53
|
+
* `__zignapi_dts__`. Non-fatal: a build that can't be introspected still counts
|
|
54
|
+
* as a successful build.
|
|
55
|
+
*/
|
|
56
|
+
function generateBindings(cwd, nodeFile) {
|
|
57
|
+
const require = createRequire(import.meta.url);
|
|
58
|
+
let addon;
|
|
59
|
+
try {
|
|
60
|
+
addon = require(join(cwd, nodeFile));
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
process.stderr.write(`note: skipped type generation (could not load ${nodeFile}: ${errMessage(err)})\n`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const dts = addon.__zignapi_dts__;
|
|
67
|
+
if (typeof dts !== "string" || dts.length === 0) {
|
|
68
|
+
process.stderr.write("note: addon has no embedded type info; skipping index.d.ts\n");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
writeFileSync(join(cwd, "index.d.ts"), `// Generated by zignapi — do not edit.\n\n${dts}`);
|
|
72
|
+
writeFileSync(join(cwd, "index.js"), `"use strict";\n` +
|
|
73
|
+
`const addon = require("./${nodeFile}");\n` +
|
|
74
|
+
`const { __zignapi_dts__, ...api } = addon;\n` +
|
|
75
|
+
`module.exports = api;\n`);
|
|
76
|
+
process.stdout.write("✔ generated index.js + index.d.ts\n");
|
|
46
77
|
}
|
|
47
78
|
/**
|
|
48
79
|
* Find the built addon under zig-out. Prefers an already-named `.node`,
|
package/dist/new.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from
|
|
|
3
3
|
import { join, dirname } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import process from "node:process";
|
|
6
|
-
import { releaseUrl, resolveZignapiSources, runZig } from "./zig.js";
|
|
6
|
+
import { releaseUrl, resolveZignapiSources, runZig, errMessage } from "./zig.js";
|
|
7
7
|
// The template tree lives at the package root (as `templates/`), so it resolves
|
|
8
8
|
// the same whether we run from the compiled `dist/` or the source `src/`.
|
|
9
9
|
const TEMPLATES_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "templates");
|
|
@@ -113,6 +113,3 @@ function validateName(name) {
|
|
|
113
113
|
`must not start with a digit)`);
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
|
-
function errMessage(err) {
|
|
117
|
-
return err instanceof Error ? err.message : String(err);
|
|
118
|
-
}
|
package/dist/zig.js
CHANGED
|
@@ -64,6 +64,10 @@ export function runZig(cwd, args, { repairZon } = {}) {
|
|
|
64
64
|
}
|
|
65
65
|
return res;
|
|
66
66
|
}
|
|
67
|
+
/** Extract a human-readable message from an unknown thrown value. */
|
|
68
|
+
export function errMessage(err) {
|
|
69
|
+
return err instanceof Error ? err.message : String(err);
|
|
70
|
+
}
|
|
67
71
|
/** Insert or replace the `.fingerprint` field in a build.zig.zon. */
|
|
68
72
|
export function patchFingerprint(zonPath, value) {
|
|
69
73
|
if (!existsSync(zonPath))
|
package/native/build.zig
CHANGED
|
@@ -42,17 +42,18 @@ pub fn build(b: *std.Build) void {
|
|
|
42
42
|
});
|
|
43
43
|
b.getInstallStep().dependOn(&check_obj.step);
|
|
44
44
|
|
|
45
|
-
// Unit tests: only
|
|
46
|
-
//
|
|
47
|
-
const tests_mod = b.createModule(.{
|
|
48
|
-
.root_source_file = b.path("src/convert.zig"),
|
|
49
|
-
.target = target,
|
|
50
|
-
.optimize = optimize,
|
|
51
|
-
.link_libc = true,
|
|
52
|
-
});
|
|
53
|
-
tests_mod.addIncludePath(headers);
|
|
54
|
-
const unit_tests = b.addTest(.{ .root_module = tests_mod });
|
|
55
|
-
const run_tests = b.addRunArtifact(unit_tests);
|
|
45
|
+
// Unit tests: only pure-comptime code (no N-API runtime calls), so the test
|
|
46
|
+
// binaries link without Node present.
|
|
56
47
|
const test_step = b.step("test", "Run zignapi unit tests");
|
|
57
|
-
|
|
48
|
+
for ([_][]const u8{ "src/convert.zig", "src/typedefs.zig" }) |root| {
|
|
49
|
+
const tests_mod = b.createModule(.{
|
|
50
|
+
.root_source_file = b.path(root),
|
|
51
|
+
.target = target,
|
|
52
|
+
.optimize = optimize,
|
|
53
|
+
.link_libc = true,
|
|
54
|
+
});
|
|
55
|
+
tests_mod.addIncludePath(headers);
|
|
56
|
+
const unit_tests = b.addTest(.{ .root_module = tests_mod });
|
|
57
|
+
test_step.dependOn(&b.addRunArtifact(unit_tests).step);
|
|
58
|
+
}
|
|
58
59
|
}
|
package/native/build.zig.zon
CHANGED
package/native/src/_check.zig
CHANGED
|
@@ -16,6 +16,9 @@ comptime {
|
|
|
16
16
|
_ = napi.getCallbackInfo;
|
|
17
17
|
_ = napi.setNamedProperty;
|
|
18
18
|
_ = napi.createFunction;
|
|
19
|
+
_ = napi.createString;
|
|
20
|
+
_ = napi.getUndefined;
|
|
21
|
+
_ = napi.createError;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
fn add(a: i32, b: i32) i32 {
|
|
@@ -34,6 +37,21 @@ fn checked(x: i32) !i32 {
|
|
|
34
37
|
return if (x < 0) error.MustBeNonNegative else x;
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
// Async: runs on the thread pool, returns a Promise.
|
|
41
|
+
fn heavy(a: i32, b: i32) i32 {
|
|
42
|
+
return a * b;
|
|
43
|
+
}
|
|
44
|
+
fn heavyChecked(x: i32) !i32 {
|
|
45
|
+
return if (x < 0) error.MustBeNonNegative else x;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Raw passthrough: `napi.Env` / `napi.Value` params + a threadsafe function.
|
|
49
|
+
fn onEvent(env: napi.Env, callback: napi.Value) void {
|
|
50
|
+
const tsfn = zignapi.ThreadsafeFunction(i32).create(env, callback) catch return;
|
|
51
|
+
tsfn.call(1) catch {};
|
|
52
|
+
tsfn.release();
|
|
53
|
+
}
|
|
54
|
+
|
|
37
55
|
comptime {
|
|
38
56
|
zignapi.register(.{
|
|
39
57
|
.add = add,
|
|
@@ -41,5 +59,8 @@ comptime {
|
|
|
41
59
|
.scale = scale,
|
|
42
60
|
.echo = echo,
|
|
43
61
|
.checked = checked,
|
|
62
|
+
.heavy = zignapi.asyncFn(heavy),
|
|
63
|
+
.heavyChecked = zignapi.asyncFn(heavyChecked),
|
|
64
|
+
.onEvent = onEvent,
|
|
44
65
|
});
|
|
45
66
|
}
|
package/native/src/async.zig
CHANGED
|
@@ -1,15 +1,209 @@
|
|
|
1
|
-
//! Async support
|
|
1
|
+
//! Async support: run a Zig function on libuv's thread pool and resolve a JS
|
|
2
|
+
//! `Promise`, plus a threadsafe-function primitive for calling JS from any
|
|
3
|
+
//! thread.
|
|
2
4
|
//!
|
|
3
|
-
//!
|
|
4
|
-
//!
|
|
5
|
-
//!
|
|
6
|
-
//!
|
|
7
|
-
//!
|
|
5
|
+
//! ## Async functions
|
|
6
|
+
//!
|
|
7
|
+
//! Wrap a function with `asyncFn` when registering it. Its arguments are
|
|
8
|
+
//! converted on the JS thread, the function body runs on a worker thread (so it
|
|
9
|
+
//! must NOT touch JS / N-API), and the result (or a thrown error) settles the
|
|
10
|
+
//! promise back on the JS thread:
|
|
11
|
+
//!
|
|
12
|
+
//! ```zig
|
|
13
|
+
//! fn heavy(a: i32, b: i32) i32 { return a * b; }
|
|
14
|
+
//! comptime { zignapi.register(.{ .heavy = zignapi.asyncFn(heavy) }); }
|
|
15
|
+
//! // JS: await addon.heavy(6, 7) === 42
|
|
16
|
+
//! ```
|
|
17
|
+
//!
|
|
18
|
+
//! ## Threadsafe functions
|
|
19
|
+
//!
|
|
20
|
+
//! `ThreadsafeFunction(T)` lets native code call a JS callback from any thread.
|
|
21
|
+
//! Create it on the JS thread from a callback the addon received as a raw
|
|
22
|
+
//! `napi.Value`, hand it to a worker thread, `call` it, then `release` it.
|
|
8
23
|
|
|
9
24
|
const std = @import("std");
|
|
25
|
+
const napi = @import("napi.zig");
|
|
26
|
+
const convert = @import("convert.zig");
|
|
27
|
+
const c = napi.c;
|
|
28
|
+
|
|
29
|
+
/// Mark a function as async when registering it (runs on the thread pool and
|
|
30
|
+
/// returns a `Promise`). See the module docs.
|
|
31
|
+
pub fn asyncFn(comptime f: anytype) AsyncMarker(f) {
|
|
32
|
+
return .{};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fn AsyncMarker(comptime f: anytype) type {
|
|
36
|
+
return struct {
|
|
37
|
+
pub const zignapi_async_task = true;
|
|
38
|
+
pub const func = f;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Whether a `register` field value's type is an `asyncFn(...)` marker.
|
|
43
|
+
pub fn isAsyncMarker(comptime FieldT: type) bool {
|
|
44
|
+
return switch (@typeInfo(FieldT)) {
|
|
45
|
+
.@"struct" => @hasDecl(FieldT, "zignapi_async_task"),
|
|
46
|
+
else => false,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Build the `napi_callback` trampoline for an async function: it returns a
|
|
51
|
+
/// Promise immediately and does the work on a libuv worker thread.
|
|
52
|
+
pub fn AsyncWrap(comptime func: anytype) type {
|
|
53
|
+
const fn_info = @typeInfo(@TypeOf(func)).@"fn";
|
|
54
|
+
const RetType = fn_info.return_type.?;
|
|
55
|
+
const Payload = switch (@typeInfo(RetType)) {
|
|
56
|
+
.error_union => |eu| eu.payload,
|
|
57
|
+
else => RetType,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const Task = struct {
|
|
61
|
+
args: std.meta.ArgsTuple(@TypeOf(func)),
|
|
62
|
+
arena: std.heap.ArenaAllocator,
|
|
63
|
+
deferred: c.napi_deferred,
|
|
64
|
+
work: c.napi_async_work,
|
|
65
|
+
payload: Payload,
|
|
66
|
+
err_name: ?[:0]const u8,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return struct {
|
|
70
|
+
pub fn callback(env: napi.Env, info: napi.CallbackInfo) callconv(.c) napi.Value {
|
|
71
|
+
var argv: [fn_info.params.len]napi.Value = undefined;
|
|
72
|
+
_ = napi.getCallbackInfo(env, info, &argv) catch {
|
|
73
|
+
napi.throwError(env, "zignapi: failed to read arguments");
|
|
74
|
+
return null;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const task = std.heap.c_allocator.create(Task) catch {
|
|
78
|
+
napi.throwError(env, "zignapi: out of memory");
|
|
79
|
+
return null;
|
|
80
|
+
};
|
|
81
|
+
task.arena = std.heap.ArenaAllocator.init(std.heap.c_allocator);
|
|
82
|
+
task.err_name = null;
|
|
83
|
+
|
|
84
|
+
// Convert arguments on the JS thread (N-API access is valid here).
|
|
85
|
+
const allocator = task.arena.allocator();
|
|
86
|
+
inline for (fn_info.params, 0..) |param, i| {
|
|
87
|
+
task.args[i] = convert.fromJs(param.type.?, env, argv[i], allocator) catch
|
|
88
|
+
return abort(task, env, "zignapi: failed to convert argument");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
var promise: napi.Value = undefined;
|
|
92
|
+
if (c.napi_create_promise(env, &task.deferred, &promise) != c.napi_ok)
|
|
93
|
+
return abort(task, env, "zignapi: failed to create promise");
|
|
94
|
+
|
|
95
|
+
const name = napi.createString(env, "zignapi_async_task") catch
|
|
96
|
+
return abort(task, env, "zignapi: failed to create async work");
|
|
97
|
+
|
|
98
|
+
_ = c.napi_create_async_work(env, null, name, execute, complete, task, &task.work);
|
|
99
|
+
_ = c.napi_queue_async_work(env, task.work);
|
|
100
|
+
return promise;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Free a partially-initialised task, throw, and return null (on the JS
|
|
104
|
+
/// thread, before the work is queued).
|
|
105
|
+
fn abort(task: *Task, env: napi.Env, msg: [:0]const u8) napi.Value {
|
|
106
|
+
task.arena.deinit();
|
|
107
|
+
std.heap.c_allocator.destroy(task);
|
|
108
|
+
napi.throwError(env, msg);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// Worker thread — no N-API / JS access allowed.
|
|
113
|
+
fn execute(env: napi.Env, data: ?*anyopaque) callconv(.c) void {
|
|
114
|
+
_ = env;
|
|
115
|
+
const task: *Task = @ptrCast(@alignCast(data.?));
|
|
116
|
+
switch (@typeInfo(RetType)) {
|
|
117
|
+
.error_union => task.payload = @call(.auto, func, task.args) catch |err| {
|
|
118
|
+
task.err_name = @errorName(err);
|
|
119
|
+
return;
|
|
120
|
+
},
|
|
121
|
+
else => task.payload = @call(.auto, func, task.args),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// JS thread — settle the promise with the result or the error.
|
|
126
|
+
fn complete(env: napi.Env, status: c.napi_status, data: ?*anyopaque) callconv(.c) void {
|
|
127
|
+
const task: *Task = @ptrCast(@alignCast(data.?));
|
|
128
|
+
defer {
|
|
129
|
+
task.arena.deinit();
|
|
130
|
+
_ = c.napi_delete_async_work(env, task.work);
|
|
131
|
+
std.heap.c_allocator.destroy(task);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (status != c.napi_ok) return reject(env, task.deferred, "zignapi: async work cancelled");
|
|
135
|
+
if (task.err_name) |name| return reject(env, task.deferred, name);
|
|
136
|
+
|
|
137
|
+
const resolution = if (Payload == void)
|
|
138
|
+
(napi.getUndefined(env) catch return reject(env, task.deferred, "zignapi: internal error"))
|
|
139
|
+
else
|
|
140
|
+
(convert.toJs(Payload, env, task.payload) catch
|
|
141
|
+
return reject(env, task.deferred, "zignapi: failed to convert result"));
|
|
142
|
+
_ = c.napi_resolve_deferred(env, task.deferred, resolution);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fn reject(env: napi.Env, deferred: c.napi_deferred, msg: []const u8) void {
|
|
148
|
+
const err = napi.createError(env, msg) catch return;
|
|
149
|
+
_ = c.napi_reject_deferred(env, deferred, err);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// A threadsafe function: a JS callback that native code can invoke from any
|
|
153
|
+
/// thread. `T` is the Zig type of the single argument passed to the callback
|
|
154
|
+
/// (converted to JS via `convert`). Create it on the JS thread, use it from
|
|
155
|
+
/// worker threads, and `release` it when done.
|
|
156
|
+
pub fn ThreadsafeFunction(comptime T: type) type {
|
|
157
|
+
return struct {
|
|
158
|
+
const Self = @This();
|
|
159
|
+
|
|
160
|
+
handle: c.napi_threadsafe_function,
|
|
161
|
+
|
|
162
|
+
/// Create from a JS callback value. Call on the JS thread.
|
|
163
|
+
pub fn create(env: napi.Env, js_callback: napi.Value) napi.Error!Self {
|
|
164
|
+
const name = try napi.createString(env, "zignapi_tsfn");
|
|
165
|
+
var handle: c.napi_threadsafe_function = undefined;
|
|
166
|
+
try napi.check(c.napi_create_threadsafe_function(
|
|
167
|
+
env,
|
|
168
|
+
js_callback,
|
|
169
|
+
null, // async_resource
|
|
170
|
+
name,
|
|
171
|
+
0, // max_queue_size: unlimited
|
|
172
|
+
1, // initial_thread_count
|
|
173
|
+
null, // thread_finalize_data
|
|
174
|
+
null, // thread_finalize_cb
|
|
175
|
+
null, // context
|
|
176
|
+
callJs,
|
|
177
|
+
&handle,
|
|
178
|
+
));
|
|
179
|
+
return .{ .handle = handle };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/// Queue a call to the JS callback with `value`. Safe from any thread.
|
|
183
|
+
pub fn call(self: Self, value: T) napi.Error!void {
|
|
184
|
+
const boxed = std.heap.c_allocator.create(T) catch return napi.Error.NapiFailure;
|
|
185
|
+
boxed.* = value;
|
|
186
|
+
if (c.napi_call_threadsafe_function(self.handle, boxed, c.napi_tsfn_blocking) != c.napi_ok) {
|
|
187
|
+
std.heap.c_allocator.destroy(boxed);
|
|
188
|
+
return napi.Error.NapiFailure;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// Release the reference so Node can tear the function down. Any thread.
|
|
193
|
+
pub fn release(self: Self) void {
|
|
194
|
+
_ = c.napi_release_threadsafe_function(self.handle, c.napi_tsfn_release);
|
|
195
|
+
}
|
|
10
196
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
197
|
+
/// JS thread — convert the queued value and invoke the callback.
|
|
198
|
+
fn callJs(env: napi.Env, js_cb: napi.Value, context: ?*anyopaque, data: ?*anyopaque) callconv(.c) void {
|
|
199
|
+
_ = context;
|
|
200
|
+
const boxed: *T = @ptrCast(@alignCast(data.?));
|
|
201
|
+
defer std.heap.c_allocator.destroy(boxed);
|
|
202
|
+
if (env == null) return; // the environment is tearing down
|
|
203
|
+
const arg = convert.toJs(T, env, boxed.*) catch return;
|
|
204
|
+
const recv = napi.getUndefined(env) catch return;
|
|
205
|
+
var argv = [_]napi.Value{arg};
|
|
206
|
+
_ = c.napi_call_function(env, recv, js_cb, argv.len, &argv, null);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
15
209
|
}
|
package/native/src/napi.zig
CHANGED
|
@@ -60,3 +60,25 @@ pub fn createFunction(env: Env, name: [:0]const u8, cb: Callback) Error!Value {
|
|
|
60
60
|
try check(c.napi_create_function(env, name.ptr, name.len, cb, null, &result));
|
|
61
61
|
return result;
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
/// Create a JS UTF-8 string from a Zig slice.
|
|
65
|
+
pub fn createString(env: Env, s: []const u8) Error!Value {
|
|
66
|
+
var result: Value = undefined;
|
|
67
|
+
try check(c.napi_create_string_utf8(env, s.ptr, s.len, &result));
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// The JS `undefined` value.
|
|
72
|
+
pub fn getUndefined(env: Env) Error!Value {
|
|
73
|
+
var result: Value = undefined;
|
|
74
|
+
try check(c.napi_get_undefined(env, &result));
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Create a JS `Error` object carrying `msg` (used to reject promises).
|
|
79
|
+
pub fn createError(env: Env, msg: []const u8) Error!Value {
|
|
80
|
+
const message = try createString(env, msg);
|
|
81
|
+
var result: Value = undefined;
|
|
82
|
+
try check(c.napi_create_error(env, null, message, &result));
|
|
83
|
+
return result;
|
|
84
|
+
}
|
package/native/src/register.zig
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
const std = @import("std");
|
|
13
13
|
const napi = @import("napi.zig");
|
|
14
14
|
const convert = @import("convert.zig");
|
|
15
|
+
const typedefs = @import("typedefs.zig");
|
|
16
|
+
const asyncwork = @import("async.zig");
|
|
15
17
|
const c = napi.c;
|
|
16
18
|
|
|
17
19
|
/// Export `napi_register_module_v1` for the given set of functions.
|
|
@@ -27,15 +29,23 @@ pub fn register(comptime defs: anytype) void {
|
|
|
27
29
|
else => @compileError("zignapi.register expects a struct literal, e.g. .{ .add = add }"),
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
// TypeScript declarations for this module, generated at comptime and
|
|
33
|
+
// embedded so `zignapi build` can emit `index.d.ts` / `index.js`.
|
|
34
|
+
const dts = typedefs.declarations(defs);
|
|
35
|
+
|
|
30
36
|
const Registrar = struct {
|
|
31
37
|
fn entry(env: napi.Env, exports: napi.Value) callconv(.c) napi.Value {
|
|
32
38
|
inline for (@typeInfo(Defs).@"struct".fields) |field| {
|
|
33
|
-
|
|
34
|
-
registerOne(env, exports, field.name, func) catch {
|
|
39
|
+
registerField(env, exports, field.name, @field(defs, field.name)) catch {
|
|
35
40
|
napi.throwError(env, "zignapi: failed to register '" ++ field.name ++ "'");
|
|
36
41
|
return null;
|
|
37
42
|
};
|
|
38
43
|
}
|
|
44
|
+
// Best-effort: attach the type declarations. Failure here must not
|
|
45
|
+
// break a working module, so ignore errors.
|
|
46
|
+
if (napi.createString(env, dts)) |v| {
|
|
47
|
+
napi.setNamedProperty(env, exports, "__zignapi_dts__", v) catch {};
|
|
48
|
+
} else |_| {}
|
|
39
49
|
return exports;
|
|
40
50
|
}
|
|
41
51
|
};
|
|
@@ -43,17 +53,27 @@ pub fn register(comptime defs: anytype) void {
|
|
|
43
53
|
@export(&Registrar.entry, .{ .name = "napi_register_module_v1", .linkage = .strong });
|
|
44
54
|
}
|
|
45
55
|
|
|
46
|
-
/// Bind a single
|
|
47
|
-
|
|
56
|
+
/// Bind a single field onto `exports` under `name`. The field is either a plain
|
|
57
|
+
/// function (registered synchronously) or an `asyncFn(...)` marker (registered
|
|
58
|
+
/// as an async, promise-returning function).
|
|
59
|
+
fn registerField(
|
|
48
60
|
env: napi.Env,
|
|
49
61
|
exports: napi.Value,
|
|
50
62
|
comptime name: [:0]const u8,
|
|
51
|
-
comptime
|
|
63
|
+
comptime field_val: anytype,
|
|
52
64
|
) !void {
|
|
53
|
-
const fn_value = try napi.createFunction(env, name,
|
|
65
|
+
const fn_value = try napi.createFunction(env, name, callbackFor(field_val));
|
|
54
66
|
try napi.setNamedProperty(env, exports, name, fn_value);
|
|
55
67
|
}
|
|
56
68
|
|
|
69
|
+
fn callbackFor(comptime field_val: anytype) napi.Callback {
|
|
70
|
+
const FieldT = @TypeOf(field_val);
|
|
71
|
+
if (comptime asyncwork.isAsyncMarker(FieldT)) {
|
|
72
|
+
return asyncwork.AsyncWrap(FieldT.func).callback;
|
|
73
|
+
}
|
|
74
|
+
return Wrap(field_val).callback;
|
|
75
|
+
}
|
|
76
|
+
|
|
57
77
|
/// Build the `napi_callback` trampoline for a specific Zig function.
|
|
58
78
|
///
|
|
59
79
|
/// Each distinct `func` produces its own type (and therefore its own callback),
|
|
@@ -77,13 +97,25 @@ fn Wrap(comptime func: anytype) type {
|
|
|
77
97
|
defer arena.deinit();
|
|
78
98
|
const allocator = arena.allocator();
|
|
79
99
|
|
|
80
|
-
// Convert each argument from JS into its Zig type.
|
|
100
|
+
// Convert each argument from JS into its Zig type. `napi.Env` and
|
|
101
|
+
// `napi.Value` parameters are passed through raw and don't consume a
|
|
102
|
+
// JS argument (like napi-rs's `Env`), so track the JS index separately.
|
|
81
103
|
var args: std.meta.ArgsTuple(@TypeOf(func)) = undefined;
|
|
104
|
+
comptime var js_arg: usize = 0;
|
|
82
105
|
inline for (fn_info.params, 0..) |param, i| {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
106
|
+
const P = param.type.?;
|
|
107
|
+
if (P == napi.Env) {
|
|
108
|
+
args[i] = env;
|
|
109
|
+
} else if (P == napi.Value) {
|
|
110
|
+
args[i] = argv[js_arg];
|
|
111
|
+
js_arg += 1;
|
|
112
|
+
} else {
|
|
113
|
+
args[i] = convert.fromJs(P, env, argv[js_arg], allocator) catch {
|
|
114
|
+
napi.throwError(env, "zignapi: failed to convert argument");
|
|
115
|
+
return null;
|
|
116
|
+
};
|
|
117
|
+
js_arg += 1;
|
|
118
|
+
}
|
|
87
119
|
}
|
|
88
120
|
|
|
89
121
|
const result = @call(.auto, func, args);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
//! Comptime generation of TypeScript declarations for the registered functions.
|
|
2
|
+
//!
|
|
3
|
+
//! The mapping reuses `convert.classify`, so the `.d.ts` and the runtime
|
|
4
|
+
//! conversions can never drift apart. `register.zig` embeds the produced string
|
|
5
|
+
//! in the addon (as the `__zignapi_dts__` export); `zignapi build` reads it back
|
|
6
|
+
//! and writes `index.d.ts` / `index.js`.
|
|
7
|
+
|
|
8
|
+
const std = @import("std");
|
|
9
|
+
const napi = @import("napi.zig");
|
|
10
|
+
const convert = @import("convert.zig");
|
|
11
|
+
const asyncwork = @import("async.zig");
|
|
12
|
+
|
|
13
|
+
/// The TypeScript type string for a Zig type. `napi.Value` (a raw JS handle,
|
|
14
|
+
/// e.g. a callback) maps to `any`; error unions map to their payload (Zig
|
|
15
|
+
/// errors surface as thrown JS exceptions); `void` maps to `void`.
|
|
16
|
+
pub fn tsType(comptime T: type) []const u8 {
|
|
17
|
+
if (T == napi.Value) return "any";
|
|
18
|
+
return switch (@typeInfo(T)) {
|
|
19
|
+
.void => "void",
|
|
20
|
+
.error_union => |eu| tsType(eu.payload),
|
|
21
|
+
else => switch (convert.classify(T)) {
|
|
22
|
+
.int, .float => "number",
|
|
23
|
+
.bool => "boolean",
|
|
24
|
+
.string => "string",
|
|
25
|
+
.unsupported => @compileError(
|
|
26
|
+
"zignapi: no TypeScript mapping for '" ++ @typeName(T) ++ "'",
|
|
27
|
+
),
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Build the `.d.ts` body: one `export function` line per registered function.
|
|
33
|
+
/// `napi.Env` parameters are skipped (they aren't JS arguments); `asyncFn`
|
|
34
|
+
/// functions return `Promise<T>`. Parameter names are `arg0`, `arg1`, …
|
|
35
|
+
pub fn declarations(comptime defs: anytype) []const u8 {
|
|
36
|
+
comptime var out: []const u8 = "";
|
|
37
|
+
inline for (@typeInfo(@TypeOf(defs)).@"struct".fields) |field| {
|
|
38
|
+
const field_val = @field(defs, field.name);
|
|
39
|
+
const is_async = asyncwork.isAsyncMarker(@TypeOf(field_val));
|
|
40
|
+
const func = if (is_async) @TypeOf(field_val).func else field_val;
|
|
41
|
+
const fn_info = @typeInfo(@TypeOf(func)).@"fn";
|
|
42
|
+
|
|
43
|
+
comptime var params: []const u8 = "";
|
|
44
|
+
comptime var js_i: usize = 0;
|
|
45
|
+
inline for (fn_info.params) |param| {
|
|
46
|
+
const P = param.type.?;
|
|
47
|
+
if (P == napi.Env) continue; // not a JS argument
|
|
48
|
+
const sep = if (js_i == 0) "" else ", ";
|
|
49
|
+
params = params ++ sep ++ std.fmt.comptimePrint("arg{d}: {s}", .{ js_i, tsType(P) });
|
|
50
|
+
js_i += 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const ret = tsType(fn_info.return_type.?);
|
|
54
|
+
const ret_str = if (is_async) "Promise<" ++ ret ++ ">" else ret;
|
|
55
|
+
out = out ++ "export function " ++ field.name ++ "(" ++ params ++ "): " ++ ret_str ++ ";\n";
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test "declarations render sync, async and passthrough signatures" {
|
|
61
|
+
const S = struct {
|
|
62
|
+
fn add(a: i32, b: i32) i32 {
|
|
63
|
+
return a + b;
|
|
64
|
+
}
|
|
65
|
+
fn greet(name: []const u8) []const u8 {
|
|
66
|
+
return name;
|
|
67
|
+
}
|
|
68
|
+
fn heavy(a: i32, b: i32) i32 {
|
|
69
|
+
return a * b;
|
|
70
|
+
}
|
|
71
|
+
fn onEvent(env: napi.Env, cb: napi.Value) void {
|
|
72
|
+
_ = env;
|
|
73
|
+
_ = cb;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const dts = comptime declarations(.{
|
|
77
|
+
.add = S.add,
|
|
78
|
+
.greet = S.greet,
|
|
79
|
+
.heavy = asyncwork.asyncFn(S.heavy),
|
|
80
|
+
.onEvent = S.onEvent,
|
|
81
|
+
});
|
|
82
|
+
try std.testing.expectEqualStrings(
|
|
83
|
+
\\export function add(arg0: number, arg1: number): number;
|
|
84
|
+
\\export function greet(arg0: string): string;
|
|
85
|
+
\\export function heavy(arg0: number, arg1: number): Promise<number>;
|
|
86
|
+
\\export function onEvent(arg0: any): void;
|
|
87
|
+
\\
|
|
88
|
+
, dts);
|
|
89
|
+
}
|
package/native/src/zignapi.zig
CHANGED
|
@@ -12,11 +12,18 @@ const std = @import("std");
|
|
|
12
12
|
|
|
13
13
|
pub const napi = @import("napi.zig");
|
|
14
14
|
pub const convert = @import("convert.zig");
|
|
15
|
+
pub const typedefs = @import("typedefs.zig");
|
|
15
16
|
pub const async_ = @import("async.zig");
|
|
16
17
|
|
|
17
18
|
/// Register Zig functions as a Node addon. See `register.zig`.
|
|
18
19
|
pub const register = @import("register.zig").register;
|
|
19
20
|
|
|
21
|
+
/// Mark a function as async (runs on libuv's thread pool, returns a `Promise`).
|
|
22
|
+
pub const asyncFn = async_.asyncFn;
|
|
23
|
+
|
|
24
|
+
/// A JS callback callable from any thread. See `async.zig`.
|
|
25
|
+
pub const ThreadsafeFunction = async_.ThreadsafeFunction;
|
|
26
|
+
|
|
20
27
|
/// The raw imported N-API C API (`napi_*` functions and types).
|
|
21
28
|
pub const c = napi.c;
|
|
22
29
|
|
package/package.json
CHANGED
package/templates/dot-gitignore
CHANGED
package/templates/package.json
CHANGED
package/templates/src/main.zig
CHANGED
|
@@ -11,11 +11,18 @@ pub fn greet(name: []const u8) []const u8 {
|
|
|
11
11
|
return "hello from Zig";
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/// Async — `zignapi.asyncFn` runs it on a worker thread and returns a
|
|
15
|
+
/// `Promise`. The body must not touch JS (it runs off the JS thread).
|
|
16
|
+
fn slowSquare(x: i32) i32 {
|
|
17
|
+
return x * x;
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
// Emits `napi_register_module_v1`. Each field becomes a property on the addon's
|
|
15
21
|
// `exports`. Add your own Zig functions here.
|
|
16
22
|
comptime {
|
|
17
23
|
zignapi.register(.{
|
|
18
24
|
.add = add,
|
|
19
25
|
.greet = greet,
|
|
26
|
+
.slowSquare = zignapi.asyncFn(slowSquare),
|
|
20
27
|
});
|
|
21
28
|
}
|
package/templates/test.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
const test = require("node:test");
|
|
2
2
|
const assert = require("node:assert");
|
|
3
|
+
const fs = require("node:fs");
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
// The generated loader (index.js) re-exports the addon; index.d.ts gives it types.
|
|
6
|
+
// Both are produced by `zignapi build`, so build before running the tests.
|
|
7
|
+
const addon = require("./index.js");
|
|
5
8
|
|
|
6
9
|
test("add(2, 3) === 5", () => {
|
|
7
10
|
assert.strictEqual(addon.add(2, 3), 5);
|
|
@@ -10,3 +13,14 @@ test("add(2, 3) === 5", () => {
|
|
|
10
13
|
test("greet returns a string", () => {
|
|
11
14
|
assert.strictEqual(addon.greet("world"), "hello from Zig");
|
|
12
15
|
});
|
|
16
|
+
|
|
17
|
+
test("slowSquare resolves a Promise", async () => {
|
|
18
|
+
assert.strictEqual(await addon.slowSquare(8), 64);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("index.d.ts declares the exports", () => {
|
|
22
|
+
const dts = fs.readFileSync("./index.d.ts", "utf8");
|
|
23
|
+
assert.match(dts, /export function add\(arg0: number, arg1: number\): number;/);
|
|
24
|
+
assert.match(dts, /export function greet\(arg0: string\): string;/);
|
|
25
|
+
assert.match(dts, /export function slowSquare\(arg0: number\): Promise<number>;/);
|
|
26
|
+
});
|