From 51997ff43f499d3c6dcd6a3ca8cd32f236e2c727 Mon Sep 17 00:00:00 2001 From: richerfu Date: Wed, 27 May 2026 09:30:51 +0800 Subject: [PATCH 1/3] Add external --- examples/basic/index.d.ts | 21 +++ examples/basic/src/external.zig | 38 ++++ examples/basic/src/hello.zig | 10 ++ node-test/napi/__tests__/values.spec.js | 25 +++ node-test/napi/src/lib.zig | 8 + node-test/napi/src/values.zig | 37 ++++ src/build/napi-tsgen.zig | 36 ++++ src/napi.zig | 2 + src/napi/util/helper.zig | 4 + src/napi/util/napi.zig | 8 + src/napi/wrapper/external.zig | 222 ++++++++++++++++++++++++ test/basic.ts | 2 + test/external.spec.ts | 33 ++++ 13 files changed, 446 insertions(+) create mode 100644 examples/basic/src/external.zig create mode 100644 src/napi/wrapper/external.zig create mode 100644 test/external.spec.ts diff --git a/examples/basic/index.d.ts b/examples/basic/index.d.ts index 77ca454..bed45ef 100644 --- a/examples/basic/index.d.ts +++ b/examples/basic/index.d.ts @@ -124,6 +124,15 @@ export declare const enum StringColor { Blue = "Blue", } +export interface ExternalObject { + readonly __napiExternal?: T; +} + +export interface ExternalPoint { + x: number; + y: number; +} + export declare function test_i32(left: number, right: number): number; export declare function test_f32(left: number, right: number): number; export declare function test_u32(left: number, right: number): number; @@ -251,3 +260,15 @@ export declare function favorite_color(): Color; export declare function is_primary(color: Color): boolean; export declare function string_enum_identity(color: StringColor): StringColor; export declare function favorite_string_color(): StringColor; +export declare function create_external(value: number): ExternalObject; +export declare function create_external_with_size_hint(value: number): ExternalObject; +export declare function get_external(external: ExternalObject): number; +export declare function get_external_size_hint(external: ExternalObject): number; +export declare function mutate_external(external: ExternalObject, value: number): void; +export declare function create_external_point(x: number, y: number): ExternalObject; +export declare function get_external_point(external: ExternalObject): ExternalPoint; +export declare function mutate_external_point( + external: ExternalObject, + x: number, + y: number, +): void; diff --git a/examples/basic/src/external.zig b/examples/basic/src/external.zig new file mode 100644 index 0000000..bb72214 --- /dev/null +++ b/examples/basic/src/external.zig @@ -0,0 +1,38 @@ +const napi = @import("napi"); + +pub const ExternalPoint = struct { + x: i32, + y: i32, +}; + +pub fn create_external(value: u32) !napi.External(u32) { + return try napi.External(u32).New(value); +} + +pub fn create_external_with_size_hint(value: u32) !napi.External(u32) { + return try napi.External(u32).NewWithSizeHint(value, 128); +} + +pub fn get_external(external: napi.External(u32)) u32 { + return external.value().*; +} + +pub fn get_external_size_hint(external: napi.External(u32)) usize { + return external.sizeHint(); +} + +pub fn mutate_external(external: napi.External(u32), value: u32) void { + external.valueMut().* = value; +} + +pub fn create_external_point(x: i32, y: i32) !napi.External(ExternalPoint) { + return try napi.External(ExternalPoint).New(.{ .x = x, .y = y }); +} + +pub fn get_external_point(external: napi.External(ExternalPoint)) ExternalPoint { + return external.value().*; +} + +pub fn mutate_external_point(external: napi.External(ExternalPoint), x: i32, y: i32) void { + external.valueMut().* = .{ .x = x, .y = y }; +} diff --git a/examples/basic/src/hello.zig b/examples/basic/src/hello.zig index 748358f..848adc1 100644 --- a/examples/basic/src/hello.zig +++ b/examples/basic/src/hello.zig @@ -21,6 +21,7 @@ const dataview = @import("dataview.zig"); const reference = @import("reference.zig"); const union_value = @import("union.zig"); const enum_value = @import("enum.zig"); +const external = @import("external.zig"); pub const test_i32 = number.test_i32; pub const test_f32 = number.test_f32; @@ -132,6 +133,15 @@ pub const is_primary = enum_value.is_primary; pub const string_enum_identity = enum_value.string_enum_identity; pub const favorite_string_color = enum_value.favorite_string_color; +pub const create_external = external.create_external; +pub const create_external_with_size_hint = external.create_external_with_size_hint; +pub const get_external = external.get_external; +pub const get_external_size_hint = external.get_external_size_hint; +pub const mutate_external = external.mutate_external; +pub const create_external_point = external.create_external_point; +pub const get_external_point = external.get_external_point; +pub const mutate_external_point = external.mutate_external_point; + comptime { napi.NODE_API_MODULE("hello", @This()); } diff --git a/node-test/napi/__tests__/values.spec.js b/node-test/napi/__tests__/values.spec.js index 3dfb301..4f88038 100644 --- a/node-test/napi/__tests__/values.spec.js +++ b/node-test/napi/__tests__/values.spec.js @@ -213,3 +213,28 @@ test("either", (t) => { t.is(bindings.eitherFromOption("zig"), "zig"); t.is(bindings.eitherFromOption(null), 0); }); + +test("External", (t) => { + const external = bindings.createExternal(10); + t.is(bindings.getExternal(external), 10); + + bindings.mutateExternal(external, 42); + t.is(bindings.getExternal(external), 42); + + const sizedExternal = bindings.createExternalWithSizeHint(7); + t.is(bindings.getExternal(sizedExternal), 7); + t.is(bindings.getExternalSizeHint(sizedExternal), 128); + + const point = bindings.createExternalPoint(3, 4); + t.deepEqual(bindings.getExternalPoint(point), { x: 3, y: 4 }); + + bindings.mutateExternalPoint(point, 5, 6); + t.deepEqual(bindings.getExternalPoint(point), { x: 5, y: 6 }); + + t.throws(() => bindings.getExternalPoint(external), { + message: /External value type does not match expected type/, + }); + t.throws(() => bindings.getExternal({}), { + message: /Expected external value/, + }); +}); diff --git a/node-test/napi/src/lib.zig b/node-test/napi/src/lib.zig index b4d8ad5..28e53c7 100644 --- a/node-test/napi/src/lib.zig +++ b/node-test/napi/src/lib.zig @@ -105,6 +105,14 @@ pub const bigintFromI128 = values.bigintFromI128; pub const eitherStringOrNumber = values.eitherStringOrNumber; pub const returnEither = values.returnEither; pub const eitherFromOption = values.eitherFromOption; +pub const createExternal = values.createExternal; +pub const createExternalWithSizeHint = values.createExternalWithSizeHint; +pub const getExternal = values.getExternal; +pub const getExternalSizeHint = values.getExternalSizeHint; +pub const mutateExternal = values.mutateExternal; +pub const createExternalPoint = values.createExternalPoint; +pub const getExternalPoint = values.getExternalPoint; +pub const mutateExternalPoint = values.mutateExternalPoint; pub const callThreadsafeFunction = threadsafe_function.callThreadsafeFunction; pub const validateArray = strict.validateArray; diff --git a/node-test/napi/src/values.zig b/node-test/napi/src/values.zig index fbb0fb7..e308753 100644 --- a/node-test/napi/src/values.zig +++ b/node-test/napi/src/values.zig @@ -20,6 +20,11 @@ pub const Point = struct { y: i32, }; +pub const ExternalPoint = struct { + x: i32, + y: i32, +}; + const NumberOrString = union(enum) { number: i32, string: []const u8, @@ -526,3 +531,35 @@ pub fn returnEither(value: bool) NumberOrString { pub fn eitherFromOption(value: ?[]const u8) NumberOrString { return if (value) |payload| .{ .string = payload } else .{ .number = 0 }; } + +pub fn createExternal(value: u32) !napi.External(u32) { + return try napi.External(u32).New(value); +} + +pub fn createExternalWithSizeHint(value: u32) !napi.External(u32) { + return try napi.External(u32).NewWithSizeHint(value, 128); +} + +pub fn getExternal(external: napi.External(u32)) u32 { + return external.value().*; +} + +pub fn getExternalSizeHint(external: napi.External(u32)) usize { + return external.sizeHint(); +} + +pub fn mutateExternal(external: napi.External(u32), value: u32) void { + external.valueMut().* = value; +} + +pub fn createExternalPoint(x: i32, y: i32) !napi.External(ExternalPoint) { + return try napi.External(ExternalPoint).New(.{ .x = x, .y = y }); +} + +pub fn getExternalPoint(external: napi.External(ExternalPoint)) ExternalPoint { + return external.value().*; +} + +pub fn mutateExternalPoint(external: napi.External(ExternalPoint), x: i32, y: i32) void { + external.valueMut().* = .{ .x = x, .y = y }; +} diff --git a/src/build/napi-tsgen.zig b/src/build/napi-tsgen.zig index a6e7d9b..3e8f157 100644 --- a/src/build/napi-tsgen.zig +++ b/src/build/napi-tsgen.zig @@ -180,6 +180,14 @@ fn isReferenceType(comptime T: type) bool { return @hasDecl(T, "is_napi_reference"); } +fn isExternalType(comptime T: type) bool { + switch (@typeInfo(T)) { + .@"struct", .@"enum", .@"union", .@"opaque" => {}, + else => return false, + } + return @hasDecl(T, "is_napi_external"); +} + fn isDataViewType(comptime T: type) bool { switch (@typeInfo(T)) { .@"struct", .@"enum", .@"union", .@"opaque" => {}, @@ -228,6 +236,7 @@ fn isObjectLikeStruct(comptime T: type) bool { if (isTypedArrayType(T)) return false; if (isDataViewType(T)) return false; if (isReferenceType(T)) return false; + if (isExternalType(T)) return false; if (isClassType(T)) return false; return true; } @@ -1075,6 +1084,12 @@ fn emitType(state: *State, comptime T: type) ![]const u8 { return emitType(state, T.referenced_type); } + if (comptime isExternalType(T)) { + try emitExternalObjectDecl(state); + const inner = try emitType(state, T.external_type); + return try std.fmt.allocPrint(state.allocator, "ExternalObject<{s}>", .{inner}); + } + if (comptime isClassType(T)) { return shortTypeName(T); } @@ -1135,6 +1150,16 @@ fn emitInterfaceDecl(state: *State, comptime T: type) !void { try append(&state.declarations, "}\n\n"); } +fn emitExternalObjectDecl(state: *State) !void { + const name = "ExternalObject"; + if (state.emitted.contains(name)) return; + try state.emitted.put(name, {}); + + try append(&state.declarations, "export interface ExternalObject {\n"); + try append(&state.declarations, " readonly __napiExternal?: T\n"); + try append(&state.declarations, "}\n\n"); +} + fn emitUnionType(state: *State, comptime T: type) ![]const u8 { const info = @typeInfo(T).@"union"; if (info.fields.len == 0) return "never"; @@ -1668,6 +1693,17 @@ fn emitSourceTypeExpr(state: *State, file_path: []const u8, type_expr: []const u if (std.mem.eql(u8, trimmed, "napi.ArrayBuffer")) return "ArrayBuffer"; if (std.mem.eql(u8, trimmed, "napi.DataView")) return "DataView"; + if (parseSingleArgTypeCall(trimmed)) |type_call| { + if (std.mem.eql(u8, type_call.callee, "napi.External") or + std.mem.endsWith(u8, type_call.callee, ".External") or + std.mem.eql(u8, type_call.callee, "External")) + { + try emitExternalObjectDecl(state); + const child_ts = try emitSourceTypeExpr(state, file_path, type_call.arg, depth + 1); + return try std.fmt.allocPrint(state.allocator, "ExternalObject<{s}>", .{child_ts}); + } + } + if (matchSourceSliceChild(trimmed)) |child| { const child_ts = try emitSourceTypeExpr(state, file_path, child, depth + 1); return try std.fmt.allocPrint(state.allocator, "Array<{s}>", .{child_ts}); diff --git a/src/napi.zig b/src/napi.zig index bb1b5dd..d0f73a3 100644 --- a/src/napi.zig +++ b/src/napi.zig @@ -15,6 +15,7 @@ const arraybuffer = @import("./napi/wrapper/arraybuffer.zig"); const typedarray = @import("./napi/wrapper/typedarray.zig"); const dataview = @import("./napi/wrapper/dataview.zig"); const reference = @import("./napi/wrapper/reference.zig"); +const external = @import("./napi/wrapper/external.zig"); const global_allocator = @import("./napi/util/allocator.zig"); const options = @import("./napi/options.zig"); @@ -68,6 +69,7 @@ pub const BigUint64Array = typedarray.BigUint64Array; pub const DataView = dataview.DataView; pub const Reference = reference.Reference; pub const Ref = reference.Reference; +pub const External = external.External; pub fn FunctionRef(comptime Args: type, comptime Return: type) type { return reference.Reference(function.Function(Args, Return)); } diff --git a/src/napi/util/helper.zig b/src/napi/util/helper.zig index 9e7427e..b75dc4a 100644 --- a/src/napi/util/helper.zig +++ b/src/napi/util/helper.zig @@ -112,6 +112,10 @@ pub fn isReference(comptime T: type) bool { return @hasDecl(T, "is_napi_reference"); } +pub fn isExternal(comptime T: type) bool { + return @hasDecl(T, "is_napi_external"); +} + pub fn isArrayList(comptime T: type) bool { const info = @typeInfo(T); if (info != .@"struct") { diff --git a/src/napi/util/napi.zig b/src/napi/util/napi.zig index ff78ab0..03d8b0b 100644 --- a/src/napi/util/napi.zig +++ b/src/napi/util/napi.zig @@ -173,6 +173,7 @@ fn valueMatchesType(env: napi.napi_env, raw: napi.napi_value, comptime T: type) if (comptime helper.isTypedArray(T)) break :blk isTypedArrayValue(env, raw); if (comptime helper.isDataView(T)) break :blk isDataViewValue(env, raw); if (comptime helper.isReference(T)) break :blk true; + if (comptime helper.isExternal(T)) break :blk napiTypeOf(env, raw) == napi.napi_external; if (comptime helper.isTuple(T)) break :blk isArrayValue(env, raw); if (comptime helper.isArrayList(T)) break :blk isArrayValue(env, raw) or isTypedArrayValue(env, raw); break :blk isPlainObjectValue(env, raw); @@ -429,6 +430,7 @@ pub const Napi = struct { helper.isTypedArray(T) or helper.isDataView(T) or helper.isReference(T) or + helper.isExternal(T) or helper.isAbortSignal(T) or T == NapiValue.BigInt or T == NapiValue.Bool or @@ -566,6 +568,9 @@ pub const Napi = struct { if (comptime helper.isReference(T)) { return T.from_napi_value(env, raw); } + if (comptime helper.isExternal(T)) { + return T.from_napi_value(env, raw); + } if (comptime helper.isTuple(T)) { return NapiValue.Array.from_napi_value(env, raw, T); @@ -715,6 +720,9 @@ pub const Napi = struct { if (comptime helper.isReference(value_type)) { return try value.to_napi_value(env); } + if (comptime helper.isExternal(value_type)) { + return try value.to_napi_value(env); + } if (comptime helper.isTuple(value_type)) { const array = try NapiValue.Array.New(Env.from_raw(env), value); return array.raw; diff --git a/src/napi/wrapper/external.zig b/src/napi/wrapper/external.zig new file mode 100644 index 0000000..3b43a0f --- /dev/null +++ b/src/napi/wrapper/external.zig @@ -0,0 +1,222 @@ +const std = @import("std"); +const napi = @import("napi-sys").napi_sys; +const Napi = @import("../util/napi.zig").Napi; +const NapiError = @import("error.zig"); +const GlobalAllocator = @import("../util/allocator.zig"); + +const external_magic: u64 = 0x5a_4e_41_50_49_45_58_54; + +const TaggedHeader = struct { + magic: u64, + type_name_ptr: [*]const u8, + type_name_len: usize, + allocator: std.mem.Allocator, + value_ptr: ?*anyopaque, + size_hint: usize, + memory_adjusted: bool, + adjusted_size: i64, + destroy: *const fn (*TaggedHeader) void, + + fn typeName(self: *const TaggedHeader) []const u8 { + return self.type_name_ptr[0..self.type_name_len]; + } +}; + +pub fn External(comptime T: type) type { + return struct { + pub const is_napi_external = true; + pub const external_type = T; + + env: napi.napi_env, + raw: napi.napi_value, + header: *TaggedHeader, + + const Self = @This(); + + pub fn New(payload: T) !Self { + return try Self.NewWithSizeHint(payload, 0); + } + + pub fn NewWithSizeHint(payload: T, size_hint: usize) !Self { + const header = try createHeader(payload, size_hint); + return Self{ + .env = null, + .raw = null, + .header = header, + }; + } + + pub fn new(payload: T) !Self { + return try Self.New(payload); + } + + pub fn newWithSizeHint(payload: T, size_hint: usize) !Self { + return try Self.NewWithSizeHint(payload, size_hint); + } + + pub fn from_raw(env: napi.napi_env, raw: napi.napi_value) Self { + return Self.from_napi_value(env, raw); + } + + pub fn from_napi_value(env: napi.napi_env, raw: napi.napi_value) Self { + var value_type: napi.napi_valuetype = undefined; + const type_status = napi.napi_typeof(env, raw, &value_type); + if (type_status != napi.napi_ok) { + NapiError.last_error = NapiError.Error.withStatus(NapiError.Status.New(type_status)); + return invalid(env, raw); + } + if (value_type != napi.napi_external) { + NapiError.last_error = NapiError.Error.withCodeAndMessage("InvalidArg", "Expected external value"); + return invalid(env, raw); + } + + var data: ?*anyopaque = null; + const status = napi.napi_get_value_external(env, raw, &data); + if (status != napi.napi_ok) { + NapiError.last_error = NapiError.Error.withStatus(NapiError.Status.New(status)); + return invalid(env, raw); + } + if (data == null) { + NapiError.last_error = NapiError.Error.withStatus(NapiError.Status.InvalidArg); + return invalid(env, raw); + } + + const header: *TaggedHeader = @ptrCast(@alignCast(data.?)); + if (header.magic != external_magic or !std.mem.eql(u8, header.typeName(), @typeName(T))) { + NapiError.last_error = NapiError.Error.withCodeAndMessage("InvalidArg", "External value type does not match expected type"); + return invalid(env, raw); + } + + return Self{ + .env = env, + .raw = raw, + .header = header, + }; + } + + pub fn to_napi_value(self: Self, env: napi.napi_env) !napi.napi_value { + if (self.raw != null) { + return self.raw; + } + + const size_hint_i64 = std.math.cast(i64, self.header.size_hint) orelse { + self.destroyDetached(); + return NapiError.Error.fromStatus(NapiError.Status.InvalidArg); + }; + + var result: napi.napi_value = undefined; + const status = napi.napi_create_external(env, self.header, externalFinalizer, null, &result); + if (status != napi.napi_ok) { + self.destroyDetached(); + return NapiError.Error.fromStatus(NapiError.Status.New(status)); + } + + if (self.header.size_hint > 0) { + var adjusted_size: i64 = 0; + const adjust_status = napi.napi_adjust_external_memory( + env, + size_hint_i64, + &adjusted_size, + ); + if (adjust_status != napi.napi_ok) { + return NapiError.Error.fromStatus(NapiError.Status.New(adjust_status)); + } + self.header.memory_adjusted = true; + self.header.adjusted_size = adjusted_size; + } + + return result; + } + + pub fn value(self: Self) *const T { + return self.asConstPtr(); + } + + pub fn valueMut(self: Self) *T { + return self.asPtr(); + } + + pub fn asConstPtr(self: Self) *const T { + return @ptrCast(@alignCast(self.header.value_ptr.?)); + } + + pub fn asPtr(self: Self) *T { + return @ptrCast(@alignCast(self.header.value_ptr.?)); + } + + pub fn sizeHint(self: Self) usize { + return self.header.size_hint; + } + + pub fn adjustedSize(self: Self) i64 { + return self.header.adjusted_size; + } + + fn invalid(env: napi.napi_env, raw: napi.napi_value) Self { + return Self{ + .env = env, + .raw = raw, + .header = undefined, + }; + } + + fn createHeader(payload: T, size_hint: usize) !*TaggedHeader { + const allocator = GlobalAllocator.globalAllocator(); + const stored = try allocator.create(T); + stored.* = payload; + errdefer { + Napi.deinit_napi_value(T, stored.*); + allocator.destroy(stored); + } + + const header = try allocator.create(TaggedHeader); + const type_name = @typeName(T); + header.* = .{ + .magic = external_magic, + .type_name_ptr = type_name.ptr, + .type_name_len = type_name.len, + .allocator = allocator, + .value_ptr = stored, + .size_hint = size_hint, + .memory_adjusted = false, + .adjusted_size = 0, + .destroy = destroyHeader, + }; + return header; + } + + fn destroyHeader(header: *TaggedHeader) void { + const allocator = header.allocator; + if (header.value_ptr) |ptr| { + const stored: *T = @ptrCast(@alignCast(ptr)); + Napi.deinit_napi_value(T, stored.*); + allocator.destroy(stored); + header.value_ptr = null; + } + header.magic = 0; + allocator.destroy(header); + } + + fn destroyDetached(self: Self) void { + self.header.destroy(self.header); + } + }; +} + +fn externalFinalizer( + env: napi.napi_env, + data: ?*anyopaque, + _: ?*anyopaque, +) callconv(.c) void { + if (data) |raw| { + const header: *TaggedHeader = @ptrCast(@alignCast(raw)); + if (header.magic != external_magic) return; + + if (env != null and header.memory_adjusted and header.size_hint > 0) { + var adjusted_size: i64 = 0; + _ = napi.napi_adjust_external_memory(env, -@as(i64, @intCast(header.size_hint)), &adjusted_size); + } + + header.destroy(header); + } +} diff --git a/test/basic.ts b/test/basic.ts index bb5ec09..1dbe884 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -1,6 +1,7 @@ import { testAsync } from "./async.spec"; import { testBinary } from "./binary.spec"; import { testErrorsAndThreadSafeFunction } from "./errors-tsfn.spec"; +import { testExternal } from "./external.spec"; import { testFunctionsAndClasses } from "./functions-classes.spec"; import { testObjectsAndArrays } from "./objects-arrays.spec"; import { testPrimitives } from "./primitives.spec"; @@ -12,6 +13,7 @@ runSuite("__ZIG_NAPI_TEST_RESULT__", async (native) => { testObjectsAndArrays(native); testBinary(native); testFunctionsAndClasses(native); + testExternal(native); await testAsync(native); testUnionsAndEnums(native); await testErrorsAndThreadSafeFunction(native); diff --git a/test/external.spec.ts b/test/external.spec.ts new file mode 100644 index 0000000..f63d087 --- /dev/null +++ b/test/external.spec.ts @@ -0,0 +1,33 @@ +import { assertArrayEqual, assertEqual, assertThrows } from "./assert"; + +type NativeAddon = ESObject; + +export function testExternal(native: NativeAddon) { + const external = native.create_external(10); + assertEqual(native.get_external(external), 10, "external value"); + + native.mutate_external(external, 42); + assertEqual(native.get_external(external), 42, "mutated external value"); + + const sizedExternal = native.create_external_with_size_hint(7); + assertEqual(native.get_external(sizedExternal), 7, "external with size hint value"); + assertEqual(native.get_external_size_hint(sizedExternal), 128, "external size hint"); + + const point = native.create_external_point(3, 4); + assertArrayEqual( + [native.get_external_point(point).x, native.get_external_point(point).y], + [3, 4], + "external point value", + ); + + native.mutate_external_point(point, 5, 6); + const mutatedPoint = native.get_external_point(point); + assertArrayEqual([mutatedPoint.x, mutatedPoint.y], [5, 6], "mutated external point"); + + assertThrows( + () => native.get_external_point(external), + "External value type does not match expected type", + "external type mismatch", + ); + assertThrows(() => native.get_external({}), "Expected external value", "non-external value"); +} From 01cb1d57d15ef9af6564e6a9223b82a36dec2a62 Mon Sep 17 00:00:00 2001 From: richerfu Date: Wed, 27 May 2026 11:08:28 +0800 Subject: [PATCH 2/3] Fix review issues --- examples/basic/index.d.ts | 5 ++- examples/basic/src/external.zig | 58 +++++++++++++++++++++++++ examples/basic/src/hello.zig | 2 + node-test/napi/__tests__/values.spec.js | 10 +++++ node-test/napi/src/lib.zig | 2 + node-test/napi/src/values.zig | 56 ++++++++++++++++++++++++ src/build/napi-tsgen.zig | 3 +- src/napi/wrapper/external.zig | 55 ++++++++++++++++------- test/external.spec.ts | 12 +++++ 9 files changed, 186 insertions(+), 17 deletions(-) diff --git a/examples/basic/index.d.ts b/examples/basic/index.d.ts index bed45ef..eb5494d 100644 --- a/examples/basic/index.d.ts +++ b/examples/basic/index.d.ts @@ -124,8 +124,9 @@ export declare const enum StringColor { Blue = "Blue", } +declare const __napiExternalBrand: unique symbol; export interface ExternalObject { - readonly __napiExternal?: T; + readonly [__napiExternalBrand]: T; } export interface ExternalPoint { @@ -262,6 +263,8 @@ export declare function string_enum_identity(color: StringColor): StringColor; export declare function favorite_string_color(): StringColor; export declare function create_external(value: number): ExternalObject; export declare function create_external_with_size_hint(value: number): ExternalObject; +export declare function create_external_pair(value: number): Array>; +export declare function create_misaligned_external(): unknown | undefined | null; export declare function get_external(external: ExternalObject): number; export declare function get_external_size_hint(external: ExternalObject): number; export declare function mutate_external(external: ExternalObject, value: number): void; diff --git a/examples/basic/src/external.zig b/examples/basic/src/external.zig index bb72214..545128d 100644 --- a/examples/basic/src/external.zig +++ b/examples/basic/src/external.zig @@ -1,4 +1,6 @@ +const std = @import("std"); const napi = @import("napi"); +const c = napi.napi_sys.napi_sys; pub const ExternalPoint = struct { x: i32, @@ -13,6 +15,62 @@ pub fn create_external_with_size_hint(value: u32) !napi.External(u32) { return try napi.External(u32).NewWithSizeHint(value, 128); } +pub fn create_external_pair(value: u32) ![2]napi.External(u32) { + const external = try napi.External(u32).New(value); + return .{ external, external }; +} + +const ForeignExternalHint = struct { + allocator: std.mem.Allocator, + ptr: [*]u64, + len: usize, + + fn destroy(self: *ForeignExternalHint) void { + const allocator_value = self.allocator; + allocator_value.free(self.ptr[0..self.len]); + allocator_value.destroy(self); + } +}; + +fn foreignExternalFinalizer( + _: c.napi_env, + _: ?*anyopaque, + hint: ?*anyopaque, +) callconv(.c) void { + if (hint) |raw_hint| { + const external_hint: *ForeignExternalHint = @ptrCast(@alignCast(raw_hint)); + external_hint.destroy(); + } +} + +pub fn create_misaligned_external(env: napi.Env) !c.napi_value { + const allocator_value = napi.globalAllocator(); + const storage = try allocator_value.alloc(u64, 2); + errdefer allocator_value.free(storage); + + const hint = try allocator_value.create(ForeignExternalHint); + errdefer allocator_value.destroy(hint); + hint.* = .{ + .allocator = allocator_value, + .ptr = storage.ptr, + .len = storage.len, + }; + + const byte_ptr: [*]u8 = @ptrCast(storage.ptr); + var raw: c.napi_value = undefined; + const status = c.napi_create_external( + env.raw, + @ptrCast(byte_ptr + 1), + foreignExternalFinalizer, + hint, + &raw, + ); + if (status != c.napi_ok) { + return napi.Error.fromStatus(napi.Status.New(status)); + } + return raw; +} + pub fn get_external(external: napi.External(u32)) u32 { return external.value().*; } diff --git a/examples/basic/src/hello.zig b/examples/basic/src/hello.zig index 848adc1..fa1d9ae 100644 --- a/examples/basic/src/hello.zig +++ b/examples/basic/src/hello.zig @@ -135,6 +135,8 @@ pub const favorite_string_color = enum_value.favorite_string_color; pub const create_external = external.create_external; pub const create_external_with_size_hint = external.create_external_with_size_hint; +pub const create_external_pair = external.create_external_pair; +pub const create_misaligned_external = external.create_misaligned_external; pub const get_external = external.get_external; pub const get_external_size_hint = external.get_external_size_hint; pub const mutate_external = external.mutate_external; diff --git a/node-test/napi/__tests__/values.spec.js b/node-test/napi/__tests__/values.spec.js index 4f88038..3cea37e 100644 --- a/node-test/napi/__tests__/values.spec.js +++ b/node-test/napi/__tests__/values.spec.js @@ -225,6 +225,13 @@ test("External", (t) => { t.is(bindings.getExternal(sizedExternal), 7); t.is(bindings.getExternalSizeHint(sizedExternal), 128); + const pair = bindings.createExternalPair(11); + t.is(pair.length, 2); + t.is(bindings.getExternal(pair[0]), 11); + t.is(bindings.getExternal(pair[1]), 11); + bindings.mutateExternal(pair[0], 12); + t.is(bindings.getExternal(pair[1]), 12); + const point = bindings.createExternalPoint(3, 4); t.deepEqual(bindings.getExternalPoint(point), { x: 3, y: 4 }); @@ -237,4 +244,7 @@ test("External", (t) => { t.throws(() => bindings.getExternal({}), { message: /Expected external value/, }); + t.throws(() => bindings.getExternal(bindings.createMisalignedExternal()), { + message: /External value was not created by zig-napi/, + }); }); diff --git a/node-test/napi/src/lib.zig b/node-test/napi/src/lib.zig index 28e53c7..60c93f2 100644 --- a/node-test/napi/src/lib.zig +++ b/node-test/napi/src/lib.zig @@ -107,6 +107,8 @@ pub const returnEither = values.returnEither; pub const eitherFromOption = values.eitherFromOption; pub const createExternal = values.createExternal; pub const createExternalWithSizeHint = values.createExternalWithSizeHint; +pub const createExternalPair = values.createExternalPair; +pub const createMisalignedExternal = values.createMisalignedExternal; pub const getExternal = values.getExternal; pub const getExternalSizeHint = values.getExternalSizeHint; pub const mutateExternal = values.mutateExternal; diff --git a/node-test/napi/src/values.zig b/node-test/napi/src/values.zig index e308753..874adc9 100644 --- a/node-test/napi/src/values.zig +++ b/node-test/napi/src/values.zig @@ -540,6 +540,62 @@ pub fn createExternalWithSizeHint(value: u32) !napi.External(u32) { return try napi.External(u32).NewWithSizeHint(value, 128); } +pub fn createExternalPair(value: u32) ![2]napi.External(u32) { + const external = try napi.External(u32).New(value); + return .{ external, external }; +} + +const ForeignExternalHint = struct { + allocator: std.mem.Allocator, + ptr: [*]u64, + len: usize, + + fn destroy(self: *ForeignExternalHint) void { + const allocator_value = self.allocator; + allocator_value.free(self.ptr[0..self.len]); + allocator_value.destroy(self); + } +}; + +fn foreignExternalFinalizer( + _: c.napi_env, + _: ?*anyopaque, + hint: ?*anyopaque, +) callconv(.c) void { + if (hint) |raw_hint| { + const external_hint: *ForeignExternalHint = @ptrCast(@alignCast(raw_hint)); + external_hint.destroy(); + } +} + +pub fn createMisalignedExternal(env: napi.Env) !c.napi_value { + const allocator_value = allocator(); + const storage = try allocator_value.alloc(u64, 2); + errdefer allocator_value.free(storage); + + const hint = try allocator_value.create(ForeignExternalHint); + errdefer allocator_value.destroy(hint); + hint.* = .{ + .allocator = allocator_value, + .ptr = storage.ptr, + .len = storage.len, + }; + + const byte_ptr: [*]u8 = @ptrCast(storage.ptr); + var raw: c.napi_value = undefined; + const status = c.napi_create_external( + env.raw, + @ptrCast(byte_ptr + 1), + foreignExternalFinalizer, + hint, + &raw, + ); + if (status != c.napi_ok) { + return napi.Error.fromStatus(napi.Status.New(status)); + } + return raw; +} + pub fn getExternal(external: napi.External(u32)) u32 { return external.value().*; } diff --git a/src/build/napi-tsgen.zig b/src/build/napi-tsgen.zig index 3e8f157..bba6b44 100644 --- a/src/build/napi-tsgen.zig +++ b/src/build/napi-tsgen.zig @@ -1155,8 +1155,9 @@ fn emitExternalObjectDecl(state: *State) !void { if (state.emitted.contains(name)) return; try state.emitted.put(name, {}); + try append(&state.declarations, "declare const __napiExternalBrand: unique symbol\n"); try append(&state.declarations, "export interface ExternalObject {\n"); - try append(&state.declarations, " readonly __napiExternal?: T\n"); + try append(&state.declarations, " readonly [__napiExternalBrand]: T\n"); try append(&state.declarations, "}\n\n"); } diff --git a/src/napi/wrapper/external.zig b/src/napi/wrapper/external.zig index 3b43a0f..1f2e50d 100644 --- a/src/napi/wrapper/external.zig +++ b/src/napi/wrapper/external.zig @@ -13,6 +13,7 @@ const TaggedHeader = struct { allocator: std.mem.Allocator, value_ptr: ?*anyopaque, size_hint: usize, + ref_count: std.atomic.Value(usize), memory_adjusted: bool, adjusted_size: i64, destroy: *const fn (*TaggedHeader) void, @@ -81,8 +82,18 @@ pub fn External(comptime T: type) type { return invalid(env, raw); } - const header: *TaggedHeader = @ptrCast(@alignCast(data.?)); - if (header.magic != external_magic or !std.mem.eql(u8, header.typeName(), @typeName(T))) { + const data_ptr = data.?; + if (@intFromPtr(data_ptr) % @alignOf(TaggedHeader) != 0) { + NapiError.last_error = NapiError.Error.withCodeAndMessage("InvalidArg", "External value was not created by zig-napi"); + return invalid(env, raw); + } + + const header: *TaggedHeader = @ptrCast(@alignCast(data_ptr)); + if (header.magic != external_magic) { + NapiError.last_error = NapiError.Error.withCodeAndMessage("InvalidArg", "External value was not created by zig-napi"); + return invalid(env, raw); + } + if (!std.mem.eql(u8, header.typeName(), @typeName(T))) { NapiError.last_error = NapiError.Error.withCodeAndMessage("InvalidArg", "External value type does not match expected type"); return invalid(env, raw); } @@ -105,24 +116,24 @@ pub fn External(comptime T: type) type { }; var result: napi.napi_value = undefined; + retainHeader(self.header); const status = napi.napi_create_external(env, self.header, externalFinalizer, null, &result); if (status != napi.napi_ok) { - self.destroyDetached(); + releaseHeader(null, self.header); return NapiError.Error.fromStatus(NapiError.Status.New(status)); } - if (self.header.size_hint > 0) { + if (self.header.size_hint > 0 and !self.header.memory_adjusted) { var adjusted_size: i64 = 0; const adjust_status = napi.napi_adjust_external_memory( env, size_hint_i64, &adjusted_size, ); - if (adjust_status != napi.napi_ok) { - return NapiError.Error.fromStatus(NapiError.Status.New(adjust_status)); + if (adjust_status == napi.napi_ok) { + self.header.memory_adjusted = true; + self.header.adjusted_size = adjusted_size; } - self.header.memory_adjusted = true; - self.header.adjusted_size = adjusted_size; } return result; @@ -178,6 +189,7 @@ pub fn External(comptime T: type) type { .allocator = allocator, .value_ptr = stored, .size_hint = size_hint, + .ref_count = std.atomic.Value(usize).init(0), .memory_adjusted = false, .adjusted_size = 0, .destroy = destroyHeader, @@ -198,11 +210,29 @@ pub fn External(comptime T: type) type { } fn destroyDetached(self: Self) void { - self.header.destroy(self.header); + if (self.header.ref_count.load(.acquire) == 0) { + self.header.destroy(self.header); + } } }; } +fn retainHeader(header: *TaggedHeader) void { + _ = header.ref_count.fetchAdd(1, .monotonic); +} + +fn releaseHeader(env: napi.napi_env, header: *TaggedHeader) void { + const previous = header.ref_count.fetchSub(1, .acq_rel); + if (previous != 1) return; + + if (env != null and header.memory_adjusted and header.size_hint > 0) { + var adjusted_size: i64 = 0; + _ = napi.napi_adjust_external_memory(env, -@as(i64, @intCast(header.size_hint)), &adjusted_size); + } + + header.destroy(header); +} + fn externalFinalizer( env: napi.napi_env, data: ?*anyopaque, @@ -212,11 +242,6 @@ fn externalFinalizer( const header: *TaggedHeader = @ptrCast(@alignCast(raw)); if (header.magic != external_magic) return; - if (env != null and header.memory_adjusted and header.size_hint > 0) { - var adjusted_size: i64 = 0; - _ = napi.napi_adjust_external_memory(env, -@as(i64, @intCast(header.size_hint)), &adjusted_size); - } - - header.destroy(header); + releaseHeader(env, header); } } diff --git a/test/external.spec.ts b/test/external.spec.ts index f63d087..3f09062 100644 --- a/test/external.spec.ts +++ b/test/external.spec.ts @@ -13,6 +13,13 @@ export function testExternal(native: NativeAddon) { assertEqual(native.get_external(sizedExternal), 7, "external with size hint value"); assertEqual(native.get_external_size_hint(sizedExternal), 128, "external size hint"); + const pair = native.create_external_pair(11); + assertEqual(pair.length, 2, "external pair length"); + assertEqual(native.get_external(pair[0]), 11, "external pair first value"); + assertEqual(native.get_external(pair[1]), 11, "external pair second value"); + native.mutate_external(pair[0], 12); + assertEqual(native.get_external(pair[1]), 12, "external pair shared payload"); + const point = native.create_external_point(3, 4); assertArrayEqual( [native.get_external_point(point).x, native.get_external_point(point).y], @@ -30,4 +37,9 @@ export function testExternal(native: NativeAddon) { "external type mismatch", ); assertThrows(() => native.get_external({}), "Expected external value", "non-external value"); + assertThrows( + () => native.get_external(native.create_misaligned_external()), + "External value was not created by zig-napi", + "misaligned external value", + ); } From 085ba192fc18d5f07b7415af50fdd7d16f439bc6 Mon Sep 17 00:00:00 2001 From: richerfu Date: Wed, 27 May 2026 11:56:41 +0800 Subject: [PATCH 3/3] Fix comment issues --- examples/basic/index.d.ts | 9 ++ examples/basic/src/external.zig | 45 +++++++ examples/basic/src/hello.zig | 5 + node-test/napi/__tests__/values.spec.js | 8 ++ node-test/napi/src/lib.zig | 5 + node-test/napi/src/values.zig | 45 +++++++ src/napi/util/napi.zig | 2 +- src/napi/wrapper/external.zig | 152 +++++++++++++++--------- test/external.spec.ts | 8 ++ 9 files changed, 225 insertions(+), 54 deletions(-) diff --git a/examples/basic/index.d.ts b/examples/basic/index.d.ts index eb5494d..eac0d5c 100644 --- a/examples/basic/index.d.ts +++ b/examples/basic/index.d.ts @@ -275,3 +275,12 @@ export declare function mutate_external_point( x: number, y: number, ): void; +export declare function external_either_kind( + value: ExternalObject | ExternalObject, +): number; +export declare function external_either_value( + value: ExternalObject | ExternalObject, +): number; +export declare function reset_detached_external_deinit_count(): void; +export declare function detached_external_deinit_count(): number; +export declare function deinit_detached_external(): number; diff --git a/examples/basic/src/external.zig b/examples/basic/src/external.zig index 545128d..a898650 100644 --- a/examples/basic/src/external.zig +++ b/examples/basic/src/external.zig @@ -7,6 +7,22 @@ pub const ExternalPoint = struct { y: i32, }; +const ExternalEither = union(enum) { + number: napi.External(u32), + point: napi.External(ExternalPoint), +}; + +var detached_external_deinits = std.atomic.Value(usize).init(0); + +const DetachedExternalPayload = struct { + value: u32, + + pub fn deinit(self: *DetachedExternalPayload) void { + _ = self; + _ = detached_external_deinits.fetchAdd(1, .monotonic); + } +}; + pub fn create_external(value: u32) !napi.External(u32) { return try napi.External(u32).New(value); } @@ -94,3 +110,32 @@ pub fn get_external_point(external: napi.External(ExternalPoint)) ExternalPoint pub fn mutate_external_point(external: napi.External(ExternalPoint), x: i32, y: i32) void { external.valueMut().* = .{ .x = x, .y = y }; } + +pub fn external_either_kind(value: ExternalEither) u32 { + return switch (value) { + .number => 1, + .point => 2, + }; +} + +pub fn external_either_value(value: ExternalEither) i32 { + return switch (value) { + .number => |external| @intCast(external.value().*), + .point => |external| external.value().x + external.value().y, + }; +} + +pub fn reset_detached_external_deinit_count() void { + detached_external_deinits.store(0, .monotonic); +} + +pub fn detached_external_deinit_count() usize { + return detached_external_deinits.load(.monotonic); +} + +pub fn deinit_detached_external() !usize { + reset_detached_external_deinit_count(); + var external = try napi.External(DetachedExternalPayload).New(.{ .value = 1 }); + external.deinit(); + return detached_external_deinit_count(); +} diff --git a/examples/basic/src/hello.zig b/examples/basic/src/hello.zig index fa1d9ae..79322c7 100644 --- a/examples/basic/src/hello.zig +++ b/examples/basic/src/hello.zig @@ -143,6 +143,11 @@ pub const mutate_external = external.mutate_external; pub const create_external_point = external.create_external_point; pub const get_external_point = external.get_external_point; pub const mutate_external_point = external.mutate_external_point; +pub const external_either_kind = external.external_either_kind; +pub const external_either_value = external.external_either_value; +pub const reset_detached_external_deinit_count = external.reset_detached_external_deinit_count; +pub const detached_external_deinit_count = external.detached_external_deinit_count; +pub const deinit_detached_external = external.deinit_detached_external; comptime { napi.NODE_API_MODULE("hello", @This()); diff --git a/node-test/napi/__tests__/values.spec.js b/node-test/napi/__tests__/values.spec.js index 3cea37e..48186f3 100644 --- a/node-test/napi/__tests__/values.spec.js +++ b/node-test/napi/__tests__/values.spec.js @@ -237,6 +237,14 @@ test("External", (t) => { bindings.mutateExternalPoint(point, 5, 6); t.deepEqual(bindings.getExternalPoint(point), { x: 5, y: 6 }); + t.is(bindings.externalEitherKind(external), 1); + t.is(bindings.externalEitherValue(external), 42); + t.is(bindings.externalEitherKind(point), 2); + t.is(bindings.externalEitherValue(point), 11); + + bindings.resetDetachedExternalDeinitCount(); + t.is(bindings.deinitDetachedExternal(), 1); + t.is(bindings.detachedExternalDeinitCount(), 1); t.throws(() => bindings.getExternalPoint(external), { message: /External value type does not match expected type/, diff --git a/node-test/napi/src/lib.zig b/node-test/napi/src/lib.zig index 60c93f2..cf49128 100644 --- a/node-test/napi/src/lib.zig +++ b/node-test/napi/src/lib.zig @@ -115,6 +115,11 @@ pub const mutateExternal = values.mutateExternal; pub const createExternalPoint = values.createExternalPoint; pub const getExternalPoint = values.getExternalPoint; pub const mutateExternalPoint = values.mutateExternalPoint; +pub const externalEitherKind = values.externalEitherKind; +pub const externalEitherValue = values.externalEitherValue; +pub const resetDetachedExternalDeinitCount = values.resetDetachedExternalDeinitCount; +pub const detachedExternalDeinitCount = values.detachedExternalDeinitCount; +pub const deinitDetachedExternal = values.deinitDetachedExternal; pub const callThreadsafeFunction = threadsafe_function.callThreadsafeFunction; pub const validateArray = strict.validateArray; diff --git a/node-test/napi/src/values.zig b/node-test/napi/src/values.zig index 874adc9..39f020a 100644 --- a/node-test/napi/src/values.zig +++ b/node-test/napi/src/values.zig @@ -30,6 +30,22 @@ const NumberOrString = union(enum) { string: []const u8, }; +const ExternalEither = union(enum) { + number: napi.External(u32), + point: napi.External(ExternalPoint), +}; + +var detached_external_deinits = std.atomic.Value(usize).init(0); + +const DetachedExternalPayload = struct { + value: u32, + + pub fn deinit(self: *DetachedExternalPayload) void { + _ = self; + _ = detached_external_deinits.fetchAdd(1, .monotonic); + } +}; + fn allocator() std.mem.Allocator { return napi.globalAllocator(); } @@ -619,3 +635,32 @@ pub fn getExternalPoint(external: napi.External(ExternalPoint)) ExternalPoint { pub fn mutateExternalPoint(external: napi.External(ExternalPoint), x: i32, y: i32) void { external.valueMut().* = .{ .x = x, .y = y }; } + +pub fn externalEitherKind(value: ExternalEither) u32 { + return switch (value) { + .number => 1, + .point => 2, + }; +} + +pub fn externalEitherValue(value: ExternalEither) i32 { + return switch (value) { + .number => |external| @intCast(external.value().*), + .point => |external| external.value().x + external.value().y, + }; +} + +pub fn resetDetachedExternalDeinitCount() void { + detached_external_deinits.store(0, .monotonic); +} + +pub fn detachedExternalDeinitCount() usize { + return detached_external_deinits.load(.monotonic); +} + +pub fn deinitDetachedExternal() !usize { + resetDetachedExternalDeinitCount(); + var external = try napi.External(DetachedExternalPayload).New(.{ .value = 1 }); + external.deinit(); + return detachedExternalDeinitCount(); +} diff --git a/src/napi/util/napi.zig b/src/napi/util/napi.zig index 03d8b0b..9aafb55 100644 --- a/src/napi/util/napi.zig +++ b/src/napi/util/napi.zig @@ -173,7 +173,7 @@ fn valueMatchesType(env: napi.napi_env, raw: napi.napi_value, comptime T: type) if (comptime helper.isTypedArray(T)) break :blk isTypedArrayValue(env, raw); if (comptime helper.isDataView(T)) break :blk isDataViewValue(env, raw); if (comptime helper.isReference(T)) break :blk true; - if (comptime helper.isExternal(T)) break :blk napiTypeOf(env, raw) == napi.napi_external; + if (comptime helper.isExternal(T)) break :blk T.matches_napi_value(env, raw); if (comptime helper.isTuple(T)) break :blk isArrayValue(env, raw); if (comptime helper.isArrayList(T)) break :blk isArrayValue(env, raw) or isTypedArrayValue(env, raw); break :blk isPlainObjectValue(env, raw); diff --git a/src/napi/wrapper/external.zig b/src/napi/wrapper/external.zig index 1f2e50d..3c11704 100644 --- a/src/napi/wrapper/external.zig +++ b/src/napi/wrapper/external.zig @@ -30,7 +30,7 @@ pub fn External(comptime T: type) type { env: napi.napi_env, raw: napi.napi_value, - header: *TaggedHeader, + header: ?*TaggedHeader, const Self = @This(); @@ -60,43 +60,7 @@ pub fn External(comptime T: type) type { } pub fn from_napi_value(env: napi.napi_env, raw: napi.napi_value) Self { - var value_type: napi.napi_valuetype = undefined; - const type_status = napi.napi_typeof(env, raw, &value_type); - if (type_status != napi.napi_ok) { - NapiError.last_error = NapiError.Error.withStatus(NapiError.Status.New(type_status)); - return invalid(env, raw); - } - if (value_type != napi.napi_external) { - NapiError.last_error = NapiError.Error.withCodeAndMessage("InvalidArg", "Expected external value"); - return invalid(env, raw); - } - - var data: ?*anyopaque = null; - const status = napi.napi_get_value_external(env, raw, &data); - if (status != napi.napi_ok) { - NapiError.last_error = NapiError.Error.withStatus(NapiError.Status.New(status)); - return invalid(env, raw); - } - if (data == null) { - NapiError.last_error = NapiError.Error.withStatus(NapiError.Status.InvalidArg); - return invalid(env, raw); - } - - const data_ptr = data.?; - if (@intFromPtr(data_ptr) % @alignOf(TaggedHeader) != 0) { - NapiError.last_error = NapiError.Error.withCodeAndMessage("InvalidArg", "External value was not created by zig-napi"); - return invalid(env, raw); - } - - const header: *TaggedHeader = @ptrCast(@alignCast(data_ptr)); - if (header.magic != external_magic) { - NapiError.last_error = NapiError.Error.withCodeAndMessage("InvalidArg", "External value was not created by zig-napi"); - return invalid(env, raw); - } - if (!std.mem.eql(u8, header.typeName(), @typeName(T))) { - NapiError.last_error = NapiError.Error.withCodeAndMessage("InvalidArg", "External value type does not match expected type"); - return invalid(env, raw); - } + const header = headerFromNapiValue(env, raw, true) orelse return invalid(env, raw); return Self{ .env = env, @@ -105,25 +69,32 @@ pub fn External(comptime T: type) type { }; } + pub fn matches_napi_value(env: napi.napi_env, raw: napi.napi_value) bool { + return headerFromNapiValue(env, raw, false) != null; + } + pub fn to_napi_value(self: Self, env: napi.napi_env) !napi.napi_value { if (self.raw != null) { return self.raw; } - const size_hint_i64 = std.math.cast(i64, self.header.size_hint) orelse { - self.destroyDetached(); + const header = self.header orelse { + return NapiError.Error.fromStatus(NapiError.Status.InvalidArg); + }; + const size_hint_i64 = std.math.cast(i64, header.size_hint) orelse { + _ = destroyDetachedHeader(header); return NapiError.Error.fromStatus(NapiError.Status.InvalidArg); }; var result: napi.napi_value = undefined; - retainHeader(self.header); - const status = napi.napi_create_external(env, self.header, externalFinalizer, null, &result); + retainHeader(header); + const status = napi.napi_create_external(env, header, externalFinalizer, null, &result); if (status != napi.napi_ok) { - releaseHeader(null, self.header); + releaseHeader(null, header); return NapiError.Error.fromStatus(NapiError.Status.New(status)); } - if (self.header.size_hint > 0 and !self.header.memory_adjusted) { + if (header.size_hint > 0 and !header.memory_adjusted) { var adjusted_size: i64 = 0; const adjust_status = napi.napi_adjust_external_memory( env, @@ -131,14 +102,83 @@ pub fn External(comptime T: type) type { &adjusted_size, ); if (adjust_status == napi.napi_ok) { - self.header.memory_adjusted = true; - self.header.adjusted_size = adjusted_size; + header.memory_adjusted = true; + header.adjusted_size = adjusted_size; } } return result; } + pub fn Deinit(self: *Self) void { + self.deinit(); + } + + pub fn deinit(self: *Self) void { + const header = self.header orelse return; + if (destroyDetachedHeader(header)) { + self.header = null; + self.raw = null; + self.env = null; + } + } + + fn headerFromNapiValue(env: napi.napi_env, raw: napi.napi_value, comptime report_error: bool) ?*TaggedHeader { + var value_type: napi.napi_valuetype = undefined; + const type_status = napi.napi_typeof(env, raw, &value_type); + if (type_status != napi.napi_ok) { + if (report_error) { + NapiError.last_error = NapiError.Error.withStatus(NapiError.Status.New(type_status)); + } + return null; + } + if (value_type != napi.napi_external) { + if (report_error) { + NapiError.last_error = NapiError.Error.withCodeAndMessage("InvalidArg", "Expected external value"); + } + return null; + } + + var data: ?*anyopaque = null; + const status = napi.napi_get_value_external(env, raw, &data); + if (status != napi.napi_ok) { + if (report_error) { + NapiError.last_error = NapiError.Error.withStatus(NapiError.Status.New(status)); + } + return null; + } + if (data == null) { + if (report_error) { + NapiError.last_error = NapiError.Error.withStatus(NapiError.Status.InvalidArg); + } + return null; + } + + const data_ptr = data.?; + if (@intFromPtr(data_ptr) % @alignOf(TaggedHeader) != 0) { + if (report_error) { + NapiError.last_error = NapiError.Error.withCodeAndMessage("InvalidArg", "External value was not created by zig-napi"); + } + return null; + } + + const header: *TaggedHeader = @ptrCast(@alignCast(data_ptr)); + if (header.magic != external_magic) { + if (report_error) { + NapiError.last_error = NapiError.Error.withCodeAndMessage("InvalidArg", "External value was not created by zig-napi"); + } + return null; + } + if (!std.mem.eql(u8, header.typeName(), @typeName(T))) { + if (report_error) { + NapiError.last_error = NapiError.Error.withCodeAndMessage("InvalidArg", "External value type does not match expected type"); + } + return null; + } + + return header; + } + pub fn value(self: Self) *const T { return self.asConstPtr(); } @@ -148,26 +188,26 @@ pub fn External(comptime T: type) type { } pub fn asConstPtr(self: Self) *const T { - return @ptrCast(@alignCast(self.header.value_ptr.?)); + return @ptrCast(@alignCast(self.header.?.value_ptr.?)); } pub fn asPtr(self: Self) *T { - return @ptrCast(@alignCast(self.header.value_ptr.?)); + return @ptrCast(@alignCast(self.header.?.value_ptr.?)); } pub fn sizeHint(self: Self) usize { - return self.header.size_hint; + return self.header.?.size_hint; } pub fn adjustedSize(self: Self) i64 { - return self.header.adjusted_size; + return self.header.?.adjusted_size; } fn invalid(env: napi.napi_env, raw: napi.napi_value) Self { return Self{ .env = env, .raw = raw, - .header = undefined, + .header = null, }; } @@ -210,13 +250,19 @@ pub fn External(comptime T: type) type { } fn destroyDetached(self: Self) void { - if (self.header.ref_count.load(.acquire) == 0) { - self.header.destroy(self.header); + if (self.header) |header| { + _ = destroyDetachedHeader(header); } } }; } +fn destroyDetachedHeader(header: *TaggedHeader) bool { + if (header.ref_count.load(.acquire) != 0) return false; + header.destroy(header); + return true; +} + fn retainHeader(header: *TaggedHeader) void { _ = header.ref_count.fetchAdd(1, .monotonic); } diff --git a/test/external.spec.ts b/test/external.spec.ts index 3f09062..71e4420 100644 --- a/test/external.spec.ts +++ b/test/external.spec.ts @@ -30,6 +30,14 @@ export function testExternal(native: NativeAddon) { native.mutate_external_point(point, 5, 6); const mutatedPoint = native.get_external_point(point); assertArrayEqual([mutatedPoint.x, mutatedPoint.y], [5, 6], "mutated external point"); + assertEqual(native.external_either_kind(external), 1, "external union number kind"); + assertEqual(native.external_either_value(external), 42, "external union number value"); + assertEqual(native.external_either_kind(point), 2, "external union point kind"); + assertEqual(native.external_either_value(point), 11, "external union point value"); + + native.reset_detached_external_deinit_count(); + assertEqual(native.deinit_detached_external(), 1, "detached external deinit return"); + assertEqual(native.detached_external_deinit_count(), 1, "detached external deinit count"); assertThrows( () => native.get_external_point(external),