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
47 changes: 47 additions & 0 deletions examples/js_dsl/mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,21 @@ describe("typed arrays", () => {
expect(Array.from(result)).toEqual([2.5, 5.0, 7.5]);
});

it("accepts empty Uint8Array and Float64Array values", () => {
expect(mod.uint8Sum(new Uint8Array(0))).toEqual(0);
const result = mod.float64Scale(new Float64Array(0), 2.5);
expect(result).toBeInstanceOf(Float64Array);
expect(result).toHaveLength(0);
});

it("accepts a detached Uint8Array value", () => {
const backing = new ArrayBuffer(8);
const view = new Uint8Array(backing, 2, 3);
structuredClone(backing, { transfer: [backing] });

expect(mod.uint8Sum(view)).toEqual(0);
});

it("allocUint8 allocates and fills via alloc pattern", () => {
const result = mod.allocUint8(5);
expect(result).toBeInstanceOf(Uint8Array);
Expand Down Expand Up @@ -307,6 +322,38 @@ describe("mixed DSL + N-API", () => {
expect(mod.dataViewInfoMatches(view, backing, 6, 4)).toBe(true);
});

it("reports zero byte length for empty and detached ArrayBuffers", () => {
expect(mod.arrayBufferByteLength(new ArrayBuffer(0))).toEqual(0);

const detached = new ArrayBuffer(8);
structuredClone(detached, { transfer: [detached] });
expect(mod.arrayBufferByteLength(detached)).toEqual(0);
});

it("reports zero byte length for an empty Buffer", () => {
expect(mod.bufferByteLength(Buffer.alloc(0))).toEqual(0);
});

it("reports zero range for empty and detached TypedArrays", () => {
const empty = new Uint8Array(0);
expect(mod.typedArrayInfoMatches(empty, empty.buffer, 0, 0)).toBe(true);

const backing = new ArrayBuffer(16);
const view = new Uint16Array(backing, 4, 3);
structuredClone(backing, { transfer: [backing] });
expect(mod.typedArrayInfoMatches(view, backing, 0, 0)).toBe(true);
});

it("reports zero range for empty and detached DataViews", () => {
const empty = new DataView(new ArrayBuffer(0));
expect(mod.dataViewInfoMatches(empty, empty.buffer, 0, 0)).toBe(true);

const backing = new ArrayBuffer(16);
const view = new DataView(backing, 4, 6);
structuredClone(backing, { transfer: [backing] });
expect(mod.dataViewInfoMatches(view, backing, 0, 0)).toBe(true);
});

it("randomBytes16 uses js.io() to produce a Uint8Array", () => {
const bytes = mod.randomBytes16();
expect(bytes).toBeInstanceOf(Uint8Array);
Expand Down
10 changes: 10 additions & 0 deletions examples/js_dsl/mod.zig
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,16 @@ pub fn nodeRelease() !String {
return String.from(std.mem.span(release));
}

pub fn arrayBufferByteLength(value: Value) !Number {
const bytes = try value.toValue().getArrayBufferInfo();
return Number.from(bytes.len);
}

pub fn bufferByteLength(value: Value) !Number {
const bytes = try value.toValue().getBufferInfo();
return Number.from(bytes.len);
}

pub fn typedArrayInfoMatches(
value: Value,
expected_arraybuffer: Value,
Expand Down
37 changes: 25 additions & 12 deletions src/Value.zig
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ value: c.napi_value,

const Value = @This();

fn byteSlice(data: ?*anyopaque, byte_length: usize) []u8 {
if (byte_length == 0) return &.{};
// The N-API zero-length case, where data may be null or arbitrary, was handled above.
const byte_ptr: [*]u8 = @ptrCast(data.?);
return byte_ptr[0..byte_length];
}

/// https://nodejs.org/api/n-api.html#napi_is_array
pub fn isArray(self: Value) NapiError!bool {
var is_array: bool = undefined;
Expand Down Expand Up @@ -107,22 +114,22 @@ pub fn getArrayLength(self: Value) NapiError!u32 {

/// https://nodejs.org/api/n-api.html#napi_get_arraybuffer_info
pub fn getArrayBufferInfo(self: Value) NapiError![]u8 {
var data: [*]u8 = undefined;
var data: ?*anyopaque = undefined;
var byte_length: usize = undefined;
try status.check(
c.napi_get_arraybuffer_info(self.env, self.value, @ptrCast(&data), &byte_length),
c.napi_get_arraybuffer_info(self.env, self.value, &data, &byte_length),
);
return data[0..byte_length];
return byteSlice(data, byte_length);
}

/// https://nodejs.org/api/n-api.html#napi_get_buffer_info
pub fn getBufferInfo(self: Value) NapiError![]u8 {
var data: [*]u8 = undefined;
var data: ?*anyopaque = undefined;
var byte_length: usize = undefined;
try status.check(
c.napi_get_buffer_info(self.env, self.value, @ptrCast(&data), &byte_length),
c.napi_get_buffer_info(self.env, self.value, &data, &byte_length),
);
return data[0..byte_length];
return byteSlice(data, byte_length);
}

/// https://nodejs.org/api/n-api.html#napi_get_prototype
Expand Down Expand Up @@ -151,7 +158,7 @@ pub const TypedarrayInfoError = NapiError || error{
pub fn getTypedarrayInfo(self: Value) TypedarrayInfoError!TypedarrayInfo {
var array_type_raw: c.napi_typedarray_type = undefined;
var length: usize = undefined;
var data: [*]u8 = undefined;
var data: ?*anyopaque = undefined;
var arraybuffer: c.napi_value = undefined;
var byte_offset: usize = undefined;
try status.check(
Expand All @@ -160,7 +167,7 @@ pub fn getTypedarrayInfo(self: Value) TypedarrayInfoError!TypedarrayInfo {
self.value,
&array_type_raw,
&length,
@ptrCast(&data),
&data,
&arraybuffer,
&byte_offset,
),
Expand All @@ -170,7 +177,7 @@ pub fn getTypedarrayInfo(self: Value) TypedarrayInfoError!TypedarrayInfo {
return .{
.array_type = array_type,
.length = length,
.data = data[0 .. length * array_type.elementSize()],
.data = byteSlice(data, length * array_type.elementSize()),
.arraybuffer = .{
.env = self.env,
.value = arraybuffer,
Expand All @@ -189,22 +196,22 @@ pub const DataViewInfo = struct {
/// https://nodejs.org/api/n-api.html#napi_get_dataview_info
pub fn getDataviewInfo(self: Value) NapiError!DataViewInfo {
var byte_length: usize = undefined;
var data: [*]u8 = undefined;
var data: ?*anyopaque = undefined;
var arraybuffer: c.napi_value = undefined;
var byte_offset: usize = undefined;
try status.check(
c.napi_get_dataview_info(
self.env,
self.value,
&byte_length,
@ptrCast(&data),
&data,
&arraybuffer,
&byte_offset,
),
);
return .{
.byte_length = byte_length,
.data = data[0..byte_length],
.data = byteSlice(data, byte_length),
.arraybuffer = .{
.env = self.env,
.value = arraybuffer,
Expand Down Expand Up @@ -569,3 +576,9 @@ pub fn objectSeal(self: Value) NapiError!void {
c.napi_object_seal(self.env, self.value),
);
}

test "byteSlice normalizes null data for zero byte length" {
const bytes = byteSlice(null, 0);

try std.testing.expectEqual(@as(usize, 0), bytes.len);
}
1 change: 1 addition & 0 deletions src/js/typed_arrays.zig
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub fn TypedArray(comptime Element: type, comptime array_type: TypedarrayType) t
pub fn toSlice(self: Self) ![]Element {
const info = try self.val.getTypedarrayInfo();
if (info.array_type != array_type) return error.TypeMismatch;
if (info.length == 0) return &.{};
const byte_ptr: [*]u8 = info.data.ptr;
const typed_ptr: [*]Element = @ptrCast(@alignCast(byte_ptr));
return typed_ptr[0..info.length];
Expand Down