mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-26 10:23:10 -04:00
Some apis want a value or undefined. For these, we can't use an Optional return type, null maps to JS null. Adds an Env.UndefinedOr(T) generic union for such return types.
3520 lines
163 KiB
Zig
3520 lines
163 KiB
Zig
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
//
|
|
// Francis Bouvier <francis@lightpanda.io>
|
|
// Pierre Tachoire <pierre@lightpanda.io>
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as
|
|
// published by the Free Software Foundation, either version 3 of the
|
|
// License, or (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
const v8 = @import("v8");
|
|
|
|
const log = @import("../log.zig");
|
|
const SubType = @import("subtype.zig").SubType;
|
|
|
|
const Allocator = std.mem.Allocator;
|
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
|
|
|
const CALL_ARENA_RETAIN = 1024 * 16;
|
|
const CONTEXT_ARENA_RETAIN = 1024 * 64;
|
|
|
|
// Global, should only be initialized once.
|
|
pub const Platform = struct {
|
|
inner: v8.Platform,
|
|
|
|
pub fn init() !Platform {
|
|
if (v8.initV8ICU() == false) {
|
|
return error.FailedToInitializeICU;
|
|
}
|
|
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();
|
|
}
|
|
};
|
|
|
|
// The Env maps to a V8 isolate, which represents a isolated sandbox for
|
|
// executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings,
|
|
// and it's where we'll start ExecutionWorlds, which actually execute JavaScript.
|
|
// The `S` parameter is arbitrary state. When we start an ExecutionWorld, an instance
|
|
// of S must be given. This instance is available to any Zig binding.
|
|
// The `types` parameter is a tuple of Zig structures we want to bind to V8.
|
|
pub fn Env(comptime State: type, comptime WebApis: type) type {
|
|
const Types = @typeInfo(WebApis.Interfaces).@"struct".fields;
|
|
|
|
// Imagine we have a type Cat which has a getter:
|
|
//
|
|
// fn get_owner(self: *Cat) *Owner {
|
|
// return self.owner;
|
|
// }
|
|
//
|
|
// When we 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_of_owner].initInstance(...);
|
|
//
|
|
// But how do we get that `index_of_owner`? `TypeLookup` is a struct
|
|
// that looks like:
|
|
//
|
|
// const TypeLookup = struct {
|
|
// comptime cat: usize = TypeMeta{.index = 0, ...},
|
|
// comptime owner: usize = TypeMeta{.index = 1, ...},
|
|
// ...
|
|
// }
|
|
//
|
|
// So to get the template index of `owner`, we can do:
|
|
//
|
|
// const index_id = @field(type_lookup, @typeName(@TypeOf(res)).index;
|
|
//
|
|
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 = s.defaultValue().?;
|
|
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 subtype: ?SubType = if (@hasDecl(Struct, "subtype")) Struct.subtype else null;
|
|
|
|
const R = Receiver(Struct);
|
|
fields[i] = .{
|
|
.name = @typeName(R),
|
|
.type = TypeMeta,
|
|
.is_comptime = true,
|
|
.alignment = @alignOf(usize),
|
|
.default_value_ptr = &TypeMeta{ .index = i, .subtype = subtype },
|
|
};
|
|
}
|
|
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 = s.defaultValue().?;
|
|
if (@hasDecl(Struct, "prototype")) {
|
|
const TI = @typeInfo(Struct.prototype);
|
|
const proto_name = @typeName(Receiver(TI.pointer.child));
|
|
prototype_index = @field(TYPE_LOOKUP, proto_name).index;
|
|
}
|
|
table[i] = prototype_index;
|
|
}
|
|
break :blk table;
|
|
};
|
|
|
|
return struct {
|
|
allocator: Allocator,
|
|
|
|
// the global isolate
|
|
isolate: v8.Isolate,
|
|
|
|
// 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)).index
|
|
// 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,
|
|
|
|
const Self = @This();
|
|
|
|
const TYPE_LOOKUP = TypeLookup{};
|
|
|
|
const Opts = struct {};
|
|
|
|
pub fn init(allocator: Allocator, _: Opts) !*Self {
|
|
// var params = v8.initCreateParams();
|
|
var params = try allocator.create(v8.CreateParams);
|
|
errdefer allocator.destroy(params);
|
|
|
|
v8.c.v8__Isolate__CreateParams__CONSTRUCT(params);
|
|
|
|
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 temp_scope: v8.HandleScope = undefined;
|
|
v8.HandleScope.init(&temp_scope, isolate);
|
|
defer temp_scope.deinit();
|
|
|
|
const env = try allocator.create(Self);
|
|
errdefer allocator.destroy(env);
|
|
|
|
env.* = .{
|
|
.isolate = isolate,
|
|
.templates = undefined,
|
|
.allocator = allocator,
|
|
.isolate_params = params,
|
|
.prototype_lookup = undefined,
|
|
};
|
|
|
|
// Populate our templates lookup. generateClass creates the
|
|
// v8.FunctionTemplate, which we store in our env.templates.
|
|
// The ordering doesn't matter. What matters is that, given a type
|
|
// we can get its index via: @field(TYPE_LOOKUP, type_name).index
|
|
const templates = &env.templates;
|
|
inline for (Types, 0..) |s, i| {
|
|
@setEvalBranchQuota(10_000);
|
|
templates[i] = v8.Persistent(v8.FunctionTemplate).init(isolate, generateClass(s.defaultValue().?, isolate)).castToFunctionTemplate();
|
|
}
|
|
|
|
// Above, we've created all our our FunctionTemplates. Now that we
|
|
// have them all, we can hook up the prototypes.
|
|
inline for (Types, 0..) |s, i| {
|
|
const Struct = s.defaultValue().?;
|
|
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).index;
|
|
templates[i].inherit(templates[proto_index]);
|
|
}
|
|
}
|
|
|
|
return env;
|
|
}
|
|
|
|
pub fn deinit(self: *Self) void {
|
|
self.isolate.exit();
|
|
self.isolate.deinit();
|
|
v8.destroyArrayBufferAllocator(self.isolate_params.array_buffer_allocator.?);
|
|
self.allocator.destroy(self.isolate_params);
|
|
self.allocator.destroy(self);
|
|
}
|
|
|
|
pub fn newInspector(self: *Self, arena: Allocator, ctx: anytype) !Inspector {
|
|
return Inspector.init(arena, self.isolate, ctx);
|
|
}
|
|
|
|
pub fn runMicrotasks(self: *const Self) void {
|
|
self.isolate.performMicrotasksCheckpoint();
|
|
}
|
|
|
|
pub fn newExecutionWorld(self: *Self) !ExecutionWorld {
|
|
return .{
|
|
.env = self,
|
|
.js_context = null,
|
|
.call_arena = ArenaAllocator.init(self.allocator),
|
|
.context_arena = ArenaAllocator.init(self.allocator),
|
|
};
|
|
}
|
|
|
|
// V8 doesn't immediately free memory associated with
|
|
// a Context, it's managed by the garbage collector. We use the
|
|
// `lowMemoryNotification` call on the isolate to encourage v8 to free
|
|
// any contexts which have been freed.
|
|
pub fn lowMemoryNotification(self: *Self) void {
|
|
var handle_scope: v8.HandleScope = undefined;
|
|
v8.HandleScope.init(&handle_scope, self.isolate);
|
|
defer handle_scope.deinit();
|
|
self.isolate.lowMemoryNotification();
|
|
}
|
|
|
|
// ExecutionWorld closely models a JS World.
|
|
// https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#World
|
|
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld
|
|
pub const ExecutionWorld = struct {
|
|
env: *Self,
|
|
|
|
// 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 JsContext, but the
|
|
// arena itself is owned by the ExecutionWorld so that we can re-use it
|
|
// from context to context.
|
|
call_arena: ArenaAllocator,
|
|
|
|
// Arena whose lifetime is for a single page load. Where
|
|
// the call_arena lives for a single function call, the context_arena
|
|
// lives for the lifetime of the entire page. The allocator will be
|
|
// owned by the JsContext, but the arena itself is owned by the ExecutionWorld
|
|
// so that we can re-use it from context to context.
|
|
context_arena: ArenaAllocator,
|
|
|
|
// Currently a context maps to a Browser's Page. Here though, it's only a
|
|
// mechanism to organization page-specific memory. The ExecutionWorld
|
|
// does all the work, but having all page-specific data structures
|
|
// grouped together helps keep things clean.
|
|
js_context: ?JsContext = null,
|
|
|
|
// no init, must be initialized via env.newExecutionWorld()
|
|
|
|
pub fn deinit(self: *ExecutionWorld) void {
|
|
if (self.js_context != null) {
|
|
self.removeJsContext();
|
|
}
|
|
|
|
self.call_arena.deinit();
|
|
self.context_arena.deinit();
|
|
}
|
|
|
|
// Only the top JsContext in the Main ExecutionWorld should hold a handle_scope.
|
|
// A v8.HandleScope is like an arena. Once created, any "Local" that
|
|
// v8 creates will be released (or at least, releasable by the v8 GC)
|
|
// when the handle_scope is freed.
|
|
// We also maintain our own "context_arena" which allows us to have
|
|
// all page related memory easily managed.
|
|
pub fn createJsContext(self: *ExecutionWorld, global: anytype, state: State, module_loader: anytype, enter: bool) !*JsContext {
|
|
std.debug.assert(self.js_context == null);
|
|
|
|
const ModuleLoader = switch (@typeInfo(@TypeOf(module_loader))) {
|
|
.@"struct" => @TypeOf(module_loader),
|
|
.pointer => |ptr| ptr.child,
|
|
.void => ErrorModuleLoader,
|
|
else => @compileError("invalid module_loader"),
|
|
};
|
|
|
|
// If necessary, turn a void context into something we can safely ptrCast
|
|
const safe_module_loader: *anyopaque = if (ModuleLoader == ErrorModuleLoader) @constCast(@ptrCast(&{})) else module_loader;
|
|
|
|
const env = self.env;
|
|
const isolate = env.isolate;
|
|
const Global = @TypeOf(global.*);
|
|
const templates = &self.env.templates;
|
|
|
|
var v8_context: v8.Context = blk: {
|
|
var temp_scope: v8.HandleScope = undefined;
|
|
v8.HandleScope.init(&temp_scope, isolate);
|
|
defer temp_scope.deinit();
|
|
|
|
const js_global = v8.FunctionTemplate.initDefault(isolate);
|
|
attachClass(Global, isolate, js_global);
|
|
|
|
const global_template = js_global.getInstanceTemplate();
|
|
global_template.setInternalFieldCount(1);
|
|
|
|
// All the FunctionTemplates that we created and setup in Env.init
|
|
// are now going to get associated with our global instance.
|
|
inline for (Types, 0..) |s, i| {
|
|
const Struct = s.defaultValue().?;
|
|
const class_name = v8.String.initUtf8(isolate, comptime classNameForStruct(Struct));
|
|
global_template.set(class_name.toName(), templates[i], v8.PropertyAttribute.None);
|
|
}
|
|
|
|
// The global object (Window) has already been hooked into the v8
|
|
// engine when the Env was initialized - like every other type.
|
|
// But the V8 global is its own FunctionTemplate instance so even
|
|
// though it's also a Window, we need to set the prototype for this
|
|
// specific instance of the the Window.
|
|
if (@hasDecl(Global, "prototype")) {
|
|
const proto_type = Receiver(@typeInfo(Global.prototype).pointer.child);
|
|
const proto_name = @typeName(proto_type);
|
|
const proto_index = @field(TYPE_LOOKUP, proto_name).index;
|
|
js_global.inherit(templates[proto_index]);
|
|
}
|
|
|
|
const context_local = v8.Context.init(isolate, global_template, null);
|
|
const v8_context = v8.Persistent(v8.Context).init(isolate, context_local).castToContext();
|
|
v8_context.enter();
|
|
errdefer if (enter) v8_context.exit();
|
|
defer if (!enter) v8_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 = s.defaultValue().?;
|
|
|
|
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).index;
|
|
const proto_obj = templates[proto_index].getFunction(v8_context).toObject();
|
|
|
|
const self_obj = templates[i].getFunction(v8_context).toObject();
|
|
_ = self_obj.setPrototype(v8_context, proto_obj);
|
|
}
|
|
}
|
|
break :blk v8_context;
|
|
};
|
|
|
|
// For a Page we only create one HandleScope, it is stored in the main World (enter==true). A page can have multple contexts, 1 for each World.
|
|
// The main Context that enters and holds the HandleScope should therefore always be created first. Following other worlds for this page
|
|
// like isolated Worlds, will thereby place their objects on the main page's HandleScope. Note: In the furure the number of context will multiply multiple frames support
|
|
var handle_scope: ?v8.HandleScope = null;
|
|
if (enter) {
|
|
handle_scope = @as(v8.HandleScope, undefined);
|
|
v8.HandleScope.init(&handle_scope.?, isolate);
|
|
}
|
|
errdefer if (enter) handle_scope.?.deinit();
|
|
|
|
{
|
|
// If we want to overwrite the built-in console, we have to
|
|
// delete the built-in one.
|
|
const js_obj = v8_context.getGlobal();
|
|
const console_key = v8.String.initUtf8(isolate, "console");
|
|
if (js_obj.deleteValue(v8_context, console_key) == false) {
|
|
return error.ConsoleDeleteError;
|
|
}
|
|
}
|
|
|
|
self.js_context = JsContext{
|
|
.state = state,
|
|
.isolate = isolate,
|
|
.v8_context = v8_context,
|
|
.templates = &env.templates,
|
|
.handle_scope = handle_scope,
|
|
.call_arena = self.call_arena.allocator(),
|
|
.context_arena = self.context_arena.allocator(),
|
|
.module_loader = .{
|
|
.ptr = safe_module_loader,
|
|
.func = ModuleLoader.fetchModuleSource,
|
|
},
|
|
};
|
|
|
|
var js_context = &self.js_context.?;
|
|
{
|
|
// 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(js_context)));
|
|
v8_context.setEmbedderData(1, data);
|
|
}
|
|
|
|
{
|
|
// Not the prettiest but we want to make the `call_arena`
|
|
// optionally available to the WebAPIs. If `state` has a
|
|
// call_arena field, fill-it in now.
|
|
const state_type_info = @typeInfo(@TypeOf(state));
|
|
if (state_type_info == .pointer and @hasField(state_type_info.pointer.child, "call_arena")) {
|
|
js_context.state.call_arena = js_context.call_arena;
|
|
}
|
|
}
|
|
|
|
// 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 = s.defaultValue().?;
|
|
if (@hasDecl(Struct, "ErrorSet")) {
|
|
const script = comptime classNameForStruct(Struct) ++ ".prototype.__proto__ = Error.prototype";
|
|
_ = try js_context.exec(script, "errorSubclass");
|
|
}
|
|
}
|
|
|
|
// Primitive attributes are set directly on the FunctionTemplate
|
|
// when we setup the environment. But we cannot set more complex
|
|
// types (v8 will crash).
|
|
//
|
|
// Plus, just to create more complex types, we always need a
|
|
// context, i.e. an Array has to have a Context to exist.
|
|
//
|
|
// As far as I can tell, getting the FunctionTemplate's object
|
|
// and setting values directly on it, for each context, is the
|
|
// way to do this.
|
|
inline for (Types, 0..) |s, i| {
|
|
const Struct = s.defaultValue().?;
|
|
inline for (@typeInfo(Struct).@"struct".decls) |declaration| {
|
|
const name = declaration.name;
|
|
if (comptime name[0] == '_') {
|
|
const value = @field(Struct, name);
|
|
|
|
if (comptime isComplexAttributeType(@typeInfo(@TypeOf(value)))) {
|
|
const js_obj = templates[i].getFunction(v8_context).toObject();
|
|
const js_name = v8.String.initUtf8(isolate, name[1..]).toName();
|
|
const js_val = try js_context.zigValueToJs(value);
|
|
if (!js_obj.setValue(v8_context, js_name, js_val)) {
|
|
log.fatal(.app, "set class attribute", .{
|
|
.@"struct" = @typeName(Struct),
|
|
.name = name,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_ = try js_context._mapZigInstanceToJs(v8_context.getGlobal(), global);
|
|
return js_context;
|
|
}
|
|
|
|
pub fn removeJsContext(self: *ExecutionWorld) void {
|
|
self.js_context.?.deinit();
|
|
self.js_context = null;
|
|
_ = self.context_arena.reset(.{ .retain_with_limit = CONTEXT_ARENA_RETAIN });
|
|
}
|
|
|
|
pub fn terminateExecution(self: *const ExecutionWorld) void {
|
|
self.env.isolate.terminateExecution();
|
|
}
|
|
|
|
pub fn resumeExecution(self: *const ExecutionWorld) void {
|
|
self.env.isolate.cancelTerminateExecution();
|
|
}
|
|
};
|
|
|
|
const PersistentObject = v8.Persistent(v8.Object);
|
|
const PersistentModule = v8.Persistent(v8.Module);
|
|
const PersistentFunction = v8.Persistent(v8.Function);
|
|
|
|
// Loosely maps to a Browser Page.
|
|
pub const JsContext = struct {
|
|
state: State,
|
|
isolate: v8.Isolate,
|
|
// This context is a persistent object. The persistent needs to be recovered and reset.
|
|
v8_context: v8.Context,
|
|
handle_scope: ?v8.HandleScope,
|
|
|
|
// references the Env.template array
|
|
templates: []v8.FunctionTemplate,
|
|
|
|
// An arena for the lifetime of a call-group. Gets reset whenever
|
|
// call_depth reaches 0.
|
|
call_arena: Allocator,
|
|
|
|
// An arena for the lifetime of the context
|
|
context_arena: Allocator,
|
|
|
|
// Because calls can be nested (i.e.a function calling a callback),
|
|
// we can only reset the call_arena when call_depth == 0. If we were
|
|
// to reset it within a callback, it would invalidate the data of
|
|
// the call which is calling the callback.
|
|
call_depth: usize = 0,
|
|
|
|
// Callbacks are PesistendObjects. When the context ends, we need
|
|
// to free every callback we created.
|
|
callbacks: std.ArrayListUnmanaged(v8.Persistent(v8.Function)) = .empty,
|
|
|
|
// Serves two purposes. Like `callbacks` above, this is used to free
|
|
// every PeristentObjet we've created during the lifetime of the context.
|
|
// More importantly, it serves as an identity map - for a given Zig
|
|
// instance, we map it to the same PersistentObject.
|
|
// The key is the @intFromPtr of the Zig value
|
|
identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .empty,
|
|
|
|
// Similar to the identity map, but used much less frequently. Some
|
|
// web APIs have to manage opaque values. Ideally, they use an
|
|
// JsObject, but the JsObject has no lifetime guarantee beyond the
|
|
// current call. They can call .persist() on their JsObject to get
|
|
// a `*PersistentObject()`. We need to track these to free them.
|
|
// The key is the @intFromPtr of the v8.Object.handle.
|
|
js_object_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .empty,
|
|
|
|
// When we need to load a resource (i.e. an external script), we call
|
|
// this function to get the source. This is always a reference to the
|
|
// Page's fetchModuleSource, but we use a function pointer
|
|
// since this js module is decoupled from the browser implementation.
|
|
module_loader: ModuleLoader,
|
|
|
|
// Some Zig types have code to execute to cleanup
|
|
destructor_callbacks: std.ArrayListUnmanaged(DestructorCallback) = .empty,
|
|
|
|
// Some Zig types have code to execute when the call scope ends
|
|
call_scope_end_callbacks: std.ArrayListUnmanaged(CallScopeEndCallback) = .empty,
|
|
|
|
// Our module cache: normalized module specifier => module.
|
|
module_cache: std.StringHashMapUnmanaged(PersistentModule) = .empty,
|
|
|
|
// Module => Path. The key is the module hashcode (module.getIdentityHash)
|
|
// and the value is the full path to the module. We need to capture this
|
|
// so that when we're asked to resolve a dependent module, and all we're
|
|
// given is the specifier, we can form the full path. The full path is
|
|
// necessary to lookup/store the dependent module in the module_cache.
|
|
module_identifier: std.AutoHashMapUnmanaged(u32, []const u8) = .empty,
|
|
|
|
const ModuleLoader = struct {
|
|
ptr: *anyopaque,
|
|
func: *const fn (ptr: *anyopaque, specifier: []const u8) anyerror!?[]const u8,
|
|
};
|
|
|
|
// no init, started with executor.createJsContext()
|
|
|
|
fn deinit(self: *JsContext) void {
|
|
{
|
|
// reverse order, as this has more chance of respecting any
|
|
// dependencies objects might have with each other.
|
|
const items = self.destructor_callbacks.items;
|
|
var i = items.len;
|
|
while (i > 0) {
|
|
i -= 1;
|
|
items[i].destructor();
|
|
}
|
|
}
|
|
|
|
{
|
|
var it = self.identity_map.valueIterator();
|
|
while (it.next()) |p| {
|
|
p.deinit();
|
|
}
|
|
}
|
|
|
|
{
|
|
var it = self.js_object_map.valueIterator();
|
|
while (it.next()) |p| {
|
|
p.deinit();
|
|
}
|
|
}
|
|
|
|
{
|
|
var it = self.module_cache.valueIterator();
|
|
while (it.next()) |p| {
|
|
p.deinit();
|
|
}
|
|
}
|
|
|
|
for (self.callbacks.items) |*cb| {
|
|
cb.deinit();
|
|
}
|
|
if (self.handle_scope) |*scope| {
|
|
scope.deinit();
|
|
self.v8_context.exit();
|
|
}
|
|
var presistent_context = v8.Persistent(v8.Context).recoverCast(self.v8_context);
|
|
presistent_context.deinit();
|
|
}
|
|
|
|
fn trackCallback(self: *JsContext, pf: PersistentFunction) !void {
|
|
return self.callbacks.append(self.context_arena, pf);
|
|
}
|
|
|
|
// Given an anytype, turns it into a v8.Object. The anytype could be:
|
|
// 1 - A V8.object already
|
|
// 2 - Our JsObject wrapper around a V8.Object
|
|
// 3 - A zig instance that has previously been given to V8
|
|
// (i.e., the value has to be known to the executor)
|
|
fn valueToExistingObject(self: *const JsContext, value: anytype) !v8.Object {
|
|
if (@TypeOf(value) == v8.Object) {
|
|
return value;
|
|
}
|
|
|
|
if (@TypeOf(value) == JsObject) {
|
|
return value.js_obj;
|
|
}
|
|
|
|
const persistent_object = self.identity_map.get(@intFromPtr(value)) orelse {
|
|
return error.InvalidThisForCallback;
|
|
};
|
|
|
|
return persistent_object.castToObject();
|
|
}
|
|
|
|
pub fn stackTrace(self: *const JsContext) !?[]const u8 {
|
|
return stackForLogs(self.call_arena, self.isolate);
|
|
}
|
|
|
|
// Executes the src
|
|
pub fn eval(self: *JsContext, src: []const u8, name: ?[]const u8) !void {
|
|
_ = try self.exec(src, name);
|
|
}
|
|
|
|
pub fn exec(self: *JsContext, src: []const u8, name: ?[]const u8) !Value {
|
|
const isolate = self.isolate;
|
|
const v8_context = self.v8_context;
|
|
|
|
var origin: ?v8.ScriptOrigin = null;
|
|
if (name) |n| {
|
|
const scr_name = v8.String.initUtf8(isolate, n);
|
|
origin = v8.ScriptOrigin.initDefault(scr_name.toValue());
|
|
}
|
|
const scr_js = v8.String.initUtf8(isolate, src);
|
|
const scr = v8.Script.compile(v8_context, scr_js, origin) catch {
|
|
return error.CompilationError;
|
|
};
|
|
|
|
const value = scr.run(v8_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: *JsContext, src: []const u8, url: []const u8, cacheable: bool) !void {
|
|
if (!cacheable) {
|
|
return self.moduleNoCache(src, url);
|
|
}
|
|
|
|
const arena = self.context_arena;
|
|
|
|
const gop = try self.module_cache.getOrPut(arena, url);
|
|
if (gop.found_existing) {
|
|
return;
|
|
}
|
|
errdefer _ = self.module_cache.remove(url);
|
|
|
|
const m = try compileModule(self.isolate, src, url);
|
|
|
|
const owned_url = try arena.dupe(u8, url);
|
|
try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_url);
|
|
errdefer _ = self.module_identifier.remove(m.getIdentityHash());
|
|
|
|
gop.key_ptr.* = owned_url;
|
|
gop.value_ptr.* = PersistentModule.init(self.isolate, m);
|
|
|
|
// resolveModuleCallback loads module's dependencies.
|
|
const v8_context = self.v8_context;
|
|
if (try m.instantiate(v8_context, resolveModuleCallback) == false) {
|
|
return error.ModuleInstantiationError;
|
|
}
|
|
|
|
_ = try m.evaluate(v8_context);
|
|
}
|
|
|
|
fn moduleNoCache(self: *JsContext, src: []const u8, url: []const u8) !void {
|
|
const m = try compileModule(self.isolate, src, url);
|
|
const v8_context = self.v8_context;
|
|
if (try m.instantiate(v8_context, resolveModuleCallback) == false) {
|
|
return error.ModuleInstantiationError;
|
|
}
|
|
const arena = self.context_arena;
|
|
const owned_url = try arena.dupe(u8, url);
|
|
try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_url);
|
|
_ = try m.evaluate(v8_context);
|
|
}
|
|
|
|
// Wrap a v8.Exception
|
|
fn createException(self: *const JsContext, e: v8.Value) Exception {
|
|
return .{
|
|
.inner = e,
|
|
.js_context = self,
|
|
};
|
|
}
|
|
|
|
// Wrap a v8.Value, largely so that we can provide a convenient
|
|
// toString function
|
|
fn createValue(self: *const JsContext, value: v8.Value) Value {
|
|
return .{
|
|
.value = value,
|
|
.js_context = self,
|
|
};
|
|
}
|
|
|
|
fn zigValueToJs(self: *const JsContext, value: anytype) !v8.Value {
|
|
return Self.zigValueToJs(self.templates, self.isolate, self.v8_context, value);
|
|
}
|
|
|
|
// See _mapZigInstanceToJs, this is wrapper that can be called
|
|
// without a Context. This is possible because we store our
|
|
// js_context in the EmbedderData of the v8.Context. So, as long as
|
|
// we have a v8.Context, we can get the js_context.
|
|
fn mapZigInstanceToJs(v8_context: v8.Context, js_obj_or_template: anytype, value: anytype) !PersistentObject {
|
|
const js_context: *JsContext = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
|
|
return js_context._mapZigInstanceToJs(js_obj_or_template, value);
|
|
}
|
|
|
|
// To turn a Zig instance into a v8 object, we need to do a number of things.
|
|
// First, if it's a struct, we need to put it on the heap
|
|
// Second, if we've already returned this instance, we should return
|
|
// the same object. Hence, our executor maintains a map of Zig objects
|
|
// to v8.PersistentObject (the "identity_map").
|
|
// Finally, if this is the first time we've seen this instance, we need to:
|
|
// 1 - get the FunctionTemplate (from our templates slice)
|
|
// 2 - Create the TaggedAnyOpaque so that, if needed, we can do the reverse
|
|
// (i.e. js -> zig)
|
|
// 3 - Create a v8.PersistentObject (because Zig owns this object, not v8)
|
|
// 4 - Store our TaggedAnyOpaque into the persistent object
|
|
// 5 - Update our identity_map (so that, if we return this same instance again,
|
|
// we can just grab it from the identity_map)
|
|
fn _mapZigInstanceToJs(self: *JsContext, js_obj_or_template: anytype, value: anytype) !PersistentObject {
|
|
const v8_context = self.v8_context;
|
|
const context_arena = self.context_arena;
|
|
|
|
const T = @TypeOf(value);
|
|
switch (@typeInfo(T)) {
|
|
.@"struct" => {
|
|
// Struct, has to be placed on the heap
|
|
const heap = try context_arena.create(T);
|
|
heap.* = value;
|
|
return self._mapZigInstanceToJs(js_obj_or_template, heap);
|
|
},
|
|
.pointer => |ptr| {
|
|
const gop = try self.identity_map.getOrPut(context_arena, @intFromPtr(value));
|
|
if (gop.found_existing) {
|
|
// we've seen this instance before, return the same
|
|
// PersistentObject.
|
|
return gop.value_ptr.*;
|
|
}
|
|
|
|
if (comptime @hasDecl(ptr.child, "destructor")) {
|
|
try self.destructor_callbacks.append(context_arena, DestructorCallback.init(value));
|
|
}
|
|
|
|
if (comptime @hasDecl(ptr.child, "jsCallScopeEnd")) {
|
|
try self.call_scope_end_callbacks.append(context_arena, CallScopeEndCallback.init(value));
|
|
}
|
|
|
|
// Sometimes we're creating a new v8.Object, like when
|
|
// we're returning a value from a function. In those cases
|
|
// we have the FunctionTemplate, and we can get an object
|
|
// by calling initInstance its InstanceTemplate.
|
|
// Sometimes though we already have the v8.Objct to bind to
|
|
// for example, when we're executing a constructor, v8 has
|
|
// already created the "this" object.
|
|
const js_obj = switch (@TypeOf(js_obj_or_template)) {
|
|
v8.Object => js_obj_or_template,
|
|
v8.FunctionTemplate => js_obj_or_template.getInstanceTemplate().initInstance(v8_context),
|
|
else => @compileError("mapZigInstanceToJs requires a v8.Object (constructors) or v8.FunctionTemplate, got: " ++ @typeName(@TypeOf(js_obj_or_template))),
|
|
};
|
|
|
|
const isolate = self.isolate;
|
|
|
|
if (isEmpty(ptr.child) == false) {
|
|
// The TAO contains the pointer ot our Zig instance as
|
|
// well as any meta data we'll need to use it later.
|
|
// See the TaggedAnyOpaque struct for more details.
|
|
const tao = try context_arena.create(TaggedAnyOpaque);
|
|
const meta = @field(TYPE_LOOKUP, @typeName(ptr.child));
|
|
tao.* = .{
|
|
.ptr = value,
|
|
.index = meta.index,
|
|
.subtype = meta.subtype,
|
|
.offset = if (@typeInfo(ptr.child) != .@"opaque" and @hasField(ptr.child, "proto")) @offsetOf(ptr.child, "proto") else -1,
|
|
};
|
|
|
|
js_obj.setInternalField(0, v8.External.init(isolate, tao));
|
|
} else {
|
|
// If the struct is empty, we don't need to do all
|
|
// the TOA stuff and setting the internal data.
|
|
// When we try to map this from JS->Zig, in
|
|
// typeTaggedAnyOpaque, we'll also know there that
|
|
// the type is empty and can create an empty instance.
|
|
}
|
|
|
|
// Do not move this _AFTER_ the postAttach code.
|
|
// postAttach is likely to call back into this function
|
|
// mutating our identity_map, and making the gop pointers
|
|
// invalid.
|
|
const js_persistent = PersistentObject.init(isolate, js_obj);
|
|
gop.value_ptr.* = js_persistent;
|
|
|
|
if (@hasDecl(ptr.child, "postAttach")) {
|
|
const obj_wrap = JsThis{ .obj = .{ .js_obj = js_obj, .js_context = self } };
|
|
switch (@typeInfo(@TypeOf(ptr.child.postAttach)).@"fn".params.len) {
|
|
2 => try value.postAttach(obj_wrap),
|
|
3 => try value.postAttach(self.state, obj_wrap),
|
|
else => @compileError(@typeName(ptr.child) ++ ".postAttach must take 2 or 3 parameters"),
|
|
}
|
|
}
|
|
|
|
return js_persistent;
|
|
},
|
|
else => @compileError("Expected a struct or pointer, got " ++ @typeName(T) ++ " (constructors must return struct or pointers)"),
|
|
}
|
|
}
|
|
|
|
fn jsValueToZig(self: *JsContext, comptime named_function: NamedFunction, comptime T: type, js_value: v8.Value) !T {
|
|
switch (@typeInfo(T)) {
|
|
.optional => |o| {
|
|
if (js_value.isNullOrUndefined()) {
|
|
return null;
|
|
}
|
|
return try self.jsValueToZig(named_function, o.child, js_value);
|
|
},
|
|
.float => |f| switch (f.bits) {
|
|
0...32 => return js_value.toF32(self.v8_context),
|
|
33...64 => return js_value.toF64(self.v8_context),
|
|
else => {},
|
|
},
|
|
.int => return jsIntToZig(T, js_value, self.v8_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 js_obj = js_value.castTo(v8.Object);
|
|
return typeTaggedAnyOpaque(named_function, *Receiver(ptr.child), js_obj);
|
|
}
|
|
},
|
|
.slice => {
|
|
if (js_value.isTypedArray()) {
|
|
const buffer_view = js_value.castTo(v8.ArrayBufferView);
|
|
const buffer = buffer_view.getBuffer();
|
|
const backing_store = v8.BackingStore.sharedPtrGet(&buffer.getBackingStore());
|
|
const data = backing_store.getData();
|
|
const byte_len = backing_store.getByteLength();
|
|
|
|
switch (ptr.child) {
|
|
u8 => {
|
|
// need this sentinel check to keep the compiler happy
|
|
if (ptr.sentinel() == null) {
|
|
if (js_value.isUint8Array() or js_value.isUint8ClampedArray()) {
|
|
const arr_ptr = @as([*]u8, @alignCast(@ptrCast(data)));
|
|
return arr_ptr[0..byte_len];
|
|
}
|
|
}
|
|
},
|
|
i8 => {
|
|
if (js_value.isInt8Array()) {
|
|
const arr_ptr = @as([*]i8, @alignCast(@ptrCast(data)));
|
|
return arr_ptr[0..byte_len];
|
|
}
|
|
},
|
|
u16 => {
|
|
if (js_value.isUint16Array()) {
|
|
const arr_ptr = @as([*]u16, @alignCast(@ptrCast(data)));
|
|
return arr_ptr[0 .. byte_len / 2];
|
|
}
|
|
},
|
|
i16 => {
|
|
if (js_value.isInt16Array()) {
|
|
const arr_ptr = @as([*]i16, @alignCast(@ptrCast(data)));
|
|
return arr_ptr[0 .. byte_len / 2];
|
|
}
|
|
},
|
|
u32 => {
|
|
if (js_value.isUint32Array()) {
|
|
const arr_ptr = @as([*]u32, @alignCast(@ptrCast(data)));
|
|
return arr_ptr[0 .. byte_len / 4];
|
|
}
|
|
},
|
|
i32 => {
|
|
if (js_value.isInt32Array()) {
|
|
const arr_ptr = @as([*]i32, @alignCast(@ptrCast(data)));
|
|
return arr_ptr[0 .. byte_len / 4];
|
|
}
|
|
},
|
|
u64 => {
|
|
if (js_value.isBigUint64Array()) {
|
|
const arr_ptr = @as([*]u64, @alignCast(@ptrCast(data)));
|
|
return arr_ptr[0 .. byte_len / 8];
|
|
}
|
|
},
|
|
i64 => {
|
|
if (js_value.isBigInt64Array()) {
|
|
const arr_ptr = @as([*]i64, @alignCast(@ptrCast(data)));
|
|
return arr_ptr[0 .. byte_len / 8];
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
return error.InvalidArgument;
|
|
}
|
|
|
|
if (ptr.child == u8) {
|
|
if (ptr.sentinel()) |s| {
|
|
if (comptime s == 0) {
|
|
return valueToStringZ(self.call_arena, js_value, self.isolate, self.v8_context);
|
|
}
|
|
} else {
|
|
return valueToString(self.call_arena, js_value, self.isolate, self.v8_context);
|
|
}
|
|
}
|
|
|
|
if (!js_value.isArray()) {
|
|
return error.InvalidArgument;
|
|
}
|
|
|
|
const v8_context = self.v8_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_arena.alloc(ptr.child, js_arr.length());
|
|
for (arr, 0..) |*a, i| {
|
|
a.* = try self.jsValueToZig(named_function, ptr.child, try js_obj.getAtIndex(v8_context, @intCast(i)));
|
|
}
|
|
return arr;
|
|
},
|
|
else => {},
|
|
},
|
|
.@"struct" => {
|
|
return try (self.jsValueToStruct(named_function, T, js_value)) orelse {
|
|
return error.InvalidArgument;
|
|
};
|
|
},
|
|
.@"union" => |u| {
|
|
// see probeJsValueToZig for some explanation of what we're
|
|
// trying to do
|
|
|
|
// the first field that we find which the js_value could be
|
|
// coerced to.
|
|
var coerce_index: ?usize = null;
|
|
|
|
// the first field that we find which the js_value is
|
|
// compatible with. A compatible field has higher precedence
|
|
// than a coercible, but still isn't a perfect match.
|
|
var compatible_index: ?usize = null;
|
|
inline for (u.fields, 0..) |field, i| {
|
|
switch (try self.probeJsValueToZig(named_function, field.type, js_value)) {
|
|
.value => |v| return @unionInit(T, field.name, v),
|
|
.ok => {
|
|
// a perfect match like above case, except the probing
|
|
// didn't get the value for us.
|
|
return @unionInit(T, field.name, try self.jsValueToZig(named_function, field.type, js_value));
|
|
},
|
|
.coerce => if (coerce_index == null) {
|
|
coerce_index = i;
|
|
},
|
|
.compatible => if (compatible_index == null) {
|
|
compatible_index = i;
|
|
},
|
|
.invalid => {},
|
|
}
|
|
}
|
|
|
|
// We didn't find a perfect match.
|
|
const closest = compatible_index orelse coerce_index orelse return error.InvalidArgument;
|
|
inline for (u.fields, 0..) |field, i| {
|
|
if (i == closest) {
|
|
return @unionInit(T, field.name, try self.jsValueToZig(named_function, field.type, js_value));
|
|
}
|
|
}
|
|
unreachable;
|
|
},
|
|
.@"enum" => |e| {
|
|
switch (@typeInfo(e.tag_type)) {
|
|
.int => return std.meta.intToEnum(T, try jsIntToZig(e.tag_type, js_value, self.v8_context)),
|
|
else => @compileError(named_function.full_name ++ " has an unsupported enum parameter type: " ++ @typeName(T)),
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
|
|
@compileError(named_function.full_name ++ " has an unsupported parameter type: " ++ @typeName(T));
|
|
}
|
|
|
|
// Extracted so that it can be used in both jsValueToZig and in
|
|
// probeJsValueToZig. Avoids having to duplicate this logic when probing.
|
|
fn jsValueToStruct(self: *JsContext, comptime named_function: NamedFunction, comptime T: type, js_value: v8.Value) !?T {
|
|
if (@hasDecl(T, "_FUNCTION_ID_KLUDGE")) {
|
|
if (!js_value.isFunction()) {
|
|
return null;
|
|
}
|
|
return try self.createFunction(js_value);
|
|
}
|
|
|
|
const js_obj = js_value.castTo(v8.Object);
|
|
|
|
if (comptime isJsObject(T)) {
|
|
// Caller wants an opaque JsObject. Probably a parameter
|
|
// that it needs to pass back into a callback
|
|
return JsObject{
|
|
.js_obj = js_obj,
|
|
.js_context = self,
|
|
};
|
|
}
|
|
|
|
if (!js_value.isObject()) {
|
|
return null;
|
|
}
|
|
|
|
const v8_context = self.v8_context;
|
|
const isolate = self.isolate;
|
|
|
|
var value: T = undefined;
|
|
inline for (@typeInfo(T).@"struct".fields) |field| {
|
|
const name = field.name;
|
|
const key = v8.String.initUtf8(isolate, name);
|
|
if (js_obj.has(v8_context, key.toValue())) {
|
|
@field(value, name) = try self.jsValueToZig(named_function, field.type, try js_obj.getValue(v8_context, key));
|
|
} else if (@typeInfo(field.type) == .optional) {
|
|
@field(value, name) = null;
|
|
} else {
|
|
const dflt = field.defaultValue() orelse return null;
|
|
@field(value, name) = dflt;
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
fn createFunction(self: *JsContext, js_value: v8.Value) !Function {
|
|
// caller should have made sure this was a function
|
|
std.debug.assert(js_value.isFunction());
|
|
|
|
const func = v8.Persistent(v8.Function).init(self.isolate, js_value.castTo(v8.Function));
|
|
try self.trackCallback(func);
|
|
|
|
return .{
|
|
.func = func,
|
|
.js_context = self,
|
|
.id = js_value.castTo(v8.Object).getIdentityHash(),
|
|
};
|
|
}
|
|
|
|
// Probing is part of trying to map a JS value to a Zig union. There's
|
|
// a lot of ambiguity in this process, in part because some JS values
|
|
// can almost always be coerced. For example, anything can be coerced
|
|
// into an integer (it just becomes 0), or a float (becomes NaN) or a
|
|
// string.
|
|
//
|
|
// The way we'll do this is that, if there's a direct match, we'll use it
|
|
// If there's a potential match, we'll keep looking for a direct match
|
|
// and only use the (first) potential match as a fallback.
|
|
//
|
|
// Finally, I considered adding this probing directly into jsValueToZig
|
|
// but I decided doing this separately was better. However, the goal is
|
|
// obviously that probing is consistent with jsValueToZig.
|
|
fn ProbeResult(comptime T: type) type {
|
|
return union(enum) {
|
|
// The js_value maps directly to T
|
|
value: T,
|
|
|
|
// The value is a T. This is almost the same as returning value: T,
|
|
// but the caller still has to get T by calling jsValueToZig.
|
|
// We prefer returning .{.ok => {}}, to avoid reducing duplication
|
|
// with jsValueToZig, but in some cases where probing has a cost
|
|
// AND yields the value anyways, we'll use .{.value = T}.
|
|
ok: void,
|
|
|
|
// the js_value is compatible with T (i.e. a int -> float),
|
|
compatible: void,
|
|
|
|
// the js_value can be coerced to T (this is a lower precedence
|
|
// than compatible)
|
|
coerce: void,
|
|
|
|
// the js_value cannot be turned into T
|
|
invalid: void,
|
|
};
|
|
}
|
|
fn probeJsValueToZig(self: *JsContext, comptime named_function: NamedFunction, comptime T: type, js_value: v8.Value) !ProbeResult(T) {
|
|
switch (@typeInfo(T)) {
|
|
.optional => |o| {
|
|
if (js_value.isNullOrUndefined()) {
|
|
return .{ .value = null };
|
|
}
|
|
return self.probeJsValueToZig(named_function, o.child, js_value);
|
|
},
|
|
.float => {
|
|
if (js_value.isNumber() or js_value.isNumberObject()) {
|
|
if (js_value.isInt32() or js_value.isUint32() or js_value.isBigInt() or js_value.isBigIntObject()) {
|
|
// int => float is a reasonable match
|
|
return .{ .compatible = {} };
|
|
}
|
|
return .{ .ok = {} };
|
|
}
|
|
// anything can be coerced into a float, it becomes NaN
|
|
return .{ .coerce = {} };
|
|
},
|
|
.int => {
|
|
if (js_value.isNumber() or js_value.isNumberObject()) {
|
|
if (js_value.isInt32() or js_value.isUint32() or js_value.isBigInt() or js_value.isBigIntObject()) {
|
|
return .{ .ok = {} };
|
|
}
|
|
// float => int is kind of reasonable, I guess
|
|
return .{ .compatible = {} };
|
|
}
|
|
// anything can be coerced into a int, it becomes 0
|
|
return .{ .coerce = {} };
|
|
},
|
|
.bool => {
|
|
if (js_value.isBoolean() or js_value.isBooleanObject()) {
|
|
return .{ .ok = {} };
|
|
}
|
|
// anything can be coerced into a boolean, it will become
|
|
// true or false based on..some complex rules I don't know.
|
|
return .{ .coerce = {} };
|
|
},
|
|
.pointer => |ptr| switch (ptr.size) {
|
|
.one => {
|
|
if (!js_value.isObject()) {
|
|
return .{ .invalid = {} };
|
|
}
|
|
if (@hasField(TypeLookup, @typeName(ptr.child))) {
|
|
const js_obj = js_value.castTo(v8.Object);
|
|
// There's a bit of overhead in doing this, so instead
|
|
// of having a version of typeTaggedAnyOpaque which
|
|
// returns a boolean or an optional, we rely on the
|
|
// main implementation and just handle the error.
|
|
const attempt = typeTaggedAnyOpaque(named_function, *Receiver(ptr.child), js_obj);
|
|
if (attempt) |value| {
|
|
return .{ .value = value };
|
|
} else |_| {
|
|
return .{ .invalid = {} };
|
|
}
|
|
}
|
|
// probably an error, but not for us to deal with
|
|
return .{ .invalid = {} };
|
|
},
|
|
.slice => {
|
|
if (js_value.isTypedArray()) {
|
|
switch (ptr.child) {
|
|
u8 => if (ptr.sentinel() == null) {
|
|
if (js_value.isUint8Array() or js_value.isUint8ClampedArray()) {
|
|
return .{ .ok = {} };
|
|
}
|
|
},
|
|
i8 => if (js_value.isInt8Array()) {
|
|
return .{ .ok = {} };
|
|
},
|
|
u16 => if (js_value.isUint16Array()) {
|
|
return .{ .ok = {} };
|
|
},
|
|
i16 => if (js_value.isInt16Array()) {
|
|
return .{ .ok = {} };
|
|
},
|
|
u32 => if (js_value.isUint32Array()) {
|
|
return .{ .ok = {} };
|
|
},
|
|
i32 => if (js_value.isInt32Array()) {
|
|
return .{ .ok = {} };
|
|
},
|
|
u64 => if (js_value.isBigUint64Array()) {
|
|
return .{ .ok = {} };
|
|
},
|
|
i64 => if (js_value.isBigInt64Array()) {
|
|
return .{ .ok = {} };
|
|
},
|
|
else => {},
|
|
}
|
|
return .{ .invalid = {} };
|
|
}
|
|
|
|
if (ptr.child == u8) {
|
|
if (js_value.isString()) {
|
|
return .{ .ok = {} };
|
|
}
|
|
// anything can be coerced into a string
|
|
return .{ .coerce = {} };
|
|
}
|
|
|
|
if (!js_value.isArray()) {
|
|
return .{ .invalid = {} };
|
|
}
|
|
|
|
// This can get tricky.
|
|
const js_arr = js_value.castTo(v8.Array);
|
|
|
|
if (js_arr.length() == 0) {
|
|
// not so tricky in this case.
|
|
return .{ .value = &.{} };
|
|
}
|
|
|
|
// We settle for just probing the first value. Ok, actually
|
|
// not tricky in this case either.
|
|
const v8_context = self.v8_context;
|
|
const js_obj = js_arr.castTo(v8.Object);
|
|
switch (try self.probeJsValueToZig(named_function, ptr.child, try js_obj.getAtIndex(v8_context, 0))) {
|
|
.value, .ok => return .{ .ok = {} },
|
|
.compatible => return .{ .compatible = {} },
|
|
.coerce => return .{ .coerce = {} },
|
|
.invalid => return .{ .invalid = {} },
|
|
}
|
|
},
|
|
else => {},
|
|
},
|
|
.@"struct" => {
|
|
// We don't want to duplicate the code for this, so we call
|
|
// the actual coversion function.
|
|
const value = (try self.jsValueToStruct(named_function, T, js_value)) orelse {
|
|
return .{ .invalid = {} };
|
|
};
|
|
return .{ .value = value };
|
|
},
|
|
else => {},
|
|
}
|
|
|
|
return .{ .invalid = {} };
|
|
}
|
|
|
|
// Callback from V8, asking us to load a module. The "specifier" is
|
|
// the src of the module to load.
|
|
fn resolveModuleCallback(
|
|
c_context: ?*const v8.C_Context,
|
|
c_specifier: ?*const v8.C_String,
|
|
import_attributes: ?*const v8.C_FixedArray,
|
|
c_referrer: ?*const v8.C_Module,
|
|
) callconv(.C) ?*const v8.C_Module {
|
|
_ = import_attributes;
|
|
|
|
const v8_context = v8.Context{ .handle = c_context.? };
|
|
const self: *JsContext = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
|
|
|
|
const specifier = jsStringToZig(self.call_arena, .{ .handle = c_specifier.? }, self.isolate) catch |err| {
|
|
log.err(.js, "resolve module", .{ .err = err });
|
|
return null;
|
|
};
|
|
const referrer = v8.Module{ .handle = c_referrer.? };
|
|
|
|
return self._resolveModuleCallback(referrer, specifier) catch |err| {
|
|
log.err(.js, "resolve module", .{
|
|
.err = err,
|
|
.specifier = specifier,
|
|
});
|
|
return null;
|
|
};
|
|
}
|
|
|
|
fn _resolveModuleCallback(
|
|
self: *JsContext,
|
|
referrer: v8.Module,
|
|
specifier: []const u8,
|
|
) !?*const v8.C_Module {
|
|
const referrer_path = self.module_identifier.get(referrer.getIdentityHash()) orelse {
|
|
// Shouldn't be possible.
|
|
return error.UnknownModuleReferrer;
|
|
};
|
|
|
|
const normalized_specifier = try @import("../url.zig").stitch(
|
|
self.call_arena,
|
|
specifier,
|
|
referrer_path,
|
|
.{ .alloc = .if_needed },
|
|
);
|
|
|
|
if (self.module_cache.get(normalized_specifier)) |pm| {
|
|
return pm.handle;
|
|
}
|
|
|
|
const module_loader = self.module_loader;
|
|
const source = try module_loader.func(module_loader.ptr, normalized_specifier) orelse return null;
|
|
|
|
var try_catch: TryCatch = undefined;
|
|
try_catch.init(self);
|
|
defer try_catch.deinit();
|
|
|
|
const m = compileModule(self.isolate, source, specifier) catch |err| {
|
|
log.warn(.js, "compile resolved module", .{
|
|
.specifier = specifier,
|
|
.stack = try_catch.stack(self.call_arena) catch null,
|
|
.src = try_catch.sourceLine(self.call_arena) catch "err",
|
|
.line = try_catch.sourceLineNumber() orelse 0,
|
|
.exception = (try_catch.exception(self.call_arena) catch @errorName(err)) orelse @errorName(err),
|
|
});
|
|
return null;
|
|
};
|
|
|
|
// We were hoping to find the module in our cache, and thus used
|
|
// the short-lived call_arena to create the normalized_specifier.
|
|
// But now this'll live for the lifetime of the context.
|
|
const arena = self.context_arena;
|
|
const owned_specifier = try arena.dupe(u8, normalized_specifier);
|
|
try self.module_cache.put(arena, owned_specifier, PersistentModule.init(self.isolate, m));
|
|
try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_specifier);
|
|
return m.handle;
|
|
}
|
|
};
|
|
|
|
pub const Function = struct {
|
|
id: usize,
|
|
js_context: *JsContext,
|
|
this: ?v8.Object = null,
|
|
func: PersistentFunction,
|
|
|
|
// We use this when mapping a JS value to a Zig object. We can't
|
|
// Say we have a Zig function that takes a Function, we can't just
|
|
// check param.type == Function, because Function is a generic.
|
|
// So, as a quick hack, we can determine if the Zig type is a
|
|
// callback by checking @hasDecl(T, "_FUNCTION_ID_KLUDGE")
|
|
const _FUNCTION_ID_KLUDGE = true;
|
|
|
|
pub const Result = struct {
|
|
stack: ?[]const u8,
|
|
exception: []const u8,
|
|
};
|
|
|
|
pub fn getName(self: *const Function, allocator: Allocator) ![]const u8 {
|
|
const name = self.func.castToFunction().getName();
|
|
return valueToString(allocator, name, self.js_context.isolate, self.js_context.v8_context);
|
|
}
|
|
|
|
pub fn setName(self: *const Function, name: []const u8) void {
|
|
const v8_name = v8.String.initUtf8(self.js_context.isolate, name);
|
|
self.func.castToFunction().setName(v8_name);
|
|
}
|
|
|
|
pub fn withThis(self: *const Function, value: anytype) !Function {
|
|
const this_obj = if (@TypeOf(value) == JsObject)
|
|
value.js_obj
|
|
else
|
|
try self.js_context.valueToExistingObject(value);
|
|
|
|
return .{
|
|
.id = self.id,
|
|
.this = this_obj,
|
|
.func = self.func,
|
|
.js_context = self.js_context,
|
|
};
|
|
}
|
|
|
|
pub fn newInstance(self: *const Function, result: *Result) !JsObject {
|
|
const context = self.js_context;
|
|
|
|
var try_catch: TryCatch = undefined;
|
|
try_catch.init(context);
|
|
defer try_catch.deinit();
|
|
|
|
// This creates a new instance using this Function as a constructor.
|
|
// This returns a generic Object
|
|
const js_obj = self.func.castToFunction().initInstance(context.v8_context, &.{}) orelse {
|
|
if (try_catch.hasCaught()) {
|
|
const allocator = context.call_arena;
|
|
result.stack = try_catch.stack(allocator) catch null;
|
|
result.exception = (try_catch.exception(allocator) catch "???") orelse "???";
|
|
} else {
|
|
result.stack = null;
|
|
result.exception = "???";
|
|
}
|
|
return error.JsConstructorFailed;
|
|
};
|
|
|
|
return .{
|
|
.js_context = context,
|
|
.js_obj = js_obj,
|
|
};
|
|
}
|
|
|
|
pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
|
|
return self.callWithThis(T, self.getThis(), args);
|
|
}
|
|
|
|
pub fn tryCall(self: *const Function, comptime T: type, args: anytype, result: *Result) !T {
|
|
return self.tryCallWithThis(T, self.getThis(), args, result);
|
|
}
|
|
|
|
pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, result: *Result) !T {
|
|
var try_catch: TryCatch = undefined;
|
|
try_catch.init(self.js_context);
|
|
defer try_catch.deinit();
|
|
|
|
return self.callWithThis(T, this, args) catch |err| {
|
|
if (try_catch.hasCaught()) {
|
|
const allocator = self.js_context.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;
|
|
};
|
|
}
|
|
|
|
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
|
|
const js_context = self.js_context;
|
|
|
|
const js_this = try js_context.valueToExistingObject(this);
|
|
|
|
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 js_context.zigValueToJs(@field(aargs, f.name));
|
|
}
|
|
|
|
const result = self.func.castToFunction().call(js_context.v8_context, js_this, &js_args);
|
|
if (result == null) {
|
|
return error.JSExecCallback;
|
|
}
|
|
|
|
if (@typeInfo(T) == .void) return {};
|
|
const named_function = comptime NamedFunction.init(T, "callResult");
|
|
return js_context.jsValueToZig(named_function, T, result.?);
|
|
}
|
|
|
|
fn getThis(self: *const Function) v8.Object {
|
|
return self.this orelse self.js_context.v8_context.getGlobal();
|
|
}
|
|
|
|
// debug/helper to print the source of the JS callback
|
|
pub fn printFunc(self: Function) !void {
|
|
const js_context = self.js_context;
|
|
const value = self.func.castToFunction().toValue();
|
|
const src = try valueToString(js_context.call_arena, value, js_context.isolate, js_context.v8_context);
|
|
std.debug.print("{s}\n", .{src});
|
|
}
|
|
};
|
|
|
|
pub const JsObject = struct {
|
|
js_context: *JsContext,
|
|
js_obj: v8.Object,
|
|
|
|
// If a Zig struct wants the JsObject parameter, it'll declare a
|
|
// function like:
|
|
// fn _length(self: *const NodeList, js_obj: Env.JsObject) usize
|
|
//
|
|
// When we're trying to call this function, we can't just do
|
|
// if (params[i].type.? == JsObject)
|
|
// Because there is _no_ JsObject, there's only an Env.JsObject, where
|
|
// Env is a generic.
|
|
// We could probably figure out a way to do this, but simply checking
|
|
// for this declaration is _a lot_ easier.
|
|
const _JSOBJECT_ID_KLUDGE = true;
|
|
|
|
const SetOpts = packed struct(u32) {
|
|
READ_ONLY: bool = false,
|
|
DONT_ENUM: bool = false,
|
|
DONT_DELETE: bool = false,
|
|
_: u29 = 0,
|
|
};
|
|
pub fn setIndex(self: JsObject, index: u32, value: anytype, opts: SetOpts) !void {
|
|
const key = switch (index) {
|
|
inline 0...1000 => |i| std.fmt.comptimePrint("{d}", .{i}),
|
|
else => try std.fmt.allocPrint(self.js_context.context_arena, "{d}", .{index}),
|
|
};
|
|
return self.set(key, value, opts);
|
|
}
|
|
|
|
pub fn set(self: JsObject, key: []const u8, value: anytype, opts: SetOpts) !void {
|
|
const js_context = self.js_context;
|
|
|
|
const js_key = v8.String.initUtf8(js_context.isolate, key);
|
|
const js_value = try js_context.zigValueToJs(value);
|
|
|
|
const res = self.js_obj.defineOwnProperty(js_context.v8_context, js_key.toName(), js_value, @bitCast(opts)) orelse false;
|
|
if (!res) {
|
|
return error.FailedToSet;
|
|
}
|
|
}
|
|
|
|
pub fn get(self: JsObject, key: []const u8) !Value {
|
|
const js_context = self.js_context;
|
|
const js_key = v8.String.initUtf8(js_context.isolate, key);
|
|
const js_val = try self.js_obj.getValue(js_context.v8_context, js_key);
|
|
return js_context.createValue(js_val);
|
|
}
|
|
|
|
pub fn isTruthy(self: JsObject) bool {
|
|
const js_value = self.js_obj.toValue();
|
|
return js_value.toBool(self.js_context.isolate);
|
|
}
|
|
|
|
pub fn toString(self: JsObject) ![]const u8 {
|
|
const js_context = self.js_context;
|
|
const js_value = self.js_obj.toValue();
|
|
return valueToString(js_context.call_arena, js_value, js_context.isolate, js_context.v8_context);
|
|
}
|
|
|
|
pub fn toDetailString(self: JsObject) ![]const u8 {
|
|
const js_context = self.js_context;
|
|
const js_value = self.js_obj.toValue();
|
|
return valueToDetailString(js_context.call_arena, js_value, js_context.isolate, js_context.v8_context);
|
|
}
|
|
|
|
pub fn format(self: JsObject, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
|
|
return writer.writeAll(try self.toString());
|
|
}
|
|
|
|
pub fn persist(self: JsObject) !JsObject {
|
|
var js_context = self.js_context;
|
|
const js_obj = self.js_obj;
|
|
const handle = js_obj.handle;
|
|
|
|
const gop = try js_context.js_object_map.getOrPut(js_context.context_arena, @intFromPtr(handle));
|
|
if (gop.found_existing == false) {
|
|
gop.value_ptr.* = PersistentObject.init(js_context.isolate, js_obj);
|
|
}
|
|
|
|
return .{
|
|
.js_context = js_context,
|
|
.js_obj = gop.value_ptr.castToObject(),
|
|
};
|
|
}
|
|
|
|
pub fn getFunction(self: JsObject, name: []const u8) !?Function {
|
|
if (self.isNullOrUndefined()) {
|
|
return null;
|
|
}
|
|
const js_context = self.js_context;
|
|
|
|
const js_name = v8.String.initUtf8(js_context.isolate, name);
|
|
|
|
const js_value = try self.js_obj.getValue(js_context.v8_context, js_name.toName());
|
|
if (!js_value.isFunction()) {
|
|
return null;
|
|
}
|
|
return try js_context.createFunction(js_value);
|
|
}
|
|
|
|
pub fn isNullOrUndefined(self: JsObject) bool {
|
|
return self.js_obj.toValue().isNullOrUndefined();
|
|
}
|
|
|
|
pub fn nameIterator(self: JsObject) ValueIterator {
|
|
const js_context = self.js_context;
|
|
const js_obj = self.js_obj;
|
|
|
|
const array = js_obj.getPropertyNames(js_context.v8_context);
|
|
const count = array.length();
|
|
|
|
return .{
|
|
.count = count,
|
|
.js_context = js_context,
|
|
.js_obj = array.castTo(v8.Object),
|
|
};
|
|
}
|
|
|
|
pub fn constructorName(self: JsObject, allocator: Allocator) ![]const u8 {
|
|
const str = try self.js_obj.getConstructorName();
|
|
return jsStringToZig(allocator, str, self.js_context.isolate);
|
|
}
|
|
|
|
pub fn toZig(self: JsObject, comptime Struct: type, comptime name: []const u8, comptime T: type) !T {
|
|
const named_function = comptime NamedFunction.init(Struct, name);
|
|
return self.js_context.jsValueToZig(named_function, T, self.js_obj.toValue());
|
|
}
|
|
};
|
|
|
|
// This only exists so that we know whether a function wants the opaque
|
|
// JS argument (JsObject), or if it wants the receiver as an opaque
|
|
// value.
|
|
// JsObject is normally used when a method wants an opaque JS object
|
|
// that it'll pass into a callback.
|
|
// JsThis is used when the function wants to do advanced manipulation
|
|
// of the v8.Object bound to the instance. For example, postAttach is an
|
|
// example of using JsThis.
|
|
pub const JsThis = struct {
|
|
obj: JsObject,
|
|
|
|
const _JSTHIS_ID_KLUDGE = true;
|
|
|
|
pub fn setIndex(self: JsThis, index: u32, value: anytype, opts: JsObject.SetOpts) !void {
|
|
return self.obj.setIndex(index, value, opts);
|
|
}
|
|
|
|
pub fn set(self: JsThis, key: []const u8, value: anytype, opts: JsObject.SetOpts) !void {
|
|
return self.obj.set(key, value, opts);
|
|
}
|
|
|
|
pub fn constructorName(self: JsThis, allocator: Allocator) ![]const u8 {
|
|
return try self.obj.constructorName(allocator);
|
|
}
|
|
};
|
|
|
|
pub const TryCatch = struct {
|
|
inner: v8.TryCatch,
|
|
js_context: *const JsContext,
|
|
|
|
pub fn init(self: *TryCatch, js_context: *const JsContext) void {
|
|
self.js_context = js_context;
|
|
self.inner.init(js_context.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 js_context = self.js_context;
|
|
return try valueToString(allocator, msg, js_context.isolate, js_context.v8_context);
|
|
}
|
|
|
|
// the caller needs to deinit the string returned
|
|
pub fn stack(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
|
const js_context = self.js_context;
|
|
const s = self.inner.getStackTrace(js_context.v8_context) orelse return null;
|
|
return try valueToString(allocator, s, js_context.isolate, js_context.v8_context);
|
|
}
|
|
|
|
// the caller needs to deinit the string returned
|
|
pub fn sourceLine(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
|
const js_context = self.js_context;
|
|
const msg = self.inner.getMessage() orelse return null;
|
|
const sl = msg.getSourceLine(js_context.v8_context) orelse return null;
|
|
return try jsStringToZig(allocator, sl, js_context.isolate);
|
|
}
|
|
|
|
pub fn sourceLineNumber(self: TryCatch) ?u32 {
|
|
const js_context = self.js_context;
|
|
const msg = self.inner.getMessage() orelse return null;
|
|
return msg.getLineNumber(js_context.v8_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();
|
|
}
|
|
};
|
|
|
|
// If a function returns a []i32, should that map to a plain-old
|
|
// JavaScript array, or a Int32Array? It's ambiguous. By default, we'll
|
|
// map arrays/slices to the JavaScript arrays. If you want a TypedArray
|
|
// wrap it in this.
|
|
// Also, this type has nothing to do with the Env. But we place it here
|
|
// for consistency. Want a callback? Env.Callback. Want a JsObject?
|
|
// Env.JsObject. Want a TypedArray? Env.TypedArray.
|
|
pub fn TypedArray(comptime T: type) type {
|
|
return struct {
|
|
// See Function._FUNCTION_ID_KLUDGE
|
|
const _TYPED_ARRAY_ID_KLUDGE = true;
|
|
|
|
values: []const T,
|
|
};
|
|
}
|
|
|
|
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, isolate: v8.Isolate, 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 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 {
|
|
// Can't assume the main Context exists (with its HandleScope)
|
|
// available when doing this. Pages (and thus the HandleScope)
|
|
// comes and goes, but CDP can keep sending messages.
|
|
const isolate = self.isolate;
|
|
var temp_scope: v8.HandleScope = undefined;
|
|
v8.HandleScope.init(&temp_scope, isolate);
|
|
defer temp_scope.deinit();
|
|
|
|
self.session.dispatchProtocolMessage(isolate, msg);
|
|
}
|
|
|
|
// From CDP docs
|
|
// https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-ExecutionContextDescription
|
|
// ----
|
|
// - name: Human readable name describing given context.
|
|
// - origin: Execution context origin (ie. URL who initialised the request)
|
|
// - auxData: Embedder-specific auxiliary data likely matching
|
|
// {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}
|
|
// - is_default_context: Whether the execution context is default, should match the auxData
|
|
pub fn contextCreated(
|
|
self: *const Inspector,
|
|
js_context: *const JsContext,
|
|
name: []const u8,
|
|
origin: []const u8,
|
|
aux_data: ?[]const u8,
|
|
is_default_context: bool,
|
|
) void {
|
|
self.inner.contextCreated(js_context.v8_context, name, origin, aux_data, is_default_context);
|
|
}
|
|
|
|
// Retrieves the RemoteObject for a given value.
|
|
// The value is loaded through the ExecutionWorld'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 context ends.
|
|
pub fn getRemoteObject(
|
|
self: *const Inspector,
|
|
js_context: *const JsContext,
|
|
group: []const u8,
|
|
value: anytype,
|
|
) !RemoteObject {
|
|
const js_value = try zigValueToJs(
|
|
js_context.templates,
|
|
js_context.isolate,
|
|
js_context.v8_context,
|
|
value,
|
|
);
|
|
|
|
// We do not want to expose this as a parameter for now
|
|
const generate_preview = false;
|
|
return self.session.wrapObject(
|
|
js_context.isolate,
|
|
js_context.v8_context,
|
|
js_value,
|
|
group,
|
|
generate_preview,
|
|
);
|
|
}
|
|
|
|
// Gets a value by object ID regardless of which context it is in.
|
|
pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []const u8) !?*anyopaque {
|
|
const unwrapped = try self.session.unwrapObject(allocator, object_id);
|
|
// The values context and groupId are not used here
|
|
const toa = getTaggedAnyOpaque(unwrapped.value) orelse return null;
|
|
if (toa.subtype == null or toa.subtype != .node) return error.ObjectIdIsNotANode;
|
|
return toa.ptr;
|
|
}
|
|
};
|
|
|
|
pub const RemoteObject = v8.RemoteObject;
|
|
|
|
pub const Exception = struct {
|
|
inner: v8.Value,
|
|
js_context: *const JsContext,
|
|
|
|
// the caller needs to deinit the string returned
|
|
pub fn exception(self: Exception, allocator: Allocator) ![]const u8 {
|
|
const js_context = self.js_context;
|
|
return try valueToString(allocator, self.inner, js_context.isolate, js_context.v8_context);
|
|
}
|
|
};
|
|
|
|
pub const Value = struct {
|
|
value: v8.Value,
|
|
js_context: *const JsContext,
|
|
|
|
// the caller needs to deinit the string returned
|
|
pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
|
|
const js_context = self.js_context;
|
|
return valueToString(allocator, self.value, js_context.isolate, js_context.v8_context);
|
|
}
|
|
};
|
|
|
|
pub const ValueIterator = struct {
|
|
count: u32,
|
|
idx: u32 = 0,
|
|
js_obj: v8.Object,
|
|
js_context: *const JsContext,
|
|
|
|
pub fn next(self: *ValueIterator) !?Value {
|
|
const idx = self.idx;
|
|
if (idx == self.count) {
|
|
return null;
|
|
}
|
|
self.idx += 1;
|
|
|
|
const js_context = self.js_context;
|
|
const js_val = try self.js_obj.getAtIndex(js_context.v8_context, idx);
|
|
return js_context.createValue(js_val);
|
|
}
|
|
};
|
|
|
|
pub fn UndefinedOr(comptime T: type) type {
|
|
return union(enum) {
|
|
value: T,
|
|
undefined: void,
|
|
};
|
|
}
|
|
|
|
fn compileModule(isolate: v8.Isolate, src: []const u8, name: []const u8) !v8.Module {
|
|
// compile
|
|
const script_name = v8.String.initUtf8(isolate, name);
|
|
const script_source = v8.String.initUtf8(isolate, src);
|
|
|
|
const origin = v8.ScriptOrigin.init(
|
|
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;
|
|
}
|
|
|
|
// Give it a Zig struct, get back a v8.FunctionTemplate.
|
|
// The FunctionTemplate is a bit like a struct container - it's where
|
|
// we'll attach functions/getters/setters and where we'll "inherit" a
|
|
// prototype type (if there is any)
|
|
fn generateClass(comptime Struct: type, isolate: v8.Isolate) v8.FunctionTemplate {
|
|
const template = generateConstructor(Struct, isolate);
|
|
attachClass(Struct, isolate, 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 createJsContext)
|
|
fn attachClass(comptime Struct: type, isolate: v8.Isolate, 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" => generateMethod(Struct, name, isolate, template_proto),
|
|
else => |ti| if (!comptime isComplexAttributeType(ti)) {
|
|
generateAttribute(Struct, name, isolate, template, template_proto);
|
|
},
|
|
}
|
|
} else if (comptime std.mem.startsWith(u8, name, "get_")) {
|
|
generateProperty(Struct, name[4..], isolate, template_proto);
|
|
} else if (comptime std.mem.startsWith(u8, name, "static_")) {
|
|
generateFunction(Struct, name[7..], isolate, 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 string_tag_callback = v8.FunctionTemplate.initCallback(isolate, struct {
|
|
fn stringTag(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
|
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
|
const class_name = v8.String.initUtf8(info.getIsolate(), comptime classNameForStruct(Struct));
|
|
info.getReturnValue().set(class_name);
|
|
}
|
|
}.stringTag);
|
|
const key = v8.Symbol.getToStringTag(isolate).toName();
|
|
template_proto.setAccessorGetter(key, string_tag_callback);
|
|
}
|
|
|
|
generateIndexer(Struct, template_proto);
|
|
generateNamedIndexer(Struct, template.getInstanceTemplate());
|
|
generateUndetectable(Struct, template.getInstanceTemplate());
|
|
}
|
|
|
|
// Even if a struct doesn't have a `constructor` function, we still
|
|
// `generateConstructor`, because this is how we create our
|
|
// FunctionTemplate. Such classes exist, but they can't be instantiated
|
|
// via `new ClassName()` - but they could, for example, be created in
|
|
// Zig and returned from a function call, which is why we need the
|
|
// FunctionTemplate.
|
|
fn generateConstructor(comptime Struct: type, isolate: v8.Isolate) v8.FunctionTemplate {
|
|
const template = v8.FunctionTemplate.initCallback(isolate, struct {
|
|
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
|
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
|
var caller = Caller(Self, State).init(info);
|
|
defer caller.deinit();
|
|
|
|
// See comment above. We generateConstructor on all types
|
|
// in order to create the FunctionTemplate, but there might
|
|
// not be an actual "constructor" function. So if someone
|
|
// does `new ClassName()` where ClassName doesn't have
|
|
// a constructor function, we'll return an error.
|
|
if (@hasDecl(Struct, "constructor") == false) {
|
|
const iso = caller.isolate;
|
|
const js_exception = iso.throwException(createException(iso, "Illegal Constructor"));
|
|
info.getReturnValue().set(js_exception);
|
|
return;
|
|
}
|
|
|
|
// Safe to call now, because if Struct.constructor didn't
|
|
// exist, the above if block would have returned.
|
|
const named_function = comptime NamedFunction.init(Struct, "constructor");
|
|
caller.constructor(Struct, named_function, info) catch |err| {
|
|
caller.handleError(Struct, named_function, err, info);
|
|
};
|
|
}
|
|
}.callback);
|
|
|
|
if (comptime isEmpty(Receiver(Struct)) == false) {
|
|
// If the struct is empty, we won't store a Zig reference inside
|
|
// the JS object, so we don't need to set the internal field count
|
|
template.getInstanceTemplate().setInternalFieldCount(1);
|
|
}
|
|
|
|
const class_name = v8.String.initUtf8(isolate, comptime classNameForStruct(Struct));
|
|
template.setClassName(class_name);
|
|
return template;
|
|
}
|
|
|
|
fn generateMethod(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, 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(isolate).toName();
|
|
} else {
|
|
js_name = v8.String.initUtf8(isolate, name[1..]).toName();
|
|
}
|
|
const function_template = v8.FunctionTemplate.initCallback(isolate, struct {
|
|
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
|
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
|
var caller = Caller(Self, State).init(info);
|
|
defer caller.deinit();
|
|
|
|
const named_function = comptime NamedFunction.init(Struct, name);
|
|
caller.method(Struct, named_function, info) catch |err| {
|
|
caller.handleError(Struct, named_function, err, info);
|
|
};
|
|
}
|
|
}.callback);
|
|
template_proto.set(js_name, function_template, v8.PropertyAttribute.None);
|
|
}
|
|
|
|
fn generateFunction(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template_proto: v8.ObjectTemplate) void {
|
|
const js_name = v8.String.initUtf8(isolate, name).toName();
|
|
const function_template = v8.FunctionTemplate.initCallback(isolate, struct {
|
|
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
|
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
|
var caller = Caller(Self, State).init(info);
|
|
defer caller.deinit();
|
|
|
|
const named_function = comptime NamedFunction.init(Struct, "static_" ++ name);
|
|
caller.function(Struct, named_function, info) catch |err| {
|
|
caller.handleError(Struct, named_function, err, info);
|
|
};
|
|
}
|
|
}.callback);
|
|
template_proto.set(js_name, function_template, v8.PropertyAttribute.None);
|
|
}
|
|
|
|
fn generateAttribute(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template: v8.FunctionTemplate, template_proto: v8.ObjectTemplate) void {
|
|
const zig_value = @field(Struct, name);
|
|
const js_value = simpleZigValueToJs(isolate, zig_value, true);
|
|
|
|
const js_name = v8.String.initUtf8(isolate, name[1..]).toName();
|
|
|
|
// apply it both to the type itself
|
|
template.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
|
|
|
|
// and to instances of the type
|
|
template_proto.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
|
|
}
|
|
|
|
fn generateProperty(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template_proto: v8.ObjectTemplate) void {
|
|
var js_name: v8.Name = undefined;
|
|
if (comptime std.mem.eql(u8, name, "symbol_toStringTag")) {
|
|
js_name = v8.Symbol.getToStringTag(isolate).toName();
|
|
} else {
|
|
js_name = v8.String.initUtf8(isolate, name).toName();
|
|
}
|
|
|
|
const getter_callback = v8.FunctionTemplate.initCallback(isolate, struct {
|
|
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
|
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
|
var caller = Caller(Self, State).init(info);
|
|
defer caller.deinit();
|
|
|
|
const named_function = comptime NamedFunction.init(Struct, "get_" ++ name);
|
|
caller.method(Struct, named_function, info) catch |err| {
|
|
caller.handleError(Struct, named_function, err, info);
|
|
};
|
|
}
|
|
}.callback);
|
|
|
|
const setter_name = "set_" ++ name;
|
|
if (@hasDecl(Struct, setter_name) == false) {
|
|
template_proto.setAccessorGetter(js_name, getter_callback);
|
|
return;
|
|
}
|
|
|
|
const setter_callback = v8.FunctionTemplate.initCallback(isolate, struct {
|
|
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
|
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
|
std.debug.assert(info.length() == 1);
|
|
|
|
var caller = Caller(Self, State).init(info);
|
|
defer caller.deinit();
|
|
|
|
const named_function = comptime NamedFunction.init(Struct, "set_" ++ name);
|
|
caller.method(Struct, named_function, info) catch |err| {
|
|
caller.handleError(Struct, named_function, err, info);
|
|
};
|
|
}
|
|
}.callback);
|
|
|
|
template_proto.setAccessorGetterAndSetter(js_name, getter_callback, setter_callback);
|
|
}
|
|
|
|
fn generateIndexer(comptime Struct: type, template_proto: v8.ObjectTemplate) void {
|
|
if (@hasDecl(Struct, "indexed_get") == false) {
|
|
return;
|
|
}
|
|
const configuration = v8.IndexedPropertyHandlerConfiguration{
|
|
.getter = struct {
|
|
fn callback(idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
|
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
|
var caller = Caller(Self, State).init(info);
|
|
defer caller.deinit();
|
|
|
|
const named_function = comptime NamedFunction.init(Struct, "indexed_get");
|
|
return caller.getIndex(Struct, named_function, idx, info) catch |err| blk: {
|
|
caller.handleError(Struct, named_function, err, info);
|
|
break :blk v8.Intercepted.No;
|
|
};
|
|
}
|
|
}.callback,
|
|
};
|
|
|
|
// If you're trying to implement setter, read:
|
|
// https://groups.google.com/g/v8-users/c/8tahYBsHpgY/m/IteS7Wn2AAAJ
|
|
// The issue I had was
|
|
// (a) where to attache it: does it go ont he instance_template
|
|
// instead of the prototype?
|
|
// (b) defining the getter or query to respond with the
|
|
// PropertyAttribute to indicate if the property can be set
|
|
template_proto.setIndexedProperty(configuration, null);
|
|
}
|
|
|
|
fn generateNamedIndexer(comptime Struct: type, template_proto: v8.ObjectTemplate) void {
|
|
if (@hasDecl(Struct, "named_get") == false) {
|
|
if (comptime builtin.mode == .Debug) {
|
|
if (log.enabled(.unknown_prop, .debug)) {
|
|
generateDebugNamedIndexer(Struct, template_proto);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
var configuration = v8.NamedPropertyHandlerConfiguration{
|
|
.getter = struct {
|
|
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
|
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
|
var caller = Caller(Self, State).init(info);
|
|
defer caller.deinit();
|
|
|
|
const named_function = comptime NamedFunction.init(Struct, "named_get");
|
|
return caller.getNamedIndex(Struct, named_function, .{ .handle = c_name.? }, info) catch |err| blk: {
|
|
caller.handleError(Struct, named_function, err, info);
|
|
break :blk v8.Intercepted.No;
|
|
};
|
|
}
|
|
}.callback,
|
|
|
|
// This is really cool. Without this, we'd intercept _all_ properties
|
|
// even those explicitly 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_set")) {
|
|
configuration.setter = struct {
|
|
fn callback(c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
|
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
|
var caller = Caller(Self, State).init(info);
|
|
defer caller.deinit();
|
|
|
|
const named_function = comptime NamedFunction.init(Struct, "named_set");
|
|
return caller.setNamedIndex(Struct, named_function, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info) catch |err| blk: {
|
|
caller.handleError(Struct, named_function, err, info);
|
|
break :blk v8.Intercepted.No;
|
|
};
|
|
}
|
|
}.callback;
|
|
}
|
|
|
|
if (@hasDecl(Struct, "named_delete")) {
|
|
configuration.deleter = struct {
|
|
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
|
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
|
var caller = Caller(Self, State).init(info);
|
|
defer caller.deinit();
|
|
|
|
const named_function = comptime NamedFunction.init(Struct, "named_delete");
|
|
return caller.deleteNamedIndex(Struct, named_function, .{ .handle = c_name.? }, info) catch |err| blk: {
|
|
caller.handleError(Struct, named_function, err, info);
|
|
break :blk v8.Intercepted.No;
|
|
};
|
|
}
|
|
}.callback;
|
|
}
|
|
template_proto.setNamedProperty(configuration, null);
|
|
}
|
|
|
|
fn generateDebugNamedIndexer(comptime Struct: type, template_proto: v8.ObjectTemplate) void {
|
|
const configuration = v8.NamedPropertyHandlerConfiguration{
|
|
.getter = struct {
|
|
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
|
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
|
const isolate = info.getIsolate();
|
|
const v8_context = isolate.getCurrentContext();
|
|
|
|
const js_context: *JsContext = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
|
|
|
|
const property = valueToString(js_context.call_arena, .{ .handle = c_name.? }, isolate, v8_context) catch "???";
|
|
log.debug(.unknown_prop, "unkown property", .{ .@"struct" = @typeName(Struct), .property = property });
|
|
return v8.Intercepted.No;
|
|
}
|
|
}.callback,
|
|
|
|
// This is really cool. Without this, we'd intercept _all_ properties
|
|
// even those explicitly 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,
|
|
};
|
|
template_proto.setNamedProperty(configuration, null);
|
|
}
|
|
|
|
fn generateUndetectable(comptime Struct: type, template: v8.ObjectTemplate) void {
|
|
const has_js_call_as_function = @hasDecl(Struct, "jsCallAsFunction");
|
|
|
|
if (has_js_call_as_function) {
|
|
template.setCallAsFunctionHandler(struct {
|
|
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
|
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
|
var caller = Caller(Self, State).init(info);
|
|
defer caller.deinit();
|
|
|
|
const named_function = comptime NamedFunction.init(Struct, "jsCallAsFunction");
|
|
caller.method(Struct, named_function, info) catch |err| {
|
|
caller.handleError(Struct, named_function, err, info);
|
|
};
|
|
}
|
|
}.callback);
|
|
}
|
|
|
|
if (@hasDecl(Struct, "mark_as_undetectable") and Struct.mark_as_undetectable) {
|
|
if (!has_js_call_as_function) {
|
|
@compileError(@typeName(Struct) ++ ": mark_as_undetectable required jsCallAsFunction to be defined. This is a hard-coded requirement in V8, because mark_as_undetectable only exists for HTMLAllCollection which is also callable.");
|
|
}
|
|
template.markAsUndetectable();
|
|
}
|
|
}
|
|
|
|
// Turns a Zig value into a JS one.
|
|
fn zigValueToJs(
|
|
templates: []v8.FunctionTemplate,
|
|
isolate: v8.Isolate,
|
|
v8_context: v8.Context,
|
|
value: anytype,
|
|
) anyerror!v8.Value {
|
|
// Check if it's a "simple" type. This is extracted so that it can be
|
|
// reused by other parts of the code. "simple" types only require an
|
|
// isolate to create (specifically, they don't our templates array)
|
|
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, .@"enum" => {
|
|
// Need to do this to keep the compiler happy
|
|
// simpleZigValueToJs handles all of these cases.
|
|
unreachable;
|
|
},
|
|
.array => {
|
|
var js_arr = v8.Array.init(isolate, value.len);
|
|
var js_obj = js_arr.castTo(v8.Object);
|
|
for (value, 0..) |v, i| {
|
|
const js_val = try zigValueToJs(templates, isolate, v8_context, v);
|
|
if (js_obj.setValueAtIndex(v8_context, @intCast(i), js_val) == false) {
|
|
return error.FailedToCreateArray;
|
|
}
|
|
}
|
|
return js_obj.toValue();
|
|
},
|
|
.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).index];
|
|
const js_obj = try JsContext.mapZigInstanceToJs(v8_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;
|
|
}
|
|
},
|
|
.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, v8_context, v);
|
|
if (js_obj.setValueAtIndex(v8_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).index];
|
|
const js_obj = try JsContext.mapZigInstanceToJs(v8_context, template, value);
|
|
return js_obj.toValue();
|
|
}
|
|
|
|
if (T == Function) {
|
|
// we're returnig a callback
|
|
return value.func.toValue();
|
|
}
|
|
|
|
if (T == JsObject) {
|
|
// we're returning a v8.Object
|
|
return value.js_obj.toValue();
|
|
}
|
|
|
|
if (s.is_tuple) {
|
|
// return the tuple struct as an array
|
|
var js_arr = v8.Array.init(isolate, @intCast(s.fields.len));
|
|
var js_obj = js_arr.castTo(v8.Object);
|
|
inline for (s.fields, 0..) |f, i| {
|
|
const js_val = try zigValueToJs(templates, isolate, v8_context, @field(value, f.name));
|
|
if (js_obj.setValueAtIndex(v8_context, @intCast(i), js_val) == false) {
|
|
return error.FailedToCreateArray;
|
|
}
|
|
}
|
|
return js_obj.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, v8_context, @field(value, f.name));
|
|
const key = v8.String.initUtf8(isolate, f.name);
|
|
if (!js_obj.setValue(v8_context, key, js_val)) {
|
|
return error.CreateObjectFailure;
|
|
}
|
|
}
|
|
return js_obj.toValue();
|
|
},
|
|
.@"union" => |un| {
|
|
if (T == std.json.Value) {
|
|
return zigJsonToJs(isolate, v8_context, value);
|
|
}
|
|
if (un.tag_type) |UnionTagType| {
|
|
inline for (un.fields) |field| {
|
|
if (value == @field(UnionTagType, field.name)) {
|
|
return zigValueToJs(templates, isolate, v8_context, @field(value, field.name));
|
|
}
|
|
}
|
|
unreachable;
|
|
}
|
|
@compileError("Cannot use untagged union: " ++ @typeName(T));
|
|
},
|
|
.optional => {
|
|
if (value) |v| {
|
|
return zigValueToJs(templates, isolate, v8_context, v);
|
|
}
|
|
return v8.initNull(isolate).toValue();
|
|
},
|
|
.error_union => return zigValueToJs(templates, isolate, v8_context, value catch |err| return err),
|
|
else => {},
|
|
}
|
|
|
|
@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(std.fmt.comptimePrint(
|
|
"{s} has a non-pointer Zig parameter type: {s}",
|
|
.{ named_function.full_name, @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(std.fmt.comptimePrint(
|
|
"{s} has an unknown Zig type: {s}",
|
|
.{ named_function.full_name, @typeName(R) },
|
|
));
|
|
}
|
|
|
|
const op = js_obj.getInternalField(0).castTo(v8.External).get();
|
|
const toa: *TaggedAnyOpaque = @alignCast(@ptrCast(op));
|
|
const expected_type_index = @field(TYPE_LOOKUP, type_name).index;
|
|
|
|
var type_index = toa.index;
|
|
if (type_index == expected_type_index) {
|
|
return @alignCast(@ptrCast(toa.ptr));
|
|
}
|
|
|
|
// search through the prototype tree
|
|
while (true) {
|
|
const prototype_index = PROTOTYPE_TABLE[type_index];
|
|
if (prototype_index == expected_type_index) {
|
|
// -1 is a sentinel value used for non-composition prototype
|
|
// This is used with netsurf and we just unsafely cast one
|
|
// type to another
|
|
const offset = toa.offset;
|
|
if (offset == -1) {
|
|
return @alignCast(@ptrCast(toa.ptr));
|
|
}
|
|
|
|
// A non-negative offset means we're using composition prototype
|
|
// (i.e. our struct has a "proto" field). the offset
|
|
// reresents the byte offset of the field. We can use that
|
|
// + the toa.ptr to get the field
|
|
return @ptrFromInt(@intFromPtr(toa.ptr) + @as(usize, @intCast(offset)));
|
|
}
|
|
if (prototype_index == type_index) {
|
|
// When a type has itself as the prototype, then we've
|
|
// reached the end of the chain.
|
|
return error.InvalidArgument;
|
|
}
|
|
type_index = prototype_index;
|
|
}
|
|
}
|
|
|
|
// An interface for types that want to have their jsDeinit function to be
|
|
// called when the call context ends
|
|
const DestructorCallback = struct {
|
|
ptr: *anyopaque,
|
|
destructorFn: *const fn (ptr: *anyopaque) void,
|
|
|
|
fn init(ptr: anytype) DestructorCallback {
|
|
const T = @TypeOf(ptr);
|
|
const ptr_info = @typeInfo(T);
|
|
|
|
const gen = struct {
|
|
pub fn destructor(pointer: *anyopaque) void {
|
|
const self: T = @ptrCast(@alignCast(pointer));
|
|
return ptr_info.pointer.child.destructor(self);
|
|
}
|
|
};
|
|
|
|
return .{
|
|
.ptr = ptr,
|
|
.destructorFn = gen.destructor,
|
|
};
|
|
}
|
|
|
|
pub fn destructor(self: DestructorCallback) void {
|
|
self.destructorFn(self.ptr);
|
|
}
|
|
};
|
|
|
|
// An interface for types that want to have their jsScopeEnd function be
|
|
// called when the call context ends
|
|
const CallScopeEndCallback = struct {
|
|
ptr: *anyopaque,
|
|
callScopeEndFn: *const fn (ptr: *anyopaque) void,
|
|
|
|
fn init(ptr: anytype) CallScopeEndCallback {
|
|
const T = @TypeOf(ptr);
|
|
const ptr_info = @typeInfo(T);
|
|
|
|
const gen = struct {
|
|
pub fn callScopeEnd(pointer: *anyopaque) void {
|
|
const self: T = @ptrCast(@alignCast(pointer));
|
|
return ptr_info.pointer.child.jsCallScopeEnd(self);
|
|
}
|
|
};
|
|
|
|
return .{
|
|
.ptr = ptr,
|
|
.callScopeEndFn = gen.callScopeEnd,
|
|
};
|
|
}
|
|
|
|
pub fn callScopeEnd(self: CallScopeEndCallback) void {
|
|
self.callScopeEndFn(self.ptr);
|
|
}
|
|
};
|
|
};
|
|
}
|
|
|
|
// We'll create a struct with all the types we want to bind to JavaScript. The
|
|
// fields for this struct will be the type names. The values, will be an
|
|
// instance of this struct.
|
|
// const TypeLookup = struct {
|
|
// comptime cat: usize = TypeMeta{.index = 0, subtype = null},
|
|
// comptime owner: usize = TypeMeta{.index = 1, subtype = .array}.
|
|
// ...
|
|
// }
|
|
// This is essentially meta data for each type.
|
|
const TypeMeta = struct {
|
|
// Every type is given a unique index. That index is used to lookup various
|
|
// things, i.e. the prototype chain.
|
|
index: usize,
|
|
|
|
// We store the type's subtype here, so that when we create an instance of
|
|
// the type, and bind it to JavaScript, we can store the subtype along with
|
|
// the created TaggedAnyOpaque.s
|
|
subtype: ?SubType,
|
|
};
|
|
|
|
// When we map a Zig instance into a JsObject, we'll normally store the a
|
|
// TaggedAnyOpaque (TAO) inside of the JsObject's internal field. This requires
|
|
// ensuring that the instance template has an InternalFieldCount of 1. However,
|
|
// for empty objects, we don't need to store the TAO, because we can't just cast
|
|
// one empty object to another, so for those, as an optimization, we do not set
|
|
// the InternalFieldCount.
|
|
fn isEmpty(comptime T: type) bool {
|
|
return @typeInfo(T) != .@"opaque" and @sizeOf(T) == 0 and @hasDecl(T, "js_legacy_factory") == false;
|
|
}
|
|
|
|
// Attributes that return a primitive type are setup directly on the
|
|
// FunctionTemplate when the Env is setup. More complex types need a v8.Context
|
|
// and cannot be set directly on the FunctionTemplate.
|
|
// We default to saying types are primitives because that's mostly what
|
|
// we have. If we add a new complex type that isn't explictly handled here,
|
|
// we'll get a compiler error in simpleZigValueToJs, and can then explicitly
|
|
// add the type here.
|
|
fn isComplexAttributeType(ti: std.builtin.Type) bool {
|
|
return switch (ti) {
|
|
.array => true,
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
// Responsible for calling Zig functions from JS invokations. This could
|
|
// probably just contained in ExecutionWorld, but having this specific logic, which
|
|
// is somewhat repetitive between constructors, functions, getters, etc contained
|
|
// here does feel like it makes it clenaer.
|
|
fn Caller(comptime E: type, comptime State: type) type {
|
|
return struct {
|
|
js_context: *E.JsContext,
|
|
v8_context: v8.Context,
|
|
isolate: v8.Isolate,
|
|
call_arena: Allocator,
|
|
|
|
const Self = @This();
|
|
|
|
// info is a v8.PropertyCallbackInfo or a v8.FunctionCallback
|
|
// All we really want from it is the isolate.
|
|
// executor = Isolate -> getCurrentContext -> getEmbedderData()
|
|
fn init(info: anytype) Self {
|
|
const isolate = info.getIsolate();
|
|
const v8_context = isolate.getCurrentContext();
|
|
const js_context: *E.JsContext = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
|
|
|
|
js_context.call_depth += 1;
|
|
return .{
|
|
.js_context = js_context,
|
|
.isolate = isolate,
|
|
.v8_context = v8_context,
|
|
.call_arena = js_context.call_arena,
|
|
};
|
|
}
|
|
|
|
fn deinit(self: *Self) void {
|
|
const js_context = self.js_context;
|
|
const call_depth = js_context.call_depth - 1;
|
|
|
|
// Because of callbacks, calls can be nested. Because of this, we
|
|
// can't clear the call_arena after _every_ call. Imagine we have
|
|
// arr.forEach((i) => { console.log(i); }
|
|
//
|
|
// First we call forEach. Inside of our forEach call,
|
|
// we call console.log. If we reset the call_arena after this call,
|
|
// it'll reset it for the `forEach` call after, which might still
|
|
// need the data.
|
|
//
|
|
// Therefore, we keep a call_depth, and only reset the call_arena
|
|
// when a top-level (call_depth == 0) function ends.
|
|
if (call_depth == 0) {
|
|
for (js_context.call_scope_end_callbacks.items) |cb| {
|
|
cb.callScopeEnd();
|
|
}
|
|
|
|
const arena: *ArenaAllocator = @alignCast(@ptrCast(js_context.call_arena.ptr));
|
|
_ = arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
|
|
}
|
|
|
|
// Set this _after_ we've executed the above code, so that if the
|
|
// above code executes any callbacks, they aren't being executed
|
|
// at scope 0, which would be wrong.
|
|
js_context.call_depth = call_depth;
|
|
}
|
|
|
|
fn constructor(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
|
|
const args = try self.getArgs(Struct, named_function, 0, info);
|
|
const res = @call(.auto, Struct.constructor, args);
|
|
|
|
const ReturnType = @typeInfo(@TypeOf(Struct.constructor)).@"fn".return_type orelse {
|
|
@compileError(@typeName(Struct) ++ " 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.JsContext.mapZigInstanceToJs(self.v8_context, this, non_error_res);
|
|
} else {
|
|
_ = try E.JsContext.mapZigInstanceToJs(self.v8_context, this, res);
|
|
}
|
|
info.getReturnValue().set(this);
|
|
}
|
|
|
|
fn method(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
|
|
const js_context = self.js_context;
|
|
const func = @field(Struct, named_function.name);
|
|
comptime assertSelfReceiver(Struct, named_function);
|
|
|
|
var args = try self.getArgs(Struct, named_function, 1, info);
|
|
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis());
|
|
|
|
// inject 'self' as the first parameter
|
|
@field(args, "0") = zig_instance;
|
|
|
|
const res = @call(.auto, func, args);
|
|
info.getReturnValue().set(try js_context.zigValueToJs(res));
|
|
}
|
|
|
|
fn function(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
|
|
const js_context = self.js_context;
|
|
const func = @field(Struct, named_function.name);
|
|
const args = try self.getArgs(Struct, named_function, 0, info);
|
|
const res = @call(.auto, func, args);
|
|
info.getReturnValue().set(try js_context.zigValueToJs(res));
|
|
}
|
|
|
|
fn getIndex(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, idx: u32, info: v8.PropertyCallbackInfo) !u8 {
|
|
const js_context = self.js_context;
|
|
const func = @field(Struct, named_function.name);
|
|
const IndexedGet = @TypeOf(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 zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis());
|
|
comptime assertSelfReceiver(Struct, named_function);
|
|
@field(args, "0") = zig_instance;
|
|
@field(args, "1") = idx;
|
|
@field(args, "2") = &has_value;
|
|
if (comptime arg_fields.len == 4) {
|
|
comptime assertIsStateArg(Struct, named_function, 3);
|
|
@field(args, "3") = js_context.state;
|
|
}
|
|
},
|
|
else => @compileError(named_function.full_name ++ " has too many parmaters"),
|
|
}
|
|
|
|
const res = @call(.auto, func, args);
|
|
if (has_value == false) {
|
|
return v8.Intercepted.No;
|
|
}
|
|
info.getReturnValue().set(try js_context.zigValueToJs(res));
|
|
return v8.Intercepted.Yes;
|
|
}
|
|
|
|
fn getNamedIndex(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 {
|
|
const func = @field(Struct, named_function.name);
|
|
comptime assertSelfReceiver(Struct, named_function);
|
|
|
|
var has_value = true;
|
|
var args = try self.getArgs(Struct, named_function, 3, info);
|
|
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis());
|
|
@field(args, "0") = zig_instance;
|
|
@field(args, "1") = try self.nameToString(name);
|
|
@field(args, "2") = &has_value;
|
|
|
|
const res = @call(.auto, func, args);
|
|
if (has_value == false) {
|
|
return v8.Intercepted.No;
|
|
}
|
|
info.getReturnValue().set(try self.js_context.zigValueToJs(res));
|
|
return v8.Intercepted.Yes;
|
|
}
|
|
|
|
fn setNamedIndex(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo) !u8 {
|
|
const js_context = self.js_context;
|
|
const func = @field(Struct, named_function.name);
|
|
comptime assertSelfReceiver(Struct, named_function);
|
|
|
|
var has_value = true;
|
|
var args = try self.getArgs(Struct, named_function, 4, info);
|
|
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis());
|
|
@field(args, "0") = zig_instance;
|
|
@field(args, "1") = try self.nameToString(name);
|
|
@field(args, "2") = try js_context.jsValueToZig(named_function, @TypeOf(@field(args, "2")), js_value);
|
|
@field(args, "3") = &has_value;
|
|
|
|
const res = @call(.auto, func, args);
|
|
return namedSetOrDeleteCall(res, has_value);
|
|
}
|
|
|
|
fn deleteNamedIndex(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 {
|
|
const func = @field(Struct, named_function.name);
|
|
comptime assertSelfReceiver(Struct, named_function);
|
|
|
|
var has_value = true;
|
|
var args = try self.getArgs(Struct, named_function, 3, info);
|
|
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis());
|
|
@field(args, "0") = zig_instance;
|
|
@field(args, "1") = try self.nameToString(name);
|
|
@field(args, "2") = &has_value;
|
|
|
|
const res = @call(.auto, func, args);
|
|
return namedSetOrDeleteCall(res, has_value);
|
|
}
|
|
|
|
fn namedSetOrDeleteCall(res: anytype, has_value: bool) !u8 {
|
|
if (@typeInfo(@TypeOf(res)) == .error_union) {
|
|
_ = try res;
|
|
}
|
|
if (has_value == false) {
|
|
return v8.Intercepted.No;
|
|
}
|
|
return v8.Intercepted.Yes;
|
|
}
|
|
|
|
fn nameToString(self: *Self, name: v8.Name) ![]const u8 {
|
|
return valueToString(self.call_arena, .{ .handle = name.handle }, self.isolate, self.v8_context);
|
|
}
|
|
|
|
fn assertSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction) void {
|
|
const func = @field(Struct, named_function.name);
|
|
const params = @typeInfo(@TypeOf(func)).@"fn".params;
|
|
if (params.len == 0) {
|
|
@compileError(named_function.full_name ++ " must have a self parameter");
|
|
}
|
|
const R = Receiver(Struct);
|
|
|
|
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 Struct: type, comptime named_function: NamedFunction, index: comptime_int) void {
|
|
const func = @field(Struct, named_function.name);
|
|
const F = @TypeOf(func);
|
|
const params = @typeInfo(F).@"fn".params;
|
|
|
|
const param = params[index].type.?;
|
|
if (param == State) {
|
|
return;
|
|
}
|
|
|
|
if (@typeInfo(State) == .pointer) {
|
|
if (param == *const @typeInfo(State).pointer.child) {
|
|
return;
|
|
}
|
|
}
|
|
@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 Struct: type, comptime named_function: NamedFunction, err: anyerror, info: anytype) void {
|
|
const isolate = self.isolate;
|
|
|
|
if (comptime builtin.mode == .Debug and @hasDecl(@TypeOf(info), "length")) {
|
|
if (log.enabled(.js, .warn)) {
|
|
logFunctionCallError(self.call_arena, self.isolate, self.v8_context, err, named_function.full_name, info);
|
|
}
|
|
}
|
|
|
|
var js_err: ?v8.Value = switch (err) {
|
|
error.InvalidArgument => createTypeException(isolate, "invalid argument"),
|
|
error.OutOfMemory => createException(isolate, "out of memory"),
|
|
error.IllegalConstructor => createException(isolate, "Illegal Contructor"),
|
|
else => blk: {
|
|
const func = @field(Struct, named_function.name);
|
|
const return_type = @typeInfo(@TypeOf(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(Struct) orelse break :blk null;
|
|
if (function_error_set == Exception or isErrorSetException(Exception, err)) {
|
|
const custom_exception = Exception.init(self.call_arena, 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.js_context.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 Struct: type, comptime named_function: NamedFunction, comptime offset: usize, info: anytype) !ParamterTypes(@TypeOf(@field(Struct, named_function.name))) {
|
|
const js_context = self.js_context;
|
|
const F = @TypeOf(@field(Struct, named_function.name));
|
|
var args: ParamterTypes(F) = undefined;
|
|
|
|
const params = @typeInfo(F).@"fn".params[offset..];
|
|
// Except for the constructor, the first parameter is always `self`
|
|
// This isn't something we'll bind from JS, so skip it.
|
|
const params_to_map = blk: {
|
|
if (params.len == 0) {
|
|
return args;
|
|
}
|
|
|
|
// If the last parameter is the State, set it, and exclude it
|
|
// from our params slice, because we don't want to bind it to
|
|
// a JS argument
|
|
if (comptime isState(params[params.len - 1].type.?)) {
|
|
@field(args, std.fmt.comptimePrint("{d}", .{params.len - 1 + offset})) = self.js_context.state;
|
|
break :blk params[0 .. params.len - 1];
|
|
}
|
|
|
|
// If the last parameter is a special JsThis, set it, and exclude it
|
|
// from our params slice, because we don't want to bind it to
|
|
// a JS argument
|
|
if (comptime isJsThis(params[params.len - 1].type.?)) {
|
|
@field(args, std.fmt.comptimePrint("{d}", .{params.len - 1 + offset})) = .{ .obj = .{
|
|
.js_context = js_context,
|
|
.js_obj = info.getThis(),
|
|
} };
|
|
|
|
// AND the 2nd last parameter is state
|
|
if (params.len > 1 and comptime isState(params[params.len - 2].type.?)) {
|
|
@field(args, std.fmt.comptimePrint("{d}", .{params.len - 2 + offset})) = self.js_context.state;
|
|
break :blk params[0 .. params.len - 2];
|
|
}
|
|
|
|
break :blk params[0 .. params.len - 1];
|
|
}
|
|
|
|
// we have neither a State nor a JsObject. All params must be
|
|
// bound to a JavaScript value.
|
|
break :blk params;
|
|
};
|
|
|
|
if (params_to_map.len == 0) {
|
|
return args;
|
|
}
|
|
|
|
const js_parameter_count = info.length();
|
|
const last_js_parameter = params_to_map.len - 1;
|
|
var is_variadic = false;
|
|
|
|
{
|
|
// 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 = params_to_map[params_to_map.len - 1].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_value = info.getArg(@as(u32, @intCast(last_js_parameter)));
|
|
if (corresponding_js_value.isArray() == false and corresponding_js_value.isTypedArray() == false and slice_type != u8) {
|
|
is_variadic = true;
|
|
if (js_parameter_count == 0) {
|
|
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
|
|
} else if (js_parameter_count >= params_to_map.len) {
|
|
const arr = try self.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
|
|
for (arr, last_js_parameter..) |*a, i| {
|
|
const js_value = info.getArg(@as(u32, @intCast(i)));
|
|
a.* = try js_context.jsValueToZig(named_function, slice_type, js_value);
|
|
}
|
|
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;
|
|
} else {
|
|
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
inline for (params_to_map, 0..) |param, i| {
|
|
const field_index = comptime i + offset;
|
|
if (comptime i == params_to_map.len - 1) {
|
|
if (is_variadic) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (comptime isState(param.type.?)) {
|
|
@compileError("State must be the last parameter (or 2nd last if there's a JsThis): " ++ named_function.full_name);
|
|
} else if (comptime isJsThis(param.type.?)) {
|
|
@compileError("JsThis must be the last parameter: " ++ named_function.full_name);
|
|
} else if (i >= js_parameter_count) {
|
|
if (@typeInfo(param.type.?) != .optional) {
|
|
return error.InvalidArgument;
|
|
}
|
|
@field(args, tupleFieldName(field_index)) = null;
|
|
} else {
|
|
const js_value = info.getArg(@as(u32, @intCast(i)));
|
|
@field(args, tupleFieldName(field_index)) = js_context.jsValueToZig(named_function, param.type.?, js_value) catch {
|
|
return error.InvalidArgument;
|
|
};
|
|
}
|
|
}
|
|
|
|
return args;
|
|
}
|
|
|
|
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;
|
|
}
|
|
};
|
|
}
|
|
|
|
fn isJsObject(comptime T: type) bool {
|
|
return @typeInfo(T) == .@"struct" and @hasDecl(T, "_JSOBJECT_ID_KLUDGE");
|
|
}
|
|
|
|
fn isJsThis(comptime T: type) bool {
|
|
return @typeInfo(T) == .@"struct" and @hasDecl(T, "_JSTHIS_ID_KLUDGE");
|
|
}
|
|
|
|
fn jsIntToZig(comptime T: type, js_value: v8.Value, v8_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(v8_context)),
|
|
16 => return jsSignedIntToZig(i16, -32_768, 32_767, try js_value.toI32(v8_context)),
|
|
32 => return jsSignedIntToZig(i32, -2_147_483_648, 2_147_483_647, try js_value.toI32(v8_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(v8_context));
|
|
},
|
|
else => {},
|
|
},
|
|
.unsigned => switch (n.bits) {
|
|
8 => return jsUnsignedIntToZig(u8, 255, try js_value.toU32(v8_context)),
|
|
16 => return jsUnsignedIntToZig(u16, 65_535, try js_value.toU32(v8_context)),
|
|
32 => return jsUnsignedIntToZig(u32, 4_294_967_295, try js_value.toU32(v8_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(v8_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;
|
|
}
|
|
|
|
// 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();
|
|
},
|
|
.@"struct" => {
|
|
const T = @TypeOf(value);
|
|
if (@hasDecl(T, "_TYPED_ARRAY_ID_KLUDGE")) {
|
|
const values = value.values;
|
|
const value_type = @typeInfo(@TypeOf(values)).pointer.child;
|
|
const len = values.len;
|
|
const bits = switch (@typeInfo(value_type)) {
|
|
.int => |n| n.bits,
|
|
.float => |f| f.bits,
|
|
else => @compileError("Invalid TypeArray type: " ++ @typeName(value_type)),
|
|
};
|
|
|
|
const buffer_len = len * bits / 8;
|
|
const backing_store = v8.BackingStore.init(isolate, buffer_len);
|
|
const data: [*]u8 = @alignCast(@ptrCast(backing_store.getData()));
|
|
@memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]);
|
|
const array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
|
|
|
|
switch (@typeInfo(value_type)) {
|
|
.int => |n| switch (n.signedness) {
|
|
.unsigned => switch (n.bits) {
|
|
8 => return v8.Uint8Array.init(array_buffer, 0, len).toValue(),
|
|
16 => return v8.Uint16Array.init(array_buffer, 0, len).toValue(),
|
|
32 => return v8.Uint32Array.init(array_buffer, 0, len).toValue(),
|
|
64 => return v8.BigUint64Array.init(array_buffer, 0, len).toValue(),
|
|
else => {},
|
|
},
|
|
.signed => switch (n.bits) {
|
|
8 => return v8.Int8Array.init(array_buffer, 0, len).toValue(),
|
|
16 => return v8.Int16Array.init(array_buffer, 0, len).toValue(),
|
|
32 => return v8.Int32Array.init(array_buffer, 0, len).toValue(),
|
|
64 => return v8.BigInt64Array.init(array_buffer, 0, len).toValue(),
|
|
else => {},
|
|
},
|
|
},
|
|
.float => |f| switch (f.bits) {
|
|
32 => return v8.Float32Array.init(array_buffer, 0, len).toValue(),
|
|
64 => return v8.Float64Array.init(array_buffer, 0, len).toValue(),
|
|
else => {},
|
|
},
|
|
else => {},
|
|
}
|
|
// We normally don't fail in this function unless fail == true
|
|
// but this can never be valid.
|
|
@compileError("Invalid TypeArray type: " ++ @typeName(value_type));
|
|
}
|
|
},
|
|
.@"union" => return simpleZigValueToJs(isolate, std.meta.activeTag(value), fail),
|
|
.@"enum" => {
|
|
const T = @TypeOf(value);
|
|
if (@hasDecl(T, "toString")) {
|
|
return simpleZigValueToJs(isolate, value.toString(), fail);
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
if (fail) {
|
|
@compileError("Unsupported Zig type " ++ @typeName(@TypeOf(value)));
|
|
}
|
|
return null;
|
|
}
|
|
|
|
pub fn zigJsonToJs(isolate: v8.Isolate, v8_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, v8_context, array_value);
|
|
if (!obj.setValueAtIndex(v8_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, v8_context, kv.value_ptr.*);
|
|
if (!obj.setValue(v8_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 parameter 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 its
|
|
// 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 netsurf)
|
|
offset: i32,
|
|
|
|
// Ptr to the Zig instance. Between the context where it's called (i.e.
|
|
// we have the comptime parameter info for all functions), and the index field
|
|
// we can figure out what type this is.
|
|
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 TaggedAnyOpaque
|
|
// which is where we store the subtype.
|
|
subtype: ?SubType,
|
|
};
|
|
|
|
fn valueToDetailString(arena: Allocator, value: v8.Value, isolate: v8.Isolate, v8_context: v8.Context) ![]u8 {
|
|
var str: ?v8.String = null;
|
|
if (value.isObject() and !value.isFunction()) {
|
|
str = try v8.Json.stringify(v8_context, value, null);
|
|
|
|
if (str.?.lenUtf8(isolate) == 2) {
|
|
// {} isn't useful, null this so that we can get the toDetailString
|
|
// (which might also be useless, but maybe not)
|
|
str = null;
|
|
}
|
|
}
|
|
|
|
if (str == null) {
|
|
str = try value.toDetailString(v8_context);
|
|
}
|
|
|
|
return jsStringToZig(arena, str.?, isolate);
|
|
}
|
|
|
|
fn valueToString(allocator: Allocator, value: v8.Value, isolate: v8.Isolate, v8_context: v8.Context) ![]u8 {
|
|
const str = try value.toString(v8_context);
|
|
return jsStringToZig(allocator, str, isolate);
|
|
}
|
|
|
|
fn valueToStringZ(allocator: Allocator, value: v8.Value, isolate: v8.Isolate, v8_context: v8.Context) ![:0]u8 {
|
|
const str = try value.toString(v8_context);
|
|
const len = str.lenUtf8(isolate);
|
|
const buf = try allocator.allocSentinel(u8, len, 0);
|
|
const n = str.writeUtf8(isolate, buf);
|
|
std.debug.assert(n == len);
|
|
return buf;
|
|
}
|
|
|
|
fn jsStringToZig(allocator: Allocator, str: v8.String, isolate: v8.Isolate) ![]u8 {
|
|
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;
|
|
}
|
|
|
|
fn stackForLogs(arena: Allocator, isolate: v8.Isolate) !?[]const u8 {
|
|
std.debug.assert(builtin.mode == .Debug);
|
|
|
|
const separator = log.separator();
|
|
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
|
var writer = buf.writer(arena);
|
|
|
|
const stack_trace = v8.StackTrace.getCurrentStackTrace(isolate, 30);
|
|
const frame_count = stack_trace.getFrameCount();
|
|
|
|
for (0..frame_count) |i| {
|
|
const frame = stack_trace.getFrame(isolate, @intCast(i));
|
|
if (frame.getScriptName()) |name| {
|
|
const script = try jsStringToZig(arena, name, isolate);
|
|
try writer.print("{s}{s}:{d}", .{ separator, script, frame.getLineNumber() });
|
|
} else {
|
|
try writer.print("{s}<anonymous>:{d}", .{ separator, frame.getLineNumber() });
|
|
}
|
|
}
|
|
return buf.items;
|
|
}
|
|
|
|
const NoopInspector = struct {
|
|
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
|
|
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
|
|
};
|
|
|
|
const ErrorModuleLoader = struct {
|
|
pub fn fetchModuleSource(_: *anyopaque, _: []const u8) !?[]const u8 {
|
|
return error.NoModuleLoadConfigured;
|
|
}
|
|
};
|
|
|
|
// If we have a struct:
|
|
// const Cat = struct {
|
|
// pub fn meow(self: *Cat) void { ... }
|
|
// }
|
|
// Then 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 Struct: type) type {
|
|
return if (@hasDecl(Struct, "Self")) Struct.Self else Struct;
|
|
}
|
|
|
|
// 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 create
|
|
// a NamedFunction as part of our binding, and pass it around incase we need
|
|
// to display an error
|
|
const NamedFunction = struct {
|
|
name: []const u8,
|
|
js_name: []const u8,
|
|
full_name: []const u8,
|
|
|
|
fn init(comptime Struct: type, comptime name: []const u8) NamedFunction {
|
|
return .{
|
|
.name = name,
|
|
.js_name = if (name[0] == '_') name[1..] else name,
|
|
.full_name = @typeName(Struct) ++ "." ++ name,
|
|
};
|
|
}
|
|
};
|
|
|
|
// This is extracted to speed up compilation. When left inlined in handleError,
|
|
// this can add as much as 10 seconds of compilation time.
|
|
fn logFunctionCallError(arena: Allocator, isolate: v8.Isolate, context: v8.Context, err: anyerror, function_name: []const u8, info: v8.FunctionCallbackInfo) void {
|
|
const args_dump = serializeFunctionArgs(arena, isolate, context, info) catch "failed to serialize args";
|
|
log.warn(.js, "function call error", .{
|
|
.name = function_name,
|
|
.err = err,
|
|
.args = args_dump,
|
|
.stack = stackForLogs(arena, isolate) catch |err1| @errorName(err1),
|
|
});
|
|
}
|
|
|
|
fn serializeFunctionArgs(arena: Allocator, isolate: v8.Isolate, context: v8.Context, info: v8.FunctionCallbackInfo) ![]const u8 {
|
|
const separator = log.separator();
|
|
const js_parameter_count = info.length();
|
|
|
|
var arr: std.ArrayListUnmanaged(u8) = .{};
|
|
for (0..js_parameter_count) |i| {
|
|
const js_value = info.getArg(@intCast(i));
|
|
const value_string = try valueToDetailString(arena, js_value, isolate, context);
|
|
const value_type = try jsStringToZig(arena, try js_value.typeOf(isolate), isolate);
|
|
try std.fmt.format(arr.writer(arena), "{s}{d}: {s} ({s})", .{
|
|
separator,
|
|
i + 1,
|
|
value_string,
|
|
value_type,
|
|
});
|
|
}
|
|
return arr.items;
|
|
}
|
|
|
|
// This is called from V8. Whenever the v8 inspector has to describe a value
|
|
// it'll call this function to gets its [optional] subtype - which, from V8's
|
|
// point of view, is an arbitrary string.
|
|
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.subtype) |st| @tagName(st) else null;
|
|
}
|
|
|
|
// Same as valueSubType above, but for the optional description field.
|
|
// From what I can tell, some drivers _need_ the description field to be
|
|
// present, even if it's empty. So if we have a subType for the value, we'll
|
|
// put an empty description.
|
|
pub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype(
|
|
_: *v8.c.InspectorClientImpl,
|
|
v8_context: *const v8.C_Context,
|
|
c_value: *const v8.C_Value,
|
|
) callconv(.C) [*c]const u8 {
|
|
_ = v8_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.subtype == 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));
|
|
}
|
|
|
|
test {
|
|
std.testing.refAllDecls(@import("test_primitive_types.zig"));
|
|
std.testing.refAllDecls(@import("test_complex_types.zig"));
|
|
std.testing.refAllDecls(@import("test_object_types.zig"));
|
|
}
|