From 7cd5742cf390148fc75b76ca2ad04ba9648800b8 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Tue, 17 Mar 2026 16:10:43 +0100 Subject: [PATCH 1/3] feat: type-tag-aware unwrap and removeWrap --- src/Env.zig | 56 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/src/Env.zig b/src/Env.zig index 8772161..7d1a59d 100644 --- a/src/Env.zig +++ b/src/Env.zig @@ -761,8 +761,23 @@ pub fn wrap( }; } -/// https://nodejs.org/api/n-api.html#napi_unwrap -pub fn unwrap(self: Env, comptime Data: type, object: Value) NapiError!*Data { +fn dataTypeTagFor(comptime Data: type) ?c.napi_type_tag { + if (!@hasDecl(Data, "type_tag")) return null; + + const raw_tag = @field(Data, "type_tag"); + const raw_tag_type = @TypeOf(raw_tag); + + if (raw_tag_type == c.napi_type_tag) { + return raw_tag; + } + if (raw_tag_type == TypeTag) { + return .{ .lower = raw_tag.lower, .upper = raw_tag.upper }; + } + + @compileError("Data.type_tag must be c.napi_type_tag or napi.TypeTag"); +} + +fn unwrapUnchecked(self: Env, comptime Data: type, object: Value) NapiError!*Data { var native_object: *Data = undefined; try status.check( c.napi_unwrap(self.env, object.value, @ptrCast(&native_object)), @@ -770,8 +785,7 @@ pub fn unwrap(self: Env, comptime Data: type, object: Value) NapiError!*Data { return native_object; } -/// https://nodejs.org/api/n-api.html#napi_remove_wrap -pub fn removeWrap(self: Env, comptime Data: type, object: Value) NapiError!*Data { +fn removeWrapUnchecked(self: Env, comptime Data: type, object: Value) NapiError!*Data { var native_object: *Data = undefined; try status.check( c.napi_remove_wrap(self.env, object.value, @ptrCast(&native_object)), @@ -779,6 +793,40 @@ pub fn removeWrap(self: Env, comptime Data: type, object: Value) NapiError!*Data return native_object; } +/// https://nodejs.org/api/n-api.html#napi_unwrap +pub fn unwrap(self: Env, comptime Data: type, object: Value) NapiError!*Data { + const maybe_type_tag = comptime dataTypeTagFor(Data); + if (maybe_type_tag) |type_tag| { + return try self.unwrapChecked(Data, object, type_tag); + } + return try self.unwrapUnchecked(Data, object); +} + +/// https://nodejs.org/api/n-api.html#napi_unwrap +pub fn unwrapChecked(self: Env, comptime Data: type, object: Value, expected_type_tag: c.napi_type_tag) NapiError!*Data { + if (!(try self.checkObjectTypeTag(object, expected_type_tag))) { + return error.InvalidArg; + } + return try self.unwrapUnchecked(Data, object); +} + +/// https://nodejs.org/api/n-api.html#napi_remove_wrap +pub fn removeWrap(self: Env, comptime Data: type, object: Value) NapiError!*Data { + const maybe_type_tag = comptime dataTypeTagFor(Data); + if (maybe_type_tag) |type_tag| { + return try self.removeWrapChecked(Data, object, type_tag); + } + return try self.removeWrapUnchecked(Data, object); +} + +/// https://nodejs.org/api/n-api.html#napi_remove_wrap +pub fn removeWrapChecked(self: Env, comptime Data: type, object: Value, expected_type_tag: c.napi_type_tag) NapiError!*Data { + if (!(try self.checkObjectTypeTag(object, expected_type_tag))) { + return error.InvalidArg; + } + return try self.removeWrapUnchecked(Data, object); +} + /// https://nodejs.org/api/n-api.html#napi_type_tag_object pub fn typeTagObject(self: Env, value: Value, type_tag: c.napi_type_tag) NapiError!void { try status.check( From ace027f085e085b83b1bcacce8c446d03d602a58 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Mon, 23 Mar 2026 16:38:41 +0100 Subject: [PATCH 2/3] remove implicit type-tag-check --- src/Env.zig | 52 +++++++++++----------------------------------------- 1 file changed, 11 insertions(+), 41 deletions(-) diff --git a/src/Env.zig b/src/Env.zig index 7d1a59d..077df04 100644 --- a/src/Env.zig +++ b/src/Env.zig @@ -761,23 +761,8 @@ pub fn wrap( }; } -fn dataTypeTagFor(comptime Data: type) ?c.napi_type_tag { - if (!@hasDecl(Data, "type_tag")) return null; - - const raw_tag = @field(Data, "type_tag"); - const raw_tag_type = @TypeOf(raw_tag); - - if (raw_tag_type == c.napi_type_tag) { - return raw_tag; - } - if (raw_tag_type == TypeTag) { - return .{ .lower = raw_tag.lower, .upper = raw_tag.upper }; - } - - @compileError("Data.type_tag must be c.napi_type_tag or napi.TypeTag"); -} - -fn unwrapUnchecked(self: Env, comptime Data: type, object: Value) NapiError!*Data { +/// https://nodejs.org/api/n-api.html#napi_unwrap +pub fn unwrap(self: Env, comptime Data: type, object: Value) NapiError!*Data { var native_object: *Data = undefined; try status.check( c.napi_unwrap(self.env, object.value, @ptrCast(&native_object)), @@ -785,46 +770,31 @@ fn unwrapUnchecked(self: Env, comptime Data: type, object: Value) NapiError!*Dat return native_object; } -fn removeWrapUnchecked(self: Env, comptime Data: type, object: Value) NapiError!*Data { - var native_object: *Data = undefined; - try status.check( - c.napi_remove_wrap(self.env, object.value, @ptrCast(&native_object)), - ); - return native_object; -} - -/// https://nodejs.org/api/n-api.html#napi_unwrap -pub fn unwrap(self: Env, comptime Data: type, object: Value) NapiError!*Data { - const maybe_type_tag = comptime dataTypeTagFor(Data); - if (maybe_type_tag) |type_tag| { - return try self.unwrapChecked(Data, object, type_tag); - } - return try self.unwrapUnchecked(Data, object); -} - /// https://nodejs.org/api/n-api.html#napi_unwrap +/// Checks the object's type tag before unwrapping. Returns `error.InvalidArg` on mismatch. pub fn unwrapChecked(self: Env, comptime Data: type, object: Value, expected_type_tag: c.napi_type_tag) NapiError!*Data { if (!(try self.checkObjectTypeTag(object, expected_type_tag))) { return error.InvalidArg; } - return try self.unwrapUnchecked(Data, object); + return try self.unwrap(Data, object); } /// https://nodejs.org/api/n-api.html#napi_remove_wrap pub fn removeWrap(self: Env, comptime Data: type, object: Value) NapiError!*Data { - const maybe_type_tag = comptime dataTypeTagFor(Data); - if (maybe_type_tag) |type_tag| { - return try self.removeWrapChecked(Data, object, type_tag); - } - return try self.removeWrapUnchecked(Data, object); + var native_object: *Data = undefined; + try status.check( + c.napi_remove_wrap(self.env, object.value, @ptrCast(&native_object)), + ); + return native_object; } /// https://nodejs.org/api/n-api.html#napi_remove_wrap +/// Checks the object's type tag before removing the wrap. Returns `error.InvalidArg` on mismatch. pub fn removeWrapChecked(self: Env, comptime Data: type, object: Value, expected_type_tag: c.napi_type_tag) NapiError!*Data { if (!(try self.checkObjectTypeTag(object, expected_type_tag))) { return error.InvalidArg; } - return try self.removeWrapUnchecked(Data, object); + return try self.removeWrap(Data, object); } /// https://nodejs.org/api/n-api.html#napi_type_tag_object From e1bd71bb7b5a34b498096dc2bf7bcea7b5bb010c Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Mon, 23 Mar 2026 16:55:37 +0100 Subject: [PATCH 3/3] add an example with tests --- build.zig | 39 +++++++++++++ examples/type_tag/mod.test.ts | 33 +++++++++++ examples/type_tag/mod.zig | 105 ++++++++++++++++++++++++++++++++++ zbuild.zon | 10 ++++ 4 files changed, 187 insertions(+) create mode 100644 examples/type_tag/mod.test.ts create mode 100644 examples/type_tag/mod.zig diff --git a/build.zig b/build.zig index 0e6ee29..30160ba 100644 --- a/build.zig +++ b/build.zig @@ -42,6 +42,29 @@ pub fn build(b: *std.Build) void { tls_install_lib_example_hello_world.dependOn(&install_lib_example_hello_world.step); b.getInstallStep().dependOn(&install_lib_example_hello_world.step); + const module_example_type_tag = b.createModule(.{ + .root_source_file = b.path("examples/type_tag/mod.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }); + b.modules.put(b.dupe("example_type_tag"), module_example_type_tag) catch @panic("OOM"); + + const lib_example_type_tag = b.addLibrary(.{ + .name = "example_type_tag", + .root_module = module_example_type_tag, + .linkage = .dynamic, + }); + + lib_example_type_tag.linker_allow_shlib_undefined = true; + const install_lib_example_type_tag = b.addInstallArtifact(lib_example_type_tag, .{ + .dest_sub_path = "example_type_tag.node", + }); + + const tls_install_lib_example_type_tag = b.step("build-lib:example_type_tag", "Install the example_type_tag library"); + tls_install_lib_example_type_tag.dependOn(&install_lib_example_type_tag.step); + b.getInstallStep().dependOn(&install_lib_example_type_tag.step); + const tls_run_test = b.step("test", "Run all tests"); const test_napi = b.addTest(.{ @@ -72,7 +95,23 @@ pub fn build(b: *std.Build) void { tls_run_test_example_hello_world.dependOn(&run_test_example_hello_world.step); tls_run_test.dependOn(&run_test_example_hello_world.step); + const test_example_type_tag = b.addTest(.{ + .name = "example_type_tag", + .root_module = module_example_type_tag, + .filters = b.option([][]const u8, "example_type_tag.filters", "example_type_tag test filters") orelse &[_][]const u8{}, + }); + const install_test_example_type_tag = b.addInstallArtifact(test_example_type_tag, .{}); + const tls_install_test_example_type_tag = b.step("build-test:example_type_tag", "Install the example_type_tag test"); + tls_install_test_example_type_tag.dependOn(&install_test_example_type_tag.step); + + const run_test_example_type_tag = b.addRunArtifact(test_example_type_tag); + const tls_run_test_example_type_tag = b.step("test:example_type_tag", "Run the example_type_tag test"); + tls_run_test_example_type_tag.dependOn(&run_test_example_type_tag.step); + tls_run_test.dependOn(&run_test_example_type_tag.step); + module_napi.addImport("build_options", options_module_build_options); module_example_hello_world.addImport("napi", module_napi); + + module_example_type_tag.addImport("napi", module_napi); } diff --git a/examples/type_tag/mod.test.ts b/examples/type_tag/mod.test.ts new file mode 100644 index 0000000..e49d300 --- /dev/null +++ b/examples/type_tag/mod.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from "vitest"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const mod = require("../../zig-out/lib/example_type_tag.node"); + +describe("type-tagged classes", () => { + it("Cat.name() returns the name", () => { + const cat = new mod.Cat("Whiskers"); + expect(cat.name()).toEqual("Whiskers"); + }); + + it("Dog.name() returns the name", () => { + const dog = new mod.Dog("Buddy"); + expect(dog.name()).toEqual("Buddy"); + }); + + it("calling Cat.name() on a Dog throws (type tag mismatch)", () => { + const dog = new mod.Dog("Buddy"); + const cat = new mod.Cat("Whiskers"); + + // Steal Cat's prototype method and call it with a Dog as `this`. + // unwrapChecked should reject this with InvalidArg. + expect(() => { + mod.Cat.prototype.name.call(dog); + }).toThrow(); + + // And the reverse + expect(() => { + mod.Dog.prototype.name.call(cat); + }).toThrow(); + }); +}); diff --git a/examples/type_tag/mod.zig b/examples/type_tag/mod.zig new file mode 100644 index 0000000..beb2cfb --- /dev/null +++ b/examples/type_tag/mod.zig @@ -0,0 +1,105 @@ +///! Demonstrates type-tagged wrapping with unwrapChecked / removeWrapChecked. +///! +///! Type tags let you verify at runtime that a JS object wraps the expected +///! native type. Without them, passing the wrong `this` (e.g. a Dog to a Cat +///! method) silently reinterprets memory. With unwrapChecked the mismatch is +///! caught and returns error.InvalidArg instead. +const std = @import("std"); +const napi = @import("napi"); +const allocator = std.heap.page_allocator; + +comptime { + napi.module.register(typeTagMod); +} + +fn typeTagMod(env: napi.Env, module: napi.Value) anyerror!void { + try module.setNamedProperty( + "Cat", + try env.defineClass( + "Cat", + 1, + Cat_ctor, + null, + &[_]napi.c.napi_property_descriptor{.{ + .utf8name = "name", + .method = napi.wrapCallback(0, Cat_name), + }}, + ), + ); + + try module.setNamedProperty( + "Dog", + try env.defineClass( + "Dog", + 1, + Dog_ctor, + null, + &[_]napi.c.napi_property_descriptor{.{ + .utf8name = "name", + .method = napi.wrapCallback(0, Dog_name), + }}, + ), + ); +} + +// --- Cat --- + +const Cat = struct { name_buf: [64]u8, len: usize }; + +const cat_type_tag = napi.c.napi_type_tag{ + .lower = 0xCAFEBABE_00000001, + .upper = 0xAAAAAAAA_AAAAAAAA, +}; + +fn Cat_finalize(_: napi.Env, cat: *Cat, _: ?*anyopaque) void { + allocator.destroy(cat); +} + +fn Cat_ctor(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const arg = cb.arg(0); + var buf: [64]u8 = undefined; + const name = try arg.getValueStringUtf8(&buf); + + const cat = try allocator.create(Cat); + cat.* = Cat{ .name_buf = buf, .len = name.len }; + + _ = try env.wrap(cb.this(), Cat, cat, Cat_finalize, null); + try env.typeTagObject(cb.this(), cat_type_tag); + return cb.this(); +} + +fn Cat_name(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const cat = try env.unwrapChecked(Cat, cb.this(), cat_type_tag); + return try env.createStringUtf8(cat.name_buf[0..cat.len]); +} + +// --- Dog --- + +const Dog = struct { name_buf: [64]u8, len: usize }; + +const dog_type_tag = napi.c.napi_type_tag{ + .lower = 0xDEADBEEF_00000002, + .upper = 0xBBBBBBBB_BBBBBBBB, +}; + +fn Dog_finalize(_: napi.Env, dog: *Dog, _: ?*anyopaque) void { + allocator.destroy(dog); +} + +fn Dog_ctor(env: napi.Env, cb: napi.CallbackInfo(1)) !napi.Value { + const arg = cb.arg(0); + var buf: [64]u8 = undefined; + const name = try arg.getValueStringUtf8(&buf); + + const dog = try allocator.create(Dog); + dog.* = Dog{ .name_buf = buf, .len = name.len }; + + _ = try env.wrap(cb.this(), Dog, dog, Dog_finalize, null); + try env.typeTagObject(cb.this(), dog_type_tag); + return cb.this(); +} + +fn Dog_name(env: napi.Env, cb: napi.CallbackInfo(0)) !napi.Value { + const dog = try env.unwrapChecked(Dog, cb.this(), dog_type_tag); + return try env.createStringUtf8(dog.name_buf[0..dog.len]); +} diff --git a/zbuild.zon b/zbuild.zon index f17a3e2..3976a0f 100644 --- a/zbuild.zon +++ b/zbuild.zon @@ -31,5 +31,15 @@ .linker_allow_shlib_undefined = true, .dest_sub_path = "example_hello_world.node", }, + .example_type_tag = .{ + .root_module = .{ + .root_source_file = "examples/type_tag/mod.zig", + .imports = .{.napi}, + .link_libc = true, + }, + .linkage = .dynamic, + .linker_allow_shlib_undefined = true, + .dest_sub_path = "example_type_tag.node", + }, }, }