diff --git a/examples/js_dsl/mod.test.ts b/examples/js_dsl/mod.test.ts index 505bd95..6d290f9 100644 --- a/examples/js_dsl/mod.test.ts +++ b/examples/js_dsl/mod.test.ts @@ -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); + }); +}); diff --git a/examples/js_dsl/mod.zig b/examples/js_dsl/mod.zig index 71804ee..0cf16f3 100644 --- a/examples/js_dsl/mod.zig +++ b/examples/js_dsl/mod.zig @@ -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 { diff --git a/src/js/export_module.zig b/src/js/export_module.zig index aa70ba1..47aa34d 100644 --- a/src/js/export_module.zig +++ b/src/js/export_module.zig @@ -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; diff --git a/src/js/wrap_class.zig b/src/js/wrap_class.zig index 7b10cef..6a34c0c 100644 --- a/src/js/wrap_class.zig +++ b/src/js/wrap_class.zig @@ -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); }