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
78 changes: 78 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 @@ -278,6 +293,69 @@ describe("mixed DSL + N-API", () => {
expect(obj).toEqual({ x: 10 });
});

it("reports the active Node version", () => {
expect(mod.nodeVersion()).toEqual(process.versions.node);
expect(mod.nodeRelease()).toEqual(process.release.name);
});

it("returns a TypedArray's backing ArrayBuffer and range", () => {
const backing = new ArrayBuffer(16);
const view = new Uint16Array(backing, 4, 3);

expect(mod.typedArrayInfoMatches(view, backing, 3, 4)).toBe(true);
});

const Float16ArrayCtor = Reflect.get(globalThis, "Float16Array");
it.skipIf(typeof Float16ArrayCtor !== "function")(
"rejects unsupported TypedArray element types",
() => {
const view = Reflect.construct(Float16ArrayCtor, [3]) as Uint16Array;

expect(() =>
mod.typedArrayInfoMatches(view, view.buffer, 3, 0),
).toThrow("UnsupportedTypedarrayType");
},
);

it("returns a DataView's backing ArrayBuffer and range", () => {
const backing = new ArrayBuffer(16);
const view = new DataView(backing, 4, 6);

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
56 changes: 56 additions & 0 deletions examples/js_dsl/mod.zig
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,62 @@ pub fn makeObject(key: String, value: Number) !Value {
return .{ .val = obj };
}

pub fn nodeVersion() !String {
const semantic_version = (try js.env().getNodeVersion()).toSemanticVersion();
var buffer: [64]u8 = undefined;
const version = std.fmt.bufPrint(
&buffer,
"{d}.{d}.{d}",
.{ semantic_version.major, semantic_version.minor, semantic_version.patch },
) catch return error.FormatError;
return String.from(version);
}

pub fn nodeRelease() !String {
const release = (try js.env().getNodeVersion()).getRelease();
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,
expected_length: Number,
expected_byte_offset: Number,
) !Boolean {
const info = try value.toValue().getTypedarrayInfo();
const length: usize = @intCast(expected_length.assertU32());
const byte_offset: usize = @intCast(expected_byte_offset.assertU32());
if (info.length != length) return Boolean.from(false);
if (info.byte_offset != byte_offset) return Boolean.from(false);
if (info.data.len != length * info.array_type.elementSize()) return Boolean.from(false);
return Boolean.from(try info.arraybuffer.strictEquals(expected_arraybuffer.toValue()));
}

pub fn dataViewInfoMatches(
value: Value,
expected_arraybuffer: Value,
expected_byte_length: Number,
expected_byte_offset: Number,
) !Boolean {
const info = try value.toValue().getDataviewInfo();
const byte_length: usize = @intCast(expected_byte_length.assertU32());
const byte_offset: usize = @intCast(expected_byte_offset.assertU32());
if (info.byte_length != byte_length) return Boolean.from(false);
if (info.byte_offset != byte_offset) return Boolean.from(false);
if (info.data.len != byte_length) return Boolean.from(false);
return Boolean.from(try info.arraybuffer.strictEquals(expected_arraybuffer.toValue()));
}

/// Generate 16 random bytes using the DSL-managed shared std.Io instance.
pub fn randomBytes16() Uint8Array {
var bytes: [16]u8 = undefined;
Expand Down
6 changes: 3 additions & 3 deletions src/Env.zig
Original file line number Diff line number Diff line change
Expand Up @@ -889,11 +889,11 @@ pub fn asyncInit(self: Env, async_resource: Value, async_resource_name: Value) N

/// https://nodejs.org/api/n-api.html#napi_get_node_version
pub fn getNodeVersion(self: Env) NapiError!NodeVersion {
var version: c.napi_node_version = undefined;
var version: [*c]const c.napi_node_version = undefined;
try status.check(
c.napi_get_node_version(self.env, @ptrCast(&version)),
c.napi_get_node_version(self.env, &version),
);
return NodeVersion{ .version = version };
return NodeVersion{ .version = version.* };
}

/// https://nodejs.org/api/n-api.html#napi_get_version
Expand Down
95 changes: 75 additions & 20 deletions src/Value.zig
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const std = @import("std");
const c = @import("c.zig").c;
const status = @import("status.zig");
const NapiError = @import("status.zig").NapiError;
Expand All @@ -12,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 @@ -106,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 All @@ -142,15 +150,40 @@ pub const TypedarrayInfo = struct {
byte_offset: usize,
};

pub const TypedarrayInfoError = NapiError || error{
UnsupportedTypedarrayType,
};

/// https://nodejs.org/api/n-api.html#napi_get_typedarray_info
pub fn getTypedarrayInfo(self: Value) NapiError!TypedarrayInfo {
var info: TypedarrayInfo = undefined;
var data: [*]u8 = undefined;
try status.check(
c.napi_get_typedarray_info(self.env, self.value, @ptrCast(&info.array_type), &info.length, @ptrCast(&data), @ptrCast(&info.arraybuffer), &info.byte_offset),
);
info.data = data[0 .. info.length * info.array_type.elementSize()];
return info;
pub fn getTypedarrayInfo(self: Value) TypedarrayInfoError!TypedarrayInfo {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note for reference. The error set change on a pub function, will be breaking for usage when user completely exhausting error union.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is what I thought a while and finally add a new error

var array_type_raw: c.napi_typedarray_type = undefined;
var length: usize = undefined;
var data: ?*anyopaque = undefined;
var arraybuffer: c.napi_value = undefined;
var byte_offset: usize = undefined;
try status.check(
c.napi_get_typedarray_info(
self.env,
self.value,
&array_type_raw,
&length,
&data,
&arraybuffer,
&byte_offset,
),
);
const array_type = std.enums.fromInt(TypedarrayType, array_type_raw) orelse
return error.UnsupportedTypedarrayType;
return .{
.array_type = array_type,
.length = length,
.data = byteSlice(data, length * array_type.elementSize()),
.arraybuffer = .{
.env = self.env,
.value = arraybuffer,
},
.byte_offset = byte_offset,
};
}

pub const DataViewInfo = struct {
Expand All @@ -162,13 +195,29 @@ pub const DataViewInfo = struct {

/// https://nodejs.org/api/n-api.html#napi_get_dataview_info
pub fn getDataviewInfo(self: Value) NapiError!DataViewInfo {
var info: DataViewInfo = undefined;
var data: [*]u8 = undefined;
try status.check(
c.napi_get_dataview_info(self.env, self.value, &info.byte_length, @ptrCast(&data), @ptrCast(&info.arraybuffer), &info.byte_offset),
var byte_length: usize = 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,
&data,
&arraybuffer,
&byte_offset,
),
);
info.data = data[0..info.byte_length];
return info;
return .{
.byte_length = byte_length,
.data = byteSlice(data, byte_length),
.arraybuffer = .{
.env = self.env,
.value = arraybuffer,
},
.byte_offset = byte_offset,
};
}

/// https://nodejs.org/api/n-api.html#napi_get_date_value
Expand Down Expand Up @@ -527,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
Loading