diff --git a/examples/basic/index.d.ts b/examples/basic/index.d.ts index 77ca454..eac0d5c 100644 --- a/examples/basic/index.d.ts +++ b/examples/basic/index.d.ts @@ -124,6 +124,16 @@ export declare const enum StringColor { Blue = "Blue", } +declare const __napiExternalBrand: unique symbol; +export interface ExternalObject { + readonly [__napiExternalBrand]: 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 +261,26 @@ 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 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; +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; +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 new file mode 100644 index 0000000..a898650 --- /dev/null +++ b/examples/basic/src/external.zig @@ -0,0 +1,141 @@ +const std = @import("std"); +const napi = @import("napi"); +const c = napi.napi_sys.napi_sys; + +pub const ExternalPoint = struct { + x: i32, + 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); +} + +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().*; +} + +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 }; +} + +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 748358f..79322c7 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,22 @@ 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 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; +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 3dfb301..48186f3 100644 --- a/node-test/napi/__tests__/values.spec.js +++ b/node-test/napi/__tests__/values.spec.js @@ -213,3 +213,46 @@ 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 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 }); + + 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/, + }); + 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 b4d8ad5..cf49128 100644 --- a/node-test/napi/src/lib.zig +++ b/node-test/napi/src/lib.zig @@ -105,6 +105,21 @@ 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 createExternalPair = values.createExternalPair; +pub const createMisalignedExternal = values.createMisalignedExternal; +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 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 fbb0fb7..39f020a 100644 --- a/node-test/napi/src/values.zig +++ b/node-test/napi/src/values.zig @@ -20,11 +20,32 @@ pub const Point = struct { y: i32, }; +pub const ExternalPoint = struct { + x: i32, + y: i32, +}; + const NumberOrString = union(enum) { number: i32, 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(); } @@ -526,3 +547,120 @@ 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 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().*; +} + +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 }; +} + +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/build/napi-tsgen.zig b/src/build/napi-tsgen.zig index a6e7d9b..bba6b44 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,17 @@ 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, "declare const __napiExternalBrand: unique symbol\n"); + try append(&state.declarations, "export interface ExternalObject {\n"); + try append(&state.declarations, " readonly [__napiExternalBrand]: 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 +1694,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..9aafb55 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 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); @@ -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..3c11704 --- /dev/null +++ b/src/napi/wrapper/external.zig @@ -0,0 +1,293 @@ +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, + ref_count: std.atomic.Value(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 { + const header = headerFromNapiValue(env, raw, true) orelse return invalid(env, raw); + + return Self{ + .env = env, + .raw = raw, + .header = header, + }; + } + + 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 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(header); + const status = napi.napi_create_external(env, header, externalFinalizer, null, &result); + if (status != napi.napi_ok) { + releaseHeader(null, header); + return NapiError.Error.fromStatus(NapiError.Status.New(status)); + } + + if (header.size_hint > 0 and !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) { + 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(); + } + + 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 = null, + }; + } + + 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, + .ref_count = std.atomic.Value(usize).init(0), + .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 { + 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); +} + +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, + _: ?*anyopaque, +) callconv(.c) void { + if (data) |raw| { + const header: *TaggedHeader = @ptrCast(@alignCast(raw)); + if (header.magic != external_magic) return; + + releaseHeader(env, 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..71e4420 --- /dev/null +++ b/test/external.spec.ts @@ -0,0 +1,53 @@ +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 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], + [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"); + 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), + "External value type does not match expected type", + "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", + ); +}