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
15 changes: 15 additions & 0 deletions examples/js_dsl/mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,3 +538,18 @@ describe("module lifecycle - worker threads", () => {
expect(mod.getEnvRefcount()).toEqual(refcountBefore);
});
});

// Section 16: Static Class Fields
describe("static class fields", () => {
it("exposes BLS PublicKey sizes as own properties of the constructor", () => {
expect(mod.BlsPublicKey.COMPRESS_SIZE).toEqual(48);
expect(mod.BlsPublicKey.SERIALIZE_SIZE).toEqual(96);
});

it("statics live on the constructor, not on instances", () => {
const pk = new mod.BlsPublicKey();
expect(pk.COMPRESS_SIZE).toBeUndefined();
expect(Object.prototype.hasOwnProperty.call(mod.BlsPublicKey, "COMPRESS_SIZE")).toBe(true);
expect(Object.prototype.hasOwnProperty.call(pk, "COMPRESS_SIZE")).toBe(false);
});
});
21 changes: 21 additions & 0 deletions examples/js_dsl/mod.zig
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,27 @@ pub fn makeToken(value: Number) Token {
return .{ .value = value.assertI32() + 1 };
}

// ============================================================================
// Section 16: Static Class Fields
// ============================================================================

/// Demonstrates auto-exposure of scalar/string `pub const` decls as own
/// properties of the JS constructor (e.g. `BlsPublicKey.COMPRESS_SIZE`).
/// BLS12-381 keys and signatures have curve-fixed serialized sizes.
/// Exposing them as static constants lets callers size buffers, validate hex lengths, etc.
pub const BlsPublicKey = struct {
pub const js_meta = js.class(.{});

pub const COMPRESS_SIZE = 48;
pub const SERIALIZE_SIZE = 96;

bytes: [96]u8,

pub fn init() BlsPublicKey {
return .{ .bytes = [_]u8{0} ** 96 };
}
};

comptime {
js.exportModule(@This(), .{
.init = struct {
Expand Down
1 change: 1 addition & 0 deletions src/js/export_module.zig
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ fn registerDecls(comptime Module: type, env: napi.Env, module: napi.Value, compt
));

const cls = napi.Value{ .env = env.env, .value = class_val };
try wrap_class.applyStaticFields(InnerType, env, cls);
try class_runtime.registerClass(InnerType, env, cls);
try module.setNamedProperty(name, cls);
exported_any = true;
Expand Down
71 changes: 71 additions & 0 deletions src/js/wrap_class.zig
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,77 @@ pub fn wrapClass(comptime T: type) type {
};
}

/// Walks `T`'s public declarations and attaches each scalar/string `pub const`
/// as an own property of the just-defined JS class constructor `class_val`.
///
/// Called by `export_module.zig` right after `napi_define_class` returns, so
/// the values land on the constructor itself (i.e. `MyClass.MY_CONST` in JS),
/// not on instances. A decl is exposed iff its value's type is an int, float,
/// bool, or string (`[]const u8` or `*const [N]u8`); functions, types, and
/// other decls are silently skipped — so existing methods/getters and the
/// `js_meta` decl naturally fall out.
pub fn applyStaticFields(comptime T: type, env: napi.Env, class_val: napi.Value) !void {
inline for (@typeInfo(T).@"struct".decls) |decl| {
const value = @field(T, decl.name);
if (comptime !isStaticValueType(@TypeOf(value))) continue;
const napi_value = try createStaticFieldValue(env, value);
const name: [:0]const u8 = decl.name ++ "";
try class_val.setNamedProperty(name, napi_value);
}
}

fn isStaticValueType(comptime T: type) bool {
return switch (@typeInfo(T)) {
.comptime_int, .int, .comptime_float, .float, .bool => true,
.pointer => |ptr| blk: {
if (ptr.size == .slice and ptr.child == u8 and ptr.is_const) break :blk true;
if (ptr.size == .one and @typeInfo(ptr.child) == .array and
@typeInfo(ptr.child).array.child == u8) break :blk true;
break :blk false;
},
else => false,
};
}

fn createStaticFieldValue(env: napi.Env, value: anytype) !napi.Value {
const T = @TypeOf(value);
switch (@typeInfo(T)) {
.comptime_int => {
if (value >= 0 and value <= std.math.maxInt(u32)) {
return env.createUint32(@intCast(value));
}
if (value >= std.math.minInt(i32) and value <= std.math.maxInt(i32)) {
return env.createInt32(@intCast(value));
}
return env.createInt64(@intCast(value));
},
.int => |info| {
if (info.signedness == .unsigned and info.bits <= 32) {
return env.createUint32(@intCast(value));
}
if (info.signedness == .signed and info.bits <= 32) {
return env.createInt32(@intCast(value));
}
return env.createInt64(@intCast(value));
},
.comptime_float, .float => return env.createDouble(@floatCast(value)),
.bool => return env.getBoolean(value),
.pointer => |ptr| {
if (ptr.size == .slice and ptr.child == u8 and ptr.is_const) {
return env.createStringUtf8(value);
}
if (ptr.size == .one and @typeInfo(ptr.child) == .array and
@typeInfo(ptr.child).array.child == u8)
{
const arr = @typeInfo(ptr.child).array;
return env.createStringUtf8(value[0..arr.len]);
}
@compileError("createStaticFieldValue: unsupported pointer type " ++ @typeName(T));
},
else => @compileError("createStaticFieldValue: unsupported type " ++ @typeName(T)),
}
}

test "wrapClass compile-time validation requires class metadata" {
try std.testing.expect(true);
}
Loading