From 1b0ea445196a73508b4ae46816c207ff6c199258 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 9 Jan 2026 18:02:18 +0800 Subject: [PATCH] merge main --- src/browser/js/Caller.zig | 519 ------------------------------ src/browser/js/ExecutionWorld.zig | 71 +--- src/browser/js/Function.zig | 10 +- src/browser/js/Inspector.zig | 4 +- src/browser/js/Isolate.zig | 8 - src/browser/js/Snapshot.zig | 27 +- src/browser/js/Value.zig | 24 +- src/browser/js/bridge.zig | 56 ++++ 8 files changed, 85 insertions(+), 634 deletions(-) delete mode 100644 src/browser/js/Caller.zig diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig deleted file mode 100644 index 63f24e22..00000000 --- a/src/browser/js/Caller.zig +++ /dev/null @@ -1,519 +0,0 @@ -// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) -// -// Francis Bouvier -// Pierre Tachoire -// -// 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 . - -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)); -} diff --git a/src/browser/js/ExecutionWorld.zig b/src/browser/js/ExecutionWorld.zig index bc016c00..e6f08bf6 100644 --- a/src/browser/js/ExecutionWorld.zig +++ b/src/browser/js/ExecutionWorld.zig @@ -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; -} diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig index a08222e8..61259fc9 100644 --- a/src/browser/js/Function.zig +++ b/src/browser/js/Function.zig @@ -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; diff --git a/src/browser/js/Inspector.zig b/src/browser/js/Inspector.zig index 67b06f64..c518a4b9 100644 --- a/src/browser/js/Inspector.zig +++ b/src/browser/js/Inspector.zig @@ -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(); diff --git a/src/browser/js/Isolate.zig b/src/browser/js/Isolate.zig index dcfeb157..fdede915 100644 --- a/src/browser/js/Isolate.zig +++ b/src/browser/js/Isolate.zig @@ -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).?; } diff --git a/src/browser/js/Snapshot.zig b/src/browser/js/Snapshot.zig index 410f4771..c48c689e 100644 --- a/src/browser/js/Snapshot.zig +++ b/src/browser/js/Snapshot.zig @@ -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); } } diff --git a/src/browser/js/Value.zig b/src/browser/js/Value.zig index 2c81942d..54415df0 100644 --- a/src/browser/js/Value.zig +++ b/src/browser/js/Value.zig @@ -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(), - }; -} diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 2d2adace..c875ad34 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -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;