Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 49 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,30 @@ Add the Zig dependency to your `build.zig.zon`:
},
```

After creating the shared-library compile step for the final `.node` artifact,
attach its package and artifact identity in the addon's `build.zig`:

```zig
const std = @import("std");
const zapi_build = @import("zapi");

fn configureAddonIdentity(
b: *std.Build,
addon: *std.Build.Step.Compile,
) void {
const manifest = @import("build.zig.zon");
zapi_build.addAddonIdentity(b, addon, manifest);
}
```

Each logically distinct addon needs a unique compile-step name and its own root
module. Configure each root module once. Aliases or installed copies of one
compiled addon keep the identity embedded in that artifact.

Zig 0.16 does not expose a `.path` dependency's `build.zig` as an import. Keep
the URL/hash dependency declaration above and use
`zig build --fork=/path/to/zapi` when developing against a local checkout.

---

## Zig Library — Quick Start
Expand Down Expand Up @@ -60,7 +84,11 @@ pub const Counter = struct {
}
};

comptime { js.exportModule(@This(), .{}); }
comptime {
js.exportModule(@This(), .{
.identity = @import("zapi_addon_identity"),
});
}
```

**JavaScript usage:**
Expand All @@ -73,7 +101,20 @@ c.increment();
c.count; // 1 (getter, not a method call)
```

`pub` functions are auto-exported, and structs with `js_meta = js.class(...)` become JS classes. One line — `comptime { js.exportModule(@This(), .{}); }` — registers everything.
`pub` functions are auto-exported, and structs with `js_meta = js.class(...)`
become JS classes. Class type tags are derived at compile time from the Zig
package name, version, fingerprint, addon artifact name, and class type name.
This keeps two loaded copies of one addon compatible while isolating different
addons and package versions. Modules that export only functions and never
accept or return DSL classes may continue to use `js.exportModule(@This(), .{})`.

```text
FNV-1a-128(package@version#fingerprint::addon::ZigType)
```

Low-level wrapper and conversion APIs take the same identity type explicitly.
Code paths that cannot accept or return DSL classes pass
`js.NoAddonIdentity`.

---

Expand Down Expand Up @@ -319,7 +360,11 @@ Import Zig modules as `pub const` to create JS namespaces. The DSL recursively r
pub const math = @import("math.zig"); // → exports.math.multiply(...)
pub const crypto = @import("crypto.zig"); // → exports.crypto.PublicKey, etc.

comptime { js.exportModule(@This(), .{}); }
comptime {
js.exportModule(@This(), .{
.identity = @import("zapi_addon_identity"),
});
}
```

Namespaces nest arbitrarily — a sub-module with more `pub const` imports creates deeper nesting.
Expand All @@ -333,6 +378,7 @@ Namespaces nest arbitrarily — a sub-module with more `pub const` imports creat
```zig
comptime {
js.exportModule(@This(), .{
.identity = @import("zapi_addon_identity"),
.init = fn (refcount: u32) !void, // called before registration (0 = first env)
.cleanup = fn (refcount: u32) void, // called on env exit (0 = last env)
});
Expand Down
39 changes: 38 additions & 1 deletion build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,42 @@ const zbuild = @import("zbuild");

pub fn build(b: *std.Build) !void {
@setEvalBranchQuota(200_000);
_ = try zbuild.configureBuild(b, @import("build.zig.zon"), .{});
const manifest = @import("build.zig.zon");
const result = try zbuild.configureBuild(b, manifest, .{});

configureExampleAddonIdentities(b, manifest, result);
}

/// Makes the final addon's package and artifact identity available to its root
/// module as `@import("zapi_addon_identity")`. Distinct logical addons in one
/// package must use unique compile-step names and root modules.
pub fn addAddonIdentity(
b: *std.Build,
addon: *std.Build.Step.Compile,
comptime manifest: anytype,
) void {
const import_name = "zapi_addon_identity";
if (addon.root_module.import_table.contains(import_name)) {
@panic(
"this root module already has a zapi addon identity; configure it once " ++
"for aliases of the same addon, or create a separate root module " ++
"for a distinct addon",
);
}

const identity = b.addOptions();
identity.addOption([]const u8, "package_name", @tagName(manifest.name));
identity.addOption([]const u8, "package_version", manifest.version);
identity.addOption(u64, "package_fingerprint", manifest.fingerprint);
identity.addOption([]const u8, "addon_name", addon.name);
addon.root_module.addOptions(import_name, identity);
}

fn configureExampleAddonIdentities(
b: *std.Build,
comptime manifest: anytype,
result: zbuild.BuildResult,
) void {
addAddonIdentity(b, result.library("example_js_dsl").?, manifest);
addAddonIdentity(b, result.library("example_addon_isolation").?, manifest);
}
26 changes: 26 additions & 0 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,21 @@
.imports = .{.zapi},
.link_libc = true,
},
.example_addon_isolation = .{
.root_source_file = "examples/addon_isolation/mod.zig",
.imports = .{.zapi},
.link_libc = true,
},
.example_register_decls = .{
.root_source_file = "examples/register_decls/mod.zig",
.imports = .{.zapi},
.link_libc = true,
},
.function_only_dsl_compile = .{
.root_source_file = "examples/function_only_dsl/mod.zig",
.imports = .{.zapi},
.link_libc = true,
},
},
.libraries = .{
.example_hello_world = .{
Expand All @@ -74,12 +84,24 @@
.linker_allow_shlib_undefined = true,
.dest_sub_path = "example_js_dsl.node",
},
.example_addon_isolation = .{
.root_module = .example_addon_isolation,
.linkage = .dynamic,
.linker_allow_shlib_undefined = true,
.dest_sub_path = "example_addon_isolation.node",
},
.example_register_decls = .{
.root_module = .example_register_decls,
.linkage = .dynamic,
.linker_allow_shlib_undefined = true,
.dest_sub_path = "example_register_decls.node",
},
.function_only_dsl_compile = .{
.root_module = .function_only_dsl_compile,
.linkage = .dynamic,
.linker_allow_shlib_undefined = true,
.dest_sub_path = "function_only_dsl_compile.node",
},
},
.tests = .{
.napi = .{ .root_module = .napi },
Expand All @@ -99,5 +121,9 @@
.root_module = .example_js_dsl,
.linker_allow_shlib_undefined = true,
},
.example_addon_isolation = .{
.root_module = .example_addon_isolation,
.linker_allow_shlib_undefined = true,
},
},
}
37 changes: 37 additions & 0 deletions examples/addon_isolation/mod.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { copyFileSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { describe, expect, it } from "vitest";

const require = createRequire(import.meta.url);
const primaryPath = require.resolve("../../zig-out/lib/example_js_dsl.node");
const primary = require(primaryPath);
const secondary = require("../../zig-out/lib/example_addon_isolation.node");
const duplicatePath = join(dirname(primaryPath), "example_js_dsl_duplicate.node");
copyFileSync(primaryPath, duplicatePath);
const duplicate = require(duplicatePath);

describe("DSL class isolation across addons", () => {
it("keeps matching class names isolated by addon", () => {
const primaryCounter = new primary.Counter(1);
const secondaryCounter = new secondary.Counter(2);

primary.incrementCounter(primaryCounter);
secondary.incrementCounter(secondaryCounter);
expect(primaryCounter.getCount()).toBe(2);
expect(secondaryCounter.getCount()).toBe(3);

expect(() => primary.incrementCounter(secondaryCounter)).toThrow(TypeError);
expect(() => secondary.incrementCounter(primaryCounter)).toThrow(TypeError);
});

it("keeps class identity across two loaded copies of one addon", () => {
const primaryCounter = new primary.Counter(1);
const duplicateCounter = new duplicate.Counter(2);

primary.incrementCounter(duplicateCounter);
duplicate.incrementCounter(primaryCounter);
expect(primaryCounter.getCount()).toBe(2);
expect(duplicateCounter.getCount()).toBe(3);
});
});
27 changes: 27 additions & 0 deletions examples/addon_isolation/mod.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const js = @import("zapi").js;
const Number = js.Number;

/// Matches `example_js_dsl.Counter`'s layout so the regression fails safely
/// instead of reinterpreting incompatible native memory.
pub const Counter = struct {
pub const js_meta = js.class(.{});
count: i32,

pub fn init(start: Number) Counter {
return .{ .count = start.assertI32() };
}

pub fn getCount(self: Counter) Number {
return Number.from(self.count);
}
};

pub fn incrementCounter(counter: *Counter) void {
counter.count += 1;
}

comptime {
js.exportModule(@This(), .{
.identity = @import("zapi_addon_identity"),
});
}
9 changes: 9 additions & 0 deletions examples/function_only_dsl/mod.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const js = @import("zapi").js;

pub fn double(value: js.Number) js.Number {
return js.Number.from(value.assertI32() * 2);
}

comptime {
js.exportModule(@This(), .{});
}
1 change: 1 addition & 0 deletions examples/js_dsl/mod.zig
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,7 @@ pub const BlsPublicKey = struct {

comptime {
js.exportModule(@This(), .{
.identity = @import("zapi_addon_identity"),
.init = struct {
fn f(refcount: u32) !void {
const count = module_init_count.fetchAdd(1, .monotonic);
Expand Down
1 change: 1 addition & 0 deletions src/js.zig
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub const BigUint64Array = typed_arrays.BigUint64Array;
pub const Promise = @import("js/promise.zig").Promise;
pub const createPromise = @import("js/promise.zig").createPromise;

pub const NoAddonIdentity = @import("js/class_runtime.zig").NoAddonIdentity;
pub const wrapFunction = @import("js/wrap_function.zig").wrapFunction;
pub const wrapClass = @import("js/wrap_class.zig").wrapClass;
pub const exportModule = @import("js/export_module.zig").exportModule;
Expand Down
Loading
Loading