zignapi 0.1.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/README.md +37 -0
- package/dist/build.js +82 -0
- package/dist/cli.js +35 -0
- package/dist/new.js +118 -0
- package/dist/zig.js +80 -0
- package/native/build.zig +58 -0
- package/native/build.zig.zon +13 -0
- package/native/src/_check.zig +45 -0
- package/native/src/async.zig +15 -0
- package/native/src/convert.zig +119 -0
- package/native/src/napi.zig +62 -0
- package/native/src/register.zig +116 -0
- package/native/src/zigbind.zig +31 -0
- package/native/vendor/node-api-headers/js_native_api.h +591 -0
- package/native/vendor/node-api-headers/js_native_api_types.h +213 -0
- package/native/vendor/node-api-headers/node_api.h +265 -0
- package/native/vendor/node-api-headers/node_api_types.h +58 -0
- package/package.json +36 -0
- package/templates/README.md +32 -0
- package/templates/build.zig +33 -0
- package/templates/build.zig.zon +13 -0
- package/templates/dot-gitignore +4 -0
- package/templates/package.json +9 -0
- package/templates/src/main.zig +21 -0
- package/templates/test.js +12 -0
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# zignapi
|
|
2
|
+
|
|
3
|
+
The CLI for [zigbind](https://github.com/zigbind/zigbind): scaffold and build
|
|
4
|
+
native Node.js addons written in Zig. Written in TypeScript, compiled to ESM with
|
|
5
|
+
**zero runtime dependencies**, no `node-gyp`. Requires **Zig 0.16.0** on `PATH`
|
|
6
|
+
and **Node >= 18**.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install -g zignapi # installs the `zignapi` command
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Commands
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
zignapi new <name> # scaffold ./<name> from the built-in template
|
|
16
|
+
zignapi build [--release] # run `zig build` in the cwd, emit ./<name>.node
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- `new` copies the template tree (substituting the project name) and pins the
|
|
20
|
+
`zigbind` Zig dependency with `zig fetch --save`.
|
|
21
|
+
- `build` runs `zig build` (adding `-Doptimize=ReleaseFast` with `--release`),
|
|
22
|
+
then copies the produced shared library to `./<name>.node`. If `build.zig.zon`
|
|
23
|
+
is missing a fingerprint, it fills in the value Zig suggests and retries.
|
|
24
|
+
|
|
25
|
+
## Development
|
|
26
|
+
|
|
27
|
+
The source is TypeScript under `src/` and compiles to `dist/` with `tsc`.
|
|
28
|
+
TypeScript is a **workspace-root** devDependency (not installed in this package),
|
|
29
|
+
so build through the workspace:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
pnpm install # the root `prepare` script compiles the CLI
|
|
33
|
+
pnpm --filter zignapi build # or rebuild explicitly (runs tsc)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The `bin` points at the compiled `dist/cli.js`; the published package ships
|
|
37
|
+
`dist/`, `templates/`, and a bundled copy of the Zig `native/` sources.
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { existsSync, copyFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { join, basename, extname } from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { runZig } from "./zig.js";
|
|
6
|
+
const BUILD_HELP = `zignapi build — build the addon in the current directory
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
zignapi build [--release]
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
--release Optimize the build (-Doptimize=ReleaseFast)
|
|
13
|
+
-h, --help Show this help
|
|
14
|
+
|
|
15
|
+
Runs "zig build" in the current directory, then copies the resulting shared
|
|
16
|
+
library to ./<name>.node (renaming a plain .dylib/.so/.dll if needed).
|
|
17
|
+
`;
|
|
18
|
+
export async function runBuild(argv) {
|
|
19
|
+
const { values } = parseArgs({
|
|
20
|
+
args: argv,
|
|
21
|
+
options: {
|
|
22
|
+
release: { type: "boolean", default: false },
|
|
23
|
+
help: { type: "boolean", short: "h", default: false },
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
if (values.help) {
|
|
27
|
+
process.stdout.write(BUILD_HELP);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const cwd = process.cwd();
|
|
31
|
+
if (!existsSync(join(cwd, "build.zig"))) {
|
|
32
|
+
throw new Error("no build.zig found in the current directory");
|
|
33
|
+
}
|
|
34
|
+
const zigArgs = ["build"];
|
|
35
|
+
if (values.release)
|
|
36
|
+
zigArgs.push("-Doptimize=ReleaseFast");
|
|
37
|
+
runZig(cwd, zigArgs, { repairZon: join(cwd, "build.zig.zon") });
|
|
38
|
+
// Locate the freshly built addon and copy it to ./<name>.node.
|
|
39
|
+
const addon = findAddon(join(cwd, "zig-out"));
|
|
40
|
+
if (!addon) {
|
|
41
|
+
throw new Error("build succeeded but no addon (.node or shared library) was found under zig-out");
|
|
42
|
+
}
|
|
43
|
+
const dest = join(cwd, addonName(addon));
|
|
44
|
+
copyFileSync(addon, dest);
|
|
45
|
+
process.stdout.write(`✔ built ${basename(dest)}\n`);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Find the built addon under zig-out. Prefers an already-named `.node`,
|
|
49
|
+
* falling back to the first shared library it finds.
|
|
50
|
+
*/
|
|
51
|
+
function findAddon(dir) {
|
|
52
|
+
const nodes = [];
|
|
53
|
+
const libs = [];
|
|
54
|
+
const walk = (d) => {
|
|
55
|
+
if (!existsSync(d))
|
|
56
|
+
return;
|
|
57
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
58
|
+
const p = join(d, entry.name);
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
walk(p);
|
|
61
|
+
}
|
|
62
|
+
else if (entry.isFile()) {
|
|
63
|
+
if (entry.name.endsWith(".node"))
|
|
64
|
+
nodes.push(p);
|
|
65
|
+
else if (/\.(dylib|so|dll)$/.test(entry.name))
|
|
66
|
+
libs.push(p);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
walk(dir);
|
|
71
|
+
return nodes[0] ?? libs[0] ?? null;
|
|
72
|
+
}
|
|
73
|
+
/** The `<name>.node` filename to copy an addon to at the project root. */
|
|
74
|
+
function addonName(addon) {
|
|
75
|
+
if (extname(addon) === ".node")
|
|
76
|
+
return basename(addon);
|
|
77
|
+
// libfoo.dylib -> foo.node
|
|
78
|
+
let name = basename(addon, extname(addon));
|
|
79
|
+
if (name.startsWith("lib"))
|
|
80
|
+
name = name.slice(3);
|
|
81
|
+
return `${name}.node`;
|
|
82
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { runNew } from "./new.js";
|
|
4
|
+
import { runBuild } from "./build.js";
|
|
5
|
+
const HELP = `zignapi — write native Node.js addons in Zig
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
zignapi new <name> Scaffold a new addon project into ./<name>
|
|
9
|
+
zignapi build [--release] Build the addon in the current directory
|
|
10
|
+
zignapi --help Show this help
|
|
11
|
+
|
|
12
|
+
Run "zignapi <command> --help" for command-specific options.
|
|
13
|
+
`;
|
|
14
|
+
async function main() {
|
|
15
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
16
|
+
switch (command) {
|
|
17
|
+
case "new":
|
|
18
|
+
return runNew(rest);
|
|
19
|
+
case "build":
|
|
20
|
+
return runBuild(rest);
|
|
21
|
+
case "-h":
|
|
22
|
+
case "--help":
|
|
23
|
+
case undefined:
|
|
24
|
+
process.stdout.write(HELP);
|
|
25
|
+
return;
|
|
26
|
+
default:
|
|
27
|
+
process.stderr.write(`zignapi: unknown command '${command}'\n\n${HELP}`);
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
main().catch((err) => {
|
|
32
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
33
|
+
process.stderr.write(`zignapi: ${message}\n`);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
});
|
package/dist/new.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { releaseUrl, resolveZigbindSources, runZig } from "./zig.js";
|
|
7
|
+
// The template tree lives at the package root (as `templates/`), so it resolves
|
|
8
|
+
// the same whether we run from the compiled `dist/` or the source `src/`.
|
|
9
|
+
const TEMPLATES_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "templates");
|
|
10
|
+
const PLACEHOLDER = /__NAME__/g;
|
|
11
|
+
const NEW_HELP = `zignapi new — scaffold a new addon project
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
zignapi new <name> [--local]
|
|
15
|
+
|
|
16
|
+
Creates ./<name> from the built-in template, substituting the project name,
|
|
17
|
+
then wires up the zigbind Zig dependency with "zig fetch --save" (pinned by
|
|
18
|
+
content hash). By default it fetches the hosted release tarball, so the project
|
|
19
|
+
is portable across machines/CI; if that's unreachable it falls back to the Zig
|
|
20
|
+
sources bundled with the CLI.
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--local Use the bundled Zig sources instead of the hosted release
|
|
24
|
+
-h, --help Show this help
|
|
25
|
+
|
|
26
|
+
<name> must be a valid Zig identifier (letters, digits, underscore; not
|
|
27
|
+
starting with a digit).
|
|
28
|
+
`;
|
|
29
|
+
export async function runNew(argv) {
|
|
30
|
+
const { values, positionals } = parseArgs({
|
|
31
|
+
args: argv,
|
|
32
|
+
allowPositionals: true,
|
|
33
|
+
options: {
|
|
34
|
+
local: { type: "boolean", default: false },
|
|
35
|
+
help: { type: "boolean", short: "h", default: false },
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
if (values.help || positionals.length === 0) {
|
|
39
|
+
process.stdout.write(NEW_HELP);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const name = positionals[0];
|
|
43
|
+
validateName(name);
|
|
44
|
+
const target = join(process.cwd(), name);
|
|
45
|
+
if (existsSync(target)) {
|
|
46
|
+
throw new Error(`directory '${name}' already exists`);
|
|
47
|
+
}
|
|
48
|
+
copyTemplate(TEMPLATES_DIR, target, name);
|
|
49
|
+
addZigbindDependency(target, { local: values.local ?? false });
|
|
50
|
+
process.stdout.write(`✔ created ${name}/\n\n` +
|
|
51
|
+
`Next steps:\n` +
|
|
52
|
+
` cd ${name}\n` +
|
|
53
|
+
` zignapi build # produces ${name}.node\n` +
|
|
54
|
+
` node --test\n`);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Add the `zigbind` Zig dependency to the freshly scaffolded project via
|
|
58
|
+
* `zig fetch --save`, pinning it by content hash. Prefers the hosted release
|
|
59
|
+
* tarball (portable across machines) and falls back to the Zig sources bundled
|
|
60
|
+
* with the CLI if that's unreachable or `local` is set. Non-fatal: if Zig isn't
|
|
61
|
+
* available at all, tell the user how to finish the wiring later.
|
|
62
|
+
*/
|
|
63
|
+
function addZigbindDependency(target, { local }) {
|
|
64
|
+
const repairZon = join(target, "build.zig.zon");
|
|
65
|
+
if (!local) {
|
|
66
|
+
const url = releaseUrl();
|
|
67
|
+
try {
|
|
68
|
+
runZig(target, ["fetch", "--save=zigbind", url], { repairZon });
|
|
69
|
+
process.stdout.write(`✔ added zigbind dependency from ${url}\n`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
process.stderr.write(`note: could not fetch the hosted zigbind release (${errMessage(err)}); ` +
|
|
74
|
+
`falling back to the bundled sources.\n`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
let sources;
|
|
78
|
+
try {
|
|
79
|
+
sources = resolveZigbindSources();
|
|
80
|
+
runZig(target, ["fetch", "--save=zigbind", sources], { repairZon });
|
|
81
|
+
process.stdout.write("✔ added zigbind dependency from bundled sources\n");
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
process.stderr.write(`warning: could not add the zigbind dependency automatically ` +
|
|
85
|
+
`(${errMessage(err)}).\nRun this inside the project once Zig 0.16.0 is ` +
|
|
86
|
+
`available:\n zig fetch --save=zigbind ${sources ?? releaseUrl()}\n`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Recursively copy the template tree, substituting the project name in both
|
|
91
|
+
* file contents and file/directory names. `dot-` prefixed template names are
|
|
92
|
+
* emitted as dotfiles (so e.g. `dot-gitignore` becomes `.gitignore`), which
|
|
93
|
+
* also sidesteps npm's habit of renaming a packaged `.gitignore`.
|
|
94
|
+
*/
|
|
95
|
+
function copyTemplate(srcDir, destDir, name) {
|
|
96
|
+
mkdirSync(destDir, { recursive: true });
|
|
97
|
+
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
98
|
+
const outName = entry.name.replace(PLACEHOLDER, name).replace(/^dot-/, ".");
|
|
99
|
+
const src = join(srcDir, entry.name);
|
|
100
|
+
const dest = join(destDir, outName);
|
|
101
|
+
if (entry.isDirectory()) {
|
|
102
|
+
copyTemplate(src, dest, name);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const content = readFileSync(src, "utf8").replace(PLACEHOLDER, name);
|
|
106
|
+
writeFileSync(dest, content);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function validateName(name) {
|
|
111
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
112
|
+
throw new Error(`invalid project name '${name}' (use letters, digits and underscores; ` +
|
|
113
|
+
`must not start with a digit)`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function errMessage(err) {
|
|
117
|
+
return err instanceof Error ? err.message : String(err);
|
|
118
|
+
}
|
package/dist/zig.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
// From the compiled entry (dist/zig.js) or the source (src/zig.ts), one level
|
|
7
|
+
// up is the package root.
|
|
8
|
+
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
/**
|
|
10
|
+
* The hosted release tarball of the zigbind Zig package, used as the default
|
|
11
|
+
* (portable, shareable) dependency source. Overridable via ZIGBIND_RELEASE_URL
|
|
12
|
+
* — handy for testing against a local `file://` tarball or a mirror. The
|
|
13
|
+
* version is taken from this CLI's package.json and must match the release tag.
|
|
14
|
+
*/
|
|
15
|
+
export function releaseUrl() {
|
|
16
|
+
const override = process.env.ZIGBIND_RELEASE_URL;
|
|
17
|
+
if (override)
|
|
18
|
+
return override;
|
|
19
|
+
const pkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
|
|
20
|
+
const v = pkg.version;
|
|
21
|
+
return `https://github.com/zigbind/zigbind/releases/download/v${v}/zigbind-${v}.tar.gz`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Locate the zigbind Zig sources (the `native/` directory) that ship with this
|
|
25
|
+
* CLI. Works both from the published npm package (bundled at `<pkg>/native`)
|
|
26
|
+
* and from the monorepo checkout (`packages/zigbind` -> `../../native`).
|
|
27
|
+
*/
|
|
28
|
+
export function resolveZigbindSources() {
|
|
29
|
+
const candidates = [
|
|
30
|
+
join(PKG_ROOT, "native"), // bundled inside the published package
|
|
31
|
+
join(PKG_ROOT, "..", "..", "native"), // monorepo layout
|
|
32
|
+
];
|
|
33
|
+
for (const dir of candidates) {
|
|
34
|
+
if (existsSync(join(dir, "build.zig.zon")))
|
|
35
|
+
return dir;
|
|
36
|
+
}
|
|
37
|
+
throw new Error("could not locate the zigbind Zig sources (native/) shipped with the CLI");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Run `zig` with the given args in `cwd`. If the run fails because
|
|
41
|
+
* `build.zig.zon` is missing/has an invalid fingerprint and `repairZon` is
|
|
42
|
+
* given, patch in the value Zig suggests and retry once. On success, forwards
|
|
43
|
+
* zig's output; on failure, forwards it and throws.
|
|
44
|
+
*/
|
|
45
|
+
export function runZig(cwd, args, { repairZon } = {}) {
|
|
46
|
+
let res = spawnSync("zig", args, { cwd, encoding: "utf8" });
|
|
47
|
+
if (res.error) {
|
|
48
|
+
if (res.error.code === "ENOENT") {
|
|
49
|
+
throw new Error("could not find `zig` on PATH (Zig 0.16.0 is required)");
|
|
50
|
+
}
|
|
51
|
+
throw res.error;
|
|
52
|
+
}
|
|
53
|
+
if (res.status !== 0 && repairZon) {
|
|
54
|
+
const suggested = (res.stderr ?? "").match(/(?:suggested value|use this value): (0x[0-9a-fA-F]+)/);
|
|
55
|
+
if (suggested) {
|
|
56
|
+
patchFingerprint(repairZon, suggested[1]);
|
|
57
|
+
res = spawnSync("zig", args, { cwd, encoding: "utf8" });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
process.stdout.write(res.stdout ?? "");
|
|
61
|
+
process.stderr.write(res.stderr ?? "");
|
|
62
|
+
if (res.status !== 0) {
|
|
63
|
+
throw new Error(`zig ${args[0] ?? ""} failed`);
|
|
64
|
+
}
|
|
65
|
+
return res;
|
|
66
|
+
}
|
|
67
|
+
/** Insert or replace the `.fingerprint` field in a build.zig.zon. */
|
|
68
|
+
export function patchFingerprint(zonPath, value) {
|
|
69
|
+
if (!existsSync(zonPath))
|
|
70
|
+
return;
|
|
71
|
+
let src = readFileSync(zonPath, "utf8");
|
|
72
|
+
if (/\.fingerprint\s*=\s*0x[0-9a-fA-F]+/.test(src)) {
|
|
73
|
+
src = src.replace(/\.fingerprint\s*=\s*0x[0-9a-fA-F]+/, `.fingerprint = ${value}`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// Insert right after the `.name = ...,` line.
|
|
77
|
+
src = src.replace(/(\.name\s*=\s*[^\n]*,\n)/, `$1 .fingerprint = ${value},\n`);
|
|
78
|
+
}
|
|
79
|
+
writeFileSync(zonPath, src);
|
|
80
|
+
}
|
package/native/build.zig
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
|
|
3
|
+
/// Build script for the `zigbind` Zig library.
|
|
4
|
+
///
|
|
5
|
+
/// This is a Zig package, not an npm package. Its main product is the Zig
|
|
6
|
+
/// module named "zigbind" (exposed via `b.addModule`), which addons import to
|
|
7
|
+
/// register their functions with Node. Building here also runs two local
|
|
8
|
+
/// checks so `zig build` in this directory validates the library on its own:
|
|
9
|
+
///
|
|
10
|
+
/// - a compile check (`src/_check.zig` built as an object) that exercises the
|
|
11
|
+
/// full comptime pipeline for every supported type, and
|
|
12
|
+
/// - `zig build test`, the pure-comptime unit tests in `src/convert.zig`.
|
|
13
|
+
pub fn build(b: *std.Build) void {
|
|
14
|
+
const target = b.standardTargetOptions(.{});
|
|
15
|
+
const optimize = b.standardOptimizeOption(.{});
|
|
16
|
+
|
|
17
|
+
const headers = b.path("vendor/node-api-headers");
|
|
18
|
+
|
|
19
|
+
// The module consumers import as `@import("zigbind")`. It links libc (for
|
|
20
|
+
// the C allocator used to marshal string arguments, and to make the N-API
|
|
21
|
+
// headers available to `@cImport`). Target/optimize are left unset here so
|
|
22
|
+
// each consumer picks them; the include path travels with the module.
|
|
23
|
+
const mod = b.addModule("zigbind", .{
|
|
24
|
+
.root_source_file = b.path("src/zigbind.zig"),
|
|
25
|
+
.link_libc = true,
|
|
26
|
+
});
|
|
27
|
+
mod.addIncludePath(headers);
|
|
28
|
+
|
|
29
|
+
// Standalone compile check. Built as an object, so the still-undefined
|
|
30
|
+
// N-API symbols are fine (they resolve when a real addon links). Failing
|
|
31
|
+
// to compile any of the conversion/registration code fails `zig build`.
|
|
32
|
+
const check_mod = b.createModule(.{
|
|
33
|
+
.root_source_file = b.path("src/_check.zig"),
|
|
34
|
+
.target = target,
|
|
35
|
+
.optimize = optimize,
|
|
36
|
+
.link_libc = true,
|
|
37
|
+
});
|
|
38
|
+
check_mod.addIncludePath(headers);
|
|
39
|
+
const check_obj = b.addObject(.{
|
|
40
|
+
.name = "zigbind-check",
|
|
41
|
+
.root_module = check_mod,
|
|
42
|
+
});
|
|
43
|
+
b.getInstallStep().dependOn(&check_obj.step);
|
|
44
|
+
|
|
45
|
+
// Unit tests: only the pure-comptime conversions (no N-API runtime calls),
|
|
46
|
+
// so the test binary links without Node present.
|
|
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);
|
|
56
|
+
const test_step = b.step("test", "Run zigbind unit tests");
|
|
57
|
+
test_step.dependOn(&run_tests.step);
|
|
58
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
.{
|
|
2
|
+
.name = .zigbind,
|
|
3
|
+
.version = "0.1.0",
|
|
4
|
+
.fingerprint = 0xb615899b08c4e528, // Changing this has security and trust implications.
|
|
5
|
+
.minimum_zig_version = "0.16.0",
|
|
6
|
+
.dependencies = .{},
|
|
7
|
+
.paths = .{
|
|
8
|
+
"build.zig",
|
|
9
|
+
"build.zig.zon",
|
|
10
|
+
"src",
|
|
11
|
+
"vendor",
|
|
12
|
+
},
|
|
13
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//! Build-only compile check (never shipped to consumers, not imported by
|
|
2
|
+
//! `zigbind.zig`). Building this as an *object* forces the whole comptime
|
|
3
|
+
//! pipeline — `register` → the per-function callback trampolines → `convert`
|
|
4
|
+
//! for every supported type, including an error union — to be analyzed, so
|
|
5
|
+
//! `zig build` in `native/` fails loudly on any API or type mistake. The
|
|
6
|
+
//! N-API symbols it references stay undefined in the object file; they are
|
|
7
|
+
//! resolved when a real addon links, exactly as in production.
|
|
8
|
+
|
|
9
|
+
const zigbind = @import("zigbind.zig");
|
|
10
|
+
const napi = zigbind.napi;
|
|
11
|
+
|
|
12
|
+
// Reference the concrete (non-generic) wrappers so they are compiled too.
|
|
13
|
+
comptime {
|
|
14
|
+
_ = napi.check;
|
|
15
|
+
_ = napi.throwError;
|
|
16
|
+
_ = napi.getCallbackInfo;
|
|
17
|
+
_ = napi.setNamedProperty;
|
|
18
|
+
_ = napi.createFunction;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fn add(a: i32, b: i32) i32 {
|
|
22
|
+
return a + b;
|
|
23
|
+
}
|
|
24
|
+
fn negate(x: bool) bool {
|
|
25
|
+
return !x;
|
|
26
|
+
}
|
|
27
|
+
fn scale(x: f64) f64 {
|
|
28
|
+
return x * 2.0;
|
|
29
|
+
}
|
|
30
|
+
fn echo(s: []const u8) []const u8 {
|
|
31
|
+
return s;
|
|
32
|
+
}
|
|
33
|
+
fn checked(x: i32) !i32 {
|
|
34
|
+
return if (x < 0) error.MustBeNonNegative else x;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
comptime {
|
|
38
|
+
zigbind.register(.{
|
|
39
|
+
.add = add,
|
|
40
|
+
.negate = negate,
|
|
41
|
+
.scale = scale,
|
|
42
|
+
.echo = echo,
|
|
43
|
+
.checked = checked,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//! Async support — stub.
|
|
2
|
+
//!
|
|
3
|
+
//! TODO: wrap `napi_create_async_work` / `napi_queue_async_work` so that Zig
|
|
4
|
+
//! functions returning something awaitable (or explicitly marked async) run on
|
|
5
|
+
//! libuv's thread pool and resolve a JS `Promise` via `napi_create_promise` /
|
|
6
|
+
//! `napi_resolve_deferred`. Nothing here is wired into `register.zig` yet;
|
|
7
|
+
//! today every exported function runs synchronously on the JS thread.
|
|
8
|
+
|
|
9
|
+
const std = @import("std");
|
|
10
|
+
|
|
11
|
+
comptime {
|
|
12
|
+
// Keep this file referenced so it participates in compilation checks even
|
|
13
|
+
// while it is only a placeholder.
|
|
14
|
+
std.debug.assert(true);
|
|
15
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
//! Comptime conversions between Zig values and JavaScript values.
|
|
2
|
+
//!
|
|
3
|
+
//! Every function is generic over the Zig type and dispatches on `@typeInfo`
|
|
4
|
+
//! at comptime, so a wrapper compiled for `fn (i32, i32) i32` only ever emits
|
|
5
|
+
//! the `int32` code paths. Supported types: `i32`/`u32`/`i64`/... (any int),
|
|
6
|
+
//! `f16`/`f32`/`f64` (any float), `bool`, and `[]const u8` (UTF-8 strings).
|
|
7
|
+
|
|
8
|
+
const std = @import("std");
|
|
9
|
+
const napi = @import("napi.zig");
|
|
10
|
+
const c = napi.c;
|
|
11
|
+
|
|
12
|
+
/// The JS-representable category a Zig type maps onto. Kept as a plain enum
|
|
13
|
+
/// (rather than `@compileError` inline) so the classification is unit-testable
|
|
14
|
+
/// at comptime without touching the N-API runtime.
|
|
15
|
+
pub const Kind = enum { int, float, bool, string, unsupported };
|
|
16
|
+
|
|
17
|
+
/// Classify a Zig type. `.unsupported` means there is no JS mapping for it.
|
|
18
|
+
pub fn classify(comptime T: type) Kind {
|
|
19
|
+
return switch (@typeInfo(T)) {
|
|
20
|
+
.int => .int,
|
|
21
|
+
.float => .float,
|
|
22
|
+
.bool => .bool,
|
|
23
|
+
.pointer => |p| if (p.size == .slice and p.child == u8 and p.is_const)
|
|
24
|
+
.string
|
|
25
|
+
else
|
|
26
|
+
.unsupported,
|
|
27
|
+
else => .unsupported,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Compile-time guard that fails with a readable message for unsupported types.
|
|
32
|
+
fn assertSupported(comptime T: type) void {
|
|
33
|
+
if (classify(T) == .unsupported) {
|
|
34
|
+
@compileError("zigbind: unsupported type '" ++ @typeName(T) ++
|
|
35
|
+
"' (supported: integers, floats, bool, []const u8)");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Read a JavaScript `value` into a Zig value of type `T`.
|
|
40
|
+
///
|
|
41
|
+
/// `allocator` is only used for `[]const u8` results (the bytes are copied out
|
|
42
|
+
/// of V8). Callers own the returned slice's lifetime through that allocator.
|
|
43
|
+
pub fn fromJs(comptime T: type, env: napi.Env, value: napi.Value, allocator: std.mem.Allocator) !T {
|
|
44
|
+
comptime assertSupported(T);
|
|
45
|
+
return switch (comptime classify(T)) {
|
|
46
|
+
.int => blk: {
|
|
47
|
+
const info = @typeInfo(T).int;
|
|
48
|
+
if (info.signedness == .unsigned and info.bits <= 32) {
|
|
49
|
+
var out: u32 = undefined;
|
|
50
|
+
try napi.check(c.napi_get_value_uint32(env, value, &out));
|
|
51
|
+
break :blk @intCast(out);
|
|
52
|
+
} else if (info.signedness == .signed and info.bits <= 32) {
|
|
53
|
+
var out: i32 = undefined;
|
|
54
|
+
try napi.check(c.napi_get_value_int32(env, value, &out));
|
|
55
|
+
break :blk @intCast(out);
|
|
56
|
+
} else {
|
|
57
|
+
var out: i64 = undefined;
|
|
58
|
+
try napi.check(c.napi_get_value_int64(env, value, &out));
|
|
59
|
+
break :blk @intCast(out);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
.float => blk: {
|
|
63
|
+
var out: f64 = undefined;
|
|
64
|
+
try napi.check(c.napi_get_value_double(env, value, &out));
|
|
65
|
+
break :blk @floatCast(out);
|
|
66
|
+
},
|
|
67
|
+
.bool => blk: {
|
|
68
|
+
var out: bool = undefined;
|
|
69
|
+
try napi.check(c.napi_get_value_bool(env, value, &out));
|
|
70
|
+
break :blk out;
|
|
71
|
+
},
|
|
72
|
+
.string => blk: {
|
|
73
|
+
// First call with a null buffer to learn the byte length, then copy.
|
|
74
|
+
var len: usize = 0;
|
|
75
|
+
try napi.check(c.napi_get_value_string_utf8(env, value, null, 0, &len));
|
|
76
|
+
const buf = try allocator.alloc(u8, len + 1);
|
|
77
|
+
var written: usize = 0;
|
|
78
|
+
try napi.check(c.napi_get_value_string_utf8(env, value, buf.ptr, buf.len, &written));
|
|
79
|
+
break :blk buf[0..written];
|
|
80
|
+
},
|
|
81
|
+
.unsupported => unreachable,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Create a JavaScript value from a Zig value of type `T`.
|
|
86
|
+
pub fn toJs(comptime T: type, env: napi.Env, value: T) !napi.Value {
|
|
87
|
+
comptime assertSupported(T);
|
|
88
|
+
var result: napi.Value = undefined;
|
|
89
|
+
switch (comptime classify(T)) {
|
|
90
|
+
.int => {
|
|
91
|
+
const info = @typeInfo(T).int;
|
|
92
|
+
if (info.signedness == .unsigned and info.bits <= 32) {
|
|
93
|
+
try napi.check(c.napi_create_uint32(env, @intCast(value), &result));
|
|
94
|
+
} else if (info.signedness == .signed and info.bits <= 32) {
|
|
95
|
+
try napi.check(c.napi_create_int32(env, @intCast(value), &result));
|
|
96
|
+
} else {
|
|
97
|
+
try napi.check(c.napi_create_int64(env, @intCast(value), &result));
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
.float => try napi.check(c.napi_create_double(env, @floatCast(value), &result)),
|
|
101
|
+
.bool => try napi.check(c.napi_get_boolean(env, value, &result)),
|
|
102
|
+
.string => try napi.check(c.napi_create_string_utf8(env, value.ptr, value.len, &result)),
|
|
103
|
+
.unsupported => unreachable,
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
test "classify maps Zig types to JS kinds" {
|
|
109
|
+
const expectEqual = std.testing.expectEqual;
|
|
110
|
+
try expectEqual(Kind.int, classify(i32));
|
|
111
|
+
try expectEqual(Kind.int, classify(u64));
|
|
112
|
+
try expectEqual(Kind.float, classify(f64));
|
|
113
|
+
try expectEqual(Kind.float, classify(f32));
|
|
114
|
+
try expectEqual(Kind.bool, classify(bool));
|
|
115
|
+
try expectEqual(Kind.string, classify([]const u8));
|
|
116
|
+
// A mutable slice is intentionally *not* treated as a JS string.
|
|
117
|
+
try expectEqual(Kind.unsupported, classify([]u8));
|
|
118
|
+
try expectEqual(Kind.unsupported, classify(struct { x: i32 }));
|
|
119
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//! Raw N-API bindings and thin wrappers.
|
|
2
|
+
//!
|
|
3
|
+
//! The N-API C headers are vendored under `vendor/node-api-headers/` and pulled
|
|
4
|
+
//! in here with `@cImport`. Everything the rest of the library needs from the C
|
|
5
|
+
//! API is re-exported through `c`, plus a few ergonomic wrappers so the comptime
|
|
6
|
+
//! machinery in `convert.zig` / `register.zig` doesn't have to spell out the raw
|
|
7
|
+
//! `napi_status` dance every time.
|
|
8
|
+
|
|
9
|
+
const std = @import("std");
|
|
10
|
+
|
|
11
|
+
/// The imported N-API C API. We pin `NAPI_VERSION` to 8 (Node >= 18) so the
|
|
12
|
+
/// headers expose exactly the surface we support.
|
|
13
|
+
pub const c = @cImport({
|
|
14
|
+
@cDefine("NAPI_VERSION", "8");
|
|
15
|
+
@cInclude("node_api.h");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Convenience aliases for the handful of opaque handles we pass around.
|
|
19
|
+
pub const Env = c.napi_env;
|
|
20
|
+
pub const Value = c.napi_value;
|
|
21
|
+
pub const CallbackInfo = c.napi_callback_info;
|
|
22
|
+
pub const Callback = c.napi_callback;
|
|
23
|
+
pub const Status = c.napi_status;
|
|
24
|
+
|
|
25
|
+
/// A call into the N-API C API returned a non-`napi_ok` status.
|
|
26
|
+
pub const Error = error{NapiFailure};
|
|
27
|
+
|
|
28
|
+
/// Turn a `napi_status` into a Zig error. `napi_ok` is `0`.
|
|
29
|
+
pub fn check(status: Status) Error!void {
|
|
30
|
+
if (status != c.napi_ok) return Error.NapiFailure;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Throw a JavaScript `Error` with the given message and return control to JS.
|
|
34
|
+
/// `msg` must be a null-terminated string. Used to surface Zig error unions.
|
|
35
|
+
pub fn throwError(env: Env, msg: [:0]const u8) void {
|
|
36
|
+
_ = c.napi_throw_error(env, null, msg.ptr);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Read the arguments of the current call into `argv`, returning the actual
|
|
40
|
+
/// argument count reported by N-API.
|
|
41
|
+
pub fn getCallbackInfo(
|
|
42
|
+
env: Env,
|
|
43
|
+
info: CallbackInfo,
|
|
44
|
+
argv: []Value,
|
|
45
|
+
) Error!usize {
|
|
46
|
+
var argc: usize = argv.len;
|
|
47
|
+
const argv_ptr: [*c]Value = if (argv.len == 0) null else argv.ptr;
|
|
48
|
+
try check(c.napi_get_cb_info(env, info, &argc, argv_ptr, null, null));
|
|
49
|
+
return argc;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Attach `value` to `object` under the (null-terminated) property `name`.
|
|
53
|
+
pub fn setNamedProperty(env: Env, object: Value, name: [:0]const u8, value: Value) Error!void {
|
|
54
|
+
try check(c.napi_set_named_property(env, object, name.ptr, value));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Create a JS function backed by the native callback `cb`, named `name`.
|
|
58
|
+
pub fn createFunction(env: Env, name: [:0]const u8, cb: Callback) Error!Value {
|
|
59
|
+
var result: Value = undefined;
|
|
60
|
+
try check(c.napi_create_function(env, name.ptr, name.len, cb, null, &result));
|
|
61
|
+
return result;
|
|
62
|
+
}
|