Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions examples/js_dsl/mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions examples/js_dsl/mod.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ============================================================================
Expand Down
13 changes: 10 additions & 3 deletions src/Env.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
31 changes: 30 additions & 1 deletion src/finalize_callback.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
37 changes: 37 additions & 0 deletions src/js/typed_arrays.zig
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const std = @import("std");
const napi = @import("../napi.zig");
const context = @import("context.zig");
const TypedarrayType = napi.value_types.TypedarrayType;
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/napi.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading