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
47 changes: 47 additions & 0 deletions examples/js_dsl/mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,28 @@ describe("primitive types", () => {
expect(mod.bigIntSign(-0xffffffffffffffffn)).toEqual(1);
});

it("rejects BigInts wider than the provided word buffer", () => {
expect(() => mod.bigIntFirstWord(2n ** 64n)).toThrow();
expect(() => mod.bigIntFirstWord(2n ** 200n)).toThrow();
});
});

describe("toI128", () => {
it("round-trips values across the i128 range", () => {
expect(mod.bigIntToI128String(0n)).toEqual("0");
expect(mod.bigIntToI128String(123n)).toEqual("123");
expect(mod.bigIntToI128String(-123n)).toEqual("-123");
expect(mod.bigIntToI128String(2n ** 127n - 1n)).toEqual((2n ** 127n - 1n).toString());
expect(mod.bigIntToI128String(-(2n ** 127n))).toEqual((-(2n ** 127n)).toString());
});

it("rejects BigInts outside the i128 range instead of crashing", () => {
expect(() => mod.bigIntToI128String(2n ** 127n)).toThrow();
expect(() => mod.bigIntToI128String(-(2n ** 127n) - 1n)).toThrow();
expect(() => mod.bigIntToI128String(2n ** 128n)).toThrow();
expect(() => mod.bigIntToI128String(2n ** 200n)).toThrow();
expect(() => mod.bigIntToI128String(-(2n ** 200n))).toThrow();
});
});

it("tomorrow adds one day", () => {
Expand All @@ -125,6 +147,25 @@ describe("primitive types", () => {
});
});

describe("value narrowing", () => {
it("narrows numbers", () => {
expect(mod.narrowToNumber(42)).toEqual(42);
});

it("rejects non-numbers", () => {
expect(() => mod.narrowToNumber("42")).toThrow();
expect(() => mod.narrowToNumber({})).toThrow();
expect(() => mod.narrowToNumber(42n)).toThrow();
});

it("narrows typed arrays by exact subtype", () => {
expect(mod.narrowToUint8ArrayLen(new Uint8Array(3))).toEqual(3);
expect(() => mod.narrowToUint8ArrayLen(new Int8Array(3))).toThrow();
expect(() => mod.narrowToUint8ArrayLen(new Uint8ClampedArray(3))).toThrow();
expect(() => mod.narrowToUint8ArrayLen([1, 2, 3])).toThrow();
});
});

// Section 4: Typed Objects
describe("typed objects", () => {
it("formatConfig returns formatted string", () => {
Expand Down Expand Up @@ -220,6 +261,12 @@ describe("Counter class", () => {
expect(() => getCount.call(new mod.Buffer(4))).toThrow();
});

it("rejects constructor calls without new", () => {
expect(() => mod.Counter(5)).toThrow(TypeError);
expect(() => mod.Point()).toThrow(TypeError);
expect(() => Reflect.apply(mod.Counter, undefined, [5])).toThrow(TypeError);
});

it("increments", () => {
const c = new mod.Counter(0);
c.increment();
Expand Down
25 changes: 22 additions & 3 deletions examples/js_dsl/mod.zig
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,10 @@ pub fn doubleBigInt(n: BigInt) !BigInt {

/// Read a BigInt's first u64 word via `getValueBigintWords` passing `null` for `sign_bit`.
///
/// Throws if word_count > 1.
/// Throws `Overflow` if the BigInt needs more than one word.
pub fn bigIntFirstWord(n: BigInt) !Number {
var words: [1]u64 = .{0};
const got = try n.toValue().getValueBigintWords(null, &words);
if (got.len > 1) return error.BigIntTooLarge;
_ = try n.toValue().getValueBigintWords(null, &words);
return Number.from(words[0]);
}

Expand All @@ -118,6 +117,14 @@ pub fn bigIntSign(n: BigInt) !Number {
return Number.from(@as(u32, sign));
}

/// Convert a BigInt to an i128 and render it as a decimal string.
pub fn bigIntToI128String(n: BigInt) !String {
const v = try n.toI128();
var buf: [48]u8 = undefined;
const s = std.fmt.bufPrint(&buf, "{d}", .{v}) catch return error.FormatError;
return String.from(s);
}

/// Add one day (86400000ms) to a Date.
pub fn tomorrow(d: Date) Date {
const ts = d.assertTimestamp();
Expand Down Expand Up @@ -309,6 +316,18 @@ pub const Buffer = struct {
// Section 10: Mixed DSL + N-API
// ============================================================================

/// Narrow an untyped value to a Number via the `js.Value` narrowing API.
pub fn narrowToNumber(v: Value) !Number {
return v.asNumber();
}

/// Narrow an untyped value to a Uint8Array and return its length.
pub fn narrowToUint8ArrayLen(v: Value) !Number {
const arr = try v.asUint8Array();
const slice = try arr.toSlice();
return Number.from(@as(u32, @intCast(slice.len)));
}

/// Return the JS typeof string for any value.
/// Demonstrates dropping down to low-level napi to call raw N-API methods.
pub fn getTypeOf(val: Value) !String {
Expand Down
10 changes: 8 additions & 2 deletions src/Value.zig
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ pub fn getTypedarrayInfo(self: Value) NapiError!TypedarrayInfo {
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()];
const elem_size = info.array_type.elementSize() orelse return error.GenericFailure;
info.data = data[0 .. info.length * elem_size];
return info;
}

Expand Down Expand Up @@ -223,16 +224,21 @@ pub fn getValueBigintUint64(self: Value, lossless: ?*bool) NapiError!u64 {
/// In Ethereum's context, this is useful for big integers defined in the spec to be
/// unsigned.
///
/// Returns `error.Overflow` if the BigInt needs more words than `words` can hold:
/// napi sets the out `word_count` to the *required* count, which may exceed the
/// buffer, so slicing by it unchecked would read out of bounds.
///
/// NOTE: napi's C entry takes `int*` (4-byte aligned). Casting a u1 to int* is UB, since that
/// is 4-bytes aligned. We use a local `c_int` for the napi call and narrow back to `u1` for the caller.
///
/// Source: https://nodejs.org/api/n-api.html#napi_get_value_bigint_words
pub fn getValueBigintWords(self: Value, sign_bit: ?*u1, words: []u64) NapiError![]u64 {
pub fn getValueBigintWords(self: Value, sign_bit: ?*u1, words: []u64) (NapiError || error{Overflow})![]u64 {
var word_count: usize = words.len;
var raw_sign: c_int = 0;
try status.check(
c.napi_get_value_bigint_words(self.env, self.value, &raw_sign, &word_count, words.ptr),
);
if (word_count > words.len) return error.Overflow;
// napi guarantees raw_sign ∈ {0, 1}
if (sign_bit) |s| s.* = @intCast(raw_sign);
return words[0..word_count];
Expand Down
11 changes: 7 additions & 4 deletions src/js/bigint.zig
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,22 @@ pub const BigInt = struct {
///
/// This function reads the BigInt as two 64-bit words and reconstructs it
/// into a Zig `i128`. It handles both positive and negative BigInts.
/// Returns an error if N-API operations fail.
/// Returns `error.Overflow` if the value is outside the i128 range, and an
/// error if N-API operations fail.
pub fn toI128(self: BigInt) !i128 {
var sign_bit: u1 = 0;
var words: [2]u64 = .{ 0, 0 };
const result = try self.val.getValueBigintWords(&sign_bit, &words);
const lo: u128 = result[0];
const lo: u128 = if (result.len > 0) result[0] else 0;
const hi: u128 = if (result.len > 1) result[1] else 0;
const magnitude: u128 = (hi << 64) | lo;
const max_positive: u128 = std.math.maxInt(i128);
if (sign_bit == 1) {
// Negative: negate the magnitude
if (magnitude == 0) return 0;
if (magnitude > max_positive + 1) return error.Overflow;
if (magnitude == max_positive + 1) return std.math.minInt(i128);
return -@as(i128, @intCast(magnitude));
}
if (magnitude > max_positive) return error.Overflow;
return @intCast(magnitude);
}

Expand Down
11 changes: 7 additions & 4 deletions src/js/class_runtime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ pub fn typeTag(comptime T: type) napi.c.napi_type_tag {
};
}

/// On error the caller keeps ownership of `native_object`: the wrap is
/// detached so the finalizer cannot fire, but the object is not destroyed.
pub fn wrapTaggedObject(comptime T: type, env: napi.Env, object: napi.Value, native_object: *T) !void {
const tag = typeTag(T);
try env.wrap(object, T, native_object, defaultFinalize(T), null, null);
errdefer if (env.removeWrap(T, object)) |removed| {
destroyNativeObject(T, removed);
} else |_| {};
errdefer _ = env.removeWrap(T, object) catch {};
if (!(try env.checkObjectTypeTag(object, tag))) {
try env.typeTagObject(object, tag);
}
Expand Down Expand Up @@ -68,9 +68,12 @@ pub fn registerClass(comptime T: type, env: napi.Env, ctor: napi.Value) !void {
.ctor_ref = try env.createReference(ctor, 1),
.next = State.head,
};
State.head = entry;
errdefer entry.ctor_ref.delete() catch {};

// Link only after the cleanup hook is registered: a hook failure must not
// leave a freed entry reachable from the list head.
try env.addEnvCleanupHook(State.Entry, entry, State.cleanupHook);
State.head = entry;
}

/// Per-thread marker set by `materializeClassInstance` to tell the generated
Expand Down
11 changes: 11 additions & 0 deletions src/js/value.zig
Original file line number Diff line number Diff line change
Expand Up @@ -245,4 +245,15 @@ pub const Value = struct {
pub fn toValue(self: Value) napi.Value {
return self.val;
}

fn expectType(self: Value, expected: ValueType) TypeError!void {
const actual = self.val.typeof() catch return error.TypeMismatch;
if (actual != expected) return error.TypeMismatch;
}

fn expectTypedArrayOfType(self: Value, expected: TypedarrayType) TypeError!void {
if (!(self.val.isTypedarray() catch return error.TypeMismatch)) return error.TypeMismatch;
const info = self.val.getTypedarrayInfo() catch return error.TypeMismatch;
if (info.array_type != expected) return error.TypeMismatch;
}
};
12 changes: 12 additions & 0 deletions src/js/wrap_class.zig
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,17 @@ pub fn wrapClass(comptime T: type) type {
return null;
};

// Without `new`, `this` is the global proxy; wrapping it would
// attach native state and a finalizer to globalThis.
const new_target = e.getNewTarget(cb_info) catch {
e.throwError("", "Failed to get new.target in constructor") catch {};
return null;
};
if (new_target.value == null) {
e.throwTypeError("", "Class constructor cannot be invoked without 'new'") catch {};
return null;
}

// Fast path: materializeClassInstance is creating this instance.
// Skip normal init; materialize will wrap the returned JS instance
// with the real native pointer after napi_new_instance returns.
Expand Down Expand Up @@ -400,6 +411,7 @@ pub fn wrapClass(comptime T: type) type {

const this_val = napi.Value{ .env = raw_env, .value = this_arg };
class_runtime.wrapTaggedObject(T, e, this_val, obj_ptr) catch {
class_runtime.destroyNativeObject(T, obj_ptr);
e.throwError("", "Failed to wrap native object") catch {};
return null;
};
Expand Down
1 change: 1 addition & 0 deletions src/js/wrap_function.zig
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ fn typedArrayName(comptime array_type: napi.value_types.TypedarrayType) []const
.float64 => "Float64Array",
.bigint64 => "BigInt64Array",
.biguint64 => "BigUint64Array",
_ => "TypedArray",
};
}

Expand Down
4 changes: 3 additions & 1 deletion src/module.zig
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ pub fn register(comptime f: fn (Env, Value) anyerror!void) void {
.value = module,
};
f(e, v) catch |err| {
e.throwError(@errorName(err), "Error in module registration") catch unreachable;
// throwError fails if an exception is already pending; that
// exception is the failure report, so never abort here.
e.throwError(@errorName(err), "Error in module registration") catch {};
};
return module;
}
Expand Down
9 changes: 9 additions & 0 deletions src/status.zig
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ pub const Status = enum(c.napi_status) {
would_deadlock = c.napi_would_deadlock,
no_external_buffers_allowed = c.napi_no_external_buffers_allowed,
cannot_run_js = c.napi_cannot_run_js,
// Non-exhaustive: future Node versions may add statuses.
_,
};

pub const NapiError = error{
Expand Down Expand Up @@ -80,6 +82,7 @@ pub fn check(code: c_uint) NapiError!void {
.would_deadlock => return error.WouldDeadlock,
.no_external_buffers_allowed => return error.NoExternalBuffersAllowed,
.cannot_run_js => return error.CannotRunJS,
_ => return error.GenericFailure,
}
}

Expand Down Expand Up @@ -112,3 +115,9 @@ test "isNapiError identifies napi errors" {
try std.testing.expect(isNapiError(error.GenericFailure));
try std.testing.expect(!isNapiError(error.OutOfMemory));
}

test "check maps unknown status codes to GenericFailure" {
// Future Node versions may return statuses this enum doesn't know about;
// they must map to an error, not invalid-enum UB.
try std.testing.expectError(error.GenericFailure, check(9999));
}
21 changes: 20 additions & 1 deletion src/value_types.zig
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ pub const ValueType = enum(c.napi_valuetype) {
function = c.napi_function,
external = c.napi_external,
bigint = c.napi_bigint,
// Non-exhaustive: future Node versions may add value types.
_,
};

/// https://nodejs.org/api/n-api.html#napi_typedarray_type
Expand All @@ -49,13 +51,18 @@ pub const TypedarrayType = enum(c.napi_typedarray_type) {
float64 = c.napi_float64_array,
bigint64 = c.napi_bigint64_array,
biguint64 = c.napi_biguint64_array,
// Non-exhaustive: engines already ship array types napi doesn't name yet
// (e.g. Float16Array); unknown values must be representable, not UB.
_,

pub fn elementSize(self: TypedarrayType) usize {
/// Returns null for typedarray types this binding doesn't know about.
pub fn elementSize(self: TypedarrayType) ?usize {
switch (self) {
.int8, .uint8, .uint8_clamped => return 1,
.int16, .uint16 => return 2,
.int32, .uint32, .float32 => return 4,
.float64, .bigint64, .biguint64 => return 8,
_ => return null,
}
}

Expand All @@ -72,10 +79,22 @@ pub const TypedarrayType = enum(c.napi_typedarray_type) {
.float64 => return f64,
.bigint64 => return i64,
.biguint64 => return u64,
_ => @compileError("unknown typedarray type"),
}
}
};

test "elementSize returns null for unknown typedarray types" {
// e.g. Float16Array on newer engines: unknown values must not be UB.
const unknown: TypedarrayType = @enumFromInt(999);
try @import("std").testing.expect(unknown.elementSize() == null);
}

test "elementSize covers known typedarray types" {
try @import("std").testing.expectEqual(@as(?usize, 1), TypedarrayType.uint8.elementSize());
try @import("std").testing.expectEqual(@as(?usize, 8), TypedarrayType.biguint64.elementSize());
}

/// https://nodejs.org/api/n-api.html#napi_property_attributes
pub const PropertyAttributes = enum(c.napi_property_attributes) {
default = c.napi_default,
Expand Down
Loading