From 3a49ee83ced5db7f36919c92ae3c296a222fe87a Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 8 Jul 2025 18:17:11 +0800 Subject: [PATCH] Improve prototype resolution for native types Prototype resolution of Zig types previously had 2 limitations (bug?). The first was that the Zig prototype chain could only be 1 deep. You couldn't do A->B->C where each of those was a Zig type (but you could do A->B->C->D->E ... so long as every other type was a C opaque value). The other limitation was that Zig prototypes only worked when the nested field was directly embedded in the struct (i.e. not a pointer). So you could do: ```zig const X = struct { proto: XParent, }; ``` But not: ```zig const X = struct { proto: *XParent, }; ``` This addresses both limitations. The first issue is solved by keeping track of the cumulative offset --- src/browser/dom/performance.zig | 2 +- src/runtime/js.zig | 276 +++++++++++++++++------------- src/runtime/test_object_types.zig | 152 ++++++++++++++++ 3 files changed, 308 insertions(+), 122 deletions(-) diff --git a/src/browser/dom/performance.zig b/src/browser/dom/performance.zig index 7346ddfc..e45cff1b 100644 --- a/src/browser/dom/performance.zig +++ b/src/browser/dom/performance.zig @@ -162,7 +162,7 @@ test "Performance: get_timeOrigin" { try testing.expect(time_origin >= 0); // Check resolution - try testing.expectDelta(@rem(time_origin * std.time.us_per_ms, 100.0), 0.0, 0.1); + try testing.expectDelta(@rem(time_origin * std.time.us_per_ms, 100.0), 0.0, 0.2); } test "Performance: now" { diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 2ca9cb6d..8d6e47d2 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -81,14 +81,14 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { // that looks like: // // const TypeLookup = struct { - // comptime cat: usize = TypeMeta{.index = 0, ...}, - // comptime owner: usize = TypeMeta{.index = 1, ...}, + // comptime cat: usize = 0, + // comptime owner: usize = 1, // ... // } // // So to get the template index of `owner`, we can do: // - // const index_id = @field(type_lookup, @typeName(@TypeOf(res)).index; + // const index_id = @field(type_lookup, @typeName(@TypeOf(res)); // const TypeLookup = comptime blk: { var fields: [Types.len]std.builtin.Type.StructField = undefined; @@ -103,15 +103,12 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { @compileError(std.fmt.comptimePrint("Prototype '{s}' for type '{s} must be a pointer", .{ @typeName(Struct.prototype), @typeName(Struct) })); } - const subtype: ?SubType = if (@hasDecl(Struct, "subtype")) Struct.subtype else null; - - const R = Receiver(Struct); fields[i] = .{ - .name = @typeName(R), - .type = TypeMeta, + .name = @typeName(Receiver(Struct)), + .type = usize, .is_comptime = true, .alignment = @alignOf(usize), - .default_value_ptr = &TypeMeta{ .index = i, .subtype = subtype }, + .default_value_ptr = &i, }; } break :blk @Type(.{ .@"struct" = .{ @@ -146,7 +143,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { if (@hasDecl(Struct, "prototype")) { const TI = @typeInfo(Struct.prototype); const proto_name = @typeName(Receiver(TI.pointer.child)); - prototype_index = @field(TYPE_LOOKUP, proto_name).index; + prototype_index = @field(TYPE_LOOKUP, proto_name); } table[i] = prototype_index; } @@ -168,7 +165,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { // access to its TunctionTemplate (the thing we need to create an instance // of it) // I.e.: - // const index = @field(TYPE_LOOKUP, @typeName(type_name)).index + // const index = @field(TYPE_LOOKUP, @typeName(type_name)) // const template = templates[index]; templates: [Types.len]v8.FunctionTemplate, @@ -177,6 +174,8 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { // index. prototype_lookup: [Types.len]u16, + meta_lookup: [Types.len]TypeMeta, + const Self = @This(); const TYPE_LOOKUP = TypeLookup{}; @@ -222,13 +221,14 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { .templates = undefined, .allocator = allocator, .isolate_params = params, + .meta_lookup = undefined, .prototype_lookup = undefined, }; // Populate our templates lookup. generateClass creates the // v8.FunctionTemplate, which we store in our env.templates. // The ordering doesn't matter. What matters is that, given a type - // we can get its index via: @field(TYPE_LOOKUP, type_name).index + // we can get its index via: @field(TYPE_LOOKUP, type_name) const templates = &env.templates; inline for (Types, 0..) |s, i| { @setEvalBranchQuota(10_000); @@ -237,6 +237,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { // Above, we've created all our our FunctionTemplates. Now that we // have them all, we can hook up the prototypes. + const meta_lookup = &env.meta_lookup; inline for (Types, 0..) |s, i| { const Struct = s.defaultValue().?; if (@hasDecl(Struct, "prototype")) { @@ -249,9 +250,32 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { // Just like we said above, given a type, we can get its // template index. - const proto_index = @field(TYPE_LOOKUP, proto_name).index; + const proto_index = @field(TYPE_LOOKUP, proto_name); templates[i].inherit(templates[proto_index]); } + + // while we're here, let's populate our meta lookup + const subtype: ?SubType = if (@hasDecl(Struct, "subtype")) Struct.subtype else null; + + const proto_offset = comptime blk: { + if (!@hasField(Struct, "proto")) { + break :blk 0; + } + const proto_info = std.meta.fieldInfo(Struct, .proto); + if (@typeInfo(proto_info.type) == .pointer) { + // we store the offset as a negative, to so that, + // when we reverse this, we know that it's + // behind a pointer that we need to resolve. + break :blk -@offsetOf(Struct, "proto"); + } + break :blk @offsetOf(Struct, "proto"); + }; + + meta_lookup[i] = .{ + .index = i, + .subtype = subtype, + .proto_offset = proto_offset, + }; } return env; @@ -391,7 +415,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { if (@hasDecl(Global, "prototype")) { const proto_type = Receiver(@typeInfo(Global.prototype).pointer.child); const proto_name = @typeName(proto_type); - const proto_index = @field(TYPE_LOOKUP, proto_name).index; + const proto_index = @field(TYPE_LOOKUP, proto_name); js_global.inherit(templates[proto_index]); } @@ -414,7 +438,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { @compileError("Type '" ++ @typeName(Struct) ++ "' defines an unknown prototype: " ++ proto_name); } - const proto_index = @field(TYPE_LOOKUP, proto_name).index; + const proto_index = @field(TYPE_LOOKUP, proto_name); const proto_obj = templates[proto_index].getFunction(v8_context).toObject(); const self_obj = templates[i].getFunction(v8_context).toObject(); @@ -449,6 +473,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { .isolate = isolate, .v8_context = v8_context, .templates = &env.templates, + .meta_lookup = &env.meta_lookup, .handle_scope = handle_scope, .call_arena = self.call_arena.allocator(), .context_arena = self.context_arena.allocator(), @@ -551,9 +576,12 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { v8_context: v8.Context, handle_scope: ?v8.HandleScope, - // references the Env.template array + // references Env.templates templates: []v8.FunctionTemplate, + // references the Env.meta_lookup + meta_lookup: []TypeMeta, + // An arena for the lifetime of a call-group. Gets reset whenever // call_depth reaches 0. call_arena: Allocator, @@ -845,12 +873,13 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { // well as any meta data we'll need to use it later. // See the TaggedAnyOpaque struct for more details. const tao = try context_arena.create(TaggedAnyOpaque); - const meta = @field(TYPE_LOOKUP, @typeName(ptr.child)); + const meta_index = @field(TYPE_LOOKUP, @typeName(ptr.child)); + const meta = self.meta_lookup[meta_index]; + tao.* = .{ .ptr = value, .index = meta.index, .subtype = meta.subtype, - .offset = if (@typeInfo(ptr.child) != .@"opaque" and @hasField(ptr.child, "proto")) @offsetOf(ptr.child, "proto") else -1, }; js_obj.setInternalField(0, v8.External.init(isolate, tao)); @@ -906,7 +935,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { } if (@hasField(TypeLookup, @typeName(ptr.child))) { const js_obj = js_value.castTo(v8.Object); - return typeTaggedAnyOpaque(named_function, *Receiver(ptr.child), js_obj); + return self.typeTaggedAnyOpaque(named_function, *Receiver(ptr.child), js_obj); } }, .slice => { @@ -1201,7 +1230,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { // of having a version of typeTaggedAnyOpaque which // returns a boolean or an optional, we rely on the // main implementation and just handle the error. - const attempt = typeTaggedAnyOpaque(named_function, *Receiver(ptr.child), js_obj); + const attempt = self.typeTaggedAnyOpaque(named_function, *Receiver(ptr.child), js_obj); if (attempt) |value| { return .{ .value = value }; } else |_| { @@ -1387,6 +1416,78 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_specifier); return m.handle; } + + // Reverses the mapZigInstanceToJs, making sure that our TaggedAnyOpaque + // contains a ptr to the correct type. + fn typeTaggedAnyOpaque(self: *const JsContext, comptime named_function: NamedFunction, comptime R: type, js_obj: v8.Object) !R { + const ti = @typeInfo(R); + if (ti != .pointer) { + @compileError(named_function.full_name ++ "has a non-pointer Zig parameter type: " ++ @typeName(R)); + } + + const T = ti.pointer.child; + if (comptime isEmpty(T)) { + // Empty structs aren't stored as TOAs and there's no data + // stored in the JSObject's IntenrnalField. Why bother when + // we can just return an empty struct here? + return @constCast(@as(*const T, &.{})); + } + + // if it isn't an empty struct, then the v8.Object should have an + // InternalFieldCount > 0, since our toa pointer should be embedded + // at index 0 of the internal field count. + if (js_obj.internalFieldCount() == 0) { + return error.InvalidArgument; + } + + const type_name = @typeName(T); + if (@hasField(TypeLookup, type_name) == false) { + @compileError(named_function.full_name ++ "has an unknown Zig type: " ++ @typeName(R)); + } + + const op = js_obj.getInternalField(0).castTo(v8.External).get(); + const toa: *TaggedAnyOpaque = @alignCast(@ptrCast(op)); + const expected_type_index = @field(TYPE_LOOKUP, type_name); + + var type_index = toa.index; + if (type_index == expected_type_index) { + return @alignCast(@ptrCast(toa.ptr)); + } + + const meta_lookup = self.meta_lookup; + + // If we have N levels deep of prototypes, then the offset is the + // sum at each level... + var total_offset: usize = 0; + + // ...unless, the proto is behind a pointer, then total_offset will + // get reset to 0, and our base_ptr will move to the address + // referenced by the proto field. + var base_ptr: usize = @intFromPtr(toa.ptr); + + // search through the prototype tree + while (true) { + const proto_offset = meta_lookup[type_index].proto_offset; + if (proto_offset < 0) { + base_ptr = @as(*align(1) usize, @ptrFromInt(base_ptr + total_offset + @as(usize, @intCast(-proto_offset)))).*; + total_offset = 0; + } else { + total_offset += @intCast(proto_offset); + } + + const prototype_index = PROTOTYPE_TABLE[type_index]; + if (prototype_index == expected_type_index) { + return @ptrFromInt(base_ptr + total_offset); + } + + if (prototype_index == type_index) { + // When a type has itself as the prototype, then we've + // reached the end of the chain. + return error.InvalidArgument; + } + type_index = prototype_index; + } + } }; pub const Function = struct { @@ -2000,7 +2101,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { const template = v8.FunctionTemplate.initCallback(isolate, struct { fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { const info = v8.FunctionCallbackInfo.initFromV8(raw_info); - var caller = Caller(Self, State).init(info); + var caller = Caller(JsContext, State).init(info); defer caller.deinit(); // See comment above. We generateConstructor on all types @@ -2045,7 +2146,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { const function_template = v8.FunctionTemplate.initCallback(isolate, struct { fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { const info = v8.FunctionCallbackInfo.initFromV8(raw_info); - var caller = Caller(Self, State).init(info); + var caller = Caller(JsContext, State).init(info); defer caller.deinit(); const named_function = comptime NamedFunction.init(Struct, name); @@ -2062,7 +2163,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { const function_template = v8.FunctionTemplate.initCallback(isolate, struct { fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { const info = v8.FunctionCallbackInfo.initFromV8(raw_info); - var caller = Caller(Self, State).init(info); + var caller = Caller(JsContext, State).init(info); defer caller.deinit(); const named_function = comptime NamedFunction.init(Struct, "static_" ++ name); @@ -2098,7 +2199,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { const getter_callback = v8.FunctionTemplate.initCallback(isolate, struct { fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { const info = v8.FunctionCallbackInfo.initFromV8(raw_info); - var caller = Caller(Self, State).init(info); + var caller = Caller(JsContext, State).init(info); defer caller.deinit(); const named_function = comptime NamedFunction.init(Struct, "get_" ++ name); @@ -2119,7 +2220,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { const info = v8.FunctionCallbackInfo.initFromV8(raw_info); std.debug.assert(info.length() == 1); - var caller = Caller(Self, State).init(info); + var caller = Caller(JsContext, State).init(info); defer caller.deinit(); const named_function = comptime NamedFunction.init(Struct, "set_" ++ name); @@ -2140,7 +2241,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { .getter = struct { fn callback(idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 { const info = v8.PropertyCallbackInfo.initFromV8(raw_info); - var caller = Caller(Self, State).init(info); + var caller = Caller(JsContext, State).init(info); defer caller.deinit(); const named_function = comptime NamedFunction.init(Struct, "indexed_get"); @@ -2176,7 +2277,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { .getter = struct { fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 { const info = v8.PropertyCallbackInfo.initFromV8(raw_info); - var caller = Caller(Self, State).init(info); + var caller = Caller(JsContext, State).init(info); defer caller.deinit(); const named_function = comptime NamedFunction.init(Struct, "named_get"); @@ -2198,7 +2299,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { configuration.setter = struct { fn callback(c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 { const info = v8.PropertyCallbackInfo.initFromV8(raw_info); - var caller = Caller(Self, State).init(info); + var caller = Caller(JsContext, State).init(info); defer caller.deinit(); const named_function = comptime NamedFunction.init(Struct, "named_set"); @@ -2214,7 +2315,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { configuration.deleter = struct { fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 { const info = v8.PropertyCallbackInfo.initFromV8(raw_info); - var caller = Caller(Self, State).init(info); + var caller = Caller(JsContext, State).init(info); defer caller.deinit(); const named_function = comptime NamedFunction.init(Struct, "named_delete"); @@ -2260,7 +2361,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { template.setCallAsFunctionHandler(struct { fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { const info = v8.FunctionCallbackInfo.initFromV8(raw_info); - var caller = Caller(Self, State).init(info); + var caller = Caller(JsContext, State).init(info); defer caller.deinit(); const named_function = comptime NamedFunction.init(Struct, "jsCallAsFunction"); @@ -2315,7 +2416,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { .one => { const type_name = @typeName(ptr.child); if (@hasField(TypeLookup, type_name)) { - const template = templates[@field(TYPE_LOOKUP, type_name).index]; + const template = templates[@field(TYPE_LOOKUP, type_name)]; const js_obj = try JsContext.mapZigInstanceToJs(v8_context, template, value); return js_obj.toValue(); } @@ -2351,7 +2452,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { .@"struct" => |s| { const type_name = @typeName(T); if (@hasField(TypeLookup, type_name)) { - const template = templates[@field(TYPE_LOOKUP, type_name).index]; + const template = templates[@field(TYPE_LOOKUP, type_name)]; const js_obj = try JsContext.mapZigInstanceToJs(v8_context, template, value); return js_obj.toValue(); } @@ -2420,69 +2521,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { @compileError("A function returns an unsupported type: " ++ @typeName(T)); } - // Reverses the mapZigInstanceToJs, making sure that our TaggedAnyOpaque - // contains a ptr to the correct type. - fn typeTaggedAnyOpaque(comptime named_function: NamedFunction, comptime R: type, js_obj: v8.Object) !R { - const ti = @typeInfo(R); - if (ti != .pointer) { - @compileError(named_function.full_name ++ "has a non-pointer Zig parameter type: " ++ @typeName(R)); - } - - const T = ti.pointer.child; - if (comptime isEmpty(T)) { - // Empty structs aren't stored as TOAs and there's no data - // stored in the JSObject's IntenrnalField. Why bother when - // we can just return an empty struct here? - return @constCast(@as(*const T, &.{})); - } - - // if it isn't an empty struct, then the v8.Object should have an - // InternalFieldCount > 0, since our toa pointer should be embedded - // at index 0 of the internal field count. - if (js_obj.internalFieldCount() == 0) { - return error.InvalidArgument; - } - - const type_name = @typeName(T); - if (@hasField(TypeLookup, type_name) == false) { - @compileError(named_function.full_name ++ "has an unknown Zig type: " ++ @typeName(R)); - } - - const op = js_obj.getInternalField(0).castTo(v8.External).get(); - const toa: *TaggedAnyOpaque = @alignCast(@ptrCast(op)); - const expected_type_index = @field(TYPE_LOOKUP, type_name).index; - - var type_index = toa.index; - if (type_index == expected_type_index) { - return @alignCast(@ptrCast(toa.ptr)); - } - - // search through the prototype tree - while (true) { - const prototype_index = PROTOTYPE_TABLE[type_index]; - if (prototype_index == expected_type_index) { - // -1 is a sentinel value used for non-composition prototype - // This is used with netsurf and we just unsafely cast one - // type to another - const offset = toa.offset; - if (offset == -1) { - return @alignCast(@ptrCast(toa.ptr)); - } - - // A non-negative offset means we're using composition prototype - // (i.e. our struct has a "proto" field). the offset - // reresents the byte offset of the field. We can use that - // + the toa.ptr to get the field - return @ptrFromInt(@intFromPtr(toa.ptr) + @as(usize, @intCast(offset))); - } - if (prototype_index == type_index) { - // When a type has itself as the prototype, then we've - // reached the end of the chain. - return error.InvalidArgument; - } - type_index = prototype_index; - } - } // An interface for types that want to have their jsDeinit function to be // called when the call context ends @@ -2542,24 +2580,24 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { }; } -// We'll create a struct with all the types we want to bind to JavaScript. The -// fields for this struct will be the type names. The values, will be an -// instance of this struct. -// const TypeLookup = struct { -// comptime cat: usize = TypeMeta{.index = 0, subtype = null}, -// comptime owner: usize = TypeMeta{.index = 1, subtype = .array}. -// ... -// } -// This is essentially meta data for each type. +// This is essentially meta data for each type. Each is stored in env.meta_lookup +// The index for a type can be retrieved via: +// const index = @field(TYPE_LOOKUP, @typeName(Receiver(Struct))); +// const meta = env.meta_lookup[index]; const TypeMeta = struct { // Every type is given a unique index. That index is used to lookup various // things, i.e. the prototype chain. - index: usize, + index: u16, // We store the type's subtype here, so that when we create an instance of // the type, and bind it to JavaScript, we can store the subtype along with // the created TaggedAnyOpaque.s subtype: ?SubType, + + // If this type has composition-based prototype, represents the byte-offset + // from ptr where the `proto` field is located. A negative offsets is used + // to indicate that the prototype field is behind a pointer. + proto_offset: i32, }; // When we map a Zig instance into a JsObject, we'll normally store the a @@ -2590,9 +2628,9 @@ fn isComplexAttributeType(ti: std.builtin.Type) bool { // probably just contained in ExecutionWorld, but having this specific logic, which // is somewhat repetitive between constructors, functions, getters, etc contained // here does feel like it makes it clenaer. -fn Caller(comptime E: type, comptime State: type) type { +fn Caller(comptime JsContext: type, comptime State: type) type { return struct { - js_context: *E.JsContext, + js_context: *JsContext, v8_context: v8.Context, isolate: v8.Isolate, call_arena: Allocator, @@ -2605,7 +2643,7 @@ fn Caller(comptime E: type, comptime State: type) type { fn init(info: anytype) Self { const isolate = info.getIsolate(); const v8_context = isolate.getCurrentContext(); - const js_context: *E.JsContext = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64()); + const js_context: *JsContext = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64()); js_context.call_depth += 1; return .{ @@ -2653,9 +2691,9 @@ fn Caller(comptime E: type, comptime State: type) type { const this = info.getThis(); if (@typeInfo(ReturnType) == .error_union) { const non_error_res = res catch |err| return err; - _ = try E.JsContext.mapZigInstanceToJs(self.v8_context, this, non_error_res); + _ = try JsContext.mapZigInstanceToJs(self.v8_context, this, non_error_res); } else { - _ = try E.JsContext.mapZigInstanceToJs(self.v8_context, this, res); + _ = try JsContext.mapZigInstanceToJs(self.v8_context, this, res); } info.getReturnValue().set(this); } @@ -2668,7 +2706,7 @@ fn Caller(comptime E: type, comptime State: type) type { const js_context = self.js_context; const func = @field(Struct, named_function.name); var args = try self.getArgs(Struct, named_function, 1, info); - const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis()); + const zig_instance = try js_context.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis()); // inject 'self' as the first parameter @field(args, "0") = zig_instance; @@ -2700,7 +2738,7 @@ fn Caller(comptime E: type, comptime State: type) type { switch (arg_fields.len) { 0, 1, 2 => @compileError(named_function.full_name ++ " must take at least a u32 and *bool parameter"), 3, 4 => { - const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis()); + const zig_instance = try js_context.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis()); comptime assertSelfReceiver(Struct, named_function); @field(args, "0") = zig_instance; @field(args, "1") = idx; @@ -2722,12 +2760,13 @@ fn Caller(comptime E: type, comptime State: type) type { } fn getNamedIndex(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 { + const js_context = self.js_context; const func = @field(Struct, named_function.name); comptime assertSelfReceiver(Struct, named_function); var has_value = true; var args = try self.getArgs(Struct, named_function, 3, info); - const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis()); + const zig_instance = try js_context.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis()); @field(args, "0") = zig_instance; @field(args, "1") = try self.nameToString(name); @field(args, "2") = &has_value; @@ -2747,7 +2786,7 @@ fn Caller(comptime E: type, comptime State: type) type { var has_value = true; var args = try self.getArgs(Struct, named_function, 4, info); - const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis()); + const zig_instance = try js_context.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis()); @field(args, "0") = zig_instance; @field(args, "1") = try self.nameToString(name); @field(args, "2") = try js_context.jsValueToZig(named_function, @TypeOf(@field(args, "2")), js_value); @@ -2758,12 +2797,13 @@ fn Caller(comptime E: type, comptime State: type) type { } fn deleteNamedIndex(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 { + const js_context = self.js_context; const func = @field(Struct, named_function.name); comptime assertSelfReceiver(Struct, named_function); var has_value = true; var args = try self.getArgs(Struct, named_function, 3, info); - const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis()); + const zig_instance = try js_context.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis()); @field(args, "0") = zig_instance; @field(args, "1") = try self.nameToString(name); @field(args, "2") = &has_value; @@ -3379,12 +3419,6 @@ const TaggedAnyOpaque = struct { // PROTOTYPE_TABLE index: u16, - // If this type has composition-based prototype, represents the byte-offset - // from ptr where the `proto` field is located. The value -1 represents - // unsafe prototype where we can just cast ptr to the destination type - // (this is used extensively with netsurf) - offset: i32, - // Ptr to the Zig instance. Between the context where it's called (i.e. // we have the comptime parameter info for all functions), and the index field // we can figure out what type this is. diff --git a/src/runtime/test_object_types.zig b/src/runtime/test_object_types.zig index b3594dc0..ebf750f4 100644 --- a/src/runtime/test_object_types.zig +++ b/src/runtime/test_object_types.zig @@ -77,6 +77,117 @@ pub const MyAPI = struct { } }; +pub const Parent = packed struct { + parent_id: i32 = 0, + + pub fn get_parent(self: *const Parent) i32 { + return self.parent_id; + } + pub fn set_parent(self: *Parent, id: i32) void { + self.parent_id = id; + } +}; + +pub const Middle = struct { + pub const prototype = *Parent; + + middle_id: i32 = 0, + _padding_1: u8 = 0, + _padding_2: u8 = 1, + _padding_3: u8 = 2, + proto: Parent, + + pub fn constructor() Middle { + return .{ + .middle_id = 0, + .proto = .{ .parent_id = 0 }, + }; + } + + pub fn get_middle(self: *const Middle) i32 { + return self.middle_id; + } + pub fn set_middle(self: *Middle, id: i32) void { + self.middle_id = id; + } +}; + +pub const Child = struct { + pub const prototype = *Middle; + + child_id: i32 = 0, + _padding_1: u8 = 0, + proto: Middle, + + pub fn constructor() Child { + return .{ + .child_id = 0, + .proto = .{ .middle_id = 0, .proto = .{ .parent_id = 0 } }, + }; + } + + pub fn get_child(self: *const Child) i32 { + return self.child_id; + } + pub fn set_child(self: *Child, id: i32) void { + self.child_id = id; + } +}; + +pub const MiddlePtr = packed struct { + pub const prototype = *Parent; + + middle_id: i32 = 0, + _padding_1: u8 = 0, + _padding_2: u8 = 1, + _padding_3: u8 = 2, + proto: *Parent, + + pub fn constructor(state: State) !MiddlePtr { + const parent = try state.arena.create(Parent); + parent.* = .{ .parent_id = 0 }; + return .{ + .middle_id = 0, + .proto = parent, + }; + } + + pub fn get_middle(self: *const MiddlePtr) i32 { + return self.middle_id; + } + pub fn set_middle(self: *MiddlePtr, id: i32) void { + self.middle_id = id; + } +}; + +pub const ChildPtr = packed struct { + pub const prototype = *MiddlePtr; + + child_id: i32 = 0, + _padding_1: u8 = 0, + _padding_2: u8 = 1, + proto: *MiddlePtr, + + pub fn constructor(state: State) !ChildPtr { + const parent = try state.arena.create(Parent); + const middle = try state.arena.create(MiddlePtr); + + parent.* = .{ .parent_id = 0 }; + middle.* = .{ .middle_id = 0, .proto = parent }; + return .{ + .child_id = 0, + .proto = middle, + }; + } + + pub fn get_child(self: *const ChildPtr) i32 { + return self.child_id; + } + pub fn set_child(self: *ChildPtr, id: i32) void { + self.child_id = id; + } +}; + const State = struct { arena: Allocator, }; @@ -90,6 +201,11 @@ test "JS: object types" { Other, MyObject, MyAPI, + Parent, + Middle, + Child, + MiddlePtr, + ChildPtr, }).init(.{ .arena = arena.allocator() }, {}); defer runner.deinit(); @@ -120,4 +236,40 @@ test "JS: object types" { // check object property .{ "myObjIndirect.a.val()", "4" }, }, .{}); + + try runner.testCases(&.{ + .{ "let m1 = new Middle();", null }, + .{ "m1.middle = 2", null }, + .{ "m1.parent = 3", null }, + .{ "m1.middle", "2" }, + .{ "m1.parent", "3" }, + }, .{}); + + try runner.testCases(&.{ + .{ "let c1 = new Child();", null }, + .{ "c1.child = 1", null }, + .{ "c1.middle = 2", null }, + .{ "c1.parent = 3", null }, + .{ "c1.child", "1" }, + .{ "c1.middle", "2" }, + .{ "c1.parent", "3" }, + }, .{}); + + try runner.testCases(&.{ + .{ "let m2 = new MiddlePtr();", null }, + .{ "m2.middle = 2", null }, + .{ "m2.parent = 3", null }, + .{ "m2.middle", "2" }, + .{ "m2.parent", "3" }, + }, .{}); + + try runner.testCases(&.{ + .{ "let c2 = new ChildPtr();", null }, + .{ "c2.child = 1", null }, + .{ "c2.middle = 2", null }, + .{ "c2.parent = 3", null }, + .{ "c2.child", "1" }, + .{ "c2.middle", "2" }, + .{ "c2.parent", "3" }, + }, .{}); }