Skip to content
Draft
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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ c.count; // 1 (getter, not a method call)
| `Function` | `Function` | `call(args)` |
| `Value` | `any` | `isNumber()`, `asNumber()`, type checking/narrowing |
| `Uint8Array` etc. | `TypedArray` | `toSlice()`, `from(slice)` |
| `OwnedUint8Array` etc. | `TypedArray` | `fromOwnedSlice(allocator, data)`, `fromSlice(allocator, data)` |
| `Promise(T)` | `Promise` | `resolve(value)`, `reject(err)` |

---
Expand Down Expand Up @@ -288,6 +289,24 @@ pub fn sum(data: Uint8Array) !Number {
}
```

Use an owned return type to transfer an allocator-owned slice to JavaScript
without copying its elements:

```zig
pub fn serialize() !js.OwnedUint8Array {
const allocator = js.allocator();
const data = try allocator.alloc(u8, 32);
// Fill data...
return js.OwnedUint8Array.fromOwnedSlice(allocator, data);
}
```

Returning the value transfers its allocation to JavaScript without copying and
leaves the Zig owner empty. Failures before N-API accepts the external memory
leave ownership in Zig so it can be released normally. The allocator must remain
valid until the ArrayBuffer finalizer runs. If external ArrayBuffers are
unsupported, the original error is returned; no copy fallback is performed.

### Promises

```zig
Expand Down
8 changes: 8 additions & 0 deletions examples/js_dsl/mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,14 @@ describe("typed arrays", () => {
expect(result).toBeInstanceOf(Uint8Array);
}
});

it("ownedUint8Array returns allocator-owned native data", () => {
for (const input of [[], [10, 20, 30, 40]]) {
const result = mod.ownedUint8Array(input);
expect(result).toBeInstanceOf(Uint8Array);
expect(Array.from(result)).toEqual(input);
}
});
});

// Section 7: Promises
Expand Down
15 changes: 15 additions & 0 deletions examples/js_dsl/mod.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const Object = js.Object;
const Function = js.Function;
const Value = js.Value;
const Uint8Array = js.Uint8Array;
const OwnedUint8Array = js.OwnedUint8Array;
const Float64Array = js.Float64Array;
const Promise = js.Promise;

Expand Down Expand Up @@ -233,6 +234,20 @@ pub fn externalUint8Array(arr: Array) !Uint8Array {
return Uint8Array.fromExternal(tmp);
}

/// Transfer an allocator-owned native allocation to a JavaScript Uint8Array.
pub fn ownedUint8Array(arr: Array) !OwnedUint8Array {
const len = try arr.length();
const alloc = js.allocator();
const data = try alloc.alloc(u8, len);
errdefer alloc.free(data);

var i: u32 = 0;
while (i < len) : (i += 1) {
data[i] = @intCast((try arr.getNumber(i)).assertI32());
}
return OwnedUint8Array.fromOwnedSlice(alloc, data);
}

// ============================================================================
// Section 7: Promises
// ============================================================================
Expand Down
12 changes: 12 additions & 0 deletions src/js.zig
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub const Function = @import("js/function.zig").Function;
pub const Value = @import("js/value.zig").Value;

pub const TypedArray = typed_arrays.TypedArray;
pub const OwnedTypedArray = typed_arrays.OwnedTypedArray;
pub const Int8Array = typed_arrays.Int8Array;
pub const Uint8Array = typed_arrays.Uint8Array;
pub const Uint8ClampedArray = typed_arrays.Uint8ClampedArray;
Expand All @@ -36,6 +37,17 @@ pub const Float32Array = typed_arrays.Float32Array;
pub const Float64Array = typed_arrays.Float64Array;
pub const BigInt64Array = typed_arrays.BigInt64Array;
pub const BigUint64Array = typed_arrays.BigUint64Array;
pub const OwnedInt8Array = typed_arrays.OwnedInt8Array;
pub const OwnedUint8Array = typed_arrays.OwnedUint8Array;
pub const OwnedUint8ClampedArray = typed_arrays.OwnedUint8ClampedArray;
pub const OwnedInt16Array = typed_arrays.OwnedInt16Array;
pub const OwnedUint16Array = typed_arrays.OwnedUint16Array;
pub const OwnedInt32Array = typed_arrays.OwnedInt32Array;
pub const OwnedUint32Array = typed_arrays.OwnedUint32Array;
pub const OwnedFloat32Array = typed_arrays.OwnedFloat32Array;
pub const OwnedFloat64Array = typed_arrays.OwnedFloat64Array;
pub const OwnedBigInt64Array = typed_arrays.OwnedBigInt64Array;
pub const OwnedBigUint64Array = typed_arrays.OwnedBigUint64Array;

pub const Promise = @import("js/promise.zig").Promise;
pub const createPromise = @import("js/promise.zig").createPromise;
Expand Down
208 changes: 208 additions & 0 deletions src/js/typed_arrays.zig
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,140 @@ pub fn TypedArray(comptime Element: type, comptime array_type: TypedarrayType) t
};
}

fn elementType(comptime array_type: TypedarrayType) type {
return switch (array_type) {
.int8 => i8,
.uint8, .uint8_clamped => u8,
.int16 => i16,
.uint16 => u16,
.int32 => i32,
.uint32 => u32,
.float32 => f32,
.float64 => f64,
.bigint64 => i64,
.biguint64 => u64,
};
}

/// Allocator-backed elements whose ownership can be transferred to a JavaScript
/// TypedArray without copying the element data.
///
/// Like other Zig owning values, an OwnedTypedArray must not be copied. After
/// ownership transfers, the source is empty and may be deinitialized normally.
pub fn OwnedTypedArray(comptime Element: type, comptime array_type: TypedarrayType) type {
if (Element != elementType(array_type)) {
@compileError(
"OwnedTypedArray element type `" ++ @typeName(Element) ++
"` does not match `" ++ @tagName(array_type) ++ "`",
);
}

return struct {
allocator: std.mem.Allocator,
data: []Element,

const Self = @This();
pub const owned_typed_array = {};
pub const expected_array_type = array_type;

/// Takes ownership of `data`, which must have been allocated by `allocator`.
/// The allocator must remain valid until the value is deinitialized or
/// finalized by JavaScript.
pub fn fromOwnedSlice(allocator: std.mem.Allocator, data: []Element) Self {
return .{
.allocator = allocator,
.data = data,
};
}

/// Copies `data` into a new owned allocation.
pub fn fromSlice(allocator: std.mem.Allocator, data: []const Element) !Self {
return .fromOwnedSlice(allocator, try allocator.dupe(Element, data));
}

/// Releases data that has not been transferred to JavaScript. This is
/// also safe after a successful transfer, when `data` is empty.
pub fn deinit(self: *Self) void {
self.allocator.free(self.data);
self.* = undefined;
}

/// Transfers ownership to a JavaScript TypedArray.
///
/// On success, `self.data` is empty and JavaScript releases the original
/// allocation through the ArrayBuffer finalizer. Failures before N-API
/// accepts the external memory leave ownership in `self`; failures after
/// ownership may have transferred leave `self.data` empty.
///
/// Unsupported external ArrayBuffers return
/// `error.NoExternalBuffersAllowed` without a copy fallback. The caller
/// may deinitialize `self` after this function returns.
pub fn intoValue(self: *Self, env: napi.Env) !napi.Value {
const data = self.data;

if (data.len == 0) {
const arraybuffer = try env.createArrayBuffer(0, null);
const value = try env.createTypedarray(array_type, 0, arraybuffer, 0);
self.data = &.{};
return value;
}

const owner = try moveToHeap(self);
const arraybuffer = env.createExternalArrayBuffer(
std.mem.sliceAsBytes(data),
finalize,
owner,
) catch |err| {
switch (err) {
error.NoExternalBuffersAllowed,
error.PendingException,
error.CannotRunJS,
=> restoreFromHeap(self, owner),
else => {},
}
return err;
};

return env.createTypedarray(array_type, data.len, arraybuffer, 0);
}

fn moveToHeap(self: *Self) !*Self {
const owner = try self.allocator.create(Self);
owner.* = self.*;
self.data = &.{};
return owner;
}

fn restoreFromHeap(self: *Self, owner: *Self) void {
const allocator = owner.allocator;
std.debug.assert(self.data.len == 0);
self.* = owner.*;
allocator.destroy(owner);
}

fn finalize(
_: napi.c.napi_env,
_: ?*anyopaque,
finalize_hint: ?*anyopaque,
) callconv(.c) void {
const owner: *Self =
@ptrCast(@alignCast(finalize_hint orelse unreachable));
release(owner);
}

fn release(owner: *Self) void {
const allocator = owner.allocator;
allocator.free(owner.data);
allocator.destroy(owner);
}
};
}

/// Returns whether `T` is an OwnedTypedArray specialization.
pub fn isOwnedTypedArray(comptime T: type) bool {
return @typeInfo(T) == .@"struct" and @hasDecl(T, "owned_typed_array");
}

// Concrete typed array types
/// Wrapper around JavaScript `Int8Array`.
pub const Int8Array = TypedArray(i8, .int8);
Expand Down Expand Up @@ -168,7 +302,81 @@ pub const BigInt64Array = TypedArray(i64, .bigint64);
/// Wrapper around JavaScript `BigUint64Array`.
pub const BigUint64Array = TypedArray(u64, .biguint64);

/// Owned native elements transferable to a JavaScript `Int8Array`.
pub const OwnedInt8Array = OwnedTypedArray(i8, .int8);

/// Owned native elements transferable to a JavaScript `Uint8Array`.
pub const OwnedUint8Array = OwnedTypedArray(u8, .uint8);

/// Owned native elements transferable to a JavaScript `Uint8ClampedArray`.
pub const OwnedUint8ClampedArray = OwnedTypedArray(u8, .uint8_clamped);

/// Owned native elements transferable to a JavaScript `Int16Array`.
pub const OwnedInt16Array = OwnedTypedArray(i16, .int16);

/// Owned native elements transferable to a JavaScript `Uint16Array`.
pub const OwnedUint16Array = OwnedTypedArray(u16, .uint16);

/// Owned native elements transferable to a JavaScript `Int32Array`.
pub const OwnedInt32Array = OwnedTypedArray(i32, .int32);

/// Owned native elements transferable to a JavaScript `Uint32Array`.
pub const OwnedUint32Array = OwnedTypedArray(u32, .uint32);

/// Owned native elements transferable to a JavaScript `Float32Array`.
pub const OwnedFloat32Array = OwnedTypedArray(f32, .float32);

/// Owned native elements transferable to a JavaScript `Float64Array`.
pub const OwnedFloat64Array = OwnedTypedArray(f64, .float64);

/// Owned native elements transferable to a JavaScript `BigInt64Array`.
pub const OwnedBigInt64Array = OwnedTypedArray(i64, .bigint64);

/// Owned native elements transferable to a JavaScript `BigUint64Array`.
pub const OwnedBigUint64Array = OwnedTypedArray(u64, .biguint64);

test "TypedArray exposes expected subtype metadata" {
try @import("std").testing.expect(Uint8Array.expected_array_type == .uint8);
try @import("std").testing.expect(Float64Array.expected_array_type == .float64);
}

test "OwnedTypedArray fromSlice owns an independent copy" {
var source = [_]u8{ 1, 2, 3 };
var array = try OwnedUint8Array.fromSlice(std.testing.allocator, &source);
defer array.deinit();

source[0] = 9;
try std.testing.expectEqualSlices(u8, &.{ 1, 2, 3 }, array.data);
}

test "OwnedTypedArray retains data when moving the owner to the heap fails" {
var failing_allocator = std.testing.FailingAllocator.init(std.testing.allocator, .{
.fail_index = 1,
});
var array = try OwnedUint8Array.fromSlice(
failing_allocator.allocator(),
&.{ 1, 2, 3 },
);

try std.testing.expectError(
error.OutOfMemory,
OwnedUint8Array.moveToHeap(&array),
);
try std.testing.expectEqualSlices(u8, &.{ 1, 2, 3 }, array.data);

array.deinit();
try std.testing.expectEqual(@as(usize, 1), failing_allocator.deallocations);
}

test "OwnedTypedArray empties the source when ownership moves to the heap" {
var array = try OwnedUint8Array.fromSlice(
std.testing.allocator,
&.{ 1, 2, 3 },
);
const owner = try OwnedUint8Array.moveToHeap(&array);
defer OwnedUint8Array.release(owner);

try std.testing.expectEqual(@as(usize, 0), array.data.len);
array.deinit();
try std.testing.expectEqualSlices(u8, &.{ 1, 2, 3 }, owner.data);
}
13 changes: 11 additions & 2 deletions src/js/wrap_function.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const napi = @import("../napi.zig");
const context = @import("context.zig");
const class_meta = @import("class_meta.zig");
const class_runtime = @import("class_runtime.zig");
const typed_arrays = @import("typed_arrays.zig");

/// Checks whether `T` is a ZAPI DSL wrapper type (a struct with a `val: napi.Value` field).
///
Expand Down Expand Up @@ -222,6 +223,16 @@ pub fn convertReturnWithCtor(comptime T: type, value: T, env: napi.c.napi_env, p
if (T == napi.Value) {
return value.value;
}
if (comptime typed_arrays.isOwnedTypedArray(T)) {
const e = napi.Env{ .env = env };
var owned = value;
defer owned.deinit();
const result = owned.intoValue(e) catch |err| {
e.throwError(@errorName(err), @errorName(err)) catch {};
return null;
};
return result.value;
}
if (comptime isDslType(T)) {
return value.val.value;
}
Expand Down Expand Up @@ -385,8 +396,6 @@ test "isDslType rejects non-DSL types" {
}

test "argTypeDescription names typed arrays and unwraps optionals" {
const typed_arrays = @import("typed_arrays.zig");

try std.testing.expectEqualStrings("a number", argTypeDescription(?@import("number.zig").Number));
try std.testing.expectEqualStrings("a Uint8Array", argTypeDescription(typed_arrays.Uint8Array));
}
Expand Down
Loading