Skip to content
Open
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
29 changes: 24 additions & 5 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()`, `toArray(len)`, `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 Expand Up @@ -450,11 +469,11 @@ const callback = napi.createCallback(0, makeExternalBuffer, .{
});
```

`OwnedBuffer.intoValue` consumes the buffer even if conversion fails, so the caller must not
deinitialize it afterwards. Once N-API accepts the external buffer, the allocator must remain valid
until its finalizer runs, including if N-API subsequently reports an error. If the environment
disallows external buffers, `intoValue` copies the bytes and releases the original allocation before
returning.
`OwnedBuffer.intoValue` empties the source after ownership transfers, so a deferred `deinit` is safe.
Failures before N-API accepts the external memory leave ownership in the source; later failures leave
it empty because the finalizer may already own the allocation. Once N-API accepts the external buffer,
the allocator must remain valid until its finalizer runs. If the environment disallows external
buffers, `intoValue` copies the bytes and consumes the source only after that copy succeeds.

### Creating Classes

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 @@ -255,6 +255,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 @@ -243,6 +244,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
112 changes: 70 additions & 42 deletions src/OwnedBuffer.zig
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! Allocator-backed bytes whose ownership can be transferred to JavaScript.
//! Like other Zig owning values, an OwnedBuffer must not be copied or deinitialized after transfer.
//! Like other Zig owning values, an OwnedBuffer must not be copied while it owns data.

const std = @import("std");
const c = @import("c.zig").c;
Expand All @@ -11,11 +11,6 @@ data: []u8,

const OwnedBuffer = @This();

const FinalizerContext = struct {
allocator: std.mem.Allocator,
data: []u8,
};

/// Takes ownership of `data`, which must have been allocated by `allocator`.
/// The allocator must remain valid until the buffer is deinitialized or finalized by JavaScript.
pub fn fromOwnedSlice(allocator: std.mem.Allocator, data: []u8) OwnedBuffer {
Expand All @@ -30,62 +25,75 @@ pub fn fromSlice(allocator: std.mem.Allocator, data: []const u8) !OwnedBuffer {
return .fromOwnedSlice(allocator, try allocator.dupe(u8, data));
}

/// Releases a buffer that has not been transferred to JavaScript.
/// 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: *OwnedBuffer) void {
self.allocator.free(self.data);
self.* = undefined;
}

/// Transfers ownership to a JavaScript Buffer.
/// This consumes the buffer even on failure; the caller must not deinitialize it afterwards.
/// On success, JavaScript releases the allocation through the N-API finalizer.
pub fn intoValue(self: OwnedBuffer, env: Env) !Value {
const allocator = self.allocator;
///
/// On success, `self.data` is empty and JavaScript releases the original
/// allocation through the Buffer 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 buffers return `error.NoExternalBuffersAllowed`
/// without a copy fallback. The caller may deinitialize `self` after this
/// function returns.
pub fn intoValue(self: *OwnedBuffer, env: Env) !Value {
const data = self.data;

if (data.len == 0) {
defer allocator.free(data);
return try env.createBuffer(0, null);
const value = try env.createBuffer(0, null);
self.allocator.free(data);
self.data = &.{};
return value;
}

const context = try createFinalizerContext(self);
const owner = try moveToHeap(self);

return env.createExternalBuffer(data, finalize, context) catch |err| {
if (err == error.NoExternalBuffersAllowed) {
defer release(context);
return try env.createBufferCopy(data, null);
return env.createExternalBuffer(data, finalize, owner) catch |err| {
switch (err) {
error.NoExternalBuffersAllowed,
error.PendingException,
error.CannotRunJS,
=> restoreFromHeap(self, owner),
else => {},
}
// Other failures may occur after the finalizer has taken ownership.
return err;
};
}

fn createFinalizerContext(self: OwnedBuffer) !*FinalizerContext {
const context = self.allocator.create(FinalizerContext) catch |err| {
self.allocator.free(self.data);
return err;
};
context.* = .{
.allocator = self.allocator,
.data = self.data,
};
return context;
fn moveToHeap(self: *OwnedBuffer) !*OwnedBuffer {
const owner = try self.allocator.create(OwnedBuffer);
owner.* = self.*;
self.data = &.{};
return owner;
}

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

fn finalize(
_: c.napi_env,
finalize_data: ?*anyopaque,
finalize_hint: ?*anyopaque,
) callconv(.c) void {
const context: *FinalizerContext = @ptrCast(@alignCast(finalize_hint orelse unreachable));
std.debug.assert(finalize_data == @as(?*anyopaque, @ptrCast(context.data.ptr)));
release(context);
const owner: *OwnedBuffer = @ptrCast(@alignCast(finalize_hint orelse unreachable));
std.debug.assert(finalize_data == @as(?*anyopaque, @ptrCast(owner.data.ptr)));
release(owner);
}

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

test "OwnedBuffer fromSlice owns an independent copy" {
Expand All @@ -97,14 +105,34 @@ test "OwnedBuffer fromSlice owns an independent copy" {
try std.testing.expectEqualSlices(u8, &.{ 1, 2, 3 }, buffer.data);
}

test "OwnedBuffer releases data when finalizer context allocation fails" {
const data = try std.testing.allocator.dupe(u8, "external");

test "OwnedBuffer retains data when moving the owner to the heap fails" {
var failing_allocator = std.testing.FailingAllocator.init(std.testing.allocator, .{
.fail_index = 0,
.fail_index = 1,
});
const buffer = OwnedBuffer.fromOwnedSlice(failing_allocator.allocator(), data);
{
var buffer = try OwnedBuffer.fromSlice(
failing_allocator.allocator(),
"external",
);
defer buffer.deinit();

try std.testing.expectError(error.OutOfMemory, moveToHeap(&buffer));
try std.testing.expectEqualSlices(u8, "external", buffer.data);
try std.testing.expectEqual(@as(usize, 0), failing_allocator.deallocations);
}

try std.testing.expectError(error.OutOfMemory, createFinalizerContext(buffer));
try std.testing.expectEqual(@as(usize, 1), failing_allocator.deallocations);
}

test "OwnedBuffer restores ownership from the heap" {
var buffer = try OwnedBuffer.fromSlice(std.testing.allocator, "external");
defer buffer.deinit();

const owner = try moveToHeap(&buffer);
const source_is_empty = buffer.data.len == 0;

restoreFromHeap(&buffer, owner);

try std.testing.expect(source_is_empty);
try std.testing.expectEqualSlices(u8, "external", buffer.data);
}
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
Loading
Loading