merge main

This commit is contained in:
Karl Seguin
2026-01-09 18:02:18 +08:00
parent 86f4ea108d
commit 1b0ea44519
8 changed files with 85 additions and 634 deletions

View File

@@ -1,519 +0,0 @@
// Copyright (C) 2023-2025 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 log = @import("../../log.zig");
const js = @import("js.zig");
const v8 = js.v8;
const Context = @import("Context.zig");
const Page = @import("../Page.zig");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const CALL_ARENA_RETAIN = 1024 * 16;
// Responsible for calling Zig functions from JS invocations. 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 cleaner.
const Caller = @This();
context: *Context,
isolate: js.Isolate,
call_arena: Allocator,
// info is a v8.PropertyCallbackInfo or a v8.FunctionCallback
// All we really want from it is the isolate.
// executor = Isolate -> getCurrentContext -> getEmbedderData()
pub fn init(info: anytype) Caller {
const v8_isolate = info.getIsolate();
const isolate = js.Isolate{ .handle = v8_isolate.handle };
const v8_context = v8_isolate.getCurrentContext();
const context: *Context = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
context.call_depth += 1;
return .{
.context = context,
.isolate = isolate,
.call_arena = context.call_arena,
};
}
pub fn deinit(self: *Caller) void {
const context = self.context;
const call_depth = 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) {
const arena: *ArenaAllocator = @ptrCast(@alignCast(context.call_arena.ptr));
_ = arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
}
context.call_depth = call_depth;
}
pub const CallOpts = struct {
dom_exception: bool = false,
null_as_undefined: bool = false,
as_typed_array: bool = false,
};
pub fn constructor(self: *Caller, comptime T: type, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
if (!info.isConstructCall()) {
self.handleError(T, @TypeOf(func), error.InvalidArgument, info, opts);
return;
}
self._constructor(func, info) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
};
}
pub fn _constructor(self: *Caller, func: anytype, info: v8.FunctionCallbackInfo) !void {
const F = @TypeOf(func);
const args = try self.getArgs(F, 0, info);
const res = @call(.auto, func, args);
const ReturnType = @typeInfo(F).@"fn".return_type orelse {
@compileError(@typeName(F) ++ " has a constructor without a return type");
};
const new_this = info.getThis();
var this = new_this;
if (@typeInfo(ReturnType) == .error_union) {
const non_error_res = res catch |err| return err;
this = (try self.context.mapZigInstanceToJs(this, non_error_res)).castToObject();
} else {
this = (try self.context.mapZigInstanceToJs(this, res)).castToObject();
}
// If we got back a different object (existing wrapper), copy the prototype
// from new object. (this happens when we're upgrading an CustomElement)
if (this.handle != new_this.handle) {
const new_prototype = new_this.getPrototype();
const v8_context = v8.Context{ .handle = self.context.handle };
_ = this.setPrototype(v8_context, new_prototype.castTo(v8.Object));
}
info.getReturnValue().set(this);
}
pub fn method(self: *Caller, comptime T: type, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
self._method(T, func, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
};
}
pub fn _method(self: *Caller, comptime T: type, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) !void {
const F = @TypeOf(func);
var handle_scope: js.HandleScope = undefined;
handle_scope.init(self.isolate);
defer handle_scope.deinit();
var args = try self.getArgs(F, 1, info);
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
const res = @call(.auto, func, args);
info.getReturnValue().set(try self.context.zigValueToJs(res, opts));
}
pub fn function(self: *Caller, comptime T: type, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
self._function(func, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
};
}
pub fn _function(self: *Caller, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) !void {
const F = @TypeOf(func);
const context = self.context;
const args = try self.getArgs(F, 0, info);
const res = @call(.auto, func, args);
info.getReturnValue().set(try context.zigValueToJs(res, opts));
}
pub fn getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._getIndex(T, func, idx, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
return v8.Intercepted.No;
};
}
pub fn _getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args = try self.getArgs(F, 2, info);
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = idx;
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, true, ret, info, opts);
}
pub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._getNamedIndex(T, func, name, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
return v8.Intercepted.No;
};
}
pub fn _getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args = try self.getArgs(F, 2, info);
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = try self.nameToString(name);
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, true, ret, info, opts);
}
pub fn setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._setNamedIndex(T, func, name, js_value, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
return v8.Intercepted.No;
};
}
pub fn _setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args: ParameterTypes(F) = undefined;
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = try self.nameToString(name);
@field(args, "2") = try self.context.jsValueToZig(@TypeOf(@field(args, "2")), js_value);
if (@typeInfo(F).@"fn".params.len == 4) {
@field(args, "3") = self.context.page;
}
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, false, ret, info, opts);
}
pub fn deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._deleteNamedIndex(T, func, name, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
return v8.Intercepted.No;
};
}
pub fn _deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args: ParameterTypes(F) = undefined;
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = try self.nameToString(name);
if (@typeInfo(F).@"fn".params.len == 3) {
@field(args, "2") = self.context.page;
}
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, false, ret, info, opts);
}
fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, comptime getter: bool, ret: anytype, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
// need to unwrap this error immediately for when opts.null_as_undefined == true
// and we need to compare it to null;
const non_error_ret = switch (@typeInfo(@TypeOf(ret))) {
.error_union => |eu| blk: {
break :blk ret catch |err| {
// We can't compare err == error.NotHandled if error.NotHandled
// isn't part of the possible error set. So we first need to check
// if error.NotHandled is part of the error set.
if (isInErrorSet(error.NotHandled, eu.error_set)) {
if (err == error.NotHandled) {
return v8.Intercepted.No;
}
}
self.handleError(T, F, err, info, opts);
return v8.Intercepted.No;
};
},
else => ret,
};
if (comptime getter) {
info.getReturnValue().set(try self.context.zigValueToJs(non_error_ret, opts));
}
return v8.Intercepted.Yes;
}
fn isInErrorSet(err: anyerror, comptime T: type) bool {
inline for (@typeInfo(T).error_set.?) |e| {
if (err == @field(anyerror, e.name)) return true;
}
return false;
}
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: *Caller, name: v8.Name) ![]const u8 {
return self.context.valueToString(.{ .handle = name.handle }, .{});
}
fn isSelfReceiver(comptime T: type, comptime F: type) bool {
return checkSelfReceiver(T, F, false);
}
fn assertSelfReceiver(comptime T: type, comptime F: type) void {
_ = checkSelfReceiver(T, F, true);
}
fn checkSelfReceiver(comptime T: type, comptime F: type, comptime fail: bool) bool {
const params = @typeInfo(F).@"fn".params;
if (params.len == 0) {
if (fail) {
@compileError(@typeName(F) ++ " must have a self parameter");
}
return false;
}
const first_param = params[0].type.?;
if (first_param != *T and first_param != *const T) {
if (fail) {
@compileError(std.fmt.comptimePrint("The first parameter to {s} must be a *{s} or *const {s}. Got: {s}", .{
@typeName(F),
@typeName(T),
@typeName(T),
@typeName(first_param),
}));
}
return false;
}
return true;
}
fn assertIsPageArg(comptime T: type, comptime F: type, index: comptime_int) void {
const param = @typeInfo(F).@"fn".params[index].type.?;
if (isPage(param)) {
return;
}
@compileError(std.fmt.comptimePrint("The {d} parameter of {s}.{s} must be a *Page or *const Page. Got: {s}", .{ index, @typeName(T), @typeName(F), @typeName(param) }));
}
fn handleError(self: *Caller, comptime T: type, comptime F: type, err: anyerror, info: anytype, comptime opts: CallOpts) void {
const isolate = self.isolate;
const v8_isolate = v8.Isolate{ .handle = isolate.handle };
if (comptime @import("builtin").mode == .Debug and @hasDecl(@TypeOf(info), "length")) {
if (log.enabled(.js, .warn)) {
self.logFunctionCallError(@typeName(T), @typeName(F), err, info);
}
}
var js_err: ?v8.Value = switch (err) {
error.InvalidArgument => createTypeException(v8_isolate, "invalid argument"),
error.OutOfMemory => js._createException(v8_isolate, "out of memory"),
error.IllegalConstructor => js._createException(v8_isolate, "Illegal Contructor"),
else => blk: {
if (!comptime opts.dom_exception) {
break :blk null;
}
const DOMException = @import("../webapi/DOMException.zig");
const ex = DOMException.fromError(err) orelse break :blk null;
break :blk self.context.zigValueToJs(ex, .{}) catch js._createException(v8_isolate, "internal error");
},
};
if (js_err == null) {
js_err = js._createException(v8_isolate, @errorName(err));
}
const js_exception = isolate.throwException(js_err.?);
info.getReturnValue().setValueHandle(js_exception.handle);
}
// 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 Page.
//
// 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 Caller, comptime F: type, comptime offset: usize, info: anytype) !ParameterTypes(F) {
const context = self.context;
var args: ParameterTypes(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 Page, set it, and exclude it
// from our params slice, because we don't want to bind it to
// a JS argument
if (comptime isPage(params[params.len - 1].type.?)) {
@field(args, tupleFieldName(params.len - 1 + offset)) = self.context.page;
break :blk params[0 .. params.len - 1];
}
// we have neither a Page 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 parameter
// 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 context.jsValueToZig(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 isPage(param.type.?)) {
@compileError("Page must be the last parameter (or 2nd last if there's a JsThis): " ++ @typeName(F));
} 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)) = context.jsValueToZig(param.type.?, js_value) catch {
return error.InvalidArgument;
};
}
}
return args;
}
// 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(self: *Caller, type_name: []const u8, func: []const u8, err: anyerror, info: v8.FunctionCallbackInfo) void {
const args_dump = self.serializeFunctionArgs(info) catch "failed to serialize args";
log.info(.js, "function call error", .{
.type = type_name,
.func = func,
.err = err,
.args = args_dump,
.stack = self.context.stackTrace() catch |err1| @errorName(err1),
});
}
fn serializeFunctionArgs(self: *Caller, info: v8.FunctionCallbackInfo) ![]const u8 {
const context = self.context;
var buf = std.Io.Writer.Allocating.init(context.call_arena);
const separator = log.separator();
for (0..info.length()) |i| {
try buf.writer.print("{s}{d} - ", .{ separator, i + 1 });
try context.debugValue(info.getArg(@intCast(i)), &buf.writer);
}
return buf.written();
}
// Takes a function, and returns a tuple for its argument. Used when we
// @call a function
fn ParameterTypes(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 switch (i) {
0 => "0",
1 => "1",
2 => "2",
3 => "3",
4 => "4",
5 => "5",
6 => "6",
7 => "7",
8 => "8",
9 => "9",
else => std.fmt.comptimePrint("{d}", .{i}),
};
}
fn isPage(comptime T: type) bool {
return T == *Page or T == *const Page;
}
fn createTypeException(isolate: v8.Isolate, msg: []const u8) v8.Value {
return v8.Exception.initTypeError(v8.String.initUtf8(isolate, msg));
}

View File

@@ -25,6 +25,7 @@ const js = @import("js.zig");
const v8 = js.v8;
const Env = @import("Env.zig");
const bridge = @import("bridge.zig");
const Context = @import("Context.zig");
const Page = @import("../Page.zig");
@@ -81,13 +82,11 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context
temp_scope.init(isolate);
defer temp_scope.deinit();
// Getting this into the snapshot is tricky (anything involving the
// global is tricky). Easier to do here
const func_tmpl_handle = isolate.createFunctionTemplateHandle();
const global_template = v8.v8__FunctionTemplate__InstanceTemplate(func_tmpl_handle).?;
var configuration: v8.NamedPropertyHandlerConfiguration = .{
.getter = unknownPropertyCallback,
const global_template = @import("Snapshot.zig").createGlobalTemplate(isolate.handle, env.templates);
v8.v8__ObjectTemplate__SetNamedHandler(global_template, &.{
.getter = bridge.unknownPropertyCallback,
.setter = null,
.query = null,
.deleter = null,
@@ -96,10 +95,9 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context
.descriptor = null,
.data = null,
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
};
v8.v8__ObjectTemplate__SetNamedHandler(global_template, &configuration);
});
const context_handle = isolate.createContextHandle(null, null);
const context_handle = v8.v8__Context__New(isolate.handle, global_template, null).?;
break :blk js.Global(Context).init(isolate.handle, context_handle);
};
@@ -169,60 +167,3 @@ pub fn terminateExecution(self: *const ExecutionWorld) void {
pub fn resumeExecution(self: *const ExecutionWorld) void {
self.env.isolate.cancelTerminateExecution();
}
pub fn unknownPropertyCallback(c_name: ?*const v8.Name, raw_info: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const isolate_handle = v8.v8__PropertyCallbackInfo__GetIsolate(raw_info).?;
const context = Context.fromIsolate(.{ .handle = isolate_handle });
const property: ?[]u8 = context.valueToString(.{ .ctx = context, .handle = c_name.? }, .{}) catch {
return v8.Intercepted.No;
};
const ignored = std.StaticStringMap(void).initComptime(.{
.{ "process", {} },
.{ "ShadyDOM", {} },
.{ "ShadyCSS", {} },
.{ "litNonce", {} },
.{ "litHtmlVersions", {} },
.{ "litElementVersions", {} },
.{ "litHtmlPolyfillSupport", {} },
.{ "litElementHydrateSupport", {} },
.{ "litElementPolyfillSupport", {} },
.{ "reactiveElementVersions", {} },
.{ "recaptcha", {} },
.{ "grecaptcha", {} },
.{ "___grecaptcha_cfg", {} },
.{ "__recaptcha_api", {} },
.{ "__google_recaptcha_client", {} },
.{ "CLOSURE_FLAGS", {} },
});
if (!ignored.has(property)) {
const page = context.page;
const document = page.document;
if (document.getElementById(property, page)) |el| {
const js_value = context.zigValueToJs(el, .{}) catch {
return v8.Intercepted.No;
};
info.getReturnValue().set(js_value);
return v8.Intercepted.Yes;
}
if (comptime IS_DEBUG) {
log.debug(.unknown_prop, "unknown global property", .{
.info = "but the property can exist in pure JS",
.stack = context.stackTrace() catch "???",
.property = property,
});
}
}
// not intercepted
return 0;
}

View File

@@ -116,15 +116,15 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
// need to increase the call_depth so that the call_arena remains valid for
// the duration of the function call. If we don't do this, the call_arena
// will be reset after each statement of the function which executes Zig code.
const call_depth = context.call_depth;
context.call_depth = call_depth + 1;
defer context.call_depth = call_depth;
const call_depth = ctx.call_depth;
ctx.call_depth = call_depth + 1;
defer ctx.call_depth = call_depth;
const js_this = blk: {
if (@TypeOf(this) == js.Object) {
break :blk this.js_obj;
break :blk this;
}
break :blk try context.zigValueToJs(this, .{});
break :blk try ctx.zigValueToJs(this, .{});
};
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;

View File

@@ -97,8 +97,8 @@ pub fn init(self: *Inspector, isolate: *v8.Isolate, ctx: anytype) !void {
pub fn deinit(self: *const Inspector) void {
var temp_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&temp_scope, self.isolate);
defer temp_scope.deinit();
v8.v8__HandleScope__CONSTRUCT(&temp_scope, self.isolate);
defer v8.v8__HandleScope__DESTRUCT(&temp_scope);
self.session.deinit();
self.client.deinit();

View File

@@ -113,14 +113,6 @@ pub fn initNumber(self: Isolate, val: anytype) js.Number {
return js.Number.init(self.handle, val);
}
pub fn createContextHandle(self: Isolate, global_tmpl: ?*const v8.ObjectTemplate, global_obj: ?*const v8.Value) *const v8.Context {
return v8.v8__Context__New(self.handle, global_tmpl, global_obj).?;
}
pub fn createFunctionTemplateHandle(self: Isolate) *const v8.FunctionTemplate {
return v8.v8__FunctionTemplate__New__DEFAULT(self.handle).?;
}
pub fn createExternal(self: Isolate, val: *anyopaque) *const v8.External {
return v8.v8__External__New(self.handle, val).?;
}

View File

@@ -114,19 +114,18 @@ fn isValid(self: Snapshot) bool {
return v8.v8__StartupData__IsValid(self.startup_data);
}
<<<<<<< HEAD
pub fn createGlobalTemplate(isolate: v8.Isolate, templates: []const v8.FunctionTemplate) v8.ObjectTemplate {
pub fn createGlobalTemplate(isolate: *v8.Isolate, templates: anytype) *const v8.ObjectTemplate {
// Set up the global template to inherit from Window's template
// This way the global object gets all Window properties through inheritance
const js_global = v8.c.v8__FunctionTemplate__New__DEFAULT(isolate);
const window_name = v8.c.v8__String__NewFromUtf8(isolate, "Window", v8.c.kNormal, 6);
v8.c.v8__FunctionTemplate__SetClassName(js_global, window_name);
const js_global = v8.v8__FunctionTemplate__New__DEFAULT(isolate);
const window_name = v8.v8__String__NewFromUtf8(isolate, "Window", v8.kNormal, 6);
v8.v8__FunctionTemplate__SetClassName(js_global, window_name);
// Find Window in JsApis by name (avoids circular import)
const window_index = comptime bridge.JsApiLookup.getId(Window.JsApi);
v8.c.v8__FunctionTemplate__Inherit(js_global, templates[window_index]);
v8.v8__FunctionTemplate__Inherit(js_global, templates[window_index]);
return v8.c.v8__FunctionTemplate__InstanceTemplate(js_global);
return v8.v8__FunctionTemplate__InstanceTemplate(js_global).?;
}
pub fn create() !Snapshot {
@@ -406,8 +405,9 @@ fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *v8.FunctionT
// Attaches JsApi members to the prototype template (normal case)
fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.FunctionTemplate) void {
const target = v8.c.v8__FunctionTemplate__PrototypeTemplate(template);
const instance = v8.c.v8__FunctionTemplate__InstanceTemplate(template);
const target = v8.v8__FunctionTemplate__PrototypeTemplate(template);
const instance = v8.v8__FunctionTemplate__InstanceTemplate(template);
const declarations = @typeInfo(JsApi).@"struct".decls;
inline for (declarations) |d| {
@@ -495,13 +495,14 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
}
if (@hasDecl(JsApi.Meta, "htmldda")) {
v8.c.v8__ObjectTemplate__MarkAsUndetectable(instance);
v8.c.v8__ObjectTemplate__SetCallAsFunctionHandler(instance, JsApi.Meta.callable.func);
v8.v8__ObjectTemplate__MarkAsUndetectable(instance);
v8.v8__ObjectTemplate__SetCallAsFunctionHandler(instance, JsApi.Meta.callable.func);
}
if (@hasDecl(JsApi.Meta, "name")) {
const js_name = v8.Symbol.getToStringTag(isolate).toName();
instance.set(js_name, v8.String.initUtf8(isolate, JsApi.Meta.name), v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
const js_name = v8.v8__Symbol__GetToStringTag(isolate);
const js_value = v8.v8__String__NewFromUtf8(isolate, JsApi.Meta.name.ptr, v8.kNormal, @intCast(JsApi.Meta.name.len));
v8.v8__Template__Set(@ptrCast(instance), js_name, js_value, v8.ReadOnly + v8.DontDelete);
}
}

View File

@@ -42,14 +42,6 @@ pub fn isArray(self: Value) bool {
return v8.v8__Value__IsArray(self.handle);
}
pub fn isNull(self: Value) bool {
return self.js_val.isNull();
}
pub fn isUndefined(self: Value) bool {
return self.js_val.isUndefined();
}
pub fn isSymbol(self: Value) bool {
return v8.v8__Value__IsSymbol(self.handle);
}
@@ -168,7 +160,7 @@ pub fn toBool(self: Value) bool {
pub fn typeOf(self: Value) js.String {
const str_handle = v8.v8__Value__TypeOf(self.handle, self.ctx.isolate.handle).?;
return js.String{ .ctx = self.ctx) .handle = str_handle };
return js.String{ .ctx = self.ctx, .handle = str_handle };
}
pub fn toF32(self: Value) !f32 {
@@ -265,7 +257,7 @@ pub fn persist(self: Value) !Value {
}
pub fn toZig(self: Value, comptime T: type) !T {
return self.context.jsValueToZig(T, self.js_val);
return self.ctx.jsValueToZig(T, self);
}
pub fn toObject(self: Value) js.Object {
@@ -307,15 +299,3 @@ pub fn format(self: Value, writer: *std.Io.Writer) !void {
const str = self.toString(.{}) catch return error.WriteFailed;
return writer.writeAll(str);
}
pub fn persist(self: Value) !Value {
var ctx = self.ctx;
const global = js.Global(Value).init(ctx.isolate.handle, self.handle);
try ctx.global_values.append(ctx.arena, global);
return .{
.ctx = ctx,
.handle = global.local(),
};
}

View File

@@ -892,6 +892,62 @@ pub const Property = union(enum) {
int: i64,
};
pub fn unknownPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const isolate_handle = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
const context = Context.fromIsolate(.{ .handle = isolate_handle });
const property: []const u8 = context.valueToString(.{ .ctx = context, .handle = c_name.? }, .{}) catch {
return 0;
};
const ignored = std.StaticStringMap(void).initComptime(.{
.{ "process", {} },
.{ "ShadyDOM", {} },
.{ "ShadyCSS", {} },
.{ "litNonce", {} },
.{ "litHtmlVersions", {} },
.{ "litElementVersions", {} },
.{ "litHtmlPolyfillSupport", {} },
.{ "litElementHydrateSupport", {} },
.{ "litElementPolyfillSupport", {} },
.{ "reactiveElementVersions", {} },
.{ "recaptcha", {} },
.{ "grecaptcha", {} },
.{ "___grecaptcha_cfg", {} },
.{ "__recaptcha_api", {} },
.{ "__google_recaptcha_client", {} },
.{ "CLOSURE_FLAGS", {} },
});
if (!ignored.has(property)) {
const page = context.page;
const document = page.document;
if (document.getElementById(property, page)) |el| {
const js_value = context.zigValueToJs(el, .{}) catch {
return 0;
};
var pc = PropertyCallbackInfo{ .handle = handle.? };
pc.getReturnValue().set(js_value);
return 1;
}
if (comptime IS_DEBUG) {
log.debug(.unknown_prop, "unknown global property", .{
.info = "but the property can exist in pure JS",
.stack = context.stackTrace() catch "???",
.property = property,
});
}
}
// not intercepted
return 0;
}
// Given a Type, returns the length of the prototype chain, including self
fn prototypeChainLength(comptime T: type) usize {
var l: usize = 1;