Skip to content
Merged
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
39 changes: 39 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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(.{
Expand Down Expand Up @@ -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);
}
33 changes: 33 additions & 0 deletions examples/type_tag/mod.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
105 changes: 105 additions & 0 deletions examples/type_tag/mod.zig
Original file line number Diff line number Diff line change
@@ -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]);
}
18 changes: 18 additions & 0 deletions src/Env.zig
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,15 @@ pub fn unwrap(self: Env, comptime Data: type, object: Value) NapiError!*Data {
return native_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.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 {
var native_object: *Data = undefined;
Expand All @@ -779,6 +788,15 @@ pub fn removeWrap(self: Env, comptime Data: type, object: Value) NapiError!*Data
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.removeWrap(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(
Expand Down
10 changes: 10 additions & 0 deletions zbuild.zon
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
},
}