Files
browser/src/runtime/js.zig
2025-04-15 15:18:06 +08:00

2201 lines
97 KiB
Zig

// Copyright 2023-2024 Lightpanda (Selecy SAS)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
const std = @import("std");
const builtin = @import("builtin");
const v8 = @import("v8");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const log = std.log.scoped(.js);
pub const Platform = struct {
inner: v8.Platform,
pub fn init() Platform {
const platform = v8.Platform.initDefault(0, true);
v8.initV8Platform(platform);
v8.initV8();
return .{ .inner = platform };
}
pub fn deinit(self: Platform) void {
_ = v8.deinitV8();
v8.deinitV8Platform();
self.inner.deinit();
}
};
pub fn Env(comptime S: type, comptime types: anytype) type {
const Types = @typeInfo(@TypeOf(types)).@"struct".fields;
// Imagine we have a type Cat which has a getter:
//
// fn get_owner(self: *Cat) *Owner {
// return self.owner;
// }
//
// When we're execute caller.getter, we'll end up doing something like:
// const res = @call(.auto, Cat.get_owner, .{cat_instance});
//
// How do we turn `res`, which is an *Owner, into something we can return
// to v8? We need the ObjectTemplate associated with Owner. How do we
// get that? Well, we store all the ObjectTemplates in an array that's
// tied to env. So we do something like:
//
// env.templates[index_id_of_owner].initInstance(...);
//
// But how do we get that `index_id_of_owner` ??
// This is where `type_lookup` comes from. We create a struct that looks like:
//
// const TypeLookup = struct {
// comptime cat: usize = 0,
// comptime owner: usize = 1,
// ...
// }
//
// With this type, which is passed into callProperty, we can do:
//
// const index_id = @field(type_lookup, @typeName(@TypeOf(res));
//
const TypeLookup = comptime blk: {
var fields: [Types.len]std.builtin.Type.StructField = undefined;
for (Types, 0..) |s, i| {
// This prototype type check has nothing to do with building our
// TypeLookup. But we put it here, early, so that the rest of the
// code doesn't have to worry about checking if Struct.Prototype is
// a pointer.
const Struct = @field(types, s.name);
if (@hasDecl(Struct, "prototype") and @typeInfo(Struct.prototype) != .pointer) {
@compileError(std.fmt.comptimePrint("Prototype '{s}' for type '{s} must be a pointer", .{ @typeName(Struct.prototype), @typeName(Struct) }));
}
const R = Receiver(@field(types, s.name));
fields[i] = .{
.name = @typeName(R),
.type = usize,
.is_comptime = true,
.alignment = @alignOf(usize),
.default_value_ptr = @ptrCast(&i),
};
}
break :blk @Type(.{ .@"struct" = .{
.layout = .auto,
.decls = &.{},
.is_tuple = false,
.fields = &fields,
} });
};
// Creates a list where the index of a type contains its prototype index
// const Animal = struct{};
// const Cat = struct{
// pub const prototype = *Animal;
// };
//
// Would create an array: [0, 0]
// Animal, at index, 0, has no prototype, so we set it to itself
// Cat, at index 1, has an Animal prototype, so we set it to 0.
//
// When we're trying to pass an argument to a Zig function, we'll know the
// target type (the function parameter type), and we'll have a
// TaggedAnyOpaque which will have the index of the type of that parameter.
// We'll use the PROTOTYPE_TABLE to see if the TaggedAnyType should be
// cast to a prototype.
const PROTOTYPE_TABLE = comptime blk: {
var table: [Types.len]u16 = undefined;
const TYPE_LOOKUP = TypeLookup{};
for (Types, 0..) |s, i| {
var prototype_index = i;
const Struct = @field(types, s.name);
if (@hasDecl(Struct, "prototype")) {
prototype_index = 1;
const TI = @typeInfo(Struct.prototype);
const proto_name = @typeName(Receiver(TI.pointer.child));
prototype_index = @field(TYPE_LOOKUP, proto_name);
}
table[i] = prototype_index;
}
break :blk table;
};
return struct {
allocator: Allocator,
// the global isolate
isolate: v8.Isolate,
// When we create JS objects/methods/properties we can associate
// abitrary data. It'll be this value.
callback_data: v8.BigInt,
// this is the global scope that all our classes are defined in
global_scope: v8.HandleScope,
// just kept around because we need to free it on deinit
isolate_params: v8.CreateParams,
// Given a type, we can lookup its index in TYPE_LOOKUP and then have
// access to its TunctionTemplate (the thing we need to create an instance
// of it)
// I.e.:
// const index = @field(TYPE_LOOKUP, @typeName(type_name))
// const template = templates[index];
templates: [Types.len]v8.FunctionTemplate,
// Given a type index (retrieved via the TYPE_LOOKUP), we can retrieve
// the index of its prototype. Types without a prototype have their own
// index.
prototype_lookup: [Types.len]u16,
// Sessions are cheap, we mostly do this so we can get a stable pointer
executor_pool: std.heap.MemoryPool(Executor),
// Send a LowMemory
gc_hints: bool,
const Self = @This();
const State = S;
const TYPE_LOOKUP = TypeLookup{};
const Opts = struct {
gc_hints: bool = false,
};
pub fn init(allocator: Allocator, opts: Opts) !*Self {
var params = v8.initCreateParams();
params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator();
errdefer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?);
var isolate = v8.Isolate.init(&params);
errdefer isolate.deinit();
isolate.enter();
errdefer isolate.exit();
var global_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&global_scope, isolate);
errdefer global_scope.deinit();
const env = try allocator.create(Self);
errdefer allocator.destroy(env);
env.* = .{
.isolate = isolate,
.templates = undefined,
.allocator = allocator,
.isolate_params = params,
.gc_hints = opts.gc_hints,
.global_scope = global_scope,
.prototype_lookup = undefined,
.executor_pool = std.heap.MemoryPool(Executor).init(allocator),
.callback_data = isolate.initBigIntU64(@intCast(@intFromPtr(env))),
};
// 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)
const templates = &env.templates;
inline for (Types, 0..) |s, i| {
templates[i] = env.generateClass(@field(types, s.name));
}
// Above, we've created all our our FunctionTemplates. Now that we
// have them all, we can hookup the prototype.
inline for (Types, 0..) |s, i| {
const Struct = @field(types, s.name);
if (@hasDecl(Struct, "prototype")) {
const TI = @typeInfo(Struct.prototype);
const proto_name = @typeName(Receiver(TI.pointer.child));
if (@hasField(TypeLookup, proto_name) == false) {
@compileError(std.fmt.comptimePrint("Prototype '{s}' for '{s}' is undefined", .{ proto_name, @typeName(Struct) }));
}
// Hey, look! This is our first real usage of the TYPE_LOOKUP.
// Just like we said above, given a type, we can get its
// template index.
const proto_index = @field(TYPE_LOOKUP, proto_name);
templates[i].inherit(templates[proto_index]);
}
}
return env;
}
pub fn deinit(self: *Self) void {
self.global_scope.deinit();
self.isolate.exit();
self.isolate.deinit();
self.executor_pool.deinit();
v8.destroyArrayBufferAllocator(self.isolate_params.array_buffer_allocator.?);
self.allocator.destroy(self);
}
pub fn runMicrotasks(self: *const Self) void {
self.isolate.performMicrotasksCheckpoint();
}
pub fn startExecutor(self: *Self, comptime Global: type, state: State, module_loader: anytype) !*Executor {
const isolate = self.isolate;
const templates = &self.templates;
var handle_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&handle_scope, isolate);
const globals = v8.FunctionTemplate.initDefault(isolate);
const global_template = globals.getInstanceTemplate();
global_template.setInternalFieldCount(1);
self.attachClass(Global, globals);
inline for (Types, 0..) |s, i| {
const Struct = @field(types, s.name);
const class_name = v8.String.initUtf8(isolate, comptime classNameForStruct(Struct));
global_template.set(class_name.toName(), templates[i], v8.PropertyAttribute.None);
}
// The global is its own Object and has to have its prototype chain setup.
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);
globals.inherit(templates[proto_index]);
}
const context = v8.Context.init(isolate, global_template, null);
context.enter();
errdefer context.exit();
// This shouldn't be necessary, but it is:
// https://groups.google.com/g/v8-users/c/qAQQBmbi--8
// TODO: see if newer V8 engines have a way around this.
inline for (Types, 0..) |s, i| {
const Struct = @field(types, s.name);
if (@hasDecl(Struct, "prototype")) {
const proto_type = Receiver(@typeInfo(Struct.prototype).pointer.child);
const proto_name = @typeName(proto_type);
if (@hasField(TypeLookup, proto_name) == false) {
@compileError("Type '" ++ @typeName(Struct) ++ "' defines an unknown prototype: " ++ proto_name);
}
const proto_index = @field(TYPE_LOOKUP, proto_name);
const proto_obj = templates[proto_index].getFunction(context).toObject();
const self_obj = templates[i].getFunction(context).toObject();
_ = self_obj.setPrototype(context, proto_obj);
}
}
const executor = try self.executor_pool.create();
errdefer self.executor_pool.destroy(executor);
{
// Given a context, we can get our executor.
// (we store a pointer to our executor in the context's
// embeddeder data)
const data = isolate.initBigIntU64(@intCast(@intFromPtr(executor)));
context.setEmbedderData(1, data);
}
const allocator = self.allocator;
executor.* = .{
.state = state,
.context = context,
.isolate = isolate,
.templates = templates,
.handle_scope = handle_scope,
.call_arena = ArenaAllocator.init(allocator),
.scope_arena = ArenaAllocator.init(allocator),
.module_loader = .{
.ptr = @ptrCast(module_loader),
.func = @TypeOf(module_loader.*).fetchModuleSource,
},
};
errdefer self.stopExecutor(executor);
// Custom exception
// NOTE: there is no way in v8 to subclass the Error built-in type
// TODO: this is an horrible hack
inline for (Types) |s| {
const Struct = @field(types, s.name);
if (@hasDecl(Struct, "ErrorSet")) {
const script = comptime classNameForStruct(Struct) ++ ".prototype.__proto__ = Error.prototype";
_ = try executor.exec(script, "errorSubclass");
}
}
return executor;
}
pub fn stopExecutor(self: *Self, executor: *Executor) void {
executor.deinit();
self.executor_pool.destroy(executor);
if (self.gc_hints) {
self.isolate.lowMemoryNotification();
}
}
fn generateClass(self: *Self, comptime Struct: type) v8.FunctionTemplate {
const template = self.generateConstructor(Struct);
self.attachClass(Struct, template);
return template;
}
// Normally this is called from generateClass. Where generateClass creates
// the constructor (hence, the FunctionTemplate), attachClass adds all
// of its functions, getters, setters, ...
// But it's extracted from generateClass because we also have 1 global
// object (i.e. the Window), which gets attached not only to the Window
// constructor/FunctionTemplate as normal, but also through the default
// FunctionTemplate of the isolate (in startExecutor)
fn attachClass(self: *Self, comptime Struct: type, template: v8.FunctionTemplate) void {
const template_proto = template.getPrototypeTemplate();
inline for (@typeInfo(Struct).@"struct".decls) |declaration| {
const name = declaration.name;
if (comptime name[0] == '_') {
switch (@typeInfo(@TypeOf(@field(Struct, name)))) {
.@"fn" => self.generateMethod(Struct, name, template_proto),
else => self.generateAttribute(Struct, name, template, template_proto),
}
} else if (comptime std.mem.startsWith(u8, name, "get_")) {
self.generateProperty(Struct, name[4..], template_proto);
}
}
if (@hasDecl(Struct, "get_symbol_toStringTag") == false) {
// If this WAS defined, then we would have created it in generateProperty.
// But if it isn't, we create a default one
const key = v8.Symbol.getToStringTag(self.isolate).toName();
template_proto.setGetter(key, struct {
fn stringTag(_: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) void {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
const class_name = v8.String.initUtf8(info.getIsolate(), comptime classNameForStruct(Struct));
info.getReturnValue().set(class_name);
}
}.stringTag);
}
self.generateIndexer(Struct, template_proto);
self.generateNamedIndexer(Struct, template_proto);
}
fn generateConstructor(self: *Self, comptime Struct: type) v8.FunctionTemplate {
const template = v8.FunctionTemplate.initCallbackData(self.isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller(Self).init(info);
defer caller.deinit();
if (@hasDecl(Struct, "constructor") == false) {
// handle this early, so we can create a named_function without
// hassling over whether the constructor actually exists
const isolate = caller.isolate;
const js_exception = isolate.throwException(createException(isolate, "illegal constructor"));
info.getReturnValue().set(js_exception);
return;
}
const named_function = NamedFunction(Struct, Struct.constructor, "constructor"){};
caller.constructor(named_function, info) catch |err| {
caller.handleError(named_function, err, info);
};
}
}.callback, self.callback_data);
template.getInstanceTemplate().setInternalFieldCount(1);
const class_name = v8.String.initUtf8(self.isolate, comptime classNameForStruct(Struct));
template.setClassName(class_name);
return template;
}
fn generateMethod(self: *Self, comptime Struct: type, comptime name: []const u8, template_proto: v8.ObjectTemplate) void {
var js_name: v8.Name = undefined;
if (comptime std.mem.eql(u8, name, "_symbol_iterator")) {
js_name = v8.Symbol.getIterator(self.isolate).toName();
} else {
js_name = v8.String.initUtf8(self.isolate, name[1..]).toName();
}
const function_template = v8.FunctionTemplate.initCallbackData(self.isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller(Self).init(info);
defer caller.deinit();
const named_function = NamedFunction(Struct, @field(Struct, name), name){};
caller.method(named_function, info) catch |err| {
caller.handleError(named_function, err, info);
};
}
}.callback, self.callback_data);
template_proto.set(js_name, function_template, v8.PropertyAttribute.None);
}
fn generateAttribute(self: *Self, comptime Struct: type, comptime name: []const u8, template: v8.FunctionTemplate, template_proto: v8.ObjectTemplate) void {
const zig_value = @field(Struct, name);
const js_value = simpleZigValueToJs(self.isolate, zig_value, true);
const js_name = v8.String.initUtf8(self.isolate, name[1..]).toName();
// apply it both to the type itself
template.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
// andto instances of the type
template_proto.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
}
fn generateProperty(self: *Self, comptime Struct: type, comptime name: []const u8, template_proto: v8.ObjectTemplate) void {
const getter = @field(Struct, "get_" ++ name);
const param_count = @typeInfo(@TypeOf(getter)).@"fn".params.len;
var js_name: v8.Name = undefined;
if (comptime std.mem.eql(u8, name, "symbol_toStringTag")) {
if (param_count != 0) {
@compileError(@typeName(Struct) ++ ".get_symbol_toStringTag() cannot take any parameters");
}
js_name = v8.Symbol.getToStringTag(self.isolate).toName();
} else {
js_name = v8.String.initUtf8(self.isolate, name).toName();
}
const getter_callback = struct {
fn callback(_: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) void {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller(Self).init(info);
defer caller.deinit();
const named_function = NamedFunction(Struct, getter, "get_" ++ name){};
caller.getter(named_function, info) catch |err| {
caller.handleError(named_function, err, info);
};
}
}.callback;
const setter_name = "set_" ++ name;
if (@hasDecl(Struct, setter_name) == false) {
template_proto.setGetterData(js_name, getter_callback, self.callback_data);
return;
}
const setter = @field(Struct, setter_name);
const setter_callback = struct {
fn callback(_: ?*const v8.C_Name, raw_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) void {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller(Self).init(info);
defer caller.deinit();
const js_value = v8.Value{ .handle = raw_value.? };
const named_function = NamedFunction(Struct, setter, "set_" ++ name){};
caller.setter(named_function, js_value, info) catch |err| {
caller.handleError(named_function, err, info);
};
}
}.callback;
template_proto.setGetterAndSetterData(js_name, getter_callback, setter_callback, self.callback_data);
}
fn generateIndexer(self: *Self, comptime Struct: type, template_proto: v8.ObjectTemplate) void {
var has_one = false;
var configuration = v8.IndexedPropertyHandlerConfiguration{};
if (@hasDecl(Struct, "indexed_get")) {
has_one = true;
configuration.getter = struct {
fn callback(idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) void {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller(Self).init(info);
defer caller.deinit();
const named_function = NamedFunction(Struct, Struct.indexed_get, "indexed_get"){};
caller.getIndex(named_function, idx, info) catch |err| {
caller.handleError(named_function, err, info);
};
}
}.callback;
}
if (@hasDecl(Struct, "indexed_set")) {
has_one = true;
configuration.setter = struct {
fn callback(idx: u32, raw_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) void {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller(Self).init(info);
defer caller.deinit();
const js_value = v8.Value{ .handle = raw_value.? };
const named_function = NamedFunction(Struct, Struct.indexed_set, "indexed_set"){};
caller.setIndex(named_function, idx, js_value, info) catch |err| {
caller.handleError(named_function, err, info);
};
}
}.callback;
}
if (has_one) {
template_proto.setIndexedProperty(configuration, self.callback_data);
}
}
fn generateNamedIndexer(self: *Self, comptime Struct: type, template_proto: v8.ObjectTemplate) void {
var has_one = false;
var configuration = v8.NamedPropertyHandlerConfiguration{
// This is really cool. Without this, we'd intercept _all_ properties
// even those explictly set. So, node.length for example would get routed
// to our `named_get`, rather than a `get_length`. This might be
// useful if we run into a type that we can't model properly in Zig.
.flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
};
if (@hasDecl(Struct, "named_get")) {
has_one = true;
configuration.getter = struct {
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) void {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller(Self).init(info);
defer caller.deinit();
const named_function = NamedFunction(Struct, Struct.named_get, "named_get"){};
caller.getNamedIndex(named_function, .{ .handle = c_name.? }, info) catch |err| {
caller.handleError(named_function, err, info);
};
}
}.callback;
}
if (@hasDecl(Struct, "named_set")) {
has_one = true;
configuration.setter = struct {
fn callback(c_name: ?*const v8.C_Name, raw_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) void {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller(Self).init(info);
defer caller.deinit();
const js_value = v8.Value{ .handle = raw_value.? };
const named_function = NamedFunction(Struct, Struct.named_set, "named_set"){};
caller.setNamedIndex(named_function, .{ .handle = c_name.? }, js_value, info) catch |err| {
caller.handleError(named_function, err, info);
};
}
}.callback;
}
if (has_one) {
template_proto.setNamedProperty(configuration, self.callback_data);
}
}
// Turns a Zig value into a JS one.
fn zigValueToJs(
templates: []v8.FunctionTemplate,
isolate: v8.Isolate,
context: v8.Context,
value: anytype,
) anyerror!v8.Value {
// Check if it's a "simple" type. This is extractd so that it can be
// reused by other parts of the code. "simple" types only require an
// isolate to create
if (simpleZigValueToJs(isolate, value, false)) |js_value| {
return js_value;
}
const T = @TypeOf(value);
switch (@typeInfo(T)) {
.void, .bool, .int, .comptime_int, .float, .comptime_float, .array => {
// Need to do this to keep the compiler happy
// If this was the case, simpleZigValueToJs would
// have handled it
unreachable;
},
.pointer => |ptr| switch (ptr.size) {
.one => {
const type_name = @typeName(ptr.child);
if (@hasField(TypeLookup, type_name)) {
const template = templates[@field(TYPE_LOOKUP, type_name)];
const js_obj = try Executor.mapZigInstanceToJs(context, template, value);
return js_obj.toValue();
}
const one_info = @typeInfo(ptr.child);
if (one_info == .array and one_info.array.child == u8) {
// Need to do this to keep the compiler happy
// If this was the case, simpleZigValueToJs would
// have handled it
unreachable;
}
@compileLog(T);
},
.slice => {
if (ptr.child == u8) {
// Need to do this to keep the compiler happy
// If this was the case, simpleZigValueToJs would
// have handled it
unreachable;
}
var js_arr = v8.Array.init(isolate, @intCast(value.len));
var js_obj = js_arr.castTo(v8.Object);
for (value, 0..) |v, i| {
const js_val = try zigValueToJs(templates, isolate, context, v);
if (js_obj.setValueAtIndex(context, @intCast(i), js_val) == false) {
return error.FailedToCreateArray;
}
}
return js_obj.toValue();
},
else => {},
},
.@"struct" => |s| {
const type_name = @typeName(T);
if (@hasField(TypeLookup, type_name)) {
const template = templates[@field(TYPE_LOOKUP, type_name)];
const js_obj = try Executor.mapZigInstanceToJs(context, template, value);
return js_obj.toValue();
}
if (T == Callback) {
// we're returnig a callback
return value.func.toValue();
}
// return the struct as a JS object
const js_obj = v8.Object.init(isolate);
inline for (s.fields) |f| {
const js_val = try zigValueToJs(templates, isolate, context, @field(value, f.name));
const key = v8.String.initUtf8(isolate, f.name);
if (!js_obj.setValue(context, key, js_val)) {
return error.CreateObjectFailure;
}
}
return js_obj.toValue();
},
.@"union" => |un| {
if (T == std.json.Value) {
return zigJsonToJs(isolate, context, value);
}
if (un.tag_type) |UnionTagType| {
inline for (un.fields) |field| {
if (value == @field(UnionTagType, field.name)) {
return zigValueToJs(templates, isolate, context, @field(value, field.name));
}
}
unreachable;
}
@compileError("Cannot use untagged union: " ++ @typeName(T));
},
.optional => {
if (value) |v| {
return zigValueToJs(templates, isolate, context, v);
}
return v8.initNull(isolate).toValue();
},
.error_union => return zigValueToJs(templates, isolate, context, value catch |err| return err),
else => {},
}
@compileLog(@typeInfo(T));
@compileError("A function returns an unsupported type: " ++ @typeName(T));
}
const PersistentObject = v8.Persistent(v8.Object);
const PersistentFunction = v8.Persistent(v8.Function);
pub const Executor = struct {
state: State,
isolate: v8.Isolate,
handle_scope: v8.HandleScope,
// @intFromPtr of our Executor is stored in this context, so given
// a context, we can always get the Executor back.
context: v8.Context,
// Arena whose lifetime is for a single getter/setter/function/etc.
// Largely used to get strings out of V8, like a stack trace from
// a TryCatch. The allocator will be owned by the Scope, but the
// arena itself is owned by the Executor so that we can re-use it
// from scope to scope.
call_arena: ArenaAllocator,
// Arena whose lifetime is for a single page load, aka a Scope. Where
// the call_arena lives for a single function call, the scope_arena
// lives for the lifetime of the entire page. The allocator will be
// owned by the Scope, but the arena itself is owned by the Executor
// so that we can re-use it from scope to scope.
scope_arena: ArenaAllocator,
// When we need to load a resource (i.e. an external script), we call
// this function to get the source. This is always a refernece to the
// Browser Session's fetchModuleSource, but we use a funciton pointer
// since this js module is decoupled from the browser implementation.
module_loader: ModuleLoader,
// A Scope maps to a Browser's Page. Here though, it's only a
// mechanism to organization page-specific memory. The Executor
// does all the work, but having all page-specific data structures
// grouped together helps keep things clean.
scope: ?Scope = null,
templates: []v8.FunctionTemplate,
const ModuleLoader = struct { ptr: *anyopaque, func: *const fn (ptr: *anyopaque, specifier: []const u8) anyerror![]const u8 };
// not public, must be destroyed via env.stopExecutor()
fn deinit(self: *Executor) void {
if (self.scope) |*s| {
s.deinit();
}
self.context.exit();
self.handle_scope.deinit();
self.call_arena.deinit();
self.scope_arena.deinit();
}
pub fn exec(self: *Executor, src: []const u8, name: ?[]const u8) !Value {
const isolate = self.isolate;
const context = self.context;
var origin: ?v8.ScriptOrigin = null;
if (name) |n| {
const scr_name = v8.String.initUtf8(isolate, n);
origin = v8.ScriptOrigin.initDefault(isolate, scr_name.toValue());
}
const scr_js = v8.String.initUtf8(isolate, src);
const scr = v8.Script.compile(context, scr_js, origin) catch {
return error.CompilationError;
};
const value = scr.run(context) catch {
return error.ExecutionError;
};
return self.createValue(value);
}
// compile and eval a JS module
// It doesn't wait for callbacks execution
pub fn module(self: *Executor, src: []const u8, name: []const u8) !Value {
const context = self.context;
const m = try self.compileModule(src, name);
// instantiate
// TODO handle ResolveModuleCallback parameters to load module's
// dependencies.
const ok = m.instantiate(context, resolveModuleCallback) catch {
return error.ExecutionError;
};
if (!ok) {
return error.ModuleInstantiationError;
}
// evaluate
const value = m.evaluate(context) catch return error.ExecutionError;
return self.createValue(value);
}
fn compileModule(self: *Executor, src: []const u8, name: []const u8) !v8.Module {
const isolate = self.isolate;
// compile
const script_name = v8.String.initUtf8(isolate, name);
const script_source = v8.String.initUtf8(isolate, src);
const origin = v8.ScriptOrigin.init(
self.isolate,
script_name.toValue(),
0, // resource_line_offset
0, // resource_column_offset
false, // resource_is_shared_cross_origin
-1, // script_id
null, // source_map_url
false, // resource_is_opaque
false, // is_wasm
true, // is_module
null, // host_defined_options
);
var script_comp_source: v8.ScriptCompilerSource = undefined;
v8.ScriptCompilerSource.init(&script_comp_source, script_source, origin, null);
defer script_comp_source.deinit();
return v8.ScriptCompiler.compileModule(
isolate,
&script_comp_source,
.kNoCompileOptions,
.kNoCacheNoReason,
) catch return error.CompilationError;
}
pub fn startScope(self: *Executor, global: anytype) !void {
std.debug.assert(self.scope == null);
var handle_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&handle_scope, self.isolate);
self.scope = Scope{
.handle_scope = handle_scope,
.arena = self.scope_arena.allocator(),
.call_arena = self.call_arena.allocator(),
};
_ = try self._mapZigInstanceToJs(self.context.getGlobal(), global);
}
pub fn endScope(self: *Executor) void {
self.scope.?.deinit();
self.scope = null;
_ = self.scope_arena.reset(.{ .retain_with_limit = 1024 * 16 });
}
fn createValue(self: *const Executor, value: v8.Value) Value {
return .{
.value = value,
.executor = self,
};
}
fn zigValueToJs(self: *const Executor, value: anytype) !v8.Value {
return Self.zigValueToJs(self.templates, self.isolate, self.context, value);
}
// An instance of the exeuctor is stored in the execution context.
// Code that only has the context can call this function, which
// will extract the executor to map the Zig instance to an JS value.
fn mapZigInstanceToJs(context: v8.Context, js_obj_or_template: anytype, value: anytype) !PersistentObject {
const executor: *Executor = @ptrFromInt(context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
return executor._mapZigInstanceToJs(js_obj_or_template, value);
}
fn _mapZigInstanceToJs(self: *Executor, js_obj_or_template: anytype, value: anytype) !PersistentObject {
const scope = &self.scope.?;
const context = self.context;
const scope_arena = scope.arena;
const T = @TypeOf(value);
switch (@typeInfo(T)) {
.@"struct" => {
const heap = try scope_arena.create(T);
heap.* = value;
return self._mapZigInstanceToJs(js_obj_or_template, heap);
},
.pointer => |ptr| {
const gop = try scope.identity_map.getOrPut(scope_arena, @intFromPtr(value));
if (gop.found_existing) {
return gop.value_ptr.*;
}
const js_obj = switch (@TypeOf(js_obj_or_template)) {
v8.Object => js_obj_or_template,
v8.FunctionTemplate => js_obj_or_template.getInstanceTemplate().initInstance(context),
else => @compileError("mapZigInstanceToJs requires a v8.Object (constructors) or v8.FunctionTemplate, got: " ++ @typeName(@TypeOf(js_obj_or_template))),
};
const tao = try scope_arena.create(TaggedAnyOpaque);
tao.* = .{
.ptr = value,
.index = @field(TYPE_LOOKUP, @typeName(ptr.child)),
.sub_type = if (@hasDecl(ptr.child, "sub_type")) ptr.child.sub_type else null,
.offset = if (@typeInfo(ptr.child) != .@"opaque" and @hasField(ptr.child, "proto")) @offsetOf(ptr.child, "proto") else -1,
};
const isolate = self.isolate;
js_obj.setInternalField(0, v8.External.init(isolate, tao));
const js_persistent = PersistentObject.init(isolate, js_obj);
gop.value_ptr.* = js_persistent;
return js_persistent;
},
else => @compileError("Expected a struct or pointer, got " ++ @typeName(T) ++ " (constructors must return struct or pointers)"),
}
}
fn resolveModuleCallback(
c_context: ?*const v8.C_Context,
c_specifier: ?*const v8.C_String,
import_attributes: ?*const v8.C_FixedArray,
referrer: ?*const v8.C_Module,
) callconv(.C) ?*const v8.C_Module {
_ = import_attributes;
_ = referrer;
std.debug.assert(c_context != null);
const context = v8.Context{ .handle = c_context.? };
const self: *Executor = @ptrFromInt(context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
var buf: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
// build the specifier value.
const specifier = valueToString(
fba.allocator(),
.{ .handle = c_specifier.? },
self.isolate,
context,
) catch |e| {
log.err("resolveModuleCallback: get ref str: {any}", .{e});
return null;
};
// not currently needed
// const referrer_module = if (referrer) |ref| v8.Module{ .handle = ref } else null;
const module_loader = self.module_loader;
const source = module_loader.func(module_loader.ptr, specifier) catch |err| {
log.err("fetchModuleSource for '{s}' fetch error: {}", .{ specifier, err });
return null;
};
const m = self.compileModule(source, specifier) catch |err| {
log.err("fetchModuleSource for '{s}' compile error: {}", .{ specifier, err });
return null;
};
return m.handle;
}
};
// Loosely maps to a Browser Page. Executor does all the work, this just
// contains all the data structures / memory we need for a page. It helps
// to keep things organized. I.e. we have a single nullable,
// scope: ?Scope = null
// in executor, rather than having one for each of these.
pub const Scope = struct {
arena: Allocator,
call_arena: Allocator,
handle_scope: v8.HandleScope,
callbacks: std.ArrayListUnmanaged(v8.Persistent(v8.Function)) = .{},
identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .{},
fn deinit(self: *Scope) void {
var it = self.identity_map.valueIterator();
while (it.next()) |p| {
p.deinit();
}
for (self.callbacks.items) |*cb| {
cb.deinit();
}
self.handle_scope.deinit();
}
fn trackCallback(self: *Scope, pf: PersistentFunction) !void {
return self.callbacks.append(self.arena, pf);
}
};
pub const Callback = struct {
id: usize,
executor: *Executor,
this: ?v8.Object = null,
func: PersistentFunction,
const _CALLBACK_ID_KLUDGE = true;
pub const Result = struct {
stack: ?[]const u8,
exception: []const u8,
};
pub fn setThis(self: *Callback, value: anytype) !void {
const persistent_object = self.executor.scope.?.identity_map.get(@intFromPtr(value)) orelse {
return error.InvalidThisForCallback;
};
self.this = persistent_object.castToObject();
}
pub fn call(self: *const Callback, args: anytype) !void {
return self.callWithThis(self.this orelse self.executor.context.getGlobal(), args);
}
pub fn tryCall(self: *const Callback, args: anytype, result: *Result) !void {
var try_catch: TryCatch = undefined;
try_catch.init(self.executor);
defer try_catch.deinit();
self.call(args) catch |err| {
if (try_catch.hasCaught()) {
const allocator = self.executor.scope.?.call_arena;
result.stack = try_catch.stack(allocator) catch null;
result.exception = (try_catch.exception(allocator) catch @errorName(err)) orelse @errorName(err);
} else {
result.stack = null;
result.exception = @errorName(err);
}
return err;
};
}
fn callWithThis(self: *const @This(), js_this: v8.Object, args: anytype) !void {
const executor = self.executor;
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
const fields = @typeInfo(@TypeOf(aargs)).@"struct".fields;
var js_args: [fields.len]v8.Value = undefined;
inline for (fields, 0..) |f, i| {
js_args[i] = try executor.zigValueToJs(@field(aargs, f.name));
}
_ = self.func.castToFunction().call(executor.context, js_this, &js_args);
}
};
pub const TryCatch = struct {
inner: v8.TryCatch,
executor: *const Executor,
pub fn init(self: *TryCatch, executor: *const Executor) void {
self.executor = executor;
self.inner.init(executor.isolate);
}
pub fn hasCaught(self: TryCatch) bool {
return self.inner.hasCaught();
}
// the caller needs to deinit the string returned
pub fn exception(self: TryCatch, allocator: Allocator) !?[]const u8 {
const msg = self.inner.getException() orelse return null;
const executor = self.executor;
return try valueToString(allocator, msg, executor.isolate, executor.context);
}
// the caller needs to deinit the string returned
pub fn stack(self: TryCatch, allocator: Allocator) !?[]const u8 {
const executor = self.executor;
const s = self.inner.getStackTrace(executor.context) orelse return null;
return try valueToString(allocator, s, executor.isolate, executor.context);
}
// a shorthand method to return either the entire stack message
// or just the exception message
// - in Debug mode return the stack if available
// - otherwhise return the exception if available
// the caller needs to deinit the string returned
pub fn err(self: TryCatch, allocator: Allocator) !?[]const u8 {
if (builtin.mode == .Debug) {
if (try self.stack(allocator)) |msg| {
return msg;
}
}
return try self.exception(allocator);
}
pub fn deinit(self: *TryCatch) void {
self.inner.deinit();
}
};
pub const Inspector = struct {
isolate: v8.Isolate,
inner: *v8.Inspector,
session: v8.InspectorSession,
// We expect allocator to be an arena
pub fn init(allocator: Allocator, executor: *const Executor, ctx: anytype) !Inspector {
const ContextT = @TypeOf(ctx);
const InspectorContainer = switch (@typeInfo(ContextT)) {
.@"struct" => ContextT,
.pointer => |ptr| ptr.child,
.void => NoopInspector,
else => @compileError("invalid context type"),
};
// If necessary, turn a void context into something we can safely ptrCast
const safe_context: *anyopaque = if (ContextT == void) @constCast(@ptrCast(&{})) else ctx;
const isolate = executor.isolate;
const channel = v8.InspectorChannel.init(safe_context, InspectorContainer.onInspectorResponse, InspectorContainer.onInspectorEvent, isolate);
const client = v8.InspectorClient.init();
const inner = try allocator.create(v8.Inspector);
v8.Inspector.init(inner, client, channel, isolate);
return .{ .inner = inner, .isolate = isolate, .session = inner.connect() };
}
pub fn deinit(self: *const Inspector) void {
self.session.deinit();
self.inner.deinit();
}
pub fn send(self: *const Inspector, msg: []const u8) void {
self.session.dispatchProtocolMessage(self.isolate, msg);
}
pub fn contextCreated(
self: *const Inspector,
executor: *const Executor,
name: []const u8,
origin: []const u8,
aux_data: ?[]const u8,
) void {
self.inner.contextCreated(executor.context, name, origin, aux_data);
}
// Retrieves the RemoteObject for a given value.
// The value is loaded through the Executor's mapZigInstanceToJs function,
// just like a method return value. Therefore, if we've mapped this
// value before, we'll get the existing JS PersistedObject and if not
// we'll create it and track it for cleanup when the scope ends.
pub fn getRemoteObject(
self: *const Inspector,
executor: *const Executor,
group: []const u8,
value: anytype,
) !RemoteObject {
const js_value = try zigValueToJs(
executor.templates,
executor.isolate,
executor.context,
value,
);
// We do not want to expose this as a parameter for now
const generate_preview = false;
return self.session.wrapObject(
executor.isolate,
executor.context,
js_value,
group,
generate_preview,
);
}
};
pub const RemoteObject = v8.RemoteObject;
pub const Value = struct {
value: v8.Value,
executor: *const Executor,
// the caller needs to deinit the string returned
pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
const executor = self.executor;
return valueToString(allocator, self.value, executor.isolate, executor.context);
}
};
// Reverses the mapZigInstanceToJs, making sure that our TaggedAnyOpaque
// contains a ptr to the correct type.
fn typeTaggedAnyOpaque(comptime named_function: anytype, comptime R: type, op: ?*anyopaque) !R {
const ti = @typeInfo(R);
if (ti != .pointer) {
@compileError(std.fmt.comptimePrint(
"{s} has a non-pointer Zig parameter type: {s}",
.{ named_function.full_name, @typeName(R) },
));
}
const type_name = @typeName(ti.pointer.child);
if (@hasField(TypeLookup, type_name) == false) {
@compileError(std.fmt.comptimePrint(
"{s} has an unknown Zig type: {s}",
.{ named_function.full_name, @typeName(R) },
));
}
const toa: *TaggedAnyOpaque = @alignCast(@ptrCast(op));
const expected_type_index = @field(TYPE_LOOKUP, @typeName(ti.pointer.child));
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) {
return error.InvalidArgument;
}
type_index = prototype_index;
}
}
};
}
fn Caller(comptime E: type) type {
const State = E.State;
const TYPE_LOOKUP = E.TYPE_LOOKUP;
const TypeLookup = @TypeOf(TYPE_LOOKUP);
return struct {
env: *E,
context: v8.Context,
isolate: v8.Isolate,
executor: *E.Executor,
call_allocator: Allocator,
const Self = @This();
fn init(info: anytype) Self {
const isolate = info.getIsolate();
const env: *E = @ptrFromInt(info.getData().castTo(v8.BigInt).getUint64());
const context = isolate.getCurrentContext();
const executor: *E.Executor = @ptrFromInt(context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
return .{
.env = env,
.isolate = isolate,
.context = context,
.executor = executor,
.call_allocator = executor.scope.?.call_arena,
};
}
fn deinit(self: *Self) void {
_ = self.executor.call_arena.reset(.{ .retain_with_limit = 4096 });
}
fn constructor(self: *Self, comptime named_function: anytype, info: v8.FunctionCallbackInfo) !void {
const S = named_function.S;
const args = try self.getArgs(named_function, 0, info);
const res = @call(.auto, S.constructor, args);
const ReturnType = @typeInfo(@TypeOf(S.constructor)).@"fn".return_type orelse {
@compileError(@typeName(S) ++ " has a constructor without a return type");
};
const this = info.getThis();
if (@typeInfo(ReturnType) == .error_union) {
const non_error_res = res catch |err| return err;
_ = try E.Executor.mapZigInstanceToJs(self.context, this, non_error_res);
} else {
_ = try E.Executor.mapZigInstanceToJs(self.context, this, res);
}
info.getReturnValue().set(this);
}
fn method(self: *Self, comptime named_function: anytype, info: v8.FunctionCallbackInfo) !void {
const S = named_function.S;
comptime assertSelfReceiver(named_function);
var args = try self.getArgs(named_function, 1, info);
const external = info.getThis().getInternalField(0).castTo(v8.External);
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), external.get());
// inject 'self' as the first parameter
@field(args, "0") = zig_instance;
const res = @call(.auto, named_function.func, args);
info.getReturnValue().set(try self.zigValueToJs(res));
}
fn getter(self: *Self, comptime named_function: anytype, info: v8.PropertyCallbackInfo) !void {
const S = named_function.S;
const Getter = @TypeOf(named_function.func);
if (@typeInfo(Getter).@"fn".return_type == null) {
@compileError(@typeName(S) ++ " has a getter without a return type: " ++ @typeName(Getter));
}
var args: ParamterTypes(Getter) = undefined;
const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields;
switch (arg_fields.len) {
0 => {}, // getters _can_ be parameterless
1, 2 => {
const external = info.getThis().getInternalField(0).castTo(v8.External);
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), external.get());
comptime assertSelfReceiver(named_function);
@field(args, "0") = zig_instance;
if (comptime arg_fields.len == 2) {
comptime assertIsStateArg(named_function, 1);
@field(args, "1") = self.executor.state;
}
},
else => @compileError(named_function.full_name + " has too many parmaters: " ++ @typeName(named_function.func)),
}
const res = @call(.auto, named_function.func, args);
info.getReturnValue().set(try self.zigValueToJs(res));
}
fn setter(self: *Self, comptime named_function: anytype, js_value: v8.Value, info: v8.PropertyCallbackInfo) !void {
const S = named_function.S;
comptime assertSelfReceiver(named_function);
const external = info.getThis().getInternalField(0).castTo(v8.External);
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), external.get());
const Setter = @TypeOf(named_function.func);
var args: ParamterTypes(Setter) = undefined;
const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields;
switch (arg_fields.len) {
0 => unreachable, // assertSelfReceiver make sure of this
1 => @compileError(named_function.full_name ++ " only has 1 parameter"),
2, 3 => {
@field(args, "0") = zig_instance;
@field(args, "1") = try self.jsValueToZig(named_function, arg_fields[1].type, js_value);
if (comptime arg_fields.len == 3) {
comptime assertIsStateArg(named_function, 2);
@field(args, "2") = self.executor.state;
}
},
else => @compileError(named_function.full_name ++ " setter with more than 3 parameters, why?"),
}
if (@typeInfo(Setter).@"fn".return_type) |return_type| {
if (@typeInfo(return_type) == .error_union) {
_ = try @call(.auto, named_function.func, args);
return;
}
}
_ = @call(.auto, named_function.func, args);
}
fn getIndex(self: *Self, comptime named_function: anytype, idx: u32, info: v8.PropertyCallbackInfo) !void {
const S = named_function.S;
const IndexedGet = @TypeOf(named_function.func);
if (@typeInfo(IndexedGet).@"fn".return_type == null) {
@compileError(named_function.full_name ++ " must have a return type");
}
var has_value = true;
var args: ParamterTypes(IndexedGet) = undefined;
const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields;
switch (arg_fields.len) {
0, 1, 2 => @compileError(named_function.full_name ++ " must take at least a u32 and *bool parameter"),
3, 4 => {
const external = info.getThis().getInternalField(0).castTo(v8.External);
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), external.get());
comptime assertSelfReceiver(named_function);
@field(args, "0") = zig_instance;
@field(args, "1") = idx;
@field(args, "2") = &has_value;
if (comptime arg_fields.len == 4) {
comptime assertIsStateArg(named_function, 3);
@field(args, "3") = self.executor.state;
}
},
else => @compileError(named_function.full_name ++ " has too many parmaters"),
}
const res = @call(.auto, S.indexed_get, args);
if (has_value == false) {
// for an indexed parameter, say nodes[10000], we should return
// undefined, not null, if the index is out of rante
info.getReturnValue().set(try self.zigValueToJs({}));
} else {
info.getReturnValue().set(try self.zigValueToJs(res));
}
}
fn setIndex(self: *Self, comptime named_function: anytype, idx: u32, js_value: v8.Value, info: v8.PropertyCallbackInfo) !void {
const S = named_function.S;
comptime assertSelfReceiver(named_function);
const external = info.getThis().getInternalField(0).castTo(v8.External);
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), external.get());
const IndexedSet = @TypeOf(named_function.func);
var args: ParamterTypes(IndexedSet) = undefined;
const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields;
switch (arg_fields.len) {
0, 1, 2 => @compileError(named_function.full_name ++ " must take at least a u32 parameter and a value"),
3, 4 => {
@field(args, "0") = zig_instance;
@field(args, "1") = idx;
@field(args, "2") = try self.jsValueToZig(named_function, arg_fields[2].type, js_value);
if (comptime arg_fields.len == 4) {
comptime assertIsStateArg(named_function, 3);
@field(args, "3") = self.executor.state;
}
},
else => @compileError(named_function.full_name ++ " has too many parmaters"),
}
switch (@typeInfo(@typeInfo(IndexedSet).@"fn".return_type.?)) {
.error_union => |eu| {
if (eu.payload == void) {
return @call(.auto, S.indexed_set, args);
}
},
.void => return @call(.auto, S.indexed_set, args),
else => {},
}
@compileError(named_function.full_name ++ " cannot have a return type");
}
fn getNamedIndex(self: *Self, comptime named_function: anytype, name: v8.Name, info: v8.PropertyCallbackInfo) !void {
const S = named_function.S;
const NamedGet = @TypeOf(named_function.func);
if (@typeInfo(NamedGet).@"fn".return_type == null) {
@compileError(named_function.full_name ++ " must have a return type");
}
var has_value = true;
var args: ParamterTypes(NamedGet) = undefined;
const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields;
switch (arg_fields.len) {
0, 1, 2 => @compileError(named_function.full_name ++ " must take at least a u32 and *bool parameter"),
3, 4 => {
const external = info.getThis().getInternalField(0).castTo(v8.External);
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), external.get());
comptime assertSelfReceiver(named_function);
@field(args, "0") = zig_instance;
@field(args, "1") = try self.nameToString(name);
@field(args, "2") = &has_value;
if (comptime arg_fields.len == 4) {
comptime assertIsStateArg(named_function, 3);
@field(args, "3") = self.executor.state;
}
},
else => @compileError(named_function.full_name ++ " has too many parmaters"),
}
const res = @call(.auto, S.named_get, args);
if (has_value == false) {
// for an indexed parameter, say nodes[10000], we should return
// undefined, not null, if the index is out of rante
info.getReturnValue().set(try self.zigValueToJs({}));
} else {
info.getReturnValue().set(try self.zigValueToJs(res));
}
}
fn setNamedIndex(self: *Self, comptime named_function: anytype, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo) !void {
const S = named_function.S;
comptime assertSelfReceiver(named_function);
const external = info.getThis().getInternalField(0).castTo(v8.External);
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), external.get());
const IndexedSet = @TypeOf(named_function.func);
var args: ParamterTypes(IndexedSet) = undefined;
const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields;
switch (arg_fields.len) {
0, 1, 2 => @compileError(named_function.full_name ++ " must take at least an u32 parameter and a value"),
3, 4 => {
@field(args, "0") = zig_instance;
@field(args, "1") = try self.nameToString(name);
@field(args, "2") = try self.jsValueToZig(named_function, arg_fields[2].type, js_value);
if (comptime arg_fields.len == 4) {
comptime assertIsStateArg(named_function, 3);
@field(args, "3") = self.executor.state;
}
},
else => @compileError(named_function.full_name ++ " has too many parmaters"),
}
switch (@typeInfo(@typeInfo(IndexedSet).@"fn".return_type.?)) {
.error_union => |eu| {
if (eu.payload == void) {
return @call(.auto, S.named_set, args);
}
},
.void => return @call(.auto, S.named_set, args),
else => {},
}
@compileError(named_function.full_name ++ " cannot have a return type");
}
fn nameToString(self: *Self, name: v8.Name) ![]const u8 {
return valueToString(self.call_allocator, .{ .handle = name.handle }, self.isolate, self.context);
}
fn assertSelfReceiver(comptime named_function: anytype) void {
const params = @typeInfo(@TypeOf(named_function.func)).@"fn".params;
if (params.len == 0) {
@compileError(named_function.full_name ++ " must have a self parameter");
}
const R = Receiver(named_function.S);
const first_param = params[0].type.?;
if (first_param != *R and first_param != *const R) {
@compileError(std.fmt.comptimePrint("The first parameter to {s} must be a *{s} or *const {s}. Got: {s}", .{ named_function.full_name, @typeName(R), @typeName(R), @typeName(first_param) }));
}
}
fn assertIsStateArg(comptime named_function: anytype, index: comptime_int) void {
const F = @TypeOf(named_function.func);
const params = @typeInfo(F).@"fn".params;
const param = params[index].type.?;
if (param != State) {
@compileError(std.fmt.comptimePrint("The {d} parameter to {s} must be a {s}. Got: {s}", .{ index, named_function.full_name, @typeName(State), @typeName(param) }));
}
}
fn handleError(self: *Self, comptime named_function: anytype, err: anyerror, info: anytype) void {
const isolate = self.isolate;
var js_err: ?v8.Value = switch (err) {
error.InvalidArgument => createTypeException(isolate, "invalid argument"),
error.OutOfMemory => createException(isolate, "out of memory"),
else => blk: {
// if (@typeInfo(@TypeOf(func)) == .void) {
// // func will be void in the case of a type without a
// // constructor. In such cases the error will always
// // be error.IllegalConstructor, which the above case
// // will handle. So it should be impossible for us to
// // get here.
// // We add this code to satisfy the compiler.
// unreachable;
// }
const return_type = @typeInfo(@TypeOf(named_function.func)).@"fn".return_type orelse {
// void return type;
break :blk null;
};
if (@typeInfo(return_type) != .error_union) {
// type defines a custom exception, but this function should
// not fail. We failed somewhere inside of js.zig and
// should return the error as-is, since it isn't related
// to our Struct
break :blk null;
}
const function_error_set = @typeInfo(return_type).error_union.error_set;
const Exception = comptime getCustomException(named_function.S) orelse break :blk null;
if (function_error_set == Exception or isErrorSetException(Exception, err)) {
const custom_exception = Exception.init(self.call_allocator, err, named_function.js_name) catch |init_err| {
switch (init_err) {
// if a custom exceptions' init wants to return a
// different error, we need to think about how to
// handle that failure.
error.OutOfMemory => break :blk createException(isolate, "out of memory"),
}
};
// ughh..how to handle an error here?
break :blk self.zigValueToJs(custom_exception) catch createException(isolate, "internal error");
}
// this error isn't part of a custom exception
break :blk null;
},
};
if (js_err == null) {
js_err = createException(isolate, @errorName(err));
}
const js_exception = isolate.throwException(js_err.?);
info.getReturnValue().setValueHandle(js_exception.handle);
}
// walk the prototype chain to see if a type declares a custom Exception
fn getCustomException(comptime Struct: type) ?type {
var S = Struct;
while (true) {
if (@hasDecl(S, "Exception")) {
return S.Exception;
}
if (@hasDecl(S, "prototype") == false) {
return null;
}
// long ago, we validated that every prototype declaration
// is a pointer.
S = @typeInfo(S.prototype).pointer.child;
}
}
// Does the error we want to return belong to the custom exeception's ErrorSet
fn isErrorSetException(comptime Exception: type, err: anytype) bool {
const Entry = std.meta.Tuple(&.{ []const u8, void });
const error_set = @typeInfo(Exception.ErrorSet).error_set.?;
const entries = comptime blk: {
var kv: [error_set.len]Entry = undefined;
for (error_set, 0..) |e, i| {
kv[i] = .{ e.name, {} };
}
break :blk kv;
};
const lookup = std.StaticStringMap(void).initComptime(entries);
return lookup.has(@errorName(err));
}
// If we call a method in javascript: cat.lives('nine');
//
// Then we'd expect a Zig function with 2 parameters: a self and the string.
// In this case, offset == 1. Offset is always 1 for setters or methods.
//
// Offset is always 0 for constructors.
//
// For constructors, setters and methods, we can further increase offset + 1
// if the first parameter is an instance of State.
//
// Finally, if the JS function is called with _more_ parameters and
// the last parameter in Zig is an array, we'll try to slurp the additional
// parameters into the array.
fn getArgs(self: *const Self, comptime named_function: anytype, comptime offset: usize, info: anytype) !ParamterTypes(@TypeOf(named_function.func)) {
const F = @TypeOf(named_function.func);
const zig_function_parameters = @typeInfo(F).@"fn".params;
var args: ParamterTypes(F) = undefined;
if (zig_function_parameters.len == 0) {
return args;
}
const adjusted_offset = blk: {
if (zig_function_parameters.len > offset and comptime isState(zig_function_parameters[offset].type.?)) {
@field(args, std.fmt.comptimePrint("{d}", .{offset})) = self.executor.state;
break :blk offset + 1;
} else {
break :blk offset;
}
};
const js_parameter_count = info.length();
const expected_js_parameters = zig_function_parameters.len - adjusted_offset;
var is_variadic = false;
const last_parameter_index = zig_function_parameters.len - 1;
{
// This is going to get complicated. If the last Zig paremeter
// is a slice AND the corresponding javascript parameter is
// NOT an an array, then we'll treat it as a variadic.
const last_parameter_type = zig_function_parameters[last_parameter_index].type.?;
const last_parameter_type_info = @typeInfo(last_parameter_type);
if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) {
const slice_type = last_parameter_type_info.pointer.child;
const corresponding_js_index = last_parameter_index - adjusted_offset;
const corresponding_js_value = info.getArg(@as(u32, @intCast(corresponding_js_index)));
if (corresponding_js_value.isArray() == false and slice_type != u8) {
const arr = try self.call_allocator.alloc(last_parameter_type_info.pointer.child, js_parameter_count - expected_js_parameters + 1);
for (arr, corresponding_js_index..) |*a, i| {
const js_value = info.getArg(@as(u32, @intCast(i)));
a.* = try self.jsValueToZig(named_function, slice_type, js_value);
}
is_variadic = true;
@field(args, tupleFieldName(last_parameter_index)) = arr;
}
}
}
inline for (zig_function_parameters[adjusted_offset..], 0..) |param, i| {
const field_index = comptime i + adjusted_offset;
if (comptime field_index == last_parameter_index) {
if (is_variadic) {
break;
}
}
if (comptime isState(param.type.?)) {
@compileError("State must be the 2nd parameter: " ++ named_function.full_name);
} else if (i >= js_parameter_count) {
if (@typeInfo(param.type.?) != .optional) {
return error.TypeError;
}
@field(args, tupleFieldName(field_index)) = null;
} else {
const js_value = info.getArg(@as(u32, @intCast(i)));
@field(args, tupleFieldName(field_index)) = self.jsValueToZig(named_function, param.type.?, js_value) catch {
return error.InvalidArgument;
};
}
}
return args;
}
fn jsValueToZig(self: *const Self, comptime named_function: anytype, comptime T: type, js_value: v8.Value) !T {
switch (@typeInfo(T)) {
.optional => |o| {
if (js_value.isNull() or js_value.isUndefined()) {
return null;
}
return try self.jsValueToZig(named_function, o.child, js_value);
},
.float => |f| switch (f.bits) {
0...32 => return js_value.toF32(self.context),
33...64 => return js_value.toF64(self.context),
else => {},
},
.int => return jsIntToZig(T, js_value, self.context),
.bool => return js_value.toBool(self.isolate),
.pointer => |ptr| switch (ptr.size) {
.one => {
if (!js_value.isObject()) {
return error.InvalidArgument;
}
if (@hasField(TypeLookup, @typeName(ptr.child))) {
const obj = js_value.castTo(v8.Object);
if (obj.internalFieldCount() == 0) {
return error.InvalidArgument;
}
return E.typeTaggedAnyOpaque(named_function, *Receiver(ptr.child), obj.getInternalField(0).castTo(v8.External).get());
}
},
.slice => {
if (ptr.child == u8) {
return valueToString(self.call_allocator, js_value, self.isolate, self.context);
}
// TODO: TypedArray
// if (js_value.isArrayBufferView()) {
// const abv = js_value.castTo(v8.ArrayBufferView);
// const ab = abv.getBuffer();
// const bs = v8.BackingStore.sharedPtrGet(&ab.getBackingStore());
// const data = bs.getData();
// var arr = @as([*]i32, @alignCast(@ptrCast(data)))[0..2];
// std.debug.print("{d} {d} {d}\n", .{arr[0], arr[1], bs.getByteLength()});
// arr[1] = 3333;
// return &.{};
// }
if (!js_value.isArray()) {
return error.InvalidArgument;
}
const context = self.context;
const js_arr = js_value.castTo(v8.Array);
const js_obj = js_arr.castTo(v8.Object);
// Newer version of V8 appear to have an optimized way
// to do this (V8::Array has an iterate method on it)
const arr = try self.call_allocator.alloc(ptr.child, js_arr.length());
for (arr, 0..) |*a, i| {
a.* = try self.jsValueToZig(named_function, ptr.child, try js_obj.getAtIndex(context, @intCast(i)));
}
return arr;
},
else => {},
},
.@"struct" => |s| {
if (@hasDecl(T, "_CALLBACK_ID_KLUDGE")) {
if (!js_value.isFunction()) {
return error.InvalidArgument;
}
const executor = self.executor;
const func = v8.Persistent(v8.Function).init(self.isolate, js_value.castTo(v8.Function));
try executor.scope.?.trackCallback(func);
return .{
.func = func,
.executor = executor,
.id = js_value.castTo(v8.Object).getIdentityHash(),
};
}
if (!js_value.isObject()) {
return error.InvalidArgument;
}
const context = self.context;
const isolate = self.isolate;
const js_obj = js_value.castTo(v8.Object);
var value: T = undefined;
inline for (s.fields) |field| {
const name = field.name;
const key = v8.String.initUtf8(isolate, name);
if (js_obj.has(context, key.toValue())) {
@field(value, name) = try self.jsValueToZig(named_function, field.type, try js_obj.getValue(context, key));
} else if (@typeInfo(field.type) == .optional) {
@field(value, name) = null;
} else {
if (field.defaultValue()) |dflt| {
@field(value, name) = dflt;
} else {
return error.JSWrongObject;
}
}
}
return value;
},
else => {},
}
@compileError(std.fmt.comptimePrint("{s} has an unsupported parameter type: {s}", .{ named_function.full_name, @typeName(T) }));
}
fn jsIntToZig(comptime T: type, js_value: v8.Value, context: v8.Context) !T {
const n = @typeInfo(T).int;
switch (n.signedness) {
.signed => switch (n.bits) {
8 => return jsSignedIntToZig(i8, -128, 127, try js_value.toI32(context)),
16 => return jsSignedIntToZig(i16, -32_768, 32_767, try js_value.toI32(context)),
32 => return jsSignedIntToZig(i32, -2_147_483_648, 2_147_483_647, try js_value.toI32(context)),
64 => {
if (js_value.isBigInt()) {
const v = js_value.castTo(v8.BigInt);
return v.getInt64();
}
return jsSignedIntToZig(i64, -2_147_483_648, 2_147_483_647, try js_value.toI32(context));
},
else => {},
},
.unsigned => switch (n.bits) {
8 => return jsUnsignedIntToZig(u8, 255, try js_value.toU32(context)),
16 => return jsUnsignedIntToZig(u16, 65_535, try js_value.toU32(context)),
32 => return jsUnsignedIntToZig(u32, 4_294_967_295, try js_value.toU32(context)),
64 => {
if (js_value.isBigInt()) {
const v = js_value.castTo(v8.BigInt);
return v.getUint64();
}
return jsUnsignedIntToZig(u64, 4_294_967_295, try js_value.toU32(context));
},
else => {},
},
}
@compileError("Only i8, i16, i32, i64, u8, u16, u32 and u64 are supported");
}
fn jsSignedIntToZig(comptime T: type, comptime min: comptime_int, max: comptime_int, maybe: i32) !T {
if (maybe >= min and maybe <= max) {
return @intCast(maybe);
}
return error.InvalidArgument;
}
fn jsUnsignedIntToZig(comptime T: type, max: comptime_int, maybe: u32) !T {
if (maybe <= max) {
return @intCast(maybe);
}
return error.InvalidArgument;
}
fn zigValueToJs(self: *const Self, value: anytype) !v8.Value {
return self.executor.zigValueToJs(value);
}
fn isState(comptime T: type) bool {
const ti = @typeInfo(State);
const Const_State = if (ti == .pointer) *const ti.pointer.child else State;
return T == State or T == Const_State;
}
};
}
// These are simple types that we can convert to JS with only an isolate. This
// is separated from the Caller's zigValueToJs to make it available when we
// don't have a caller (i.e., when setting static attributes on types)
fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bool) if (fail) v8.Value else ?v8.Value {
switch (@typeInfo(@TypeOf(value))) {
.void => return v8.initUndefined(isolate).toValue(),
.bool => return v8.getValue(if (value) v8.initTrue(isolate) else v8.initFalse(isolate)),
.int => |n| switch (n.signedness) {
.signed => {
if (value >= -2_147_483_648 and value <= 2_147_483_647) {
return v8.Integer.initI32(isolate, @intCast(value)).toValue();
}
if (comptime n.bits <= 64) {
return v8.getValue(v8.BigInt.initI64(isolate, @intCast(value)));
}
@compileError(@typeName(value) ++ " is not supported");
},
.unsigned => {
if (value <= 4_294_967_295) {
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
}
if (comptime n.bits <= 64) {
return v8.getValue(v8.BigInt.initU64(isolate, @intCast(value)));
}
@compileError(@typeName(value) ++ " is not supported");
},
},
.comptime_int => {
if (value >= 0) {
if (value <= 4_294_967_295) {
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
}
return v8.BigInt.initU64(isolate, @intCast(value)).toValue();
}
if (value >= -2_147_483_648) {
return v8.Integer.initI32(isolate, @intCast(value)).toValue();
}
return v8.BigInt.initI64(isolate, @intCast(value)).toValue();
},
.comptime_float => return v8.Number.init(isolate, value).toValue(),
.float => |f| switch (f.bits) {
64 => return v8.Number.init(isolate, value).toValue(),
32 => return v8.Number.init(isolate, @floatCast(value)).toValue(),
else => @compileError(@typeName(value) ++ " is not supported"),
},
.pointer => |ptr| {
if (ptr.size == .slice and ptr.child == u8) {
return v8.String.initUtf8(isolate, value).toValue();
}
if (ptr.size == .one) {
const one_info = @typeInfo(ptr.child);
if (one_info == .array and one_info.array.child == u8) {
return v8.String.initUtf8(isolate, value).toValue();
}
}
},
.array => return simpleZigValueToJs(isolate, &value, fail),
.optional => {
if (value) |v| {
return simpleZigValueToJs(isolate, v, fail);
}
return v8.initNull(isolate).toValue();
},
.@"union" => return simpleZigValueToJs(isolate, std.meta.activeTag(value), fail),
else => {},
}
if (fail) {
@compileError("Unsupported Zig type " ++ @typeName(@TypeOf(value)));
}
return null;
}
pub fn zigJsonToJs(isolate: v8.Isolate, context: v8.Context, value: std.json.Value) !v8.Value {
switch (value) {
.bool => |v| return simpleZigValueToJs(isolate, v, true),
.float => |v| return simpleZigValueToJs(isolate, v, true),
.integer => |v| return simpleZigValueToJs(isolate, v, true),
.string => |v| return simpleZigValueToJs(isolate, v, true),
.null => return isolate.initNull().toValue(),
// TODO handle number_string.
// It is used to represent too big numbers.
.number_string => return error.TODO,
.array => |v| {
const a = v8.Array.init(isolate, @intCast(v.items.len));
const obj = a.castTo(v8.Object);
for (v.items, 0..) |array_value, i| {
const js_val = try zigJsonToJs(isolate, context, array_value);
if (!obj.setValueAtIndex(context, @intCast(i), js_val)) {
return error.JSObjectSetValue;
}
}
return obj.toValue();
},
.object => |v| {
var obj = v8.Object.init(isolate);
var it = v.iterator();
while (it.next()) |kv| {
const js_key = v8.String.initUtf8(isolate, kv.key_ptr.*);
const js_val = try zigJsonToJs(isolate, context, kv.value_ptr.*);
if (!obj.setValue(context, js_key, js_val)) {
return error.JSObjectSetValue;
}
}
return obj.toValue();
},
}
}
// Takes a function, and returns a tuple for its argument. Used when we
// @call a function
fn ParamterTypes(comptime F: type) type {
const params = @typeInfo(F).@"fn".params;
var fields: [params.len]std.builtin.Type.StructField = undefined;
inline for (params, 0..) |param, i| {
fields[i] = .{
.name = tupleFieldName(i),
.type = param.type.?,
.default_value_ptr = null,
.is_comptime = false,
.alignment = @alignOf(param.type.?),
};
}
return @Type(.{ .@"struct" = .{
.layout = .auto,
.decls = &.{},
.fields = &fields,
.is_tuple = true,
} });
}
fn tupleFieldName(comptime i: usize) [:0]const u8 {
return std.fmt.comptimePrint("{d}", .{i});
}
fn createException(isolate: v8.Isolate, msg: []const u8) v8.Value {
return v8.Exception.initError(v8.String.initUtf8(isolate, msg));
}
fn createTypeException(isolate: v8.Isolate, msg: []const u8) v8.Value {
return v8.Exception.initTypeError(v8.String.initUtf8(isolate, msg));
}
fn classNameForStruct(comptime Struct: type) []const u8 {
if (@hasDecl(Struct, "js_name")) {
return Struct.js_name;
}
@setEvalBranchQuota(10_000);
const full_name = @typeName(Struct);
const last = std.mem.lastIndexOfScalar(u8, full_name, '.') orelse return full_name;
return full_name[last + 1 ..];
}
// When we return a Zig object to V8, we put it on the heap and pass it into
// v8 as an *anyopaque (i.e. void *). When V8 gives us back the value, say, as a
// function parameter, we know what type it _should_ be. Above, in Caller.method
// (for example), we know all the parameter types. So if a Zig function takes
// a single parameter (its receiver), we know what that type is.
//
// In a simple/perfect world, we could use this knowledge to cast the *anyopaque
// to the parameter type:
// const arg: @typeInfo(@TypeOf(function)).@"fn".params[0] = @ptrCast(v8_data);
//
// But there are 2 reasons we can't do that.
//
// == Reason 1 ==
// The JS code might pass the wrong type:
//
// var cat = new Cat();
// cat.setOwner(new Cat());
//
// The zig _setOwner method expects the 2nd paramter to be an *Owner, but
// the JS code passed a *Cat.
//
// To solve this issue, we tag every returned value so that we can check what
// type it is. In the above case, we'd expect an *Owner, but the tag would tell
// us that we got a *Cat. We use the type index in our Types lookup as the tag.
//
// == Reason 2 ==
// Because of prototype inheritance, even "correct" code can be a challenge. For
// example, say the above JavaScript is fixed:
//
// var cat = new Cat();
// cat.setOwner(new Owner("Leto"));
//
// The issue is that setOwner might not expect an *Owner, but rather a
// *Person, which is the prototype for Owner. Now our Zig code is expecting
// a *Person, but it was (correctly) given an *Owner.
// For this reason, we also store the prototype's type index.
//
// One of the prototype mechanisms that we support is via composition. Owner
// can have a "proto: *Person" field. For this reason, we also store the offset
// of the proto field, so that, given an intFromPtr(*Owner) we can access it's
// proto field.
//
// The other prototype mechanism that we support is for netsurf, where we just
// cast one type to another. In this case, we'll store an offset of -1 (as a
// sentinel to indicate that we should just cast directly).
const TaggedAnyOpaque = struct {
// The type of object this is. The type is captured as an index, which
// corresponds to both a field in TYPE_LOOKUP and the index of
// 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 netsuf.)
offset: i32,
// Ptr to the Zig instance. We'll know its possible type based on the context
// where it's called, but it's exact type might be
ptr: *anyopaque,
// When we're asked to describe an object via the Inspector, we _must_ include
// the proper subtype (and description) fields in the returned JSON.
// V8 will give us a Value and ask us for the subtype. From the v8.Value we
// can get a v8.Object, and from the v8.Object, we can get out TaggedAnyOpque
sub_type: ?[*c]const u8,
};
fn valueToString(allocator: Allocator, value: v8.Value, isolate: v8.Isolate, context: v8.Context) ![]u8 {
const str = try value.toString(context);
const len = str.lenUtf8(isolate);
const buf = try allocator.alloc(u8, len);
const n = str.writeUtf8(isolate, buf);
std.debug.assert(n == len);
return buf;
}
pub const ObjectId = struct {
id: usize,
pub fn set(obj: v8.Object) ObjectId {
return .{ .id = obj.getIdentityHash() };
}
pub fn get(self: ObjectId) usize {
return self.id;
}
};
const NoopInspector = struct {
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
};
// If we have a struct:
// const Cat = struct {
// pub fn meow(self: *Cat) void { ... }
// }
// The obviously, the receiver of its methods are going to be a *Cat (or *const Cat)
//
// However, we can also do:
// const Cat = struct {
// pub const Self = OtherImpl;
// pub fn meow(self: *OtherImpl) void { ... }
// }
// In which case, as we see above, the receiver is derived from the Self declaration
fn Receiver(comptime S: type) type {
return if (@hasDecl(S, "Self")) S.Self else S;
}
// We want the function name, or more precisely, the "Struct.function" for
// displaying helpful @compileError.
// However, there's no way to get the name from a std.Builtin.Fn,
// so we capture it early, when we iterate through the declarations.
fn NamedFunction(comptime S: type, comptime function: anytype, comptime name: []const u8) type {
const full_name = @typeName(S) ++ "." ++ name;
const js_name = if (name[0] == '_') name[1..] else name;
return struct {
S: type = S,
full_name: []const u8 = full_name,
func: @TypeOf(function) = function,
js_name: []const u8 = js_name,
};
}
pub export fn v8_inspector__Client__IMPL__valueSubtype(
_: *v8.c.InspectorClientImpl,
c_value: *const v8.C_Value,
) callconv(.C) [*c]const u8 {
const external_entry = getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
return if (external_entry.sub_type) |st| st else null;
}
pub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype(
_: *v8.c.InspectorClientImpl,
context: *const v8.C_Context,
c_value: *const v8.C_Value,
) callconv(.C) [*c]const u8 {
_ = context;
// We _must_ include a non-null description in order for the subtype value
// to be included. Besides that, I don't know if the value has any meaning
const external_entry = getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
return if (external_entry.sub_type == null) null else "";
}
fn getTaggedAnyOpaque(value: v8.Value) ?*TaggedAnyOpaque {
if (value.isObject() == false) {
return null;
}
const obj = value.castTo(v8.Object);
if (obj.internalFieldCount() == 0) {
return null;
}
const external_data = obj.getInternalField(0).castTo(v8.External).get().?;
return @alignCast(@ptrCast(external_data));
}