diff --git a/examples/js_dsl/mod.test.ts b/examples/js_dsl/mod.test.ts index 9c195e0..505bd95 100644 --- a/examples/js_dsl/mod.test.ts +++ b/examples/js_dsl/mod.test.ts @@ -160,6 +160,18 @@ describe("typed arrays", () => { expect(result).toBeInstanceOf(Uint8Array); expect(result.length).toEqual(0); }); + + it("externalUint8Array copies a JS array into a native-backed Uint8Array", () => { + const test_cases = [ + { input: [] }, + { input: [10, 20, 30, 40] }, + ]; + + for (const tc of test_cases) { + const result = mod.externalUint8Array(tc.input); + expect(result).toBeInstanceOf(Uint8Array); + } + }); }); // Section 7: Promises diff --git a/examples/js_dsl/mod.zig b/examples/js_dsl/mod.zig index b46afa8..71804ee 100644 --- a/examples/js_dsl/mod.zig +++ b/examples/js_dsl/mod.zig @@ -190,6 +190,24 @@ pub fn allocUint8(len: Number) !Uint8Array { return arr; } +/// Build a Uint8Array backed by an *external* (native-heap) ArrayBuffer. +/// +/// Takes a JS array of numbers, copies the bytes into a buffer owned by +/// `js.allocator()`, and hands the pointer to V8. The DSL wires up a +/// `SliceFinalizeCallback` so the native buffer is freed automatically when +/// V8 collects the ArrayBuffer — no manual cleanup needed on the JS side. +pub fn externalUint8Array(arr: Array) !Uint8Array { + const len = try arr.length(); + const alloc = js.allocator(); + const tmp = try alloc.alloc(u8, len); + defer alloc.free(tmp); + var i: u32 = 0; + while (i < len) : (i += 1) { + tmp[i] = @intCast((try arr.getNumber(i)).assertI32()); + } + return Uint8Array.fromExternal(tmp); +} + // ============================================================================ // Section 7: Promises // ============================================================================ diff --git a/src/Env.zig b/src/Env.zig index 3b6bf1a..4735d80 100644 --- a/src/Env.zig +++ b/src/Env.zig @@ -293,7 +293,7 @@ pub fn createDate(self: Env, time: f64) NapiError!Value { pub fn createExternal(self: Env, data: [*]const u8, finalize_cb: c.napi_finalize, finalize_hint: ?*anyopaque) NapiError!Value { var value: c.napi_value = undefined; try status.check( - c.napi_create_external(self.env, @constCast(@ptrCast(data)), finalize_cb, finalize_hint, &value), + c.napi_create_external(self.env, @ptrCast(@constCast(data)), finalize_cb, finalize_hint, &value), ); return Value{ .env = self.env, @@ -305,7 +305,14 @@ pub fn createExternal(self: Env, data: [*]const u8, finalize_cb: c.napi_finalize pub fn createExternalArrayBuffer(self: Env, data: []const u8, finalize_cb: c.napi_finalize, finalize_hint: ?*anyopaque) NapiError!Value { var value: c.napi_value = undefined; try status.check( - c.napi_create_external_arraybuffer(self.env, @constCast(@ptrCast(data.ptr)), data.len, finalize_cb, finalize_hint, &value), + c.napi_create_external_arraybuffer( + self.env, + @ptrCast(@constCast(data.ptr)), + data.len, + finalize_cb, + finalize_hint, + &value, + ), ); return Value{ .env = self.env, @@ -317,7 +324,7 @@ pub fn createExternalArrayBuffer(self: Env, data: []const u8, finalize_cb: c.nap pub fn createExternalBuffer(self: Env, data: []const u8, finalize_cb: c.napi_finalize, finalize_hint: ?*anyopaque) NapiError!Value { var value: c.napi_value = undefined; try status.check( - c.napi_create_external_buffer(self.env, data.len, @constCast(@ptrCast(data.ptr)), finalize_cb, finalize_hint, &value), + c.napi_create_external_buffer(self.env, data.len, @ptrCast(@constCast(data.ptr)), finalize_cb, finalize_hint, &value), ); return Value{ .env = self.env, diff --git a/src/finalize_callback.zig b/src/finalize_callback.zig index 2bd6fee..144688c 100644 --- a/src/finalize_callback.zig +++ b/src/finalize_callback.zig @@ -19,10 +19,39 @@ pub fn wrapFinalizeCallback( if (data == null) return; return finalize_cb( Env{ .env = env }, - @alignCast(@ptrCast(data)), + @ptrCast(@alignCast(data)), hint, ); } }; return wrapper.f; } + +/// Typed finalizer for externally backed buffers. +/// +/// The C-ABI `napi_finalize` parameters (`?*anyopaque` data, `?*anyopaque` hint) +/// are unpacked into a single `[]Element` slice. By convention this helper +/// expects the hint pointer slot to carry the element count (set via +/// `@ptrFromInt(slice.len)` at creation time). +pub fn SliceFinalizeCallback(comptime Element: type) type { + return *const fn (Env, []Element) void; +} + +pub fn wrapSliceFinalizeCallback( + comptime Element: type, + comptime finalize_cb: SliceFinalizeCallback(Element), +) c.napi_finalize { + const wrapper = struct { + pub fn f( + env: c.napi_env, + data: ?*anyopaque, + hint: ?*anyopaque, + ) callconv(.c) void { + if (data == null) return; + const len: usize = @intFromPtr(hint); + const ptr: [*]Element = @ptrCast(@alignCast(data)); + return finalize_cb(Env{ .env = env }, ptr[0..len]); + } + }; + return wrapper.f; +} diff --git a/src/js/typed_arrays.zig b/src/js/typed_arrays.zig index d2edae0..2f1f365 100644 --- a/src/js/typed_arrays.zig +++ b/src/js/typed_arrays.zig @@ -1,3 +1,4 @@ +const std = @import("std"); const napi = @import("../napi.zig"); const context = @import("context.zig"); const TypedarrayType = napi.value_types.TypedarrayType; @@ -48,6 +49,42 @@ pub fn TypedArray(comptime Element: type, comptime array_type: TypedarrayType) t return typed_ptr[0..info.length]; } + /// Creates a new JavaScript TypedArray backed by an *external* (native-heap) + /// ArrayBuffer. + /// + /// The contents of `slice` are copied into a freshly allocated native buffer + /// (via `context.allocator()`). + /// + /// V8 holds the pointer to manage the JS-side lifetime; the + /// native buffer is freed by a finalizer when V8 collects the ArrayBuffer. + pub fn fromExternal(slice: []const Element) !Self { + const e = context.env(); + const buf = try context.allocator().dupe(Element, slice); + + const byte_len = slice.len * @sizeOf(Element); + const len_hint: ?*anyopaque = @ptrFromInt(slice.len); + const finalize_cb = comptime napi.wrapSliceFinalizeCallback(Element, externalFinalizer); + const arraybuffer = e.createExternalArrayBuffer(std.mem.sliceAsBytes(buf), finalize_cb, len_hint) catch |err| { + context.allocator().free(buf); + return err; + }; + + _ = try e.adjustExternalMemory(@intCast(byte_len)); + const val = try e.createTypedarray(array_type, slice.len, arraybuffer, 0); + return .{ .val = val }; + } + + /// Finalizer for buffers allocated by `fromExternal`. Frees the native + /// allocation and reverses the matching `adjustExternalMemory` accounting. + /// + /// Caller is responsible for calling a matching `adjustExternalMemory` at + /// the appropriate callsite to let V8 know about native heap memory usage. + fn externalFinalizer(env: napi.Env, data: []Element) void { + const byte_len = data.len * @sizeOf(Element); + context.allocator().free(data); + _ = env.adjustExternalMemory(-@as(i64, @intCast(byte_len))) catch {}; + } + /// Creates a new JavaScript TypedArray from a Zig slice by copying the data. /// /// This function allocates a new `ArrayBuffer` in V8, copies the contents diff --git a/src/napi.zig b/src/napi.zig index aad1c2b..bbba986 100644 --- a/src/napi.zig +++ b/src/napi.zig @@ -14,11 +14,13 @@ pub const Ref = @import("Ref.zig"); pub const CallbackInfo = @import("callback_info.zig").CallbackInfo; pub const Callback = @import("callback.zig").Callback; pub const FinalizeCallback = @import("finalize_callback.zig").FinalizeCallback; +pub const SliceFinalizeCallback = @import("finalize_callback.zig").SliceFinalizeCallback; pub const value_types = @import("value_types.zig"); pub const createCallback = @import("create_callback.zig").createCallback; pub const registerDecls = @import("register_decls.zig").registerDecls; pub const wrapFinalizeCallback = @import("finalize_callback.zig").wrapFinalizeCallback; +pub const wrapSliceFinalizeCallback = @import("finalize_callback.zig").wrapSliceFinalizeCallback; pub const wrapCallback = @import("callback.zig").wrapCallback; pub const AsyncWork = @import("async_work.zig").AsyncWork;