diff --git a/node-test/napi/__tests__/values.spec.js b/node-test/napi/__tests__/values.spec.js index 48186f3..a587ae5 100644 --- a/node-test/napi/__tests__/values.spec.js +++ b/node-test/napi/__tests__/values.spec.js @@ -83,6 +83,35 @@ test("object", (t) => { t.deepEqual(bindings.listObjKeys({ z: 1, a: 2 }).sort(), ["a", "z"]); }); +test("native wrap", (t) => { + bindings.resetNativeWrapDeinitCount(); + const wrapped = bindings.createNativeWrap(7); + t.is(wrapped.kind, "native-wrap"); + t.true(bindings.nativeWrapMatches(wrapped)); + t.is(bindings.getNativeWrapValue(wrapped), 7); + t.is(bindings.getNativeWrapFromEnv(wrapped), 7); + + bindings.mutateNativeWrapValue(wrapped, 42); + t.is(bindings.getNativeWrapValue(wrapped), 42); + + t.throws(() => bindings.getNativeWrapWrongType(wrapped), { + message: /Native wrapped object type does not match expected type/, + }); + t.throws(() => bindings.dropNativeWrapWrongType(wrapped), { + message: /Native wrapped object type does not match expected type/, + }); + t.is(bindings.getNativeWrapValue(wrapped), 42); + t.is(bindings.nativeWrapDeinitCount(), 0); + + t.throws(() => bindings.getNativeWrapValue({}), { + message: /Object is not wrapped by zig-napi/, + }); + + bindings.dropNativeWrap(wrapped); + t.is(bindings.nativeWrapDeinitCount(), 1); + t.false(bindings.nativeWrapMatches(wrapped)); +}); + test("global, undefined, null and symbol", (t) => { t.is(bindings.getGlobal(), globalThis); t.is(bindings.getUndefined(), undefined); diff --git a/node-test/napi/src/lib.zig b/node-test/napi/src/lib.zig index cf49128..4bab7ca 100644 --- a/node-test/napi/src/lib.zig +++ b/node-test/napi/src/lib.zig @@ -37,6 +37,16 @@ pub const callFunctionWithArg = values.callFunctionWithArg; pub const createFunction = values.createFunction; pub const createObj = values.createObj; pub const listObjKeys = values.listObjKeys; +pub const createNativeWrap = values.createNativeWrap; +pub const getNativeWrapValue = values.getNativeWrapValue; +pub const mutateNativeWrapValue = values.mutateNativeWrapValue; +pub const dropNativeWrap = values.dropNativeWrap; +pub const dropNativeWrapWrongType = values.dropNativeWrapWrongType; +pub const getNativeWrapWrongType = values.getNativeWrapWrongType; +pub const getNativeWrapFromEnv = values.getNativeWrapFromEnv; +pub const nativeWrapMatches = values.nativeWrapMatches; +pub const resetNativeWrapDeinitCount = values.resetNativeWrapDeinitCount; +pub const nativeWrapDeinitCount = values.nativeWrapDeinitCount; pub const getGlobal = values.getGlobal; pub const getUndefined = values.getUndefined; pub const getNull = values.getNull; diff --git a/node-test/napi/src/values.zig b/node-test/napi/src/values.zig index 39f020a..c2f5242 100644 --- a/node-test/napi/src/values.zig +++ b/node-test/napi/src/values.zig @@ -25,6 +25,21 @@ pub const ExternalPoint = struct { y: i32, }; +pub const NativeWrapPayload = struct { + value: u32, + + const Self = @This(); + + pub fn deinit(self: *Self) void { + _ = self; + _ = native_wrap_deinits.fetchAdd(1, .monotonic); + } +}; + +pub const OtherNativeWrapPayload = struct { + value: u32, +}; + const NumberOrString = union(enum) { number: i32, string: []const u8, @@ -36,6 +51,7 @@ const ExternalEither = union(enum) { }; var detached_external_deinits = std.atomic.Value(usize).init(0); +var native_wrap_deinits = std.atomic.Value(usize).init(0); const DetachedExternalPayload = struct { value: u32, @@ -184,6 +200,53 @@ pub fn indexmapPassthrough(object: napi.Object) c.napi_value { return object.raw; } +pub fn createNativeWrap(env: napi.Env, value: u32) !napi.Object { + var object = try napi.Object.Create(env); + try object.Set("kind", "native-wrap"); + try object.wrapWithSizeHint(NativeWrapPayload{ .value = value }, 64); + return object; +} + +pub fn getNativeWrapValue(object: napi.Object) !u32 { + const payload = try object.unwrap(NativeWrapPayload); + return payload.value; +} + +pub fn mutateNativeWrapValue(object: napi.Object, value: u32) !void { + const payload = try object.unwrap(NativeWrapPayload); + payload.value = value; +} + +pub fn dropNativeWrap(object: napi.Object) !void { + try object.dropWrapped(NativeWrapPayload); +} + +pub fn dropNativeWrapWrongType(object: napi.Object) !void { + try object.dropWrapped(OtherNativeWrapPayload); +} + +pub fn getNativeWrapWrongType(object: napi.Object) !u32 { + const payload = try object.unwrap(OtherNativeWrapPayload); + return payload.value; +} + +pub fn getNativeWrapFromEnv(env: napi.Env, object: napi.Object) !u32 { + const payload = try env.unwrap(object, NativeWrapPayload); + return payload.value; +} + +pub fn nativeWrapMatches(object: napi.Object) bool { + return object.matchesWrapped(NativeWrapPayload); +} + +pub fn resetNativeWrapDeinitCount() void { + native_wrap_deinits.store(0, .monotonic); +} + +pub fn nativeWrapDeinitCount() usize { + return native_wrap_deinits.load(.monotonic); +} + pub fn enumToI32(value: CustomNumEnum) i32 { return @intFromEnum(value); } diff --git a/src/napi.zig b/src/napi.zig index d0f73a3..dd83795 100644 --- a/src/napi.zig +++ b/src/napi.zig @@ -16,6 +16,7 @@ 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 native_wrap = @import("./napi/wrapper/native_wrap.zig"); const global_allocator = @import("./napi/util/allocator.zig"); const options = @import("./napi/options.zig"); @@ -70,6 +71,7 @@ pub const DataView = dataview.DataView; pub const Reference = reference.Reference; pub const Ref = reference.Reference; pub const External = external.External; +pub const NativeWrap = native_wrap; pub fn FunctionRef(comptime Args: type, comptime Return: type) type { return reference.Reference(function.Function(Args, Return)); } diff --git a/src/napi/env.zig b/src/napi/env.zig index d5a5ba6..fa15f9a 100644 --- a/src/napi/env.zig +++ b/src/napi/env.zig @@ -1,6 +1,7 @@ const napi = @import("napi-sys").napi_sys; const Undefined = @import("./value/undefined.zig").Undefined; const Null = @import("./value/null.zig").Null; +const native_wrap = @import("./wrapper/native_wrap.zig"); pub const Env = struct { raw: napi.napi_env, @@ -22,4 +23,40 @@ pub const Env = struct { _ = napi.napi_get_null(self.raw, &result); return Null.from_raw(self.raw, result); } + + pub fn wrap(self: Env, js_object: anytype, payload: anytype) !void { + return self.wrapWithSizeHint(js_object, payload, 0); + } + + pub fn wrapWithSizeHint(self: Env, js_object: anytype, payload: anytype, size_hint: usize) !void { + try native_wrap.wrap(self.raw, objectRaw(js_object), payload, size_hint); + } + + pub fn unwrap(self: Env, js_object: anytype, comptime T: type) !*T { + return try native_wrap.unwrap(self.raw, objectRaw(js_object), T); + } + + pub fn unwrapConst(self: Env, js_object: anytype, comptime T: type) !*const T { + return try native_wrap.unwrapConst(self.raw, objectRaw(js_object), T); + } + + pub fn dropWrapped(self: Env, js_object: anytype, comptime T: type) !void { + try native_wrap.dropWrapped(self.raw, objectRaw(js_object), T); + } + + pub fn matchesWrapped(self: Env, js_object: anytype, comptime T: type) bool { + return native_wrap.matches(self.raw, objectRaw(js_object), T); + } }; + +fn objectRaw(js_object: anytype) napi.napi_value { + const ObjectType = @TypeOf(js_object); + const ValueType = switch (@typeInfo(ObjectType)) { + .pointer => |ptr| ptr.child, + else => ObjectType, + }; + if (!@hasField(ValueType, "raw")) { + @compileError("Expected an object-like value with a raw napi_value field, got: " ++ @typeName(ObjectType)); + } + return js_object.raw; +} diff --git a/src/napi/value/object.zig b/src/napi/value/object.zig index f6c9709..c7e2cd8 100644 --- a/src/napi/value/object.zig +++ b/src/napi/value/object.zig @@ -12,6 +12,7 @@ const helper = @import("../util/helper.zig"); const Napi = @import("../util/napi.zig").Napi; const NapiError = @import("../wrapper/error.zig"); const Reference = @import("../wrapper/reference.zig").Reference; +const native_wrap = @import("../wrapper/native_wrap.zig"); pub const Object = struct { env: napi.napi_env, @@ -134,4 +135,44 @@ pub const Object = struct { pub fn CreateRef(self: Object) !Reference(Object) { return Reference(Object).New(Env.from_raw(self.env), self); } + + pub fn Wrap(self: Object, payload: anytype) !void { + try self.wrap(payload); + } + + pub fn WrapWithSizeHint(self: Object, payload: anytype, size_hint: usize) !void { + try self.wrapWithSizeHint(payload, size_hint); + } + + pub fn wrap(self: Object, payload: anytype) !void { + try self.wrapWithSizeHint(payload, 0); + } + + pub fn wrapWithSizeHint(self: Object, payload: anytype, size_hint: usize) !void { + try native_wrap.wrap(self.env, self.raw, payload, size_hint); + } + + pub fn Unwrap(self: Object, comptime T: type) !*T { + return try self.unwrap(T); + } + + pub fn unwrap(self: Object, comptime T: type) !*T { + return try native_wrap.unwrap(self.env, self.raw, T); + } + + pub fn unwrapConst(self: Object, comptime T: type) !*const T { + return try native_wrap.unwrapConst(self.env, self.raw, T); + } + + pub fn DropWrapped(self: Object, comptime T: type) !void { + try self.dropWrapped(T); + } + + pub fn dropWrapped(self: Object, comptime T: type) !void { + try native_wrap.dropWrapped(self.env, self.raw, T); + } + + pub fn matchesWrapped(self: Object, comptime T: type) bool { + return native_wrap.matches(self.env, self.raw, T); + } }; diff --git a/src/napi/wrapper/native_wrap.zig b/src/napi/wrapper/native_wrap.zig new file mode 100644 index 0000000..a147e79 --- /dev/null +++ b/src/napi/wrapper/native_wrap.zig @@ -0,0 +1,215 @@ +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 native_wrap_magic: u64 = 0x5a_4e_41_50_49_57_52_50; + +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, + destroy: *const fn (*TaggedHeader) void, + + fn typeName(self: *const TaggedHeader) []const u8 { + return self.type_name_ptr[0..self.type_name_len]; + } +}; + +pub fn wrap(env: napi.napi_env, raw_object: napi.napi_value, payload: anytype, size_hint: usize) !void { + const Payload = @TypeOf(payload); + const header = try createHeader(Payload, payload, size_hint); + var owned = true; + errdefer if (owned) destroyHeaderRaw(header); + + const size_hint_i64 = std.math.cast(i64, size_hint) orelse { + return NapiError.Error.fromStatus(NapiError.Status.InvalidArg); + }; + + const status = napi.napi_wrap(env, raw_object, header, finalizer, null, null); + if (status != napi.napi_ok) { + return NapiError.Error.fromStatus(NapiError.Status.New(status)); + } + owned = false; + + if (size_hint == 0) { + return; + } + + 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; + return; + } + + var removed: ?*anyopaque = null; + const remove_status = napi.napi_remove_wrap(env, raw_object, &removed); + if (remove_status == napi.napi_ok and removed != null) { + const removed_header: *TaggedHeader = @ptrCast(@alignCast(removed.?)); + destroyHeaderRaw(removed_header); + } + return NapiError.Error.fromStatus(NapiError.Status.New(adjust_status)); +} + +pub fn unwrap(env: napi.napi_env, raw_object: napi.napi_value, comptime T: type) !*T { + const header = try headerFromObject(env, raw_object, T); + return @ptrCast(@alignCast(header.value_ptr.?)); +} + +pub fn unwrapConst(env: napi.napi_env, raw_object: napi.napi_value, comptime T: type) !*const T { + return try unwrap(env, raw_object, T); +} + +pub fn dropWrapped(env: napi.napi_env, raw_object: napi.napi_value, comptime T: type) !void { + const header = try headerFromObject(env, raw_object, T); + + var removed: ?*anyopaque = null; + const status = napi.napi_remove_wrap(env, raw_object, &removed); + if (status != napi.napi_ok) { + return NapiError.Error.fromStatus(NapiError.Status.New(status)); + } + if (removed == null) { + return NapiError.Error.fromReason("Object is not wrapped by zig-napi"); + } + if (removed.? != @as(*anyopaque, @ptrCast(header))) { + return NapiError.Error.fromReason("Removed native wrap did not match the unwrapped value"); + } + + if (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.memory_adjusted = false; + } + destroyHeaderRaw(header); +} + +pub fn matches(env: napi.napi_env, raw_object: napi.napi_value, comptime T: type) bool { + return headerFromObjectInternal(env, raw_object, T, false) != null; +} + +fn createHeader(comptime T: type, payload: T, size_hint: usize) !*TaggedHeader { + const allocator = GlobalAllocator.globalAllocator(); + const stored = try allocator.create(T); + stored.* = payload; + errdefer { + destroyStoredValue(T, allocator, stored); + } + + const header = try allocator.create(TaggedHeader); + const type_name = @typeName(T); + header.* = .{ + .magic = native_wrap_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, + .destroy = destroyTypedHeader(T), + }; + return header; +} + +fn headerFromObject(env: napi.napi_env, raw_object: napi.napi_value, comptime T: type) !*TaggedHeader { + return headerFromObjectInternal(env, raw_object, T, true) orelse error.GenericFailure; +} + +fn headerFromObjectInternal( + env: napi.napi_env, + raw_object: napi.napi_value, + comptime T: type, + comptime report_error: bool, +) ?*TaggedHeader { + var data: ?*anyopaque = null; + const status = napi.napi_unwrap(env, raw_object, &data); + if (status != napi.napi_ok) { + if (report_error) { + NapiError.last_error = if (status == napi.napi_invalid_arg) + NapiError.Error.withReason("Object is not wrapped by zig-napi") + else + NapiError.Error.withStatus(NapiError.Status.New(status)); + } + return null; + } + if (data == null) { + if (report_error) { + NapiError.last_error = NapiError.Error.withReason("Object is not wrapped by zig-napi"); + } + return null; + } + + const data_ptr = data.?; + if (@intFromPtr(data_ptr) % @alignOf(TaggedHeader) != 0) { + if (report_error) { + NapiError.last_error = NapiError.Error.withReason("Wrapped object was not created by zig-napi"); + } + return null; + } + + const header: *TaggedHeader = @ptrCast(@alignCast(data_ptr)); + if (header.magic != native_wrap_magic) { + if (report_error) { + NapiError.last_error = NapiError.Error.withReason("Wrapped object 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.withReason("Native wrapped object type does not match expected type"); + } + return null; + } + + return header; +} + +fn destroyHeaderRaw(header: *TaggedHeader) void { + header.destroy(header); +} + +fn destroyStoredValue(comptime T: type, allocator: std.mem.Allocator, stored: *T) void { + const previous_allocator = GlobalAllocator.globalAllocator(); + GlobalAllocator.global_manager.set(allocator); + defer GlobalAllocator.global_manager.set(previous_allocator); + + Napi.deinit_napi_value(T, stored.*); + allocator.destroy(stored); +} + +fn destroyTypedHeader(comptime T: type) *const fn (*TaggedHeader) void { + return struct { + fn destroy(header: *TaggedHeader) void { + const allocator = header.allocator; + if (header.value_ptr) |ptr| { + const stored: *T = @ptrCast(@alignCast(ptr)); + destroyStoredValue(T, allocator, stored); + header.value_ptr = null; + } + header.magic = 0; + allocator.destroy(header); + } + }.destroy; +} + +fn finalizer( + env: napi.napi_env, + data: ?*anyopaque, + _: ?*anyopaque, +) callconv(.c) void { + if (data) |raw| { + const header: *TaggedHeader = @ptrCast(@alignCast(raw)); + if (header.magic != native_wrap_magic) return; + if (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.memory_adjusted = false; + } + destroyHeaderRaw(header); + } +}