Merge pull request #857 from lightpanda-io/improved_native_proto

Improve prototype resolution for native types
This commit is contained in:
Karl Seguin
2025-07-09 10:01:38 +08:00
committed by GitHub
3 changed files with 308 additions and 122 deletions

View File

@@ -162,7 +162,7 @@ test "Performance: get_timeOrigin" {
try testing.expect(time_origin >= 0); try testing.expect(time_origin >= 0);
// Check resolution // 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" { test "Performance: now" {

View File

@@ -81,14 +81,14 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
// that looks like: // that looks like:
// //
// const TypeLookup = struct { // const TypeLookup = struct {
// comptime cat: usize = TypeMeta{.index = 0, ...}, // comptime cat: usize = 0,
// comptime owner: usize = TypeMeta{.index = 1, ...}, // comptime owner: usize = 1,
// ... // ...
// } // }
// //
// So to get the template index of `owner`, we can do: // 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: { const TypeLookup = comptime blk: {
var fields: [Types.len]std.builtin.Type.StructField = undefined; 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) })); @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] = .{ fields[i] = .{
.name = @typeName(R), .name = @typeName(Receiver(Struct)),
.type = TypeMeta, .type = usize,
.is_comptime = true, .is_comptime = true,
.alignment = @alignOf(usize), .alignment = @alignOf(usize),
.default_value_ptr = &TypeMeta{ .index = i, .subtype = subtype }, .default_value_ptr = &i,
}; };
} }
break :blk @Type(.{ .@"struct" = .{ break :blk @Type(.{ .@"struct" = .{
@@ -146,7 +143,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
if (@hasDecl(Struct, "prototype")) { if (@hasDecl(Struct, "prototype")) {
const TI = @typeInfo(Struct.prototype); const TI = @typeInfo(Struct.prototype);
const proto_name = @typeName(Receiver(TI.pointer.child)); 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; 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 // access to its TunctionTemplate (the thing we need to create an instance
// of it) // of it)
// I.e.: // I.e.:
// const index = @field(TYPE_LOOKUP, @typeName(type_name)).index // const index = @field(TYPE_LOOKUP, @typeName(type_name))
// const template = templates[index]; // const template = templates[index];
templates: [Types.len]v8.FunctionTemplate, templates: [Types.len]v8.FunctionTemplate,
@@ -177,6 +174,8 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
// index. // index.
prototype_lookup: [Types.len]u16, prototype_lookup: [Types.len]u16,
meta_lookup: [Types.len]TypeMeta,
const Self = @This(); const Self = @This();
const TYPE_LOOKUP = TypeLookup{}; const TYPE_LOOKUP = TypeLookup{};
@@ -222,13 +221,14 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
.templates = undefined, .templates = undefined,
.allocator = allocator, .allocator = allocator,
.isolate_params = params, .isolate_params = params,
.meta_lookup = undefined,
.prototype_lookup = undefined, .prototype_lookup = undefined,
}; };
// Populate our templates lookup. generateClass creates the // Populate our templates lookup. generateClass creates the
// v8.FunctionTemplate, which we store in our env.templates. // v8.FunctionTemplate, which we store in our env.templates.
// The ordering doesn't matter. What matters is that, given a type // 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; const templates = &env.templates;
inline for (Types, 0..) |s, i| { inline for (Types, 0..) |s, i| {
@setEvalBranchQuota(10_000); @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 // Above, we've created all our our FunctionTemplates. Now that we
// have them all, we can hook up the prototypes. // have them all, we can hook up the prototypes.
const meta_lookup = &env.meta_lookup;
inline for (Types, 0..) |s, i| { inline for (Types, 0..) |s, i| {
const Struct = s.defaultValue().?; const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "prototype")) { 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 // Just like we said above, given a type, we can get its
// template index. // 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]); 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; return env;
@@ -391,7 +415,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
if (@hasDecl(Global, "prototype")) { if (@hasDecl(Global, "prototype")) {
const proto_type = Receiver(@typeInfo(Global.prototype).pointer.child); const proto_type = Receiver(@typeInfo(Global.prototype).pointer.child);
const proto_name = @typeName(proto_type); 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]); 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); @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 proto_obj = templates[proto_index].getFunction(v8_context).toObject();
const self_obj = templates[i].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, .isolate = isolate,
.v8_context = v8_context, .v8_context = v8_context,
.templates = &env.templates, .templates = &env.templates,
.meta_lookup = &env.meta_lookup,
.handle_scope = handle_scope, .handle_scope = handle_scope,
.call_arena = self.call_arena.allocator(), .call_arena = self.call_arena.allocator(),
.context_arena = self.context_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, v8_context: v8.Context,
handle_scope: ?v8.HandleScope, handle_scope: ?v8.HandleScope,
// references the Env.template array // references Env.templates
templates: []v8.FunctionTemplate, templates: []v8.FunctionTemplate,
// references the Env.meta_lookup
meta_lookup: []TypeMeta,
// An arena for the lifetime of a call-group. Gets reset whenever // An arena for the lifetime of a call-group. Gets reset whenever
// call_depth reaches 0. // call_depth reaches 0.
call_arena: Allocator, 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. // well as any meta data we'll need to use it later.
// See the TaggedAnyOpaque struct for more details. // See the TaggedAnyOpaque struct for more details.
const tao = try context_arena.create(TaggedAnyOpaque); 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.* = .{ tao.* = .{
.ptr = value, .ptr = value,
.index = meta.index, .index = meta.index,
.subtype = meta.subtype, .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)); 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))) { if (@hasField(TypeLookup, @typeName(ptr.child))) {
const js_obj = js_value.castTo(v8.Object); 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 => { .slice => {
@@ -1201,7 +1230,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
// of having a version of typeTaggedAnyOpaque which // of having a version of typeTaggedAnyOpaque which
// returns a boolean or an optional, we rely on the // returns a boolean or an optional, we rely on the
// main implementation and just handle the error. // 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| { if (attempt) |value| {
return .{ .value = value }; return .{ .value = value };
} else |_| { } 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); try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_specifier);
return m.handle; 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 { 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 { const template = v8.FunctionTemplate.initCallback(isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info); const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller(Self, State).init(info); var caller = Caller(JsContext, State).init(info);
defer caller.deinit(); defer caller.deinit();
// See comment above. We generateConstructor on all types // 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 { const function_template = v8.FunctionTemplate.initCallback(isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info); const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller(Self, State).init(info); var caller = Caller(JsContext, State).init(info);
defer caller.deinit(); defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, name); 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 { const function_template = v8.FunctionTemplate.initCallback(isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info); const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller(Self, State).init(info); var caller = Caller(JsContext, State).init(info);
defer caller.deinit(); defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "static_" ++ name); 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 { const getter_callback = v8.FunctionTemplate.initCallback(isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info); const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller(Self, State).init(info); var caller = Caller(JsContext, State).init(info);
defer caller.deinit(); defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "get_" ++ name); 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); const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
std.debug.assert(info.length() == 1); std.debug.assert(info.length() == 1);
var caller = Caller(Self, State).init(info); var caller = Caller(JsContext, State).init(info);
defer caller.deinit(); defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "set_" ++ name); 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 { .getter = struct {
fn callback(idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 { fn callback(idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info); const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller(Self, State).init(info); var caller = Caller(JsContext, State).init(info);
defer caller.deinit(); defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "indexed_get"); 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 { .getter = struct {
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 { fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info); const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller(Self, State).init(info); var caller = Caller(JsContext, State).init(info);
defer caller.deinit(); defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "named_get"); 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 { 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 { 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); const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller(Self, State).init(info); var caller = Caller(JsContext, State).init(info);
defer caller.deinit(); defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "named_set"); 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 { configuration.deleter = struct {
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 { fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info); const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller(Self, State).init(info); var caller = Caller(JsContext, State).init(info);
defer caller.deinit(); defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "named_delete"); 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 { template.setCallAsFunctionHandler(struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info); const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller(Self, State).init(info); var caller = Caller(JsContext, State).init(info);
defer caller.deinit(); defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "jsCallAsFunction"); const named_function = comptime NamedFunction.init(Struct, "jsCallAsFunction");
@@ -2315,7 +2416,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
.one => { .one => {
const type_name = @typeName(ptr.child); const type_name = @typeName(ptr.child);
if (@hasField(TypeLookup, type_name)) { 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); const js_obj = try JsContext.mapZigInstanceToJs(v8_context, template, value);
return js_obj.toValue(); return js_obj.toValue();
} }
@@ -2351,7 +2452,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
.@"struct" => |s| { .@"struct" => |s| {
const type_name = @typeName(T); const type_name = @typeName(T);
if (@hasField(TypeLookup, type_name)) { 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); const js_obj = try JsContext.mapZigInstanceToJs(v8_context, template, value);
return js_obj.toValue(); 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)); @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 // An interface for types that want to have their jsDeinit function to be
// called when the call context ends // 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 // This is essentially meta data for each type. Each is stored in env.meta_lookup
// fields for this struct will be the type names. The values, will be an // The index for a type can be retrieved via:
// instance of this struct. // const index = @field(TYPE_LOOKUP, @typeName(Receiver(Struct)));
// const TypeLookup = struct { // const meta = env.meta_lookup[index];
// comptime cat: usize = TypeMeta{.index = 0, subtype = null},
// comptime owner: usize = TypeMeta{.index = 1, subtype = .array}.
// ...
// }
// This is essentially meta data for each type.
const TypeMeta = struct { const TypeMeta = struct {
// Every type is given a unique index. That index is used to lookup various // Every type is given a unique index. That index is used to lookup various
// things, i.e. the prototype chain. // 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 // 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 type, and bind it to JavaScript, we can store the subtype along with
// the created TaggedAnyOpaque.s // the created TaggedAnyOpaque.s
subtype: ?SubType, 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 // 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 // probably just contained in ExecutionWorld, but having this specific logic, which
// is somewhat repetitive between constructors, functions, getters, etc contained // is somewhat repetitive between constructors, functions, getters, etc contained
// here does feel like it makes it clenaer. // 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 { return struct {
js_context: *E.JsContext, js_context: *JsContext,
v8_context: v8.Context, v8_context: v8.Context,
isolate: v8.Isolate, isolate: v8.Isolate,
call_arena: Allocator, call_arena: Allocator,
@@ -2605,7 +2643,7 @@ fn Caller(comptime E: type, comptime State: type) type {
fn init(info: anytype) Self { fn init(info: anytype) Self {
const isolate = info.getIsolate(); const isolate = info.getIsolate();
const v8_context = isolate.getCurrentContext(); 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; js_context.call_depth += 1;
return .{ return .{
@@ -2653,9 +2691,9 @@ fn Caller(comptime E: type, comptime State: type) type {
const this = info.getThis(); const this = info.getThis();
if (@typeInfo(ReturnType) == .error_union) { if (@typeInfo(ReturnType) == .error_union) {
const non_error_res = res catch |err| return err; 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 { } else {
_ = try E.JsContext.mapZigInstanceToJs(self.v8_context, this, res); _ = try JsContext.mapZigInstanceToJs(self.v8_context, this, res);
} }
info.getReturnValue().set(this); info.getReturnValue().set(this);
} }
@@ -2668,7 +2706,7 @@ fn Caller(comptime E: type, comptime State: type) type {
const js_context = self.js_context; const js_context = self.js_context;
const func = @field(Struct, named_function.name); const func = @field(Struct, named_function.name);
var args = try self.getArgs(Struct, named_function, 1, info); 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 // inject 'self' as the first parameter
@field(args, "0") = zig_instance; @field(args, "0") = zig_instance;
@@ -2700,7 +2738,7 @@ fn Caller(comptime E: type, comptime State: type) type {
switch (arg_fields.len) { switch (arg_fields.len) {
0, 1, 2 => @compileError(named_function.full_name ++ " must take at least a u32 and *bool parameter"), 0, 1, 2 => @compileError(named_function.full_name ++ " must take at least a u32 and *bool parameter"),
3, 4 => { 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); comptime assertSelfReceiver(Struct, named_function);
@field(args, "0") = zig_instance; @field(args, "0") = zig_instance;
@field(args, "1") = idx; @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 { 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); const func = @field(Struct, named_function.name);
comptime assertSelfReceiver(Struct, named_function); comptime assertSelfReceiver(Struct, named_function);
var has_value = true; var has_value = true;
var args = try self.getArgs(Struct, named_function, 3, info); 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, "0") = zig_instance;
@field(args, "1") = try self.nameToString(name); @field(args, "1") = try self.nameToString(name);
@field(args, "2") = &has_value; @field(args, "2") = &has_value;
@@ -2747,7 +2786,7 @@ fn Caller(comptime E: type, comptime State: type) type {
var has_value = true; var has_value = true;
var args = try self.getArgs(Struct, named_function, 4, info); 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, "0") = zig_instance;
@field(args, "1") = try self.nameToString(name); @field(args, "1") = try self.nameToString(name);
@field(args, "2") = try js_context.jsValueToZig(named_function, @TypeOf(@field(args, "2")), js_value); @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 { 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); const func = @field(Struct, named_function.name);
comptime assertSelfReceiver(Struct, named_function); comptime assertSelfReceiver(Struct, named_function);
var has_value = true; var has_value = true;
var args = try self.getArgs(Struct, named_function, 3, info); 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, "0") = zig_instance;
@field(args, "1") = try self.nameToString(name); @field(args, "1") = try self.nameToString(name);
@field(args, "2") = &has_value; @field(args, "2") = &has_value;
@@ -3379,12 +3419,6 @@ const TaggedAnyOpaque = struct {
// PROTOTYPE_TABLE // PROTOTYPE_TABLE
index: u16, 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. // 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 have the comptime parameter info for all functions), and the index field
// we can figure out what type this is. // we can figure out what type this is.

View File

@@ -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 { const State = struct {
arena: Allocator, arena: Allocator,
}; };
@@ -90,6 +201,11 @@ test "JS: object types" {
Other, Other,
MyObject, MyObject,
MyAPI, MyAPI,
Parent,
Middle,
Child,
MiddlePtr,
ChildPtr,
}).init(.{ .arena = arena.allocator() }, {}); }).init(.{ .arena = arena.allocator() }, {});
defer runner.deinit(); defer runner.deinit();
@@ -120,4 +236,40 @@ test "JS: object types" {
// check object property // check object property
.{ "myObjIndirect.a.val()", "4" }, .{ "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" },
}, .{});
} }