From 62aa564df19fc7ea47df253713965fdf1ac80e46 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 15 Jan 2026 08:52:13 +0800 Subject: [PATCH] Remove Global v8::Local When we create a js.Context, we create the underlying v8.Context and store it for the duration of the page lifetime. This works because we have a global HandleScope - the v8.Context (which is really a v8::Local) is that to the global HandleScope, effectively making it a global. If we want to remove our global HandleScope, then we can no longer pin the v8.Context in our js.Context. Our js.Context now only holds a v8.Global of the v8.Context (v8::Global zig calls, we create a js.Caller (as always) 2 - For zig -> v8 calls, we go through the js.Context (as always) 3 - The shared functionality, which works on a v8.Context, now belongs to js.Local For #1 (v8 -> zig), creating a js.Local for a js.Caller is really simple and centralized. v8 largely gives us everything we need from the FunctionCallbackInfo or PropertyCallbackInfo. For #2, it's messier, because we can only create a local v8::Context if we have a HandleScope, which we may or may not. Unfortunately, in many cases, what to do becomes the responsibility of the caller and much of the code has to become aware of this local-ness. What does it means for our code? The impact is on WebAPIs that store .Global. Because the global can't do anything. You always need to convert that .Global to a local (e.g. js.Function.Global -> js.Function). If you're 100% sure the WebAPI is only being invoked by a v8 callback, you can use `page.js.local.?.toLocal(some_global).call(...)` to get the local value. If you're 100% sure the WebAPI is only being invoked by Zig, you need to create `js.Local.Scope` to get access to a local: ```zig var ls: js.Local.Scope = undefined; page.js.localScope(&ls); defer ls.deinit(); ls.toLocal(some_global).call(...) // can also access `&ls.local` for APIs that require a *const js.Local ``` For functions that can be invoked by either V8 or Zig, you should generally push the responsibility to the caller by accepting a `local: *const js.Local`. If the caller is a v8 callback, it can pass `page.js.local.?`. If the caller is a Zig callback, it can create a `Local.Scope`. As an alternative, it is possible to simply pass the *Page, and check `if page.js.local == null` and, if so, create a Local.Scope. But this should only be done for performance reasons. We currently only do this in 1 place, and it's because the Zig caller doesn't know whether a Local will actually be needed and it's potentially called on every element creating from the parser. --- src/browser/EventManager.zig | 12 +- src/browser/Page.zig | 25 +- src/browser/Scheduler.zig | 1 + src/browser/ScriptManager.zig | 44 +- src/browser/js/Array.zig | 20 +- src/browser/js/Caller.zig | 535 ++++++ src/browser/js/Context.zig | 1639 +++-------------- src/browser/js/Env.zig | 18 +- src/browser/js/ExecutionWorld.zig | 37 +- src/browser/js/Function.zig | 65 +- src/browser/js/Inspector.zig | 36 +- src/browser/js/Local.zig | 1325 +++++++++++++ src/browser/js/Module.zig | 33 +- src/browser/js/Object.zig | 78 +- src/browser/js/Promise.zig | 32 +- src/browser/js/PromiseResolver.zig | 44 +- src/browser/js/String.zig | 6 +- src/browser/js/TaggedOpaque.zig | 156 ++ src/browser/js/TryCatch.zig | 25 +- src/browser/js/Value.zig | 60 +- src/browser/js/bridge.zig | 688 +------ src/browser/js/global.zig | 48 - src/browser/js/js.zig | 68 +- src/browser/tests/history.html | 2 +- src/browser/tests/history_after_nav.skip.html | 5 - src/browser/webapi/AbortController.zig | 2 +- src/browser/webapi/AbortSignal.zig | 21 +- src/browser/webapi/Blob.zig | 6 +- src/browser/webapi/Console.zig | 4 +- src/browser/webapi/CustomElementRegistry.zig | 19 +- src/browser/webapi/DOMNodeIterator.zig | 14 +- src/browser/webapi/DOMTreeWalker.zig | 40 +- src/browser/webapi/Document.zig | 2 +- src/browser/webapi/History.zig | 5 +- src/browser/webapi/IntersectionObserver.zig | 7 +- src/browser/webapi/MessagePort.zig | 12 +- src/browser/webapi/MutationObserver.zig | 6 +- src/browser/webapi/NodeFilter.zig | 4 +- src/browser/webapi/PerformanceObserver.zig | 7 +- src/browser/webapi/Window.zig | 16 +- src/browser/webapi/animation/Animation.zig | 18 +- src/browser/webapi/css/CSSStyleSheet.zig | 2 +- src/browser/webapi/element/html/Body.zig | 4 +- src/browser/webapi/element/html/Custom.zig | 32 +- src/browser/webapi/element/html/Script.zig | 8 +- src/browser/webapi/event/PopStateEvent.zig | 2 +- src/browser/webapi/navigation/Navigation.zig | 23 +- .../navigation/NavigationEventTarget.zig | 3 +- .../navigation/NavigationHistoryEntry.zig | 2 +- src/browser/webapi/net/Fetch.zig | 19 +- src/browser/webapi/net/Response.zig | 9 +- src/browser/webapi/net/XMLHttpRequest.zig | 58 +- .../webapi/net/XMLHttpRequestEventTarget.zig | 5 +- src/browser/webapi/streams/ReadableStream.zig | 38 +- .../ReadableStreamDefaultController.zig | 16 +- .../streams/ReadableStreamDefaultReader.zig | 10 +- src/cdp/domains/dom.zig | 37 +- src/cdp/domains/page.zig | 22 +- src/cdp/domains/target.zig | 7 +- src/testing.zig | 9 +- 60 files changed, 2878 insertions(+), 2613 deletions(-) create mode 100644 src/browser/js/Caller.zig create mode 100644 src/browser/js/Local.zig create mode 100644 src/browser/js/TaggedOpaque.zig delete mode 100644 src/browser/js/global.zig diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 0854bdce..93227ad5 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -367,14 +367,18 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe event._target = getAdjustedTarget(original_target, current_target); } + var ls: js.Local.Scope = undefined; + page.js.localScope(&ls); + defer ls.deinit(); + switch (listener.function) { - .value => |value| try value.local().callWithThis(void, current_target, .{event}), + .value => |value| try ls.toLocal(value).callWithThis(void, current_target, .{event}), .string => |string| { const str = try page.call_arena.dupeZ(u8, string.str()); - try self.page.js.eval(str, null); + try ls.local.eval(str, null); }, - .object => |*obj_global| { - const obj = obj_global.local(); + .object => |obj_global| { + const obj = ls.toLocal(obj_global); if (try obj.getFunction("handleEvent")) |handleEvent| { try handleEvent.callWithThis(void, obj, .{event}); } diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 65e6ca0a..ea3966c8 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -561,21 +561,24 @@ fn _documentIsComplete(self: *Page) !void { const event = try Event.initTrusted("load", .{}, self); // this event is weird, it's dispatched directly on the window, but // with the document as the target + + var ls: JS.Local.Scope = undefined; + self.js.localScope(&ls); + defer ls.deinit(); + event._target = self.document.asEventTarget(); - const on_load = if (self.window._on_load) |*g| g.local() else null; try self._event_manager.dispatchWithFunction( self.window.asEventTarget(), event, - on_load, + ls.toLocal(self.window._on_load), .{ .inject_target = false, .context = "page load" }, ); const pageshow_event = try PageTransitionEvent.initTrusted("pageshow", .{}, self); - const on_pageshow = if (self.window._on_pageshow) |*g| g.local() else null; try self._event_manager.dispatchWithFunction( self.window.asEventTarget(), pageshow_event.asEvent(), - on_pageshow, + ls.toLocal(self.window._on_pageshow), .{ .context = "page show" }, ); } @@ -747,10 +750,6 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult { var timer = try std.time.Timer.start(); var ms_remaining = wait_ms; - var try_catch: JS.TryCatch = undefined; - try_catch.init(self.js); - defer try_catch.deinit(); - var scheduler = &self.scheduler; var http_client = self._session.browser.http_client; @@ -805,10 +804,6 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult { // it AFTER. const ms_to_next_task = try scheduler.run(); - if (try_catch.caught(self.call_arena)) |caught| { - log.info(.js, "page wait", .{ .caught = caught, .src = "scheduler" }); - } - const http_active = http_client.active; const total_network_activity = http_active + http_client.intercepted; if (self._notified_network_almost_idle.check(total_network_activity <= 2)) { @@ -1998,8 +1993,12 @@ pub fn createElementNS(self: *Page, namespace: Element.Namespace, name: []const self._upgrading_element = node; defer self._upgrading_element = prev_upgrading; + var ls: JS.Local.Scope = undefined; + self.js.localScope(&ls); + defer ls.deinit(); + var caught: JS.TryCatch.Caught = undefined; - _ = def.constructor.local().newInstance(&caught) catch |err| { + _ = ls.toLocal(def.constructor).newInstance(&caught) catch |err| { log.warn(.js, "custom element constructor", .{ .name = name, .err = err, .caught = caught }); return node; }; diff --git a/src/browser/Scheduler.zig b/src/browser/Scheduler.zig index 39e524cc..6c963dbb 100644 --- a/src/browser/Scheduler.zig +++ b/src/browser/Scheduler.zig @@ -19,6 +19,7 @@ const std = @import("std"); const builtin = @import("builtin"); +const js = @import("js/js.zig"); const log = @import("../log.zig"); const milliTimestamp = @import("../datetime.zig").milliTimestamp; diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 62507138..e9139238 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -270,11 +270,15 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e }); if (comptime IS_DEBUG) { + var ls: js.Local.Scope = undefined; + page.js.localScope(&ls); + defer ls.deinit(); + log.debug(.http, "script queue", .{ .ctx = ctx, .url = remote_url.?, .element = element, - .stack = page.js.stackTrace() catch "???", + .stack = ls.local.stackTrace() catch "???", }); } } @@ -356,11 +360,15 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers); if (comptime IS_DEBUG) { + var ls: js.Local.Scope = undefined; + self.page.js.localScope(&ls); + defer ls.deinit(); + log.debug(.http, "script queue", .{ .url = url, .ctx = "module", .referrer = referrer, - .stack = self.page.js.stackTrace() catch "???", + .stack = ls.local.stackTrace() catch "???", }); } @@ -447,11 +455,15 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers); if (comptime IS_DEBUG) { + var ls: js.Local.Scope = undefined; + self.page.js.localScope(&ls); + defer ls.deinit(); + log.debug(.http, "script queue", .{ .url = url, .ctx = "dynamic module", .referrer = referrer, - .stack = self.page.js.stackTrace() catch "???", + .stack = ls.local.stackTrace() catch "???", }); } @@ -782,6 +794,12 @@ pub const Script = struct { .cacheable = cacheable, }); + var ls: js.Local.Scope = undefined; + page.js.localScope(&ls); + defer ls.deinit(); + + const local = &ls.local; + // Handle importmap special case here: the content is a JSON containing // imports. if (self.kind == .importmap) { @@ -792,25 +810,24 @@ pub const Script = struct { .kind = self.kind, .cacheable = cacheable, }); - self.executeCallback("error", script_element._on_error, page); + self.executeCallback("error", local.toLocal(script_element._on_error), page); return; }; - self.executeCallback("load", script_element._on_load, page); + self.executeCallback("load", local.toLocal(script_element._on_load), page); return; } - const js_context = page.js; var try_catch: js.TryCatch = undefined; - try_catch.init(js_context); + try_catch.init(local); defer try_catch.deinit(); const success = blk: { const content = self.source.content(); switch (self.kind) { - .javascript => _ = js_context.eval(content, url) catch break :blk false, + .javascript => _ = local.eval(content, url) catch break :blk false, .module => { // We don't care about waiting for the evaluation here. - js_context.module(false, content, url, cacheable) catch break :blk false; + page.js.module(false, local, content, url, cacheable) catch break :blk false; }, .importmap => unreachable, // handled before the try/catch. } @@ -830,7 +847,7 @@ pub const Script = struct { } if (success) { - self.executeCallback("load", script_element._on_load, page); + self.executeCallback("load", local.toLocal(script_element._on_load), page); return; } @@ -841,12 +858,11 @@ pub const Script = struct { .cacheable = cacheable, }); - self.executeCallback("error", script_element._on_error, page); + self.executeCallback("error", local.toLocal(script_element._on_error), page); } - fn executeCallback(self: *const Script, comptime typ: []const u8, cb_: ?js.Function.Global, page: *Page) void { - const cb_global = cb_ orelse return; - const cb = cb_global.local(); + fn executeCallback(self: *const Script, comptime typ: []const u8, cb_: ?js.Function, page: *Page) void { + const cb = cb_ orelse return; const Event = @import("webapi/Event.zig"); const event = Event.initTrusted(typ, .{}, page) catch |err| { diff --git a/src/browser/js/Array.zig b/src/browser/js/Array.zig index 98442468..69137f0e 100644 --- a/src/browser/js/Array.zig +++ b/src/browser/js/Array.zig @@ -22,7 +22,7 @@ const v8 = js.v8; const Array = @This(); -ctx: *js.Context, +local: *const js.Local, handle: *const v8.Array, pub fn len(self: Array) usize { @@ -30,39 +30,37 @@ pub fn len(self: Array) usize { } pub fn get(self: Array, index: u32) !js.Value { - const ctx = self.ctx; + const ctx = self.local.ctx; const idx = js.Integer.init(ctx.isolate.handle, index); - const handle = v8.v8__Object__Get(@ptrCast(self.handle), ctx.handle, idx.handle) orelse { + const handle = v8.v8__Object__Get(@ptrCast(self.handle), self.local.handle, idx.handle) orelse { return error.JsException; }; return .{ - .ctx = self.ctx, + .local = self.local, .handle = handle, }; } -pub fn set(self: Array, index: u32, value: anytype, comptime opts: js.bridge.Caller.CallOpts) !bool { - const ctx = self.ctx; - - const js_value = try ctx.zigValueToJs(value, opts); +pub fn set(self: Array, index: u32, value: anytype, comptime opts: js.Caller.CallOpts) !bool { + const js_value = try self.local.zigValueToJs(value, opts); var out: v8.MaybeBool = undefined; - v8.v8__Object__SetAtIndex(@ptrCast(self.handle), ctx.handle, index, js_value.handle, &out); + v8.v8__Object__SetAtIndex(@ptrCast(self.handle), self.local.handle, index, js_value.handle, &out); return out.has_value; } pub fn toObject(self: Array) js.Object { return .{ - .ctx = self.ctx, + .local = self.local, .handle = @ptrCast(self.handle), }; } pub fn toValue(self: Array) js.Value { return .{ - .ctx = self.ctx, + .local = self.local, .handle = @ptrCast(self.handle), }; } diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig new file mode 100644 index 00000000..517c6f75 --- /dev/null +++ b/src/browser/js/Caller.zig @@ -0,0 +1,535 @@ +// Copyright (C) 2023-2026 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 Page = @import("../Page.zig"); + +const js = @import("js.zig"); +const bridge = @import("bridge.zig"); +const Context = @import("Context.zig"); +const TaggedOpaque = @import("TaggedOpaque.zig"); + +const v8 = js.v8; +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; + +const CALL_ARENA_RETAIN = 1024 * 16; +const IS_DEBUG = @import("builtin").mode == .Debug; + +const Caller = @This(); +local: js.Local, +prev_local: ?*const js.Local, + +// Takes the raw v8 isolate and extracts the context from it. +pub fn init(self: *Caller, v8_isolate: *v8.Isolate) void { + const v8_context_handle = v8.v8__Isolate__GetCurrentContext(v8_isolate); + + const embedder_data = v8.v8__Context__GetEmbedderData(v8_context_handle, 1); + var lossless: bool = undefined; + const ctx: *Context = @ptrFromInt(v8.v8__BigInt__Uint64Value(embedder_data, &lossless)); + + ctx.call_depth += 1; + self.* = Caller{ + .local = .{ + .ctx = ctx, + .handle = v8_context_handle.?, + .call_arena = ctx.call_arena, + .isolate = .{ .handle = v8_isolate }, + }, + .prev_local = ctx.local, + }; + ctx.local = &self.local; +} + +pub fn deinit(self: *Caller) void { + const ctx = self.local.ctx; + const call_depth = ctx.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(ctx.call_arena.ptr)); + _ = arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN }); + } + + ctx.call_depth = call_depth; + ctx.local = self.prev_local; +} + +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, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void { + const info = FunctionCallbackInfo{ .handle = handle }; + self._constructor(func, info) catch |err| { + self.handleError(T, @TypeOf(func), err, info, opts); + }; +} + +fn _constructor(self: *Caller, func: anytype, info: 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_handle = info.getThis(); + var this = js.Object{ .local = &self.local, .handle = new_this_handle }; + if (@typeInfo(ReturnType) == .error_union) { + const non_error_res = res catch |err| return err; + this = try self.local.mapZigInstanceToJs(new_this_handle, non_error_res); + } else { + this = try self.local.mapZigInstanceToJs(new_this_handle, res); + } + + // 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 prototype_handle = v8.v8__Object__GetPrototype(new_this_handle).?; + var out: v8.MaybeBool = undefined; + v8.v8__Object__SetPrototype(this.handle, self.local.handle, prototype_handle, &out); + if (comptime IS_DEBUG) { + std.debug.assert(out.has_value and out.value); + } + } + + info.getReturnValue().set(this.handle); +} + +pub fn method(self: *Caller, comptime T: type, func: anytype, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void { + const info = FunctionCallbackInfo{ .handle = handle }; + self._method(T, func, info, opts) catch |err| { + self.handleError(T, @TypeOf(func), err, info, opts); + }; +} + +fn _method(self: *Caller, comptime T: type, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) !void { + const F = @TypeOf(func); + var handle_scope: js.HandleScope = undefined; + handle_scope.init(self.local.isolate); + defer handle_scope.deinit(); + + var args = try self.getArgs(F, 1, info); + @field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis()); + const res = @call(.auto, func, args); + info.getReturnValue().set(try self.local.zigValueToJs(res, opts)); +} + +pub fn function(self: *Caller, comptime T: type, func: anytype, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void { + const info = FunctionCallbackInfo{ .handle = handle }; + self._function(func, info, opts) catch |err| { + self.handleError(T, @TypeOf(func), err, info, opts); + }; +} + +fn _function(self: *Caller, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) !void { + const F = @TypeOf(func); + const args = try self.getArgs(F, 0, info); + const res = @call(.auto, func, args); + info.getReturnValue().set(try self.local.zigValueToJs(res, opts)); +} + +pub fn getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 { + const info = PropertyCallbackInfo{ .handle = handle }; + return self._getIndex(T, func, idx, info, opts) catch |err| { + self.handleError(T, @TypeOf(func), err, info, opts); + // not intercepted + return 0; + }; +} + +fn _getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 { + const F = @TypeOf(func); + var args = try self.getArgs(F, 2, info); + @field(args, "0") = try TaggedOpaque.fromJS(*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: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 { + const info = PropertyCallbackInfo{ .handle = handle }; + return self._getNamedIndex(T, func, name, info, opts) catch |err| { + self.handleError(T, @TypeOf(func), err, info, opts); + // not intercepted + return 0; + }; +} + +fn _getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 { + const F = @TypeOf(func); + var args = try self.getArgs(F, 2, info); + @field(args, "0") = try TaggedOpaque.fromJS(*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: *const v8.Name, js_value: *const v8.Value, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 { + const info = PropertyCallbackInfo{ .handle = handle }; + return self._setNamedIndex(T, func, name, .{ .local = &self.local, .handle = js_value }, info, opts) catch |err| { + self.handleError(T, @TypeOf(func), err, info, opts); + // not intercepted + return 0; + }; +} + +fn _setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, js_value: js.Value, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 { + const F = @TypeOf(func); + var args: ParameterTypes(F) = undefined; + @field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis()); + @field(args, "1") = try self.nameToString(name); + @field(args, "2") = try self.local.jsValueToZig(@TypeOf(@field(args, "2")), js_value); + if (@typeInfo(F).@"fn".params.len == 4) { + @field(args, "3") = self.local.ctx.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: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 { + const info = PropertyCallbackInfo{ .handle = handle }; + return self._deleteNamedIndex(T, func, name, info, opts) catch |err| { + self.handleError(T, @TypeOf(func), err, info, opts); + return 0; + }; +} + +fn _deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 { + const F = @TypeOf(func); + var args: ParameterTypes(F) = undefined; + @field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis()); + @field(args, "1") = try self.nameToString(name); + if (@typeInfo(F).@"fn".params.len == 3) { + @field(args, "2") = self.local.ctx.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: 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) { + // not intercepted + return 0; + } + } + self.handleError(T, F, err, info, opts); + // not intercepted + return 0; + }; + }, + else => ret, + }; + + if (comptime getter) { + info.getReturnValue().set(try self.local.zigValueToJs(non_error_ret, opts)); + } + // intercepted + return 1; +} + +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 nameToString(self: *const Caller, name: *const v8.Name) ![]const u8 { + return self.local.valueHandleToString(@ptrCast(name), .{}); +} + +fn handleError(self: *Caller, comptime T: type, comptime F: type, err: anyerror, info: anytype, comptime opts: CallOpts) void { + const isolate = self.local.isolate; + + if (comptime @import("builtin").mode == .Debug and @TypeOf(info) == FunctionCallbackInfo) { + if (log.enabled(.js, .warn)) { + self.logFunctionCallError(@typeName(T), @typeName(F), err, info); + } + } + + const js_err: *const v8.Value = switch (err) { + error.InvalidArgument => isolate.createTypeError("invalid argument"), + error.OutOfMemory => isolate.createError("out of memory"), + error.IllegalConstructor => isolate.createError("Illegal Contructor"), + else => blk: { + if (comptime opts.dom_exception) { + const DOMException = @import("../webapi/DOMException.zig"); + if (DOMException.fromError(err)) |ex| { + const value = self.local.zigValueToJs(ex, .{}) catch break :blk isolate.createError("internal error"); + break :blk value.handle; + } + } + break :blk isolate.createError(@errorName(err)); + }, + }; + + const js_exception = isolate.throwException(js_err); + info.getReturnValue().setValueHandle(js_exception); +} + +// 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 local = &self.local; + 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)) = local.ctx.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(@intCast(last_js_parameter), local); + 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 local.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1); + for (arr, last_js_parameter..) |*a, i| { + a.* = try local.jsValueToZig(slice_type, info.getArg(@intCast(i), local)); + } + @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_val = info.getArg(@intCast(i), local); + @field(args, tupleFieldName(field_index)) = local.jsValueToZig(param.type.?, js_val) 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: 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.local.stackTrace() catch |err1| @errorName(err1), + }); +} + +fn serializeFunctionArgs(self: *Caller, info: FunctionCallbackInfo) ![]const u8 { + const local = &self.local; + var buf = std.Io.Writer.Allocating.init(local.call_arena); + + const separator = log.separator(); + for (0..info.length()) |i| { + try buf.writer.print("{s}{d} - ", .{ separator, i + 1 }); + const js_value = info.getArg(@intCast(i), local); + try local.debugValue(js_value, &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; +} + +// These wrap the raw v8 C API to provide a cleaner interface. +pub const FunctionCallbackInfo = struct { + handle: *const v8.FunctionCallbackInfo, + + pub fn length(self: FunctionCallbackInfo) u32 { + return @intCast(v8.v8__FunctionCallbackInfo__Length(self.handle)); + } + + pub fn getArg(self: FunctionCallbackInfo, index: u32, local: *const js.Local) js.Value { + return .{ .local = local, .handle = v8.v8__FunctionCallbackInfo__INDEX(self.handle, @intCast(index)).? }; + } + + pub fn getThis(self: FunctionCallbackInfo) *const v8.Object { + return v8.v8__FunctionCallbackInfo__This(self.handle).?; + } + + pub fn getReturnValue(self: FunctionCallbackInfo) ReturnValue { + var rv: v8.ReturnValue = undefined; + v8.v8__FunctionCallbackInfo__GetReturnValue(self.handle, &rv); + return .{ .handle = rv }; + } +}; + +pub const PropertyCallbackInfo = struct { + handle: *const v8.PropertyCallbackInfo, + + pub fn getThis(self: PropertyCallbackInfo) *const v8.Object { + return v8.v8__PropertyCallbackInfo__This(self.handle).?; + } + + pub fn getReturnValue(self: PropertyCallbackInfo) ReturnValue { + var rv: v8.ReturnValue = undefined; + v8.v8__PropertyCallbackInfo__GetReturnValue(self.handle, &rv); + return .{ .handle = rv }; + } +}; + +const ReturnValue = struct { + handle: v8.ReturnValue, + + pub fn set(self: ReturnValue, value: anytype) void { + const T = @TypeOf(value); + if (T == *const v8.Object) { + self.setValueHandle(@ptrCast(value)); + } else if (T == *const v8.Value) { + self.setValueHandle(value); + } else if (T == js.Value) { + self.setValueHandle(value.handle); + } else { + @compileError("Unsupported type for ReturnValue.set: " ++ @typeName(T)); + } + } + + pub fn setValueHandle(self: ReturnValue, handle: *const v8.Value) void { + v8.v8__ReturnValue__Set(self.handle, handle); + } +}; diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index f19fccda..b42727c1 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -21,16 +21,16 @@ const std = @import("std"); const log = @import("../../log.zig"); const js = @import("js.zig"); -const v8 = js.v8; - const bridge = @import("bridge.zig"); -const Caller = bridge.Caller; +const TaggedOpaque = @import("TaggedOpaque.zig"); const Page = @import("../Page.zig"); const ScriptManager = @import("../ScriptManager.zig"); +const v8 = js.v8; +const Caller = js.Caller; + const Allocator = std.mem.Allocator; -const TaggedAnyOpaque = js.TaggedAnyOpaque; const IS_DEBUG = @import("builtin").mode == .Debug; @@ -40,11 +40,15 @@ const Context = @This(); id: usize, page: *Page, isolate: js.Isolate, -// This context is a persistent object. The persistent needs to be recovered and reset. -handle: *const v8.Context, + +// The v8::Global. When necessary, we can create a v8::Local<> +// from this, and we can free it when the context is done. +handle: v8.Global, + +// The Context Global (our Window) as a v8::Global. +global_global: v8.Global, handle_scope: ?js.HandleScope, - cpu_profiler: ?*v8.CpuProfiler = null, // references Env.templates @@ -62,12 +66,16 @@ call_arena: Allocator, // the call which is calling the callback. call_depth: usize = 0, +// When a Caller is active (V8->Zig callback), this points to its Local. +// When null, Zig->V8 calls must create a temporary Local with HandleScope. +local: ?*const js.Local = null, + // Serves two purposes. Like `global_objects`, this is used to free // every Global(Object) 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 Global(Object). // The key is the @intFromPtr of the Zig value -identity_map: std.AutoHashMapUnmanaged(usize, js.Global(js.Object)) = .empty, +identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, // Some web APIs have to manage opaque values. Ideally, they use an // js.Object, but the js.Object has no lifetime guarantee beyond the @@ -79,10 +87,10 @@ identity_map: std.AutoHashMapUnmanaged(usize, js.Global(js.Object)) = .empty, // we now simply persist every time persist() is called. global_values: std.ArrayList(v8.Global) = .empty, global_objects: std.ArrayList(v8.Global) = .empty, -global_modules: std.ArrayList(js.Global(js.Module)) = .empty, -global_promises: std.ArrayList(js.Global(js.Promise)) = .empty, +global_modules: std.ArrayList(v8.Global) = .empty, +global_promises: std.ArrayList(v8.Global) = .empty, global_functions: std.ArrayList(v8.Global) = .empty, -global_promise_resolvers: std.ArrayList(js.Global(js.PromiseResolver)) = .empty, +global_promise_resolvers: std.ArrayList(v8.Global) = .empty, // Our module cache: normalized module specifier => module. module_cache: std.StringHashMapUnmanaged(ModuleEntry) = .empty, @@ -100,22 +108,22 @@ script_manager: ?*ScriptManager, const ModuleEntry = struct { // Can be null if we're asynchrously loading the module, in // which case resolver_promise cannot be null. - module: ?js.Module = null, + module: ?js.Module.Global = null, // The promise of the evaluating module. The resolved value is // meaningless to us, but the resolver promise needs to chain // to this, since we need to know when it's complete. - module_promise: ?js.Promise = null, + module_promise: ?js.Promise.Global = null, // The promise for the resolver which is loading the module. // (AKA, the first time we try to load it). This resolver will // chain to the module_promise and, when it's done evaluating // will resolve its namespace. Any other attempt to load the // module willchain to this. - resolver_promise: ?js.Promise = null, + resolver_promise: ?js.Promise.Global = null, }; -pub fn fromC(c_context: *const v8.Context) *Context { +fn fromC(c_context: *const v8.Context) *Context { const data = v8.v8__Context__GetEmbedderData(c_context, 1).?; const big_int = js.BigInt{ .handle = @ptrCast(data) }; return @ptrFromInt(big_int.getUint64()); @@ -128,16 +136,11 @@ pub fn fromIsolate(isolate: js.Isolate) *Context { return @ptrFromInt(big_int.getUint64()); } -pub fn setupGlobal(self: *Context) !void { - const global = v8.v8__Context__Global(self.handle).?; - _ = try self.mapZigInstanceToJs(global, self.page.window); -} - pub fn deinit(self: *Context) void { { var it = self.identity_map.valueIterator(); - while (it.next()) |p| { - p.deinit(); + while (it.next()) |global| { + v8.v8__Global__Reset(global); } } @@ -150,7 +153,7 @@ pub fn deinit(self: *Context) void { } for (self.global_modules.items) |*global| { - global.deinit(); + v8.v8__Global__Reset(global); } for (self.global_functions.items) |*global| { @@ -158,29 +161,67 @@ pub fn deinit(self: *Context) void { } for (self.global_promises.items) |*global| { - global.deinit(); + v8.v8__Global__Reset(global); } for (self.global_promise_resolvers.items) |*global| { - global.deinit(); + v8.v8__Global__Reset(global); } - if (self.handle_scope) |*scope| { - v8.v8__Context__Exit(self.handle); - scope.deinit(); + if (self.handle_scope) |_| { + var ls: js.Local.Scope = undefined; + self.localScope(&ls); + defer ls.deinit(); + v8.v8__Context__Exit(ls.local.handle); } + + // v8.v8__Global__Reset(&self.global_global); + // v8.v8__Global__Reset(&self.handle); } -// == Executors == -pub fn eval(self: *Context, src: []const u8, name: ?[]const u8) !void { - _ = try self.exec(src, name); +// Any operation on the context have to be made from a local. +pub fn localScope(self: *Context, ls: *js.Local.Scope) void { + const isolate = self.isolate; + // TODO: add and init ls.hs for the handlescope + ls.local = .{ + .ctx = self, + .handle = @ptrCast(v8.v8__Global__Get(&self.handle, isolate.handle)), + .isolate = isolate, + .call_arena = self.call_arena, + }; } -pub fn exec(self: *Context, src: []const u8, name: ?[]const u8) !js.Value { - return self.compileAndRun(src, name); +pub fn toLocal(self: *Context, global: anytype) js.Local.ToLocalReturnType(@TypeOf(global)) { + const l = self.local orelse @panic("toLocal called without active Caller context"); + return l.toLocal(global); } -pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url: []const u8, cacheable: bool) !(if (want_result) ModuleEntry else void) { +// This isn't expected to be called often. It's for converting attributes into +// function calls, e.g. will turn that "doSomething" +// string into a js.Function which looks like: function(e) { doSomething(e) } +// There might be more efficient ways to do this, but doing it this way means +// our code only has to worry about js.Funtion, not some union of a js.Function +// or a string. +pub fn stringToPersistedFunction(self: *Context, str: []const u8) !js.Function.Global { + var ls: js.Local.Scope = undefined; + self.localScope(&ls); + defer ls.deinit(); + + var extra: []const u8 = ""; + const normalized = std.mem.trim(u8, str, &std.ascii.whitespace); + if (normalized.len > 0 and normalized[normalized.len - 1] != ')') { + extra = "(e)"; + } + const full = try std.fmt.allocPrintSentinel(self.call_arena, "(function(e) {{ {s}{s} }})", .{ normalized, extra }, 0); + + const js_val = try ls.local.compileAndRun(full, null); + if (!js_val.isFunction()) { + return error.StringFunctionError; + } + return try (js.Function{ .local = &ls.local, .handle = @ptrCast(js_val.handle) }).persist(); +} + +pub fn module(self: *Context, comptime want_result: bool, local: *const js.Local, src: []const u8, url: []const u8, cacheable: bool) !(if (want_result) ModuleEntry else void) { const mod, const owned_url = blk: { const arena = self.arena; @@ -199,7 +240,7 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url: } const owned_url = try arena.dupeZ(u8, url); - const m = try self.compileModule(src, owned_url); + const m = try compileModule(local, src, owned_url); if (cacheable) { // compileModule is synchronous - nothing can modify the cache during compilation @@ -213,7 +254,7 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url: break :blk .{ m, owned_url }; }; - try self.postCompileModule(mod, owned_url); + try self.postCompileModule(mod, owned_url, local); if (try mod.instantiate(resolveModuleCallback) == false) { return error.ModuleInstantiationError; @@ -260,31 +301,51 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url: return if (comptime want_result) entry.* else {}; } -// This isn't expected to be called often. It's for converting attributes into -// function calls, e.g. will turn that "doSomething" -// string into a js.Function which looks like: function(e) { doSomething(e) } -// There might be more efficient ways to do this, but doing it this way means -// our code only has to worry about js.Funtion, not some union of a js.Function -// or a string. -pub fn stringToFunction(self: *Context, str: []const u8) !js.Function { - var extra: []const u8 = ""; - const normalized = std.mem.trim(u8, str, &std.ascii.whitespace); - if (normalized.len > 0 and normalized[normalized.len - 1] != ')') { - extra = "(e)"; - } - const full = try std.fmt.allocPrintSentinel(self.call_arena, "(function(e) {{ {s}{s} }})", .{ normalized, extra }, 0); +fn compileModule(local: *const js.Local, src: []const u8, name: []const u8) !js.Module { + var origin_handle: v8.ScriptOrigin = undefined; + v8.v8__ScriptOrigin__CONSTRUCT2( + &origin_handle, + local.isolate.initStringHandle(name), + 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 + ); - const js_value = try self.compileAndRun(full, null); - if (!js_value.isFunction()) { - return error.StringFunctionError; - } - return self.newFunction(js_value); + var source_handle: v8.ScriptCompilerSource = undefined; + v8.v8__ScriptCompiler__Source__CONSTRUCT2( + local.isolate.initStringHandle(src), + &origin_handle, + null, // cached data + &source_handle, + ); + + defer v8.v8__ScriptCompiler__Source__DESTRUCT(&source_handle); + + const module_handle = v8.v8__ScriptCompiler__CompileModule( + local.isolate.handle, + &source_handle, + v8.kNoCompileOptions, + v8.kNoCacheNoReason, + ) orelse { + return error.JsException; + }; + + return .{ + .local = local, + .handle = module_handle, + }; } // After we compile a module, whether it's a top-level one, or a nested one, // we always want to track its identity (so that, if this module imports other // modules, we can resolve the full URL), and preload any dependent modules. -fn postCompileModule(self: *Context, mod: js.Module, url: [:0]const u8) !void { +fn postCompileModule(self: *Context, mod: js.Module, url: [:0]const u8, local: *const js.Local) !void { try self.module_identifier.putNoClobber(self.arena, mod.getIdentityHash(), url); // Non-async modules are blocking. We can download them in parallel, but @@ -294,7 +355,7 @@ fn postCompileModule(self: *Context, mod: js.Module, url: [:0]const u8) !void { const request_len = requests.len(); const script_manager = self.script_manager.?; for (0..request_len) |i| { - const specifier = try self.jsStringToZigZ(requests.get(i).specifier(), .{}); + const specifier = try local.jsStringToZigZ(requests.get(i).specifier(), .{}); const normalized_specifier = try script_manager.resolveSpecifier( self.call_arena, url, @@ -310,879 +371,19 @@ fn postCompileModule(self: *Context, mod: js.Module, url: [:0]const u8) !void { } } -// == Creators == -pub fn newFunction(self: *Context, js_value: js.Value) !js.Function { - // caller should have made sure this was a function - if (comptime IS_DEBUG) { - std.debug.assert(js_value.isFunction()); - } - +fn newFunctionWithData(local: *const js.Local, comptime callback: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void, data: *anyopaque) js.Function { + const external = local.isolate.createExternal(data); + const handle = v8.v8__Function__New__DEFAULT2(local.handle, callback, @ptrCast(external)).?; return .{ - .ctx = self, - .handle = @ptrCast(js_value.handle), - }; -} - -pub fn newString(self: *Context, str: []const u8) js.String { - return .{ - .ctx = self, - .handle = self.isolate.initStringHandle(str), - }; -} - -pub fn newObject(self: *Context) js.Object { - return .{ - .ctx = self, - .handle = v8.v8__Object__New(self.isolate.handle).?, - }; -} - -pub fn newArray(self: *Context, len: u32) js.Array { - return .{ - .ctx = self, - .handle = v8.v8__Array__New(self.isolate.handle, @intCast(len)).?, - }; -} - -fn newFunctionWithData(self: *Context, comptime callback: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void, data: *anyopaque) js.Function { - const external = self.isolate.createExternal(data); - const handle = v8.v8__Function__New__DEFAULT2(self.handle, callback, @ptrCast(external)).?; - return .{ - .ctx = self, + .local = local, .handle = handle, }; } -pub fn parseJSON(self: *Context, json: []const u8) !js.Value { - const string_handle = self.isolate.initStringHandle(json); - const value_handle = v8.v8__JSON__Parse(self.handle, string_handle) orelse return error.JsException; - return .{ - .ctx = self, - .handle = value_handle, - }; -} - -pub fn throw(self: *Context, err: []const u8) js.Exception { - const handle = self.isolate.createError(err); - return .{ - .ctx = self, - .handle = handle, - }; -} - -pub fn debugContextId(self: *const Context) i32 { - return v8.v8__Context__DebugContextId(self.handle); -} - -pub fn zigValueToJs(self: *Context, value: anytype, comptime opts: Caller.CallOpts) !js.Value { - const isolate = self.isolate; - - // 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 (js.simpleZigValueToJs(isolate, value, false, opts.null_as_undefined)) |js_value_handle| { - return .{ .ctx = self, .handle = js_value_handle }; - } - - const T = @TypeOf(value); - switch (@typeInfo(T)) { - .void, .bool, .int, .comptime_int, .float, .comptime_float, .@"enum", .null => { - // Need to do this to keep the compiler happy - // simpleZigValueToJs handles all of these cases. - unreachable; - }, - .array => { - var js_arr = self.newArray(value.len); - for (value, 0..) |v, i| { - if (try js_arr.set(@intCast(i), v, opts) == false) { - return error.FailedToCreateArray; - } - } - return js_arr.toValue(); - }, - .pointer => |ptr| switch (ptr.size) { - .one => { - if (@typeInfo(ptr.child) == .@"struct" and @hasDecl(ptr.child, "JsApi")) { - if (bridge.JsApiLookup.has(ptr.child.JsApi)) { - const js_obj = try self.mapZigInstanceToJs(null, value); - return js_obj.toValue(); - } - } - - if (@typeInfo(ptr.child) == .@"struct" and @hasDecl(ptr.child, "runtimeGenericWrap")) { - const wrap = try value.runtimeGenericWrap(self.page); - return zigValueToJs(self, wrap, opts); - } - - 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 = self.newArray(@intCast(value.len)); - for (value, 0..) |v, i| { - if (try js_arr.set(@intCast(i), v, opts) == false) { - return error.FailedToCreateArray; - } - } - return js_arr.toValue(); - }, - else => {}, - }, - .@"struct" => |s| { - if (@hasDecl(T, "JsApi")) { - if (bridge.JsApiLookup.has(T.JsApi)) { - const js_obj = try self.mapZigInstanceToJs(null, value); - return js_obj.toValue(); - } - } - - if (T == js.Function) { - // we're returning a callback - return .{ .ctx = self, .handle = @ptrCast(value.handle) }; - } - - if (T == js.Function.Global) { - // Auto-convert Global to local for bridge - return .{ .ctx = self, .handle = @ptrCast(value.local().handle) }; - } - - if (T == js.Object) { - // we're returning a v8.Object - return .{ .ctx = self, .handle = @ptrCast(value.handle) }; - } - - if (T == js.Object.Global) { - // Auto-convert Global to local for bridge - return .{ .ctx = self, .handle = @ptrCast(value.local().handle) }; - } - - if (T == js.Value.Global) { - // Auto-convert Global to local for bridge - return .{ .ctx = self, .handle = @ptrCast(value.local().handle) }; - } - - if (T == js.Value) { - return value; - } - - if (T == js.Promise) { - return .{ .ctx = self, .handle = @ptrCast(value.handle) }; - } - - if (T == js.Exception) { - return .{ .ctx = self, .handle = isolate.throwException(value.handle) }; - } - - if (T == js.String) { - return .{ .ctx = self, .handle = @ptrCast(value.handle) }; - } - - if (@hasDecl(T, "runtimeGenericWrap")) { - const wrap = try value.runtimeGenericWrap(self.page); - return zigValueToJs(self, wrap, opts); - } - - if (s.is_tuple) { - // return the tuple struct as an array - var js_arr = self.newArray(@intCast(s.fields.len)); - inline for (s.fields, 0..) |f, i| { - if (try js_arr.set(@intCast(i), @field(value, f.name), opts) == false) { - return error.FailedToCreateArray; - } - } - return js_arr.toValue(); - } - - const js_obj = self.newObject(); - inline for (s.fields) |f| { - if (try js_obj.set(f.name, @field(value, f.name), opts) == false) { - return error.CreateObjectFailure; - } - } - return js_obj.toValue(); - }, - .@"union" => |un| { - if (T == std.json.Value) { - return zigJsonToJs(self, value); - } - if (un.tag_type) |UnionTagType| { - inline for (un.fields) |field| { - if (value == @field(UnionTagType, field.name)) { - return self.zigValueToJs(@field(value, field.name), opts); - } - } - unreachable; - } - @compileError("Cannot use untagged union: " ++ @typeName(T)); - }, - .optional => { - if (value) |v| { - return self.zigValueToJs(v, opts); - } - // would be handled by simpleZigValueToJs - unreachable; - }, - .error_union => return self.zigValueToJs(try value, opts), - else => {}, - } - - @compileError("A function returns an unsupported type: " ++ @typeName(T)); -} - -// 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.Global(js.Object) (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.Global(js.Object) (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) -pub fn mapZigInstanceToJs(self: *Context, js_obj_handle: ?*const v8.Object, value: anytype) !js.Object { - const arena = self.arena; - - const T = @TypeOf(value); - switch (@typeInfo(T)) { - .@"struct" => { - // Struct, has to be placed on the heap - const heap = try arena.create(T); - heap.* = value; - return self.mapZigInstanceToJs(js_obj_handle, heap); - }, - .pointer => |ptr| { - const resolved = resolveValue(value); - - const gop = try self.identity_map.getOrPut(arena, @intFromPtr(resolved.ptr)); - if (gop.found_existing) { - // we've seen this instance before, return the same object - return .{ .ctx = self, .handle = gop.value_ptr.*.local() }; - } - - const isolate = self.isolate; - const JsApi = bridge.Struct(ptr.child).JsApi; - - // Sometimes we're creating a new Object, like when - // we're returning a value from a function. In those cases - // we have to get the object template, and we can get an object - // by calling initInstance its InstanceTemplate. - // Sometimes though we already have the Object to bind to - // for example, when we're executing a constructor, v8 has - // already created the "this" object. - const js_obj = js.Object{ - .ctx = self, - .handle = js_obj_handle orelse blk: { - const function_template_handle = self.templates[resolved.class_id]; - const object_template_handle = v8.v8__FunctionTemplate__InstanceTemplate(function_template_handle).?; - break :blk v8.v8__ObjectTemplate__NewInstance(object_template_handle, self.handle).?; - }, - }; - - if (!@hasDecl(JsApi.Meta, "empty_with_no_proto")) { - // The TAO contains the pointer to 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 arena.create(TaggedAnyOpaque); - tao.* = .{ - .value = resolved.ptr, - .prototype_chain = resolved.prototype_chain.ptr, - .prototype_len = @intCast(resolved.prototype_chain.len), - .subtype = if (@hasDecl(JsApi.Meta, "subtype")) JsApi.Meta.subype else .node, - }; - - // Skip setting internal field for the global object (Window) - // Window accessors get the instance from context.page.window instead - if (resolved.class_id != @import("../webapi/Window.zig").JsApi.Meta.class_id) { - v8.v8__Object__SetInternalField(js_obj.handle, 0, isolate.createExternal(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. - } - - // dont' use js_obj.persist(), because we don't want to track this in - // context.global_objects, we want to track it in context.identity_map. - const global = js.Global(js.Object).init(isolate.handle, js_obj.handle); - gop.value_ptr.* = global; - return .{ .ctx = self, .handle = global.local() }; - }, - else => @compileError("Expected a struct or pointer, got " ++ @typeName(T) ++ " (constructors must return struct or pointers)"), - } -} - -pub fn jsValueToZig(self: *Context, comptime T: type, js_value: js.Value) !T { - switch (@typeInfo(T)) { - .optional => |o| { - // If type type is a ?js.Value or a ?js.Object, then we want to pass - // a js.Object, not null. Consider a function, - // _doSomething(arg: ?Env.JsObjet) void { ... } - // - // And then these two calls: - // doSomething(); - // doSomething(null); - // - // In the first case, we'll pass `null`. But in the - // second, we'll pass a js.Object which represents - // null. - // If we don't have this code, both cases will - // pass in `null` and the the doSomething won't - // be able to tell if `null` was explicitly passed - // or whether no parameter was passed. - if (comptime o.child == js.Value) { - return js.Value{ - .ctx = self, - .handle = js_value.handle, - }; - } - - if (comptime o.child == js.Object) { - return js.Object{ - .ctx = self, - .handle = @ptrCast(js_value.handle), - }; - } - - if (js_value.isNullOrUndefined()) { - return null; - } - return try self.jsValueToZig(o.child, js_value); - }, - .float => |f| switch (f.bits) { - 0...32 => return js_value.toF32(), - 33...64 => return js_value.toF64(), - else => {}, - }, - .int => return jsIntToZig(T, js_value), - .bool => return js_value.toBool(), - .pointer => |ptr| switch (ptr.size) { - .one => { - if (!js_value.isObject()) { - return error.InvalidArgument; - } - if (@hasDecl(ptr.child, "JsApi")) { - std.debug.assert(bridge.JsApiLookup.has(ptr.child.JsApi)); - return typeTaggedAnyOpaque(*ptr.child, js_value.handle); - } - }, - .slice => { - if (ptr.sentinel() == null) { - if (try self.jsValueToTypedArray(ptr.child, js_value)) |value| { - return value; - } - } - - if (ptr.child == u8) { - if (ptr.sentinel()) |s| { - if (comptime s == 0) { - return self.valueToStringZ(js_value, .{}); - } - } else { - return self.valueToString(js_value, .{}); - } - } - - if (!js_value.isArray()) { - return error.InvalidArgument; - } - const js_arr = js_value.toArray(); - const arr = try self.call_arena.alloc(ptr.child, js_arr.len()); - for (arr, 0..) |*a, i| { - const item_value = try js_arr.get(@intCast(i)); - a.* = try self.jsValueToZig(ptr.child, item_value); - } - return arr; - }, - else => {}, - }, - .array => |arr| { - // Retrieve fixed-size array as slice - const slice_type = []arr.child; - const slice_value = try self.jsValueToZig(slice_type, js_value); - if (slice_value.len != arr.len) { - // Exact length match, we could allow smaller arrays, but we would not be able to communicate how many were written - return error.InvalidArgument; - } - return @as(*T, @ptrCast(slice_value.ptr)).*; - }, - .@"struct" => { - return try (self.jsValueToStruct(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(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(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(field.type, js_value)); - } - } - unreachable; - }, - .@"enum" => |e| { - if (@hasDecl(T, "js_enum_from_string")) { - if (!js_value.isString()) { - return error.InvalidArgument; - } - return std.meta.stringToEnum(T, try self.valueToString(js_value, .{})) orelse return error.InvalidArgument; - } - switch (@typeInfo(e.tag_type)) { - .int => return std.meta.intToEnum(T, try jsIntToZig(e.tag_type, js_value)), - else => @compileError("unsupported enum parameter type: " ++ @typeName(T)), - } - }, - else => {}, - } - - @compileError("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: *Context, comptime T: type, js_value: js.Value) !?T { - return switch (T) { - js.Function => { - if (!js_value.isFunction()) { - return null; - } - return try self.newFunction(js_value); - }, - js.Function.Global => { - if (!js_value.isFunction()) { - return null; - } - const func = try self.newFunction(js_value); - return try func.persist(); - }, - // zig fmt: off - js.TypedArray(u8), js.TypedArray(u16), js.TypedArray(u32), js.TypedArray(u64), - js.TypedArray(i8), js.TypedArray(i16), js.TypedArray(i32), js.TypedArray(i64), - js.TypedArray(f32), js.TypedArray(f64), - // zig fmt: on - => { - const ValueType = @typeInfo(std.meta.fieldInfo(T, .values).type).pointer.child; - const arr = (try self.jsValueToTypedArray(ValueType, js_value)) orelse return null; - return .{ .values = arr }; - }, - js.String => .{ .string = try self.valueToString(js_value, .{ .allocator = self.arena }) }, - // Caller wants an opaque js.Object. Probably a parameter - // that it needs to pass back into a callback. - js.Value => js.Value{ - .ctx = self, - .handle = js_value.handle, - }, - // Caller wants an opaque js.Object. Probably a parameter - // that it needs to pass back into a callback. - js.Object => { - if (!js_value.isObject()) { - return null; - } - return js.Object{ - .ctx = self, - .handle = @ptrCast(js_value.handle), - }; - }, - js.Object.Global => { - if (!js_value.isObject()) { - return null; - } - const obj = js.Object{ - .ctx = self, - .handle = @ptrCast(js_value.handle), - }; - return try obj.persist(); - }, - js.Value.Global => { - return try js_value.persist(); - }, - else => { - if (!js_value.isObject()) { - return null; - } - - const isolate = self.isolate; - const js_obj = js_value.toObject(); - - var value: T = undefined; - inline for (@typeInfo(T).@"struct".fields) |field| { - const name = field.name; - const key = isolate.initStringHandle(name); - if (js_obj.has(key)) { - @field(value, name) = try self.jsValueToZig(field.type, try js_obj.get(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 jsValueToTypedArray(_: *Context, comptime T: type, js_value: js.Value) !?[]T { - var force_u8 = false; - var array_buffer: ?*const v8.ArrayBuffer = null; - var byte_len: usize = undefined; - var byte_offset: usize = undefined; - - if (js_value.isTypedArray()) { - const buffer_handle: *const v8.ArrayBufferView = @ptrCast(js_value.handle); - byte_len = v8.v8__ArrayBufferView__ByteLength(buffer_handle); - byte_offset = v8.v8__ArrayBufferView__ByteOffset(buffer_handle); - array_buffer = v8.v8__ArrayBufferView__Buffer(buffer_handle).?; - } else if (js_value.isArrayBufferView()) { - force_u8 = true; - const buffer_handle: *const v8.ArrayBufferView = @ptrCast(js_value.handle); - byte_len = v8.v8__ArrayBufferView__ByteLength(buffer_handle); - byte_offset = v8.v8__ArrayBufferView__ByteOffset(buffer_handle); - array_buffer = v8.v8__ArrayBufferView__Buffer(buffer_handle).?; - } else if (js_value.isArrayBuffer()) { - force_u8 = true; - array_buffer = @ptrCast(js_value.handle); - byte_len = v8.v8__ArrayBuffer__ByteLength(array_buffer); - byte_offset = 0; - } - - const backing_store_ptr = v8.v8__ArrayBuffer__GetBackingStore(array_buffer orelse return null); - const backing_store_handle = v8.std__shared_ptr__v8__BackingStore__get(&backing_store_ptr).?; - const data = v8.v8__BackingStore__Data(backing_store_handle); - - switch (T) { - u8 => { - if (force_u8 or js_value.isUint8Array() or js_value.isUint8ClampedArray()) { - if (byte_len == 0) return &[_]u8{}; - const arr_ptr = @as([*]u8, @ptrCast(@alignCast(data))); - return arr_ptr[byte_offset .. byte_offset + byte_len]; - } - }, - i8 => { - if (js_value.isInt8Array()) { - if (byte_len == 0) return &[_]i8{}; - const arr_ptr = @as([*]i8, @ptrCast(@alignCast(data))); - return arr_ptr[byte_offset .. byte_offset + byte_len]; - } - }, - u16 => { - if (js_value.isUint16Array()) { - if (byte_len == 0) return &[_]u16{}; - const arr_ptr = @as([*]u16, @ptrCast(@alignCast(data))); - return arr_ptr[byte_offset .. byte_offset + byte_len / 2]; - } - }, - i16 => { - if (js_value.isInt16Array()) { - if (byte_len == 0) return &[_]i16{}; - const arr_ptr = @as([*]i16, @ptrCast(@alignCast(data))); - return arr_ptr[byte_offset .. byte_offset + byte_len / 2]; - } - }, - u32 => { - if (js_value.isUint32Array()) { - if (byte_len == 0) return &[_]u32{}; - const arr_ptr = @as([*]u32, @ptrCast(@alignCast(data))); - return arr_ptr[byte_offset .. byte_offset + byte_len / 4]; - } - }, - i32 => { - if (js_value.isInt32Array()) { - if (byte_len == 0) return &[_]i32{}; - const arr_ptr = @as([*]i32, @ptrCast(@alignCast(data))); - return arr_ptr[byte_offset .. byte_offset + byte_len / 4]; - } - }, - u64 => { - if (js_value.isBigUint64Array()) { - if (byte_len == 0) return &[_]u64{}; - const arr_ptr = @as([*]u64, @ptrCast(@alignCast(data))); - return arr_ptr[byte_offset .. byte_offset + byte_len / 8]; - } - }, - i64 => { - if (js_value.isBigInt64Array()) { - if (byte_len == 0) return &[_]i64{}; - const arr_ptr = @as([*]i64, @ptrCast(@alignCast(data))); - return arr_ptr[byte_offset .. byte_offset + byte_len / 8]; - } - }, - else => {}, - } - return error.InvalidArgument; -} - -// Every WebApi type has a class_id as T.JsApi.Meta.class_id. We use this to create -// a JSValue class of the correct type. However, given a Node, we don't want -// to create a Node class, we want to create a class of the most specific type. -// In other words, given a Node{._type = .{.document .{}}}, we want to create -// a Document, not a Node. -// This function recursively walks the _type union field (if there is one) to -// get the most specific class_id possible. -const Resolved = struct { - ptr: *anyopaque, - class_id: u16, - prototype_chain: []const js.PrototypeChainEntry, -}; -fn resolveValue(value: anytype) Resolved { - const T = bridge.Struct(@TypeOf(value)); - if (!@hasField(T, "_type") or @typeInfo(@TypeOf(value._type)) != .@"union") { - return resolveT(T, value); - } - - const U = @typeInfo(@TypeOf(value._type)).@"union"; - inline for (U.fields) |field| { - if (value._type == @field(U.tag_type.?, field.name)) { - const child = switch (@typeInfo(field.type)) { - .pointer => @field(value._type, field.name), - .@"struct" => &@field(value._type, field.name), - .void => { - // Unusual case, but the Event (and maybe others) can be - // returned as-is. In that case, it has a dummy void type. - return resolveT(T, value); - }, - else => @compileError(@typeName(field.type) ++ " has an unsupported _type field"), - }; - return resolveValue(child); - } - } - unreachable; -} - -fn resolveT(comptime T: type, value: *anyopaque) Resolved { - return .{ - .ptr = value, - .class_id = T.JsApi.Meta.class_id, - .prototype_chain = &T.JsApi.Meta.prototype_chain, - }; -} - -// == Stringifiers == -pub fn valueToString(self: *Context, js_val: js.Value, opts: ToStringOpts) ![]u8 { - return self._valueToString(false, js_val, opts); -} - -pub fn valueToStringZ(self: *Context, js_val: js.Value, opts: ToStringOpts) ![:0]u8 { - return self._valueToString(true, js_val, opts); -} - -fn _valueToString(self: *Context, comptime null_terminate: bool, js_val: js.Value, opts: ToStringOpts) !(if (null_terminate) [:0]u8 else []u8) { - if (js_val.isSymbol()) { - const symbol_handle = v8.v8__Symbol__Description(@ptrCast(js_val.handle), self.isolate.handle).?; - return self._valueToString(null_terminate, .{ .ctx = self, .handle = symbol_handle }, opts); - } - - const string_handle = v8.v8__Value__ToString(js_val.handle, self.handle) orelse { - return error.JsException; - }; - - return self._jsStringToZig(null_terminate, string_handle, opts); -} - -const ToStringOpts = struct { - allocator: ?Allocator = null, -}; -pub fn jsStringToZig(self: *const Context, str: anytype, opts: ToStringOpts) ![]u8 { - return self._jsStringToZig(false, str, opts); -} -pub fn jsStringToZigZ(self: *const Context, str: anytype, opts: ToStringOpts) ![:0]u8 { - return self._jsStringToZig(true, str, opts); -} -fn _jsStringToZig(self: *const Context, comptime null_terminate: bool, str: anytype, opts: ToStringOpts) !(if (null_terminate) [:0]u8 else []u8) { - const handle = if (@TypeOf(str) == js.String) str.handle else str; - - const len = v8.v8__String__Utf8Length(handle, self.isolate.handle); - const allocator = opts.allocator orelse self.call_arena; - const buf = try (if (comptime null_terminate) allocator.allocSentinel(u8, @intCast(len), 0) else allocator.alloc(u8, @intCast(len))); - const n = v8.v8__String__WriteUtf8(handle, self.isolate.handle, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8); - std.debug.assert(n == len); - - return buf; -} - -pub fn debugValue(self: *Context, js_val: js.Value, writer: *std.Io.Writer) !void { - var seen: std.AutoHashMapUnmanaged(u32, void) = .empty; - return _debugValue(self, js_val, &seen, 0, writer) catch error.WriteFailed; -} - -fn _debugValue(self: *Context, js_val: js.Value, seen: *std.AutoHashMapUnmanaged(u32, void), depth: usize, writer: *std.Io.Writer) !void { - if (js_val.isNull()) { - // I think null can sometimes appear as an object, so check this and - // handle it first. - return writer.writeAll("null"); - } - - if (!js_val.isObject()) { - // handle these explicitly, so we don't include the type (we only want to include - // it when there's some ambiguity, e.g. the string "true") - if (js_val.isUndefined()) { - return writer.writeAll("undefined"); - } - if (js_val.isTrue()) { - return writer.writeAll("true"); - } - if (js_val.isFalse()) { - return writer.writeAll("false"); - } - - if (js_val.isSymbol()) { - const symbol_handle = v8.v8__Symbol__Description(@ptrCast(js_val.handle), self.isolate.handle).?; - const js_sym_str = try self.valueToString(.{ .ctx = self, .handle = symbol_handle }, .{}); - return writer.print("{s} (symbol)", .{js_sym_str}); - } - const js_type = try self.jsStringToZig(js_val.typeOf(), .{}); - const js_val_str = try self.valueToString(js_val, .{}); - if (js_val_str.len > 2000) { - try writer.writeAll(js_val_str[0..2000]); - try writer.writeAll(" ... (truncated)"); - } else { - try writer.writeAll(js_val_str); - } - return writer.print(" ({s})", .{js_type}); - } - - const js_obj = js_val.toObject(); - { - // explicit scope because gop will become invalid in recursive call - const gop = try seen.getOrPut(self.call_arena, js_obj.getId()); - if (gop.found_existing) { - return writer.writeAll("\n"); - } - gop.value_ptr.* = {}; - } - - const names_arr = js_obj.getOwnPropertyNames(); - const len = names_arr.len(); - - if (depth > 20) { - return writer.writeAll("...deeply nested object..."); - } - const own_len = js_obj.getOwnPropertyNames().len(); - if (own_len == 0) { - const js_val_str = try self.valueToString(js_val, .{}); - if (js_val_str.len > 2000) { - try writer.writeAll(js_val_str[0..2000]); - return writer.writeAll(" ... (truncated)"); - } - return writer.writeAll(js_val_str); - } - - const all_len = js_obj.getPropertyNames().len(); - try writer.print("({d}/{d})", .{ own_len, all_len }); - for (0..len) |i| { - if (i == 0) { - try writer.writeByte('\n'); - } - const field_name = try names_arr.get(@intCast(i)); - const name = try self.valueToString(field_name, .{}); - try writer.splatByteAll(' ', depth); - try writer.writeAll(name); - try writer.writeAll(": "); - const field_val = try js_obj.get(name); - try self._debugValue(field_val, seen, depth + 1, writer); - if (i != len - 1) { - try writer.writeByte('\n'); - } - } -} - -pub fn stackTrace(self: *const Context) !?[]const u8 { - if (comptime !IS_DEBUG) { - return "not available"; - } - - const isolate = self.isolate; - const separator = log.separator(); - - var buf: std.ArrayList(u8) = .empty; - var writer = buf.writer(self.call_arena); - - const stack_trace_handle = v8.v8__StackTrace__CurrentStackTrace__STATIC(isolate.handle, 30).?; - const frame_count = v8.v8__StackTrace__GetFrameCount(stack_trace_handle); - - if (v8.v8__StackTrace__CurrentScriptNameOrSourceURL__STATIC(isolate.handle)) |script| { - try writer.print("{s}<{s}>", .{ separator, try self.jsStringToZig(script, .{}) }); - } - - for (0..@intCast(frame_count)) |i| { - const frame_handle = v8.v8__StackTrace__GetFrame(stack_trace_handle, isolate.handle, @intCast(i)).?; - if (v8.v8__StackFrame__GetFunctionName(frame_handle)) |name| { - const script = try self.jsStringToZig(name, .{}); - try writer.print("{s}{s}:{d}", .{ separator, script, v8.v8__StackFrame__GetLineNumber(frame_handle) }); - } else { - try writer.print("{s}:{d}", .{ separator, v8.v8__StackFrame__GetLineNumber(frame_handle) }); - } - } - return buf.items; -} - -// == Promise Helpers == -pub fn rejectPromise(self: *Context, value: anytype) !js.Promise { - var resolver = js.PromiseResolver.init(self); - resolver.reject("Context.rejectPromise", value); - return resolver.promise(); -} - -pub fn resolvePromise(self: *Context, value: anytype) !js.Promise { - var resolver = js.PromiseResolver.init(self); - resolver.resolve("Context.resolvePromise", value); - return resolver.promise(); -} - pub fn runMicrotasks(self: *Context) void { self.isolate.performMicrotasksCheckpoint(); } -pub fn createPromiseResolver(self: *Context) js.PromiseResolver { - return js.PromiseResolver.init(self); -} - // == Callbacks == // Callback from V8, asking us to load a module. The "specifier" is // the src of the module to load. @@ -1195,14 +396,20 @@ fn resolveModuleCallback( _ = import_attributes; const self = fromC(c_context.?); + var local = js.Local{ + .ctx = self, + .handle = c_context.?, + .isolate = self.isolate, + .call_arena = self.call_arena, + }; - const specifier = self.jsStringToZigZ(c_specifier.?, .{}) catch |err| { + const specifier = local.jsStringToZigZ(c_specifier.?, .{}) catch |err| { log.err(.js, "resolve module", .{ .err = err }); return null; }; - const referrer = js.Module{ .ctx = self, .handle = c_referrer.? }; + const referrer = js.Module{ .local = &local, .handle = c_referrer.? }; - return self._resolveModuleCallback(referrer, specifier) catch |err| { + return self._resolveModuleCallback(referrer, specifier, &local) catch |err| { log.err(.js, "resolve module", .{ .err = err, .specifier = specifier, @@ -1222,15 +429,21 @@ pub fn dynamicModuleCallback( _ = import_attrs; const self = fromC(c_context.?); - - const resource = self.jsStringToZigZ(resource_name.?, .{}) catch |err| { - log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" }); - return @constCast((self.rejectPromise("Out of memory") catch return null).handle); + var local = js.Local{ + .ctx = self, + .handle = c_context.?, + .call_arena = self.call_arena, + .isolate = self.isolate, }; - const specifier = self.jsStringToZigZ(v8_specifier.?, .{}) catch |err| { + const resource = local.jsStringToZigZ(resource_name.?, .{}) catch |err| { + log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" }); + return @constCast((local.rejectPromise("Out of memory") catch return null).handle); + }; + + const specifier = local.jsStringToZigZ(v8_specifier.?, .{}) catch |err| { log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback2" }); - return @constCast((self.rejectPromise("Out of memory") catch return null).handle); + return @constCast((local.rejectPromise("Out of memory") catch return null).handle); }; const normalized_specifier = self.script_manager.?.resolveSpecifier( @@ -1239,22 +452,30 @@ pub fn dynamicModuleCallback( specifier, ) catch |err| { log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback3" }); - return @constCast((self.rejectPromise("Out of memory") catch return null).handle); + return @constCast((local.rejectPromise("Out of memory") catch return null).handle); }; - const promise = self._dynamicModuleCallback(normalized_specifier, resource) catch |err| blk: { + const promise = self._dynamicModuleCallback(normalized_specifier, resource, &local) catch |err| blk: { log.err(.js, "dynamic module callback", .{ .err = err, }); - break :blk self.rejectPromise("Failed to load module") catch return null; + break :blk local.rejectPromise("Failed to load module") catch return null; }; return @constCast(promise.handle); } pub fn metaObjectCallback(c_context: ?*v8.Context, c_module: ?*v8.Module, c_meta: ?*v8.Value) callconv(.c) void { + // @HandleScope implement this without a fat context/local.. const self = fromC(c_context.?); - const m = js.Module{ .ctx = self, .handle = c_module.? }; - const meta = js.Object{ .ctx = self, .handle = @ptrCast(c_meta.?) }; + var local = js.Local{ + .ctx = self, + .handle = c_context.?, + .isolate = self.isolate, + .call_arena = self.call_arena, + }; + + const m = js.Module{ .local = &local, .handle = c_module.? }; + const meta = js.Object{ .local = &local, .handle = @ptrCast(c_meta.?) }; const url = self.module_identifier.get(m.getIdentityHash()) orelse { // Shouldn't be possible. @@ -1262,7 +483,7 @@ pub fn metaObjectCallback(c_context: ?*v8.Context, c_module: ?*v8.Module, c_meta return; }; - const js_value = self.zigValueToJs(url, .{}) catch { + const js_value = local.zigValueToJs(url, .{}) catch { log.err(.js, "import meta", .{ .err = error.FailedToConvertUrl }); return; }; @@ -1272,7 +493,7 @@ pub fn metaObjectCallback(c_context: ?*v8.Context, c_module: ?*v8.Module, c_meta } } -fn _resolveModuleCallback(self: *Context, referrer: js.Module, specifier: [:0]const u8) !?*const v8.Module { +fn _resolveModuleCallback(self: *Context, referrer: js.Module, specifier: [:0]const u8, local: *const js.Local) !?*const v8.Module { const referrer_path = self.module_identifier.get(referrer.getIdentityHash()) orelse { // Shouldn't be possible. return error.UnknownModuleReferrer; @@ -1286,20 +507,20 @@ fn _resolveModuleCallback(self: *Context, referrer: js.Module, specifier: [:0]co const entry = self.module_cache.getPtr(normalized_specifier).?; if (entry.module) |m| { - return m.handle; + return local.toLocal(m).handle; } var source = try self.script_manager.?.waitForImport(normalized_specifier); defer source.deinit(); var try_catch: js.TryCatch = undefined; - try_catch.init(self); + try_catch.init(local); defer try_catch.deinit(); - const mod = try self.compileModule(source.src(), normalized_specifier); - try self.postCompileModule(mod, normalized_specifier); + const mod = try compileModule(local, source.src(), normalized_specifier); + try self.postCompileModule(mod, normalized_specifier, local); entry.module = try mod.persist(); - return entry.module.?.handle; + return mod.handle; } // Will get passed to ScriptManager and then passed back to us when @@ -1307,23 +528,22 @@ fn _resolveModuleCallback(self: *Context, referrer: js.Module, specifier: [:0]co const DynamicModuleResolveState = struct { // The module that we're resolving (we'll actually resolve its // namespace) - module: ?js.Module, + module: ?js.Module.Global, context_id: usize, context: *Context, specifier: [:0]const u8, - resolver: js.PromiseResolver, + resolver: js.PromiseResolver.Global, }; -fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []const u8) !js.Promise { +fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []const u8, local: *const js.Local) !js.Promise { const gop = try self.module_cache.getOrPut(self.arena, specifier); - if (gop.found_existing and gop.value_ptr.resolver_promise != null) { - // This is easy, there's already something responsible - // for loading the module. Maybe it's still loading, maybe - // it's complete. Whatever, we can just return that promise. - return gop.value_ptr.resolver_promise.?; + if (gop.found_existing) { + if (gop.value_ptr.resolver_promise) |rp| { + return local.toLocal(rp); + } } - const resolver = try self.createPromiseResolver().persist(); + const resolver = local.createPromiseResolver(); const state = try self.arena.create(DynamicModuleResolveState); state.* = .{ @@ -1331,10 +551,10 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c .context = self, .specifier = specifier, .context_id = self.id, - .resolver = resolver, + .resolver = try resolver.persist(), }; - const promise = try resolver.promise().persist(); + const promise = resolver.promise(); if (!gop.found_existing) { // this module hasn't been seen before. This is the most @@ -1346,12 +566,12 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c gop.value_ptr.* = ModuleEntry{ .module = null, .module_promise = null, - .resolver_promise = promise, + .resolver_promise = try promise.persist(), }; // Next, we need to actually load it. self.script_manager.?.getAsyncImport(specifier, dynamicModuleSourceCallback, state, referrer) catch |err| { - const error_msg = self.newString(@errorName(err)); + const error_msg = local.newString(@errorName(err)); _ = resolver.reject("dynamic module get async", error_msg); }; @@ -1374,19 +594,20 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c // If the module hasn't been evaluated yet (it was only instantiated // as a static import dependency), we need to evaluate it now. if (gop.value_ptr.module_promise == null) { - const mod = gop.value_ptr.module.?; + const mod = local.toLocal(gop.value_ptr.module.?); const status = mod.getStatus(); if (status == .kEvaluated or status == .kEvaluating) { // Module was already evaluated (shouldn't normally happen, but handle it). // Create a pre-resolved promise with the module namespace. - const module_resolver = try self.createPromiseResolver().persist(); - _ = module_resolver.resolve("resolve module", mod.getModuleNamespace()); + const module_resolver = local.createPromiseResolver(); + module_resolver.resolve("resolve module", mod.getModuleNamespace()); + _ = try module_resolver.persist(); gop.value_ptr.module_promise = try module_resolver.promise().persist(); } else { // the module was loaded, but not evaluated, we _have_ to evaluate it now const evaluated = mod.evaluate() catch { std.debug.assert(status == .kErrored); - _ = resolver.reject("module evaluation", self.newString("Module evaluation failed")); + _ = resolver.reject("module evaluation", local.newString("Module evaluation failed")); return promise; }; std.debug.assert(evaluated.isPromise()); @@ -1397,11 +618,11 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c // like before, we want to set this up so that if anything else // tries to load this module, it can just return our promise // since we're going to be doing all the work. - gop.value_ptr.resolver_promise = promise; + gop.value_ptr.resolver_promise = try promise.persist(); // But we can skip direclty to `resolveDynamicModule` which is // what the above callback will eventually do. - self.resolveDynamicModule(state, gop.value_ptr.*); + self.resolveDynamicModule(state, gop.value_ptr.*, local); return promise; } @@ -1409,8 +630,14 @@ fn dynamicModuleSourceCallback(ctx: *anyopaque, module_source_: anyerror!ScriptM const state: *DynamicModuleResolveState = @ptrCast(@alignCast(ctx)); var self = state.context; + var ls: js.Local.Scope = undefined; + self.localScope(&ls); + defer ls.deinit(); + + const local = &ls.local; + var ms = module_source_ catch |err| { - _ = state.resolver.reject("dynamic module source", self.newString(@errorName(err))); + _ = local.toLocal(state.resolver).reject("dynamic module source", local.newString(@errorName(err))); return; }; @@ -1418,24 +645,24 @@ fn dynamicModuleSourceCallback(ctx: *anyopaque, module_source_: anyerror!ScriptM defer ms.deinit(); var try_catch: js.TryCatch = undefined; - try_catch.init(self); + try_catch.init(local); defer try_catch.deinit(); - break :blk self.module(true, ms.src(), state.specifier, true) catch |err| { + break :blk self.module(true, local, ms.src(), state.specifier, true) catch |err| { const caught = try_catch.caughtOrError(self.call_arena, err); log.err(.js, "module compilation failed", .{ .caught = caught, .specifier = state.specifier, }); - _ = state.resolver.reject("dynamic compilation failure", self.newString(caught.exception orelse "")); + _ = local.toLocal(state.resolver).reject("dynamic compilation failure", local.newString(caught.exception orelse "")); return; }; }; - self.resolveDynamicModule(state, module_entry); + self.resolveDynamicModule(state, module_entry, local); } -fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, module_entry: ModuleEntry) void { +fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, module_entry: ModuleEntry, local: *const js.Local) void { defer self.runMicrotasks(); // we can only be here if the module has been evaluated and if @@ -1452,16 +679,17 @@ fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, modul // last value of the module. But, for module loading, we need to // resolve to the module's namespace. - const then_callback = self.newFunctionWithData(struct { + const then_callback = newFunctionWithData(local, struct { pub fn callback(callback_handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { const isolate = v8.v8__FunctionCallbackInfo__GetIsolate(callback_handle).?; - var caller = Caller.init(isolate); - defer caller.deinit(); + var c: Caller = undefined; + c.init(isolate); + defer c.deinit(); const info_data = v8.v8__FunctionCallbackInfo__Data(callback_handle).?; const s: *DynamicModuleResolveState = @ptrCast(@alignCast(v8.v8__External__Value(@ptrCast(info_data)))); - if (s.context_id != caller.context.id) { + if (s.context_id != c.local.ctx.id) { // The microtask is tied to the isolate, not the context // it can be resolved while another context is active // (Which seems crazy to me). If that happens, then @@ -1469,489 +697,46 @@ fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, modul // (most of the fields in state are not valid) return; } - - defer caller.context.runMicrotasks(); - const namespace = s.module.?.getModuleNamespace(); - _ = s.resolver.resolve("resolve namespace", namespace); + const l = c.local; + defer l.ctx.runMicrotasks(); + const namespace = l.toLocal(s.module.?).getModuleNamespace(); + _ = l.toLocal(s.resolver).resolve("resolve namespace", namespace); } }.callback, @ptrCast(state)); - const catch_callback = self.newFunctionWithData(struct { + const catch_callback = newFunctionWithData(local, struct { pub fn callback(callback_handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { const isolate = v8.v8__FunctionCallbackInfo__GetIsolate(callback_handle).?; - var caller = Caller.init(isolate); - defer caller.deinit(); + var c: Caller = undefined; + c.init(isolate); + defer c.deinit(); const info_data = v8.v8__FunctionCallbackInfo__Data(callback_handle).?; const s: *DynamicModuleResolveState = @ptrCast(@alignCast(v8.v8__External__Value(@ptrCast(info_data)))); - const ctx = caller.context; + const l = &c.local; + const ctx = l.ctx; if (s.context_id != ctx.id) { return; } defer ctx.runMicrotasks(); - _ = s.resolver.reject("catch callback", js.Value{ - .ctx = ctx, + _ = l.toLocal(s.resolver).reject("catch callback", js.Value{ + .local = l, .handle = v8.v8__FunctionCallbackInfo__Data(callback_handle).?, }); } }.callback, @ptrCast(state)); - _ = module_entry.module_promise.?.thenAndCatch(then_callback, catch_callback) catch |err| { + _ = local.toLocal(module_entry.module_promise.?).thenAndCatch(then_callback, catch_callback) catch |err| { log.err(.js, "module evaluation is promise", .{ .err = err, .specifier = state.specifier, }); - _ = state.resolver.reject("module promise", self.newString("Failed to evaluate promise")); + _ = local.toLocal(state.resolver).reject("module promise", local.newString("Failed to evaluate promise")); }; } -// == Zig <-> JS == - -// Reverses the mapZigInstanceToJs, making sure that our TaggedAnyOpaque -// contains a ptr to the correct type. -pub fn typeTaggedAnyOpaque(comptime R: type, js_obj_handle: *const v8.Object) !R { - const ti = @typeInfo(R); - if (ti != .pointer) { - @compileError("non-pointer Zig parameter type: " ++ @typeName(R)); - } - - const T = ti.pointer.child; - const JsApi = bridge.Struct(T).JsApi; - - if (@hasDecl(JsApi.Meta, "empty_with_no_proto")) { - // 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, &.{})); - } - - const internal_field_count = v8.v8__Object__InternalFieldCount(js_obj_handle); - // Special case for Window: the global object doesn't have internal fields - // Window instance is stored in context.page.window instead - if (internal_field_count == 0) { - // Normally, this would be an error. All JsObject that map to a Zig type - // are either `empty_with_no_proto` (handled above) or have an - // interalFieldCount. The only exception to that is the Window... - const isolate = v8.v8__Object__GetIsolate(js_obj_handle).?; - const context = fromIsolate(.{ .handle = isolate }); - - const Window = @import("../webapi/Window.zig"); - if (T == Window) { - return context.page.window; - } - - // ... Or the window's prototype. - // We could make this all comptime-fancy, but it's easier to hard-code - // the EventTarget - - const EventTarget = @import("../webapi/EventTarget.zig"); - if (T == EventTarget) { - return context.page.window._proto; - } - - // Type not found in Window's prototype chain - return error.InvalidArgument; - } - - // 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 (internal_field_count == 0) { - return error.InvalidArgument; - } - - if (!bridge.JsApiLookup.has(JsApi)) { - @compileError("unknown Zig type: " ++ @typeName(R)); - } - - const internal_field_handle = v8.v8__Object__GetInternalField(js_obj_handle, 0).?; - const tao: *TaggedAnyOpaque = @ptrCast(@alignCast(v8.v8__External__Value(internal_field_handle))); - const expected_type_index = bridge.JsApiLookup.getId(JsApi); - - const prototype_chain = tao.prototype_chain[0..tao.prototype_len]; - if (prototype_chain[0].index == expected_type_index) { - return @ptrCast(@alignCast(tao.value)); - } - - // Ok, let's walk up the chain - var ptr = @intFromPtr(tao.value); - for (prototype_chain[1..]) |proto| { - ptr += proto.offset; // the offset to the _proto field - const proto_ptr: **anyopaque = @ptrFromInt(ptr); - if (proto.index == expected_type_index) { - return @ptrCast(@alignCast(proto_ptr.*)); - } - ptr = @intFromPtr(proto_ptr.*); - } - return error.InvalidArgument; -} - -// 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: *Context, comptime T: type, js_value: js.Value) !ProbeResult(T) { - switch (@typeInfo(T)) { - .optional => |o| { - if (js_value.isNullOrUndefined()) { - return .{ .value = null }; - } - return self.probeJsValueToZig(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 (bridge.JsApiLookup.has(ptr.child.JsApi)) { - // 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(*ptr.child, @ptrCast(js_value.handle)); - 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.toArray(); - - if (js_arr.len() == 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 first_val = try js_arr.get(0); - switch (try self.probeJsValueToZig(ptr.child, first_val)) { - .value, .ok => return .{ .ok = {} }, - .compatible => return .{ .compatible = {} }, - .coerce => return .{ .coerce = {} }, - .invalid => return .{ .invalid = {} }, - } - }, - else => {}, - }, - .array => |arr| { - // Retrieve fixed-size array as slice then probe - const slice_type = []arr.child; - switch (try self.probeJsValueToZig(slice_type, js_value)) { - .value => |slice_value| { - if (slice_value.len == arr.len) { - return .{ .value = @as(*T, @ptrCast(slice_value.ptr)).* }; - } - return .{ .invalid = {} }; - }, - .ok => { - // Exact length match, we could allow smaller arrays as .compatible, but we would not be able to communicate how many were written - if (js_value.isArray()) { - const js_arr = js_value.toArray(); - if (js_arr.len() == arr.len) { - return .{ .ok = {} }; - } - } else if (js_value.isString() and arr.child == u8) { - const str = try js_value.toString(self.handle); - if (str.lenUtf8(self.isolate) == arr.len) { - return .{ .ok = {} }; - } - } - return .{ .invalid = {} }; - }, - .compatible => return .{ .compatible = {} }, - .coerce => return .{ .coerce = {} }, - .invalid => return .{ .invalid = {} }, - } - }, - .@"struct" => { - // We don't want to duplicate the code for this, so we call - // the actual conversion function. - const value = (try self.jsValueToStruct(T, js_value)) orelse { - return .{ .invalid = {} }; - }; - return .{ .value = value }; - }, - else => {}, - } - - return .{ .invalid = {} }; -} - -fn jsIntToZig(comptime T: type, js_value: js.Value) !T { - const n = @typeInfo(T).int; - switch (n.signedness) { - .signed => switch (n.bits) { - 8 => return jsSignedIntToZig(i8, -128, 127, try js_value.toI32()), - 16 => return jsSignedIntToZig(i16, -32_768, 32_767, try js_value.toI32()), - 32 => return jsSignedIntToZig(i32, -2_147_483_648, 2_147_483_647, try js_value.toI32()), - 64 => { - if (js_value.isBigInt()) { - const v = js_value.toBigInt(); - return v.getInt64(); - } - return jsSignedIntToZig(i64, -2_147_483_648, 2_147_483_647, try js_value.toI32()); - }, - else => {}, - }, - .unsigned => switch (n.bits) { - 8 => return jsUnsignedIntToZig(u8, 255, try js_value.toU32()), - 16 => return jsUnsignedIntToZig(u16, 65_535, try js_value.toU32()), - 32 => { - if (js_value.isBigInt()) { - const v = js_value.toBigInt(); - const large = v.getUint64(); - if (large <= 4_294_967_295) { - return @intCast(large); - } - return error.InvalidArgument; - } - return jsUnsignedIntToZig(u32, 4_294_967_295, try js_value.toU32()); - }, - 64 => { - if (js_value.isBigInt()) { - const v = js_value.toBigInt(); - return v.getUint64(); - } - return jsUnsignedIntToZig(u64, 4_294_967_295, try js_value.toU32()); - }, - else => {}, - }, - } - @compileError("Only i8, i16, i32, i64, u8, u16, u32 and u64 are supported"); -} - -fn jsSignedIntToZig(comptime T: type, comptime min: comptime_int, max: comptime_int, maybe: i32) !T { - if (maybe >= min and maybe <= max) { - return @intCast(maybe); - } - return error.InvalidArgument; -} - -fn jsUnsignedIntToZig(comptime T: type, max: comptime_int, maybe: u32) !T { - if (maybe <= max) { - return @intCast(maybe); - } - return error.InvalidArgument; -} - -fn compileAndRun(self: *Context, src: []const u8, name: ?[]const u8) !js.Value { - const script_name = self.isolate.initStringHandle(name orelse "anonymous"); - const script_source = self.isolate.initStringHandle(src); - - // Create ScriptOrigin - var origin: v8.ScriptOrigin = undefined; - v8.v8__ScriptOrigin__CONSTRUCT(&origin, @ptrCast(script_name)); - - // Create ScriptCompilerSource - var script_comp_source: v8.ScriptCompilerSource = undefined; - v8.v8__ScriptCompiler__Source__CONSTRUCT2(script_source, &origin, null, &script_comp_source); - defer v8.v8__ScriptCompiler__Source__DESTRUCT(&script_comp_source); - - // Compile the script - const v8_script = v8.v8__ScriptCompiler__Compile( - self.handle, - &script_comp_source, - v8.kNoCompileOptions, - v8.kNoCacheNoReason, - ) orelse return error.CompilationError; - - // Run the script - const result = v8.v8__Script__Run(v8_script, self.handle) orelse return error.ExecutionError; - return .{ .ctx = self, .handle = result }; -} - -fn compileModule(self: *Context, src: []const u8, name: []const u8) !js.Module { - var origin_handle: v8.ScriptOrigin = undefined; - v8.v8__ScriptOrigin__CONSTRUCT2( - &origin_handle, - self.isolate.initStringHandle(name), - 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 source_handle: v8.ScriptCompilerSource = undefined; - v8.v8__ScriptCompiler__Source__CONSTRUCT2( - self.isolate.initStringHandle(src), - &origin_handle, - null, // cached data - &source_handle, - ); - - defer v8.v8__ScriptCompiler__Source__DESTRUCT(&source_handle); - - const module_handle = v8.v8__ScriptCompiler__CompileModule( - self.isolate.handle, - &source_handle, - v8.kNoCompileOptions, - v8.kNoCacheNoReason, - ) orelse { - return error.JsException; - }; - - return .{ - .ctx = self, - .handle = module_handle, - }; -} - -fn zigJsonToJs(self: *Context, value: std.json.Value) !js.Value { - const isolate = self.isolate; - - switch (value) { - .bool => |v| return .{ .ctx = self, .handle = js.simpleZigValueToJs(isolate, v, true, false) }, - .float => |v| return .{ .ctx = self, .handle = js.simpleZigValueToJs(isolate, v, true, false) }, - .integer => |v| return .{ .ctx = self, .handle = js.simpleZigValueToJs(isolate, v, true, false) }, - .string => |v| return .{ .ctx = self, .handle = js.simpleZigValueToJs(isolate, v, true, false) }, - .null => return .{ .ctx = self, .handle = isolate.initNull() }, - - // TODO handle number_string. - // It is used to represent too big numbers. - .number_string => return error.TODO, - - .array => |v| { - const js_arr = self.newArray(@intCast(v.items.len)); - for (v.items, 0..) |array_value, i| { - if (try js_arr.set(@intCast(i), array_value, .{}) == false) { - return error.JSObjectSetValue; - } - } - return js_arr.toArray(); - }, - .object => |v| { - var js_obj = self.newObject(); - var it = v.iterator(); - while (it.next()) |kv| { - if (try js_obj.set(kv.key_ptr.*, kv.value_ptr.*, .{}) == false) { - return error.JSObjectSetValue; - } - } - return .{ .ctx = self, .handle = @ptrCast(js_obj.handle) }; - }, - } -} - // Microtasks pub fn queueMutationDelivery(self: *Context) !void { self.isolate.enqueueMicrotask(struct { diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index ff00ffcb..57ebc249 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -187,19 +187,27 @@ pub fn dumpMemoryStats(self: *Env) void { fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void { const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?; - const isolate_handle = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?; - const js_isolate = js.Isolate{ .handle = isolate_handle }; - const context = Context.fromIsolate(js_isolate); + const v8_isolate = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?; + const js_isolate = js.Isolate{ .handle = v8_isolate }; + const ctx = Context.fromIsolate(js_isolate); + + const local = js.Local{ + .ctx = ctx, + .isolate = js_isolate, + .handle = v8.v8__Isolate__GetCurrentContext(v8_isolate).?, + .call_arena = ctx.call_arena, + }; const value = if (v8.v8__PromiseRejectMessage__GetValue(&message_handle)) |v8_value| - context.valueToString(.{ .ctx = context, .handle = v8_value }, .{}) catch |err| @errorName(err) + // @HandleScope - no reason to create a js.Context here + local.valueHandleToString(v8_value, .{}) catch |err| @errorName(err) else "no value"; log.debug(.js, "unhandled rejection", .{ .value = value, - .stack = context.stackTrace() catch |err| @errorName(err) orelse "???", + .stack = local.stackTrace() catch |err| @errorName(err) orelse "???", .note = "This should be updated to call window.unhandledrejection", }); } diff --git a/src/browser/js/ExecutionWorld.zig b/src/browser/js/ExecutionWorld.zig index c65fcaa0..fbeb445f 100644 --- a/src/browser/js/ExecutionWorld.zig +++ b/src/browser/js/ExecutionWorld.zig @@ -53,7 +53,6 @@ context_arena: ArenaAllocator, // does all the work, but having all page-specific data structures // grouped together helps keep things clean. context: ?Context = null, -persisted_context: ?js.Global(Context) = null, // no init, must be initialized via env.newExecutionWorld() @@ -77,7 +76,12 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context const isolate = env.isolate; const arena = self.context_arena.allocator(); - const persisted_context: js.Global(Context) = blk: { + // our window wrapped in a v8::Global + var global_global: v8.Global = undefined; + + // Create the v8::Context and wrap it in a v8::Global + var context_global: v8.Global = undefined; + const v8_context = blk: { var temp_scope: js.HandleScope = undefined; temp_scope.init(isolate); defer temp_scope.deinit(); @@ -97,14 +101,15 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context .flags = v8.kOnlyInterceptStrings | v8.kNonMasking, }); - const context_handle = v8.v8__Context__New(isolate.handle, global_template, null).?; - break :blk js.Global(Context).init(isolate.handle, context_handle); + const local_context = v8.v8__Context__New(isolate.handle, global_template, null).?; + v8.v8__Global__New(isolate.handle, local_context, &context_global); + + const global_obj = v8.v8__Context__Global(local_context).?; + v8.v8__Global__New(isolate.handle, global_obj, &global_global); + + break :blk local_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 - const v8_context = persisted_context.local(); var handle_scope: ?js.HandleScope = null; if (enter) { handle_scope = @as(js.HandleScope, undefined); @@ -123,23 +128,24 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context .page = page, .id = context_id, .isolate = isolate, - .handle = v8_context, + .handle = context_global, + .global_global = global_global, .templates = env.templates, .handle_scope = handle_scope, .script_manager = &page._script_manager, .call_arena = page.call_arena, .arena = arena, }; - self.persisted_context = persisted_context; var context = &self.context.?; + try context.identity_map.putNoClobber(arena, @intFromPtr(page.window), global_global); + // Store a pointer to our context inside the v8 context so that, given // a v8 context, we can get our context out - const data = isolate.initBigInt(@intFromPtr(context)); - v8.v8__Context__SetEmbedderData(context.handle, 1, @ptrCast(data.handle)); + const data = isolate.initBigInt(@intFromPtr(&self.context.?)); + v8.v8__Context__SetEmbedderData(v8_context, 1, @ptrCast(data.handle)); - try context.setupGlobal(); - return context; + return &self.context.?; } pub fn removeContext(self: *ExecutionWorld) void { @@ -147,9 +153,6 @@ pub fn removeContext(self: *ExecutionWorld) void { context.deinit(); self.context = null; - self.persisted_context.?.deinit(); - self.persisted_context = null; - self.env.isolate.notifyContextDisposed(); _ = self.context_arena.reset(.{ .retain_with_limit = CONTEXT_ARENA_RETAIN }); } diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig index 28e9b49c..9f83f70d 100644 --- a/src/browser/js/Function.zig +++ b/src/browser/js/Function.zig @@ -24,7 +24,7 @@ const Allocator = std.mem.Allocator; const Function = @This(); -ctx: *js.Context, +local: *const js.Local, this: ?*const v8.Object = null, handle: *const v8.Function, @@ -34,34 +34,35 @@ pub const Result = struct { }; pub fn withThis(self: *const Function, value: anytype) !Function { + const local = self.local; const this_obj = if (@TypeOf(value) == js.Object) value.handle else - (try self.ctx.zigValueToJs(value, .{})).handle; + (try local.zigValueToJs(value, .{})).handle; return .{ - .ctx = self.ctx, + .local = local, .this = this_obj, .handle = self.handle, }; } pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Object { - const ctx = self.ctx; + const local = self.local; var try_catch: js.TryCatch = undefined; - try_catch.init(ctx); + try_catch.init(local); defer try_catch.deinit(); // This creates a new instance using this Function as a constructor. // const c_args = @as(?[*]const ?*c.Value, @ptrCast(&.{})); - const handle = v8.v8__Function__NewInstance(self.handle, ctx.handle, 0, null) orelse { - caught.* = try_catch.caughtOrError(ctx.call_arena, error.Unknown); + const handle = v8.v8__Function__NewInstance(self.handle, local.handle, 0, null) orelse { + caught.* = try_catch.caughtOrError(local.call_arena, error.Unknown); return error.JsConstructorFailed; }; return .{ - .ctx = ctx, + .local = local, .handle = handle, }; } @@ -77,17 +78,17 @@ pub fn tryCall(self: *const Function, comptime T: type, args: anytype, caught: * pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T { var try_catch: js.TryCatch = undefined; - try_catch.init(self.ctx); + try_catch.init(self.local); defer try_catch.deinit(); return self.callWithThis(T, this, args) catch |err| { - caught.* = try_catch.caughtOrError(self.ctx.call_arena, err); + caught.* = try_catch.caughtOrError(self.local.ctx.call_arena, err); return err; }; } pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T { - const ctx = self.ctx; + const local = self.local; // When we're calling a function from within JavaScript itself, this isn't // necessary. We're within a Caller instantiation, which will already have @@ -98,6 +99,7 @@ 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 ctx = local.ctx; const call_depth = ctx.call_depth; ctx.call_depth = call_depth + 1; defer ctx.call_depth = call_depth; @@ -106,7 +108,7 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args if (@TypeOf(this) == js.Object) { break :blk this; } - break :blk try ctx.zigValueToJs(this, .{}); + break :blk try local.zigValueToJs(this, .{}); }; const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args; @@ -116,15 +118,15 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args const fields = s.fields; var js_args: [fields.len]*const v8.Value = undefined; inline for (fields, 0..) |f, i| { - js_args[i] = (try ctx.zigValueToJs(@field(aargs, f.name), .{})).handle; + js_args[i] = (try local.zigValueToJs(@field(aargs, f.name), .{})).handle; } const cargs: [fields.len]*const v8.Value = js_args; break :blk &cargs; }, .pointer => blk: { - var values = try ctx.call_arena.alloc(*const v8.Value, args.len); + var values = try local.call_arena.alloc(*const v8.Value, args.len); for (args, 0..) |a, i| { - values[i] = (try ctx.zigValueToJs(a, .{})).handle; + values[i] = (try local.zigValueToJs(a, .{})).handle; } break :blk values; }, @@ -132,7 +134,7 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args }; const c_args = @as(?[*]const ?*v8.Value, @ptrCast(js_args.ptr)); - const handle = v8.v8__Function__Call(self.handle, ctx.handle, js_this.handle, @as(c_int, @intCast(js_args.len)), c_args) orelse { + const handle = v8.v8__Function__Call(self.handle, local.handle, js_this.handle, @as(c_int, @intCast(js_args.len)), c_args) orelse { // std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"}); return error.JSExecCallback; }; @@ -140,13 +142,13 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args if (@typeInfo(T) == .void) { return {}; } - return ctx.jsValueToZig(T, .{ .ctx = ctx, .handle = handle }); + return local.jsValueToZig(T, .{ .local = local, .handle = handle }); } fn getThis(self: *const Function) js.Object { - const handle = if (self.this) |t| t else v8.v8__Context__Global(self.ctx.handle).?; + const handle = if (self.this) |t| t else v8.v8__Context__Global(self.local.handle).?; return .{ - .ctx = self.ctx, + .local = self.local, .handle = handle, }; } @@ -156,30 +158,24 @@ pub fn src(self: *const Function) ![]const u8 { } pub fn getPropertyValue(self: *const Function, name: []const u8) !?js.Value { - const ctx = self.ctx; - const key = ctx.isolate.initStringHandle(name); - const handle = v8.v8__Object__Get(self.handle, ctx.handle, key) orelse { + const local = self.local; + const key = local.isolate.initStringHandle(name); + const handle = v8.v8__Object__Get(self.handle, self.local.handle, key) orelse { return error.JsException; }; return .{ - .ctx = ctx, + .local = local, .handle = handle, }; } pub fn persist(self: *const Function) !Global { - var ctx = self.ctx; - + var ctx = self.local.ctx; var global: v8.Global = undefined; v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); - try ctx.global_functions.append(ctx.arena, global); - - return .{ - .handle = global, - .ctx = ctx, - }; + return .{ .handle = global }; } pub fn persistWithThis(self: *const Function, value: anytype) !Global { @@ -189,16 +185,15 @@ pub fn persistWithThis(self: *const Function, value: anytype) !Global { pub const Global = struct { handle: v8.Global, - ctx: *js.Context, pub fn deinit(self: *Global) void { v8.v8__Global__Reset(&self.handle); } - pub fn local(self: *const Global) Function { + pub fn local(self: *const Global, l: *const js.Local) Function { return .{ - .ctx = self.ctx, - .handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)), + .local = l, + .handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)), }; } diff --git a/src/browser/js/Inspector.zig b/src/browser/js/Inspector.zig index c518a4b9..e2dcee96 100644 --- a/src/browser/js/Inspector.zig +++ b/src/browser/js/Inspector.zig @@ -20,7 +20,7 @@ const std = @import("std"); const js = @import("js.zig"); const v8 = js.v8; -const Context = @import("Context.zig"); +const TaggedOpaque = @import("TaggedOpaque.zig"); const Allocator = std.mem.Allocator; const RndGen = std.Random.DefaultPrng; @@ -36,7 +36,7 @@ client: Client, channel: Channel, session: Session, rnd: RndGen = RndGen.init(0), -default_context: ?*const v8.Context = null, +default_context: ?v8.Global, // We expect allocator to be an arena // Note: This initializes the pre-allocated inspector in-place @@ -96,9 +96,9 @@ 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.v8__HandleScope__CONSTRUCT(&temp_scope, self.isolate); - defer v8.v8__HandleScope__DESTRUCT(&temp_scope); + var hs: v8.HandleScope = undefined; + v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate); + defer v8.v8__HandleScope__DESTRUCT(&hs); self.session.deinit(); self.client.deinit(); @@ -128,7 +128,7 @@ pub fn send(self: *const Inspector, msg: []const u8) void { // - is_default_context: Whether the execution context is default, should match the auxData pub fn contextCreated( self: *Inspector, - context: *const Context, + local: *const js.Local, name: []const u8, origin: []const u8, aux_data: []const u8, @@ -143,11 +143,11 @@ pub fn contextCreated( aux_data.ptr, aux_data.len, CONTEXT_GROUP_ID, - context.handle, + local.handle, ); if (is_default_context) { - self.default_context = context.handle; + self.default_context = local.ctx.handle; } } @@ -158,18 +158,18 @@ pub fn contextCreated( // we'll create it and track it for cleanup when the context ends. pub fn getRemoteObject( self: *const Inspector, - context: *Context, + local: *const js.Local, group: []const u8, value: anytype, ) !RemoteObject { - const js_value = try context.zigValueToJs(value, .{}); + const js_val = try local.zigValueToJs(value, .{}); // We do not want to expose this as a parameter for now const generate_preview = false; return self.session.wrapObject( - context.isolate.handle, - context.handle, - js_value.handle, + local.isolate.handle, + local.handle, + js_val.handle, group, generate_preview, ); @@ -188,11 +188,10 @@ pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []con if (!v8.v8__Value__IsObject(js_val)) { return error.ObjectIdIsNotANode; } + const Node = @import("../webapi/Node.zig"); // Cast to *const v8.Object for typeTaggedAnyOpaque - return Context.typeTaggedAnyOpaque(*Node, @ptrCast(js_val)) catch { - return error.ObjectIdIsNotANode; - }; + return TaggedOpaque.fromJS(*Node, @ptrCast(js_val)) catch return error.ObjectIdIsNotANode; } pub const RemoteObject = struct { @@ -399,7 +398,7 @@ fn fromData(data: *anyopaque) *Inspector { return @ptrCast(@alignCast(data)); } -pub fn getTaggedAnyOpaque(value: *const v8.Value) ?*js.TaggedAnyOpaque { +pub fn getTaggedOpaque(value: *const v8.Value) ?*TaggedOpaque { if (!v8.v8__Value__IsObject(value)) { return null; } @@ -469,7 +468,8 @@ pub export fn v8_inspector__Client__IMPL__ensureDefaultContextInGroup( data: *anyopaque, ) callconv(.c) ?*const v8.Context { const inspector: *Inspector = @ptrCast(@alignCast(data)); - return inspector.default_context; + const global_handle = inspector.default_context orelse return null; + return v8.v8__Global__Get(&global_handle, inspector.isolate); } pub export fn v8_inspector__Channel__IMPL__sendResponse( diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig new file mode 100644 index 00000000..6db4bcec --- /dev/null +++ b/src/browser/js/Local.zig @@ -0,0 +1,1325 @@ +// Copyright (C) 2023-2026 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 bridge = @import("bridge.zig"); +const Caller = @import("Caller.zig"); +const Context = @import("Context.zig"); +const Isolate = @import("Isolate.zig"); +const TaggedOpaque = @import("TaggedOpaque.zig"); + +const v8 = js.v8; +const CallOpts = Caller.CallOpts; +const Allocator = std.mem.Allocator; + +// Where js.Context has a lifetime tied to the page, and holds the +// v8::Global, this has a much shorter lifetime and holds a +// v8::Local. In V8, you need a Local or get anything +// done, but the local only exists for the lifetime of the HandleScope it was +// created on. When V8 calls into Zig, things are pretty straightforward, since +// that callback gives us the currenty-entered V8::Local. But when Zig +// has to call into V8, it's a bit more messy. +// As a general rule, think of it this way: +// 1 - Caller.zig is for V8 -> Zig +// 2 - Context.zig is for Zig -> V8 +// The Local is encapsulates the data and logic they both need. It just happens +// that it's easier to use Local from Caller than from Context. +const Local = @This(); + +ctx: *Context, +handle: *const v8.Context, + +// available on ctx, but accessed often, so pushed into the Local +isolate: Isolate, +call_arena: std.mem.Allocator, + +pub fn newString(self: *const Local, str: []const u8) js.String { + return .{ + .local = self, + .handle = self.isolate.initStringHandle(str), + }; +} + +pub fn newObject(self: *const Local) js.Object { + return .{ + .local = self, + .handle = v8.v8__Object__New(self.isolate.handle).?, + }; +} + +pub fn newArray(self: *const Local, len: u32) js.Array { + return .{ + .local = self, + .handle = v8.v8__Array__New(self.isolate.handle, @intCast(len)).?, + }; +} + +// == Executors == +pub fn eval(self: *const Local, src: []const u8, name: ?[]const u8) !void { + _ = try self.exec(src, name); +} + +pub fn exec(self: *const Local, src: []const u8, name: ?[]const u8) !js.Value { + return self.compileAndRun(src, name); +} + +pub fn compileAndRun(self: *const Local, src: []const u8, name: ?[]const u8) !js.Value { + const script_name = self.isolate.initStringHandle(name orelse "anonymous"); + const script_source = self.isolate.initStringHandle(src); + + // Create ScriptOrigin + var origin: v8.ScriptOrigin = undefined; + v8.v8__ScriptOrigin__CONSTRUCT(&origin, @ptrCast(script_name)); + + // Create ScriptCompilerSource + var script_comp_source: v8.ScriptCompilerSource = undefined; + v8.v8__ScriptCompiler__Source__CONSTRUCT2(script_source, &origin, null, &script_comp_source); + defer v8.v8__ScriptCompiler__Source__DESTRUCT(&script_comp_source); + + // Compile the script + const v8_script = v8.v8__ScriptCompiler__Compile( + self.handle, + &script_comp_source, + v8.kNoCompileOptions, + v8.kNoCacheNoReason, + ) orelse return error.CompilationError; + + // Run the script + const result = v8.v8__Script__Run(v8_script, self.handle) orelse return error.ExecutionError; + return .{ .local = self, .handle = result }; +} + +// == Zig -> JS == + +// 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.Global(js.Object) (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.Global(js.Object) (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) +pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object, value: anytype) !js.Object { + const ctx = self.ctx; + const arena = ctx.arena; + + const T = @TypeOf(value); + switch (@typeInfo(T)) { + .@"struct" => { + // Struct, has to be placed on the heap + const heap = try arena.create(T); + heap.* = value; + return self.mapZigInstanceToJs(js_obj_handle, heap); + }, + .pointer => |ptr| { + const resolved = resolveValue(value); + + const gop = try ctx.identity_map.getOrPut(arena, @intFromPtr(resolved.ptr)); + if (gop.found_existing) { + // we've seen this instance before, return the same object + return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self); + } + + const isolate = self.isolate; + const JsApi = bridge.Struct(ptr.child).JsApi; + + // Sometimes we're creating a new Object, like when + // we're returning a value from a function. In those cases + // we have to get the object template, and we can get an object + // by calling initInstance its InstanceTemplate. + // Sometimes though we already have the Object to bind to + // for example, when we're executing a constructor, v8 has + // already created the "this" object. + const js_obj = js.Object{ + .local = self, + .handle = js_obj_handle orelse blk: { + const function_template_handle = ctx.templates[resolved.class_id]; + const object_template_handle = v8.v8__FunctionTemplate__InstanceTemplate(function_template_handle).?; + break :blk v8.v8__ObjectTemplate__NewInstance(object_template_handle, self.handle).?; + }, + }; + + if (!@hasDecl(JsApi.Meta, "empty_with_no_proto")) { + // The TAO contains the pointer to our Zig instance as + // well as any meta data we'll need to use it later. + // See the TaggedOpaque struct for more details. + const tao = try arena.create(TaggedOpaque); + tao.* = .{ + .value = resolved.ptr, + .prototype_chain = resolved.prototype_chain.ptr, + .prototype_len = @intCast(resolved.prototype_chain.len), + .subtype = if (@hasDecl(JsApi.Meta, "subtype")) JsApi.Meta.subype else .node, + }; + + // Skip setting internal field for the global object (Window) + // Window accessors get the instance from context.page.window instead + // if (resolved.class_id != @import("../webapi/Window.zig").JsApi.Meta.class_id) { + v8.v8__Object__SetInternalField(js_obj.handle, 0, isolate.createExternal(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 + // TaggedOpaque, we'll also know there that + // the type is empty and can create an empty instance. + } + + // dont' use js_obj.persist(), because we don't want to track this in + // context.global_objects, we want to track it in context.identity_map. + v8.v8__Global__New(isolate.handle, js_obj.handle, gop.value_ptr); + return js_obj; + }, + else => @compileError("Expected a struct or pointer, got " ++ @typeName(T) ++ " (constructors must return struct or pointers)"), + } +} + +pub fn zigValueToJs(self: *const Local, value: anytype, comptime opts: CallOpts) !js.Value { + const isolate = self.isolate; + + // 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 (js.simpleZigValueToJs(isolate, value, false, opts.null_as_undefined)) |js_value_handle| { + return .{ .local = self, .handle = js_value_handle }; + } + + const T = @TypeOf(value); + switch (@typeInfo(T)) { + .void, .bool, .int, .comptime_int, .float, .comptime_float, .@"enum", .null => { + // Need to do this to keep the compiler happy + // simpleZigValueToJs handles all of these cases. + unreachable; + }, + .array => { + var js_arr = self.newArray(value.len); + for (value, 0..) |v, i| { + if (try js_arr.set(@intCast(i), v, opts) == false) { + return error.FailedToCreateArray; + } + } + return js_arr.toValue(); + }, + .pointer => |ptr| switch (ptr.size) { + .one => { + if (@typeInfo(ptr.child) == .@"struct" and @hasDecl(ptr.child, "JsApi")) { + if (bridge.JsApiLookup.has(ptr.child.JsApi)) { + const js_obj = try self.mapZigInstanceToJs(null, value); + return js_obj.toValue(); + } + } + + if (@typeInfo(ptr.child) == .@"struct" and @hasDecl(ptr.child, "runtimeGenericWrap")) { + const wrap = try value.runtimeGenericWrap(self.ctx.page); + return self.zigValueToJs(wrap, opts); + } + + 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 = self.newArray(@intCast(value.len)); + for (value, 0..) |v, i| { + if (try js_arr.set(@intCast(i), v, opts) == false) { + return error.FailedToCreateArray; + } + } + return js_arr.toValue(); + }, + else => {}, + }, + .@"struct" => |s| { + if (@hasDecl(T, "JsApi")) { + if (bridge.JsApiLookup.has(T.JsApi)) { + const js_obj = try self.mapZigInstanceToJs(null, value); + return js_obj.toValue(); + } + } + + if (T == js.Function) { + // we're returning a callback + return .{ .local = self, .handle = @ptrCast(value.handle) }; + } + + if (T == js.Function.Global) { + // Auto-convert Global to local for bridge + return .{ .local = self, .handle = @ptrCast(value.local(self).handle) }; + } + + if (T == js.Object) { + // we're returning a v8.Object + return .{ .local = self, .handle = @ptrCast(value.handle) }; + } + + if (T == js.Object.Global) { + // Auto-convert Global to local for bridge + return .{ .local = self, .handle = @ptrCast(value.local(self).handle) }; + } + + if (T == js.Value.Global) { + // Auto-convert Global to local for bridge + return .{ .local = self, .handle = @ptrCast(value.local(self).handle) }; + } + + if (T == js.Promise.Global) { + // Auto-convert Global to local for bridge + return .{ .local = self, .handle = @ptrCast(value.local(self).handle) }; + } + + if (T == js.PromiseResolver.Global) { + // Auto-convert Global to local for bridge + return .{ .local = self, .handle = @ptrCast(value.local(self).handle) }; + } + + if (T == js.Module.Global) { + // Auto-convert Global to local for bridge + return .{ .local = self, .handle = @ptrCast(value.local(self).handle) }; + } + + if (T == js.Value) { + return value; + } + + if (T == js.Promise) { + return .{ .local = self, .handle = @ptrCast(value.handle) }; + } + + if (T == js.Exception) { + return .{ .local = self, .handle = isolate.throwException(value.handle) }; + } + + if (T == js.String) { + return .{ .local = self, .handle = @ptrCast(value.handle) }; + } + + if (@hasDecl(T, "runtimeGenericWrap")) { + const wrap = try value.runtimeGenericWrap(self.ctx.page); + return self.zigValueToJs(wrap, opts); + } + + if (s.is_tuple) { + // return the tuple struct as an array + var js_arr = self.newArray(@intCast(s.fields.len)); + inline for (s.fields, 0..) |f, i| { + if (try js_arr.set(@intCast(i), @field(value, f.name), opts) == false) { + return error.FailedToCreateArray; + } + } + return js_arr.toValue(); + } + + const js_obj = self.newObject(); + inline for (s.fields) |f| { + if (try js_obj.set(f.name, @field(value, f.name), opts) == false) { + return error.CreateObjectFailure; + } + } + return js_obj.toValue(); + }, + .@"union" => |un| { + if (T == std.json.Value) { + return self.zigJsonToJs(value); + } + if (un.tag_type) |UnionTagType| { + inline for (un.fields) |field| { + if (value == @field(UnionTagType, field.name)) { + return self.zigValueToJs(@field(value, field.name), opts); + } + } + unreachable; + } + @compileError("Cannot use untagged union: " ++ @typeName(T)); + }, + .optional => { + if (value) |v| { + return self.zigValueToJs(v, opts); + } + // would be handled by simpleZigValueToJs + unreachable; + }, + .error_union => return self.zigValueToJs(try value, opts), + else => {}, + } + + @compileError("A function returns an unsupported type: " ++ @typeName(T)); +} + +fn zigJsonToJs(self: *const Local, value: std.json.Value) !js.Value { + const isolate = self.isolate; + + switch (value) { + .bool => |v| return .{ .local = self, .handle = js.simpleZigValueToJs(isolate, v, true, false) }, + .float => |v| return .{ .local = self, .handle = js.simpleZigValueToJs(isolate, v, true, false) }, + .integer => |v| return .{ .local = self, .handle = js.simpleZigValueToJs(isolate, v, true, false) }, + .string => |v| return .{ .local = self, .handle = js.simpleZigValueToJs(isolate, v, true, false) }, + .null => return .{ .local = self, .handle = isolate.initNull() }, + + // TODO handle number_string. + // It is used to represent too big numbers. + .number_string => return error.TODO, + + .array => |v| { + const js_arr = self.newArray(@intCast(v.items.len)); + for (v.items, 0..) |array_value, i| { + if (try js_arr.set(@intCast(i), array_value, .{}) == false) { + return error.JSObjectSetValue; + } + } + return js_arr.toArray(); + }, + .object => |v| { + var js_obj = self.newObject(); + var it = v.iterator(); + while (it.next()) |kv| { + if (try js_obj.set(kv.key_ptr.*, kv.value_ptr.*, .{}) == false) { + return error.JSObjectSetValue; + } + } + return .{ .local = self, .handle = @ptrCast(js_obj.handle) }; + }, + } +} + +// == JS -> Zig == + +pub fn jsValueToZig(self: *const Local, comptime T: type, js_val: js.Value) !T { + switch (@typeInfo(T)) { + .optional => |o| { + // If type type is a ?js.Value or a ?js.Object, then we want to pass + // a js.Object, not null. Consider a function, + // _doSomething(arg: ?Env.JsObjet) void { ... } + // + // And then these two calls: + // doSomething(); + // doSomething(null); + // + // In the first case, we'll pass `null`. But in the + // second, we'll pass a js.Object which represents + // null. + // If we don't have this code, both cases will + // pass in `null` and the the doSomething won't + // be able to tell if `null` was explicitly passed + // or whether no parameter was passed. + if (comptime o.child == js.Value) { + return js_val; + } + + if (comptime o.child == js.Object) { + return js.Object{ + .local = self, + .handle = @ptrCast(js_val.handle), + }; + } + + if (js_val.isNullOrUndefined()) { + return null; + } + return try self.jsValueToZig(o.child, js_val); + }, + .float => |f| switch (f.bits) { + 0...32 => return js_val.toF32(), + 33...64 => return js_val.toF64(), + else => {}, + }, + .int => return jsIntToZig(T, js_val), + .bool => return js_val.toBool(), + .pointer => |ptr| switch (ptr.size) { + .one => { + if (!js_val.isObject()) { + return error.InvalidArgument; + } + if (@hasDecl(ptr.child, "JsApi")) { + std.debug.assert(bridge.JsApiLookup.has(ptr.child.JsApi)); + return TaggedOpaque.fromJS(*ptr.child, @ptrCast(js_val.handle)); + } + }, + .slice => { + if (ptr.sentinel() == null) { + if (try jsValueToTypedArray(ptr.child, js_val)) |value| { + return value; + } + } + + if (ptr.child == u8) { + if (ptr.sentinel()) |s| { + if (comptime s == 0) { + return self.valueToStringZ(js_val, .{}); + } + } else { + return self.valueToString(js_val, .{}); + } + } + + if (!js_val.isArray()) { + return error.InvalidArgument; + } + const js_arr = js_val.toArray(); + const arr = try self.call_arena.alloc(ptr.child, js_arr.len()); + for (arr, 0..) |*a, i| { + const item_value = try js_arr.get(@intCast(i)); + a.* = try self.jsValueToZig(ptr.child, item_value); + } + return arr; + }, + else => {}, + }, + .array => |arr| { + // Retrieve fixed-size array as slice + const slice_type = []arr.child; + const slice_value = try self.jsValueToZig(slice_type, js_val); + if (slice_value.len != arr.len) { + // Exact length match, we could allow smaller arrays, but we would not be able to communicate how many were written + return error.InvalidArgument; + } + return @as(*T, @ptrCast(slice_value.ptr)).*; + }, + .@"struct" => { + return try (self.jsValueToStruct(T, js_val)) 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_val could be + // coerced to. + var coerce_index: ?usize = null; + + // the first field that we find which the js_val 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(field.type, js_val)) { + .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(field.type, js_val)); + }, + .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(field.type, js_val)); + } + } + unreachable; + }, + .@"enum" => |e| { + if (@hasDecl(T, "js_enum_from_string")) { + if (!js_val.isString()) { + return error.InvalidArgument; + } + return std.meta.stringToEnum(T, try self.valueToString(js_val, .{})) orelse return error.InvalidArgument; + } + switch (@typeInfo(e.tag_type)) { + .int => return std.meta.intToEnum(T, try jsIntToZig(e.tag_type, js_val)), + else => @compileError("unsupported enum parameter type: " ++ @typeName(T)), + } + }, + else => {}, + } + + @compileError("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: *const Local, comptime T: type, js_val: js.Value) !?T { + return switch (T) { + js.Function => { + if (!js_val.isFunction()) { + return null; + } + return .{ .local = self, .handle = @ptrCast(js_val.handle) }; + }, + js.Function.Global => { + if (!js_val.isFunction()) { + return null; + } + return try (js.Function{ .local = self, .handle = @ptrCast(js_val.handle) }).persist(); + }, + // zig fmt: off + js.TypedArray(u8), js.TypedArray(u16), js.TypedArray(u32), js.TypedArray(u64), + js.TypedArray(i8), js.TypedArray(i16), js.TypedArray(i32), js.TypedArray(i64), + js.TypedArray(f32), js.TypedArray(f64), + // zig fmt: on + => { + const ValueType = @typeInfo(std.meta.fieldInfo(T, .values).type).pointer.child; + const arr = (try jsValueToTypedArray(ValueType, js_val)) orelse return null; + return .{ .values = arr }; + }, + js.Value => js_val, + js.Value.Global => return try js_val.persist(), + js.Object => { + if (!js_val.isObject()) { + return null; + } + return js.Object{ + .local = self, + .handle = @ptrCast(js_val.handle), + }; + }, + js.Object.Global => { + if (!js_val.isObject()) { + return null; + } + const obj = js.Object{ + .local = self, + .handle = @ptrCast(js_val.handle), + }; + return try obj.persist(); + }, + + js.Promise.Global => { + if (!js_val.isPromise()) { + return null; + } + const promise = js.Promise{ + .ctx = self, + .handle = @ptrCast(js_val.handle), + }; + return try promise.persist(); + }, + else => { + if (!js_val.isObject()) { + return null; + } + + const isolate = self.isolate; + const js_obj = js_val.toObject(); + + var value: T = undefined; + inline for (@typeInfo(T).@"struct".fields) |field| { + const name = field.name; + const key = isolate.initStringHandle(name); + if (js_obj.has(key)) { + @field(value, name) = try self.jsValueToZig(field.type, try js_obj.get(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 jsValueToTypedArray(comptime T: type, js_val: js.Value) !?[]T { + var force_u8 = false; + var array_buffer: ?*const v8.ArrayBuffer = null; + var byte_len: usize = undefined; + var byte_offset: usize = undefined; + + if (js_val.isTypedArray()) { + const buffer_handle: *const v8.ArrayBufferView = @ptrCast(js_val.handle); + byte_len = v8.v8__ArrayBufferView__ByteLength(buffer_handle); + byte_offset = v8.v8__ArrayBufferView__ByteOffset(buffer_handle); + array_buffer = v8.v8__ArrayBufferView__Buffer(buffer_handle).?; + } else if (js_val.isArrayBufferView()) { + force_u8 = true; + const buffer_handle: *const v8.ArrayBufferView = @ptrCast(js_val.handle); + byte_len = v8.v8__ArrayBufferView__ByteLength(buffer_handle); + byte_offset = v8.v8__ArrayBufferView__ByteOffset(buffer_handle); + array_buffer = v8.v8__ArrayBufferView__Buffer(buffer_handle).?; + } else if (js_val.isArrayBuffer()) { + force_u8 = true; + array_buffer = @ptrCast(js_val.handle); + byte_len = v8.v8__ArrayBuffer__ByteLength(array_buffer); + byte_offset = 0; + } + + const backing_store_ptr = v8.v8__ArrayBuffer__GetBackingStore(array_buffer orelse return null); + const backing_store_handle = v8.std__shared_ptr__v8__BackingStore__get(&backing_store_ptr).?; + const data = v8.v8__BackingStore__Data(backing_store_handle); + + switch (T) { + u8 => { + if (force_u8 or js_val.isUint8Array() or js_val.isUint8ClampedArray()) { + if (byte_len == 0) return &[_]u8{}; + const arr_ptr = @as([*]u8, @ptrCast(@alignCast(data))); + return arr_ptr[byte_offset .. byte_offset + byte_len]; + } + }, + i8 => { + if (js_val.isInt8Array()) { + if (byte_len == 0) return &[_]i8{}; + const arr_ptr = @as([*]i8, @ptrCast(@alignCast(data))); + return arr_ptr[byte_offset .. byte_offset + byte_len]; + } + }, + u16 => { + if (js_val.isUint16Array()) { + if (byte_len == 0) return &[_]u16{}; + const arr_ptr = @as([*]u16, @ptrCast(@alignCast(data))); + return arr_ptr[byte_offset .. byte_offset + byte_len / 2]; + } + }, + i16 => { + if (js_val.isInt16Array()) { + if (byte_len == 0) return &[_]i16{}; + const arr_ptr = @as([*]i16, @ptrCast(@alignCast(data))); + return arr_ptr[byte_offset .. byte_offset + byte_len / 2]; + } + }, + u32 => { + if (js_val.isUint32Array()) { + if (byte_len == 0) return &[_]u32{}; + const arr_ptr = @as([*]u32, @ptrCast(@alignCast(data))); + return arr_ptr[byte_offset .. byte_offset + byte_len / 4]; + } + }, + i32 => { + if (js_val.isInt32Array()) { + if (byte_len == 0) return &[_]i32{}; + const arr_ptr = @as([*]i32, @ptrCast(@alignCast(data))); + return arr_ptr[byte_offset .. byte_offset + byte_len / 4]; + } + }, + u64 => { + if (js_val.isBigUint64Array()) { + if (byte_len == 0) return &[_]u64{}; + const arr_ptr = @as([*]u64, @ptrCast(@alignCast(data))); + return arr_ptr[byte_offset .. byte_offset + byte_len / 8]; + } + }, + i64 => { + if (js_val.isBigInt64Array()) { + if (byte_len == 0) return &[_]i64{}; + const arr_ptr = @as([*]i64, @ptrCast(@alignCast(data))); + return arr_ptr[byte_offset .. byte_offset + byte_len / 8]; + } + }, + else => {}, + } + return error.InvalidArgument; +} + +// 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: *const Local, comptime T: type, js_val: js.Value) !ProbeResult(T) { + switch (@typeInfo(T)) { + .optional => |o| { + if (js_val.isNullOrUndefined()) { + return .{ .value = null }; + } + return self.probeJsValueToZig(o.child, js_val); + }, + .float => { + if (js_val.isNumber() or js_val.isNumberObject()) { + if (js_val.isInt32() or js_val.isUint32() or js_val.isBigInt() or js_val.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_val.isNumber() or js_val.isNumberObject()) { + if (js_val.isInt32() or js_val.isUint32() or js_val.isBigInt() or js_val.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_val.isBoolean() or js_val.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_val.isObject()) { + return .{ .invalid = {} }; + } + if (bridge.JsApiLookup.has(ptr.child.JsApi)) { + // There's a bit of overhead in doing this, so instead + // of having a version of TaggedOpaque which + // returns a boolean or an optional, we rely on the + // main implementation and just handle the error. + const attempt = TaggedOpaque.fromJS(*ptr.child, @ptrCast(js_val.handle)); + if (attempt) |value| { + return .{ .value = value }; + } else |_| { + return .{ .invalid = {} }; + } + } + // probably an error, but not for us to deal with + return .{ .invalid = {} }; + }, + .slice => { + if (js_val.isTypedArray()) { + switch (ptr.child) { + u8 => if (ptr.sentinel() == null) { + if (js_val.isUint8Array() or js_val.isUint8ClampedArray()) { + return .{ .ok = {} }; + } + }, + i8 => if (js_val.isInt8Array()) { + return .{ .ok = {} }; + }, + u16 => if (js_val.isUint16Array()) { + return .{ .ok = {} }; + }, + i16 => if (js_val.isInt16Array()) { + return .{ .ok = {} }; + }, + u32 => if (js_val.isUint32Array()) { + return .{ .ok = {} }; + }, + i32 => if (js_val.isInt32Array()) { + return .{ .ok = {} }; + }, + u64 => if (js_val.isBigUint64Array()) { + return .{ .ok = {} }; + }, + i64 => if (js_val.isBigInt64Array()) { + return .{ .ok = {} }; + }, + else => {}, + } + return .{ .invalid = {} }; + } + + if (ptr.child == u8) { + if (js_val.isString()) { + return .{ .ok = {} }; + } + // anything can be coerced into a string + return .{ .coerce = {} }; + } + + if (!js_val.isArray()) { + return .{ .invalid = {} }; + } + + // This can get tricky. + const js_arr = js_val.toArray(); + + if (js_arr.len() == 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 first_val = try js_arr.get(0); + switch (try self.probeJsValueToZig(ptr.child, first_val)) { + .value, .ok => return .{ .ok = {} }, + .compatible => return .{ .compatible = {} }, + .coerce => return .{ .coerce = {} }, + .invalid => return .{ .invalid = {} }, + } + }, + else => {}, + }, + .array => |arr| { + // Retrieve fixed-size array as slice then probe + const slice_type = []arr.child; + switch (try self.probeJsValueToZig(slice_type, js_val)) { + .value => |slice_value| { + if (slice_value.len == arr.len) { + return .{ .value = @as(*T, @ptrCast(slice_value.ptr)).* }; + } + return .{ .invalid = {} }; + }, + .ok => { + // Exact length match, we could allow smaller arrays as .compatible, but we would not be able to communicate how many were written + if (js_val.isArray()) { + const js_arr = js_val.toArray(); + if (js_arr.len() == arr.len) { + return .{ .ok = {} }; + } + } else if (js_val.isString() and arr.child == u8) { + const str = try js_val.toString(self.local); + if (str.lenUtf8(self.isolate) == arr.len) { + return .{ .ok = {} }; + } + } + return .{ .invalid = {} }; + }, + .compatible => return .{ .compatible = {} }, + .coerce => return .{ .coerce = {} }, + .invalid => return .{ .invalid = {} }, + } + }, + .@"struct" => { + // We don't want to duplicate the code for this, so we call + // the actual conversion function. + const value = (try self.jsValueToStruct(T, js_val)) orelse { + return .{ .invalid = {} }; + }; + return .{ .value = value }; + }, + else => {}, + } + + return .{ .invalid = {} }; +} + +fn jsIntToZig(comptime T: type, js_value: js.Value) !T { + const n = @typeInfo(T).int; + switch (n.signedness) { + .signed => switch (n.bits) { + 8 => return jsSignedIntToZig(i8, -128, 127, try js_value.toI32()), + 16 => return jsSignedIntToZig(i16, -32_768, 32_767, try js_value.toI32()), + 32 => return jsSignedIntToZig(i32, -2_147_483_648, 2_147_483_647, try js_value.toI32()), + 64 => { + if (js_value.isBigInt()) { + const v = js_value.toBigInt(); + return v.getInt64(); + } + return jsSignedIntToZig(i64, -2_147_483_648, 2_147_483_647, try js_value.toI32()); + }, + else => {}, + }, + .unsigned => switch (n.bits) { + 8 => return jsUnsignedIntToZig(u8, 255, try js_value.toU32()), + 16 => return jsUnsignedIntToZig(u16, 65_535, try js_value.toU32()), + 32 => { + if (js_value.isBigInt()) { + const v = js_value.toBigInt(); + const large = v.getUint64(); + if (large <= 4_294_967_295) { + return @intCast(large); + } + return error.InvalidArgument; + } + return jsUnsignedIntToZig(u32, 4_294_967_295, try js_value.toU32()); + }, + 64 => { + if (js_value.isBigInt()) { + const v = js_value.toBigInt(); + return v.getUint64(); + } + return jsUnsignedIntToZig(u64, 4_294_967_295, try js_value.toU32()); + }, + 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; +} + +// Every WebApi type has a class_id as T.JsApi.Meta.class_id. We use this to create +// a JSValue class of the correct type. However, given a Node, we don't want +// to create a Node class, we want to create a class of the most specific type. +// In other words, given a Node{._type = .{.document .{}}}, we want to create +// a Document, not a Node. +// This function recursively walks the _type union field (if there is one) to +// get the most specific class_id possible. +const Resolved = struct { + ptr: *anyopaque, + class_id: u16, + prototype_chain: []const @import("TaggedOpaque.zig").PrototypeChainEntry, +}; +pub fn resolveValue(value: anytype) Resolved { + const T = bridge.Struct(@TypeOf(value)); + if (!@hasField(T, "_type") or @typeInfo(@TypeOf(value._type)) != .@"union") { + return resolveT(T, value); + } + + const U = @typeInfo(@TypeOf(value._type)).@"union"; + inline for (U.fields) |field| { + if (value._type == @field(U.tag_type.?, field.name)) { + const child = switch (@typeInfo(field.type)) { + .pointer => @field(value._type, field.name), + .@"struct" => &@field(value._type, field.name), + .void => { + // Unusual case, but the Event (and maybe others) can be + // returned as-is. In that case, it has a dummy void type. + return resolveT(T, value); + }, + else => @compileError(@typeName(field.type) ++ " has an unsupported _type field"), + }; + return resolveValue(child); + } + } + unreachable; +} + +fn resolveT(comptime T: type, value: *anyopaque) Resolved { + return .{ + .ptr = value, + .class_id = T.JsApi.Meta.class_id, + .prototype_chain = &T.JsApi.Meta.prototype_chain, + }; +} + +pub fn stackTrace(self: *const Local) !?[]const u8 { + const isolate = self.isolate; + const separator = log.separator(); + + var buf: std.ArrayList(u8) = .empty; + var writer = buf.writer(self.call_arena); + + const stack_trace_handle = v8.v8__StackTrace__CurrentStackTrace__STATIC(isolate.handle, 30).?; + const frame_count = v8.v8__StackTrace__GetFrameCount(stack_trace_handle); + + if (v8.v8__StackTrace__CurrentScriptNameOrSourceURL__STATIC(isolate.handle)) |script| { + try writer.print("{s}<{s}>", .{ separator, try self.jsStringToZig(script, .{}) }); + } + + for (0..@intCast(frame_count)) |i| { + const frame_handle = v8.v8__StackTrace__GetFrame(stack_trace_handle, isolate.handle, @intCast(i)).?; + if (v8.v8__StackFrame__GetFunctionName(frame_handle)) |name| { + const script = try self.jsStringToZig(name, .{}); + try writer.print("{s}{s}:{d}", .{ separator, script, v8.v8__StackFrame__GetLineNumber(frame_handle) }); + } else { + try writer.print("{s}:{d}", .{ separator, v8.v8__StackFrame__GetLineNumber(frame_handle) }); + } + } + return buf.items; +} + +// == Stringifiers == +const ToStringOpts = struct { + allocator: ?Allocator = null, +}; +pub fn valueToString(self: *const Local, js_val: js.Value, opts: ToStringOpts) ![]u8 { + return self.valueHandleToString(js_val.handle, opts); +} +pub fn valueToStringZ(self: *const Local, js_val: js.Value, opts: ToStringOpts) ![:0]u8 { + return self.valueHandleToStringZ(js_val.handle, opts); +} + +pub fn valueHandleToString(self: *const Local, js_val: *const v8.Value, opts: ToStringOpts) ![]u8 { + return self._valueToString(false, js_val, opts); +} +pub fn valueHandleToStringZ(self: *const Local, js_val: *const v8.Value, opts: ToStringOpts) ![:0]u8 { + return self._valueToString(true, js_val, opts); +} + +fn _valueToString(self: *const Local, comptime null_terminate: bool, value_handle: *const v8.Value, opts: ToStringOpts) !(if (null_terminate) [:0]u8 else []u8) { + var resolved_value_handle = value_handle; + if (v8.v8__Value__IsSymbol(value_handle)) { + const symbol_handle = v8.v8__Symbol__Description(@ptrCast(value_handle), self.isolate.handle).?; + resolved_value_handle = @ptrCast(symbol_handle); + } + + const string_handle = v8.v8__Value__ToString(resolved_value_handle, self.handle) orelse { + return error.JsException; + }; + + return self._jsStringToZig(null_terminate, string_handle, opts); +} + +pub fn jsStringToZig(self: *const Local, str: anytype, opts: ToStringOpts) ![]u8 { + return self._jsStringToZig(false, str, opts); +} +pub fn jsStringToZigZ(self: *const Local, str: anytype, opts: ToStringOpts) ![:0]u8 { + return self._jsStringToZig(true, str, opts); +} +fn _jsStringToZig(self: *const Local, comptime null_terminate: bool, str: anytype, opts: ToStringOpts) !(if (null_terminate) [:0]u8 else []u8) { + const handle = if (@TypeOf(str) == js.String) str.handle else str; + + const len = v8.v8__String__Utf8Length(handle, self.isolate.handle); + const allocator = opts.allocator orelse self.call_arena; + const buf = try (if (comptime null_terminate) allocator.allocSentinel(u8, @intCast(len), 0) else allocator.alloc(u8, @intCast(len))); + const n = v8.v8__String__WriteUtf8(handle, self.isolate.handle, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8); + std.debug.assert(n == len); + + return buf; +} + +// == Promise Helpers == +pub fn rejectPromise(self: *const Local, value: anytype) !js.Promise { + var resolver = js.PromiseResolver.init(self); + resolver.reject("Local.rejectPromise", value); + return resolver.promise(); +} + +pub fn resolvePromise(self: *const Local, value: anytype) !js.Promise { + var resolver = js.PromiseResolver.init(self); + resolver.resolve("Local.resolvePromise", value); + return resolver.promise(); +} + +pub fn createPromiseResolver(self: *const Local) js.PromiseResolver { + return js.PromiseResolver.init(self); +} + +pub fn debugValue(self: *const Local, js_val: js.Value, writer: *std.Io.Writer) !void { + var seen: std.AutoHashMapUnmanaged(u32, void) = .empty; + return self._debugValue(js_val, &seen, 0, writer) catch error.WriteFailed; +} + +fn _debugValue(self: *const Local, js_val: js.Value, seen: *std.AutoHashMapUnmanaged(u32, void), depth: usize, writer: *std.Io.Writer) !void { + if (js_val.isNull()) { + // I think null can sometimes appear as an object, so check this and + // handle it first. + return writer.writeAll("null"); + } + + if (!js_val.isObject()) { + // handle these explicitly, so we don't include the type (we only want to include + // it when there's some ambiguity, e.g. the string "true") + if (js_val.isUndefined()) { + return writer.writeAll("undefined"); + } + if (js_val.isTrue()) { + return writer.writeAll("true"); + } + if (js_val.isFalse()) { + return writer.writeAll("false"); + } + + if (js_val.isSymbol()) { + const symbol_handle = v8.v8__Symbol__Description(@ptrCast(js_val.handle), self.isolate.handle).?; + const js_sym_str = try self.valueToString(.{ .local = self, .handle = symbol_handle }, .{}); + return writer.print("{s} (symbol)", .{js_sym_str}); + } + const js_type = try self.jsStringToZig(js_val.typeOf(), .{}); + const js_val_str = try self.valueToString(js_val, .{}); + if (js_val_str.len > 2000) { + try writer.writeAll(js_val_str[0..2000]); + try writer.writeAll(" ... (truncated)"); + } else { + try writer.writeAll(js_val_str); + } + return writer.print(" ({s})", .{js_type}); + } + + const js_obj = js_val.toObject(); + { + // explicit scope because gop will become invalid in recursive call + const gop = try seen.getOrPut(self.call_arena, js_obj.getId()); + if (gop.found_existing) { + return writer.writeAll("\n"); + } + gop.value_ptr.* = {}; + } + + const names_arr = js_obj.getOwnPropertyNames(); + const len = names_arr.len(); + + if (depth > 20) { + return writer.writeAll("...deeply nested object..."); + } + const own_len = js_obj.getOwnPropertyNames().len(); + if (own_len == 0) { + const js_val_str = try self.valueToString(js_val, .{}); + if (js_val_str.len > 2000) { + try writer.writeAll(js_val_str[0..2000]); + return writer.writeAll(" ... (truncated)"); + } + return writer.writeAll(js_val_str); + } + + const all_len = js_obj.getPropertyNames().len(); + try writer.print("({d}/{d})", .{ own_len, all_len }); + for (0..len) |i| { + if (i == 0) { + try writer.writeByte('\n'); + } + const field_name = try names_arr.get(@intCast(i)); + const name = try self.valueToString(field_name, .{}); + try writer.splatByteAll(' ', depth); + try writer.writeAll(name); + try writer.writeAll(": "); + const field_val = try js_obj.get(name); + try self._debugValue(field_val, seen, depth + 1, writer); + if (i != len - 1) { + try writer.writeByte('\n'); + } + } +} + +// == Misc == +pub fn parseJSON(self: *const Local, json: []const u8) !js.Value { + const string_handle = self.isolate.initStringHandle(json); + const value_handle = v8.v8__JSON__Parse(self.handle, string_handle) orelse return error.JsException; + return .{ + .local = self, + .handle = value_handle, + }; +} + +pub fn throw(self: *const Local, err: []const u8) js.Exception { + const handle = self.isolate.createError(err); + return .{ + .local = self, + .handle = handle, + }; +} + +// Convert a Global (or optional Global) to a Local (or optional Local). +// Meant to be used from either page.js.toLocal, where the context must have an +// non-null local (orelse panic), or from a LocalScope +pub fn toLocal(self: *const Local, global: anytype) ToLocalReturnType(@TypeOf(global)) { + const T = @TypeOf(global); + if (@typeInfo(T) == .optional) { + const unwrapped = global orelse return null; + return unwrapped.local(self); + } + return global.local(self); +} + +pub fn ToLocalReturnType(comptime T: type) type { + if (@typeInfo(T) == .optional) { + const GlobalType = @typeInfo(T).optional.child; + const struct_info = @typeInfo(GlobalType).@"struct"; + inline for (struct_info.decls) |decl| { + if (std.mem.eql(u8, decl.name, "local")) { + const Fn = @TypeOf(@field(GlobalType, "local")); + const fn_info = @typeInfo(Fn).@"fn"; + return ?fn_info.return_type.?; + } + } + @compileError("Type does not have local method"); + } else { + const struct_info = @typeInfo(T).@"struct"; + inline for (struct_info.decls) |decl| { + if (std.mem.eql(u8, decl.name, "local")) { + const Fn = @TypeOf(@field(T, "local")); + const fn_info = @typeInfo(Fn).@"fn"; + return fn_info.return_type.?; + } + } + @compileError("Type does not have local method"); + } +} + +pub fn debugContextId(self: *const Local) i32 { + return v8.v8__Context__DebugContextId(self.handle); +} + +// Encapsulates a Local and a HandleScope (TODO). When we're going from V8->Zig +// we easily get both a Local and a HandleScope via Caller.init. +// But when we're going from Zig -> V8, things are more complicated. + +// 1 - In some cases, we're going from Zig -> V8, but the origin is actually V8, +// so it's really V8 -> Zig -> V8. For example, when element.click() is called, +// V8 will call the Element.click method, which could then call back into V8 for +// a click handler. +// +// 2 - In other cases, it's always initiated from Zig, e.g. window.setTimeout or +// window.onload. +// +// 3 - Yet in other cases, it might could be either. Event dispatching can both be +// initiated from Zig and from V8. +// +// When JS execution is Zig initiated (or if we aren't sure whether it's Zig +// initiated or not), we need to create a Local.Scope: +// +// var ls: js.Local.Scope = udnefined; +// page.js.localScope(&ls); +// defer ls.deinit(); +// // can use ls.local as needed. +// +// Note: Zig code that is 100% guaranteed to be v8-initiated can get a local via: +// page.js.local.? +pub const Scope = struct { + local: Local, + + pub fn deinit(self: *const Scope) void { + _ = self; + } + + pub fn toLocal(self: *Scope, global: anytype) ToLocalReturnType(@TypeOf(global)) { + return self.local.toLocal(global); + } +}; diff --git a/src/browser/js/Module.zig b/src/browser/js/Module.zig index 44d14b43..c77c46fa 100644 --- a/src/browser/js/Module.zig +++ b/src/browser/js/Module.zig @@ -21,7 +21,7 @@ const v8 = js.v8; const Module = @This(); -ctx: *js.Context, +local: *const js.Local, handle: *const v8.Module, pub const Status = enum(u32) { @@ -39,21 +39,21 @@ pub fn getStatus(self: Module) Status { pub fn getException(self: Module) js.Value { return .{ - .ctx = self.ctx, + .local = self.local, .handle = v8.v8__Module__GetException(self.handle).?, }; } pub fn getModuleRequests(self: Module) Requests { return .{ - .ctx = self.ctx.handle, + .context_handle = self.local.handle, .handle = v8.v8__Module__GetModuleRequests(self.handle).?, }; } pub fn instantiate(self: Module, cb: v8.ResolveModuleCallback) !bool { var out: v8.MaybeBool = undefined; - v8.v8__Module__InstantiateModule(self.handle, self.ctx.handle, cb, &out); + v8.v8__Module__InstantiateModule(self.handle, self.local.handle, cb, &out); if (out.has_value) { return out.value; } @@ -61,15 +61,14 @@ pub fn instantiate(self: Module, cb: v8.ResolveModuleCallback) !bool { } pub fn evaluate(self: Module) !js.Value { - const ctx = self.ctx; - const res = v8.v8__Module__Evaluate(self.handle, ctx.handle) orelse return error.JsException; + const res = v8.v8__Module__Evaluate(self.handle, self.local.handle) orelse return error.JsException; if (self.getStatus() == .kErrored) { return error.JsException; } return .{ - .ctx = ctx, + .local = self.local, .handle = res, }; } @@ -80,7 +79,7 @@ pub fn getIdentityHash(self: Module) u32 { pub fn getModuleNamespace(self: Module) js.Value { return .{ - .ctx = self.ctx, + .local = self.local, .handle = v8.v8__Module__GetModuleNamespace(self.handle).?, }; } @@ -90,28 +89,24 @@ pub fn getScriptId(self: Module) u32 { } pub fn persist(self: Module) !Global { - var ctx = self.ctx; + var ctx = self.local.ctx; var global: v8.Global = undefined; v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); try ctx.global_modules.append(ctx.arena, global); - return .{ - .handle = global, - .ctx = ctx, - }; + return .{ .handle = global }; } pub const Global = struct { handle: v8.Global, - ctx: *js.Context, pub fn deinit(self: *Global) void { v8.v8__Global__Reset(&self.handle); } - pub fn local(self: *const Global) Module { + pub fn local(self: *const Global, l: *const js.Local) Module { return .{ - .ctx = self.ctx, - .handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)), + .local = l, + .handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)), }; } @@ -121,15 +116,15 @@ pub const Global = struct { }; const Requests = struct { - ctx: *const v8.Context, handle: *const v8.FixedArray, + context_handle: *const v8.Context, pub fn len(self: Requests) usize { return @intCast(v8.v8__FixedArray__Length(self.handle)); } pub fn get(self: Requests, idx: usize) Request { - return .{ .handle = v8.v8__FixedArray__Get(self.handle, self.ctx, @intCast(idx)).? }; + return .{ .handle = v8.v8__FixedArray__Get(self.handle, self.context_handle, @intCast(idx)).? }; } }; diff --git a/src/browser/js/Object.zig b/src/browser/js/Object.zig index 291de401..7c7f0e6c 100644 --- a/src/browser/js/Object.zig +++ b/src/browser/js/Object.zig @@ -28,7 +28,7 @@ const Allocator = std.mem.Allocator; const Object = @This(); -ctx: *js.Context, +local: *const js.Local, handle: *const v8.Object, pub fn getId(self: Object) u32 { @@ -36,11 +36,11 @@ pub fn getId(self: Object) u32 { } pub fn has(self: Object, key: anytype) bool { - const ctx = self.ctx; + const ctx = self.local.ctx; const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key); var out: v8.MaybeBool = undefined; - v8.v8__Object__Has(self.handle, self.ctx.handle, key_handle, &out); + v8.v8__Object__Has(self.handle, self.local.handle, key_handle, &out); if (out.has_value) { return out.value; } @@ -48,34 +48,34 @@ pub fn has(self: Object, key: anytype) bool { } pub fn get(self: Object, key: anytype) !js.Value { - const ctx = self.ctx; + const ctx = self.local.ctx; const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key); - const js_val_handle = v8.v8__Object__Get(self.handle, ctx.handle, key_handle) orelse return error.JsException; + const js_val_handle = v8.v8__Object__Get(self.handle, self.local.handle, key_handle) orelse return error.JsException; return .{ - .ctx = ctx, + .local = self.local, .handle = js_val_handle, }; } -pub fn set(self: Object, key: anytype, value: anytype, comptime opts: js.bridge.Caller.CallOpts) !bool { - const ctx = self.ctx; +pub fn set(self: Object, key: anytype, value: anytype, comptime opts: js.Caller.CallOpts) !bool { + const ctx = self.local.ctx; - const js_value = try ctx.zigValueToJs(value, opts); + const js_value = try self.local.zigValueToJs(value, opts); const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key); var out: v8.MaybeBool = undefined; - v8.v8__Object__Set(self.handle, ctx.handle, key_handle, js_value.handle, &out); + v8.v8__Object__Set(self.handle, self.local.handle, key_handle, js_value.handle, &out); return out.has_value; } pub fn defineOwnProperty(self: Object, name: []const u8, value: js.Value, attr: v8.PropertyAttribute) ?bool { - const ctx = self.ctx; + const ctx = self.local.ctx; const name_handle = ctx.isolate.initStringHandle(name); var out: v8.MaybeBool = undefined; - v8.v8__Object__DefineOwnProperty(self.handle, ctx.handle, @ptrCast(name_handle), value.handle, attr, &out); + v8.v8__Object__DefineOwnProperty(self.handle, self.local.handle, @ptrCast(name_handle), value.handle, attr, &out); if (out.has_value) { return out.value; @@ -85,52 +85,49 @@ pub fn defineOwnProperty(self: Object, name: []const u8, value: js.Value, attr: } pub fn toString(self: Object) ![]const u8 { - return self.ctx.valueToString(self.toValue(), .{}); + return self.local.ctx.valueToString(self.toValue(), .{}); } pub fn toValue(self: Object) js.Value { return .{ - .ctx = self.ctx, + .local = self.local, .handle = @ptrCast(self.handle), }; } pub fn format(self: Object, writer: *std.Io.Writer) !void { if (comptime IS_DEBUG) { - return self.ctx.debugValue(self.toValue(), writer); + return self.local.ctx.debugValue(self.toValue(), writer); } const str = self.toString() catch return error.WriteFailed; return writer.writeAll(str); } pub fn persist(self: Object) !Global { - var ctx = self.ctx; + var ctx = self.local.ctx; var global: v8.Global = undefined; v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); try ctx.global_objects.append(ctx.arena, global); - return .{ - .handle = global, - .ctx = ctx, - }; + return .{ .handle = global }; } pub fn getFunction(self: Object, name: []const u8) !?js.Function { if (self.isNullOrUndefined()) { return null; } - const ctx = self.ctx; + const local = self.local; - const js_name = ctx.isolate.initStringHandle(name); - const js_val_handle = v8.v8__Object__Get(self.handle, ctx.handle, js_name) orelse return error.JsException; + const js_name = local.isolate.initStringHandle(name); + const js_val_handle = v8.v8__Object__Get(self.handle, local.handle, js_name) orelse return error.JsException; if (v8.v8__Value__IsFunction(js_val_handle) == false) { return null; } return .{ - .ctx = ctx, + .local = local, .handle = @ptrCast(js_val_handle), }; } @@ -145,51 +142,48 @@ pub fn isNullOrUndefined(self: Object) bool { } pub fn getOwnPropertyNames(self: Object) js.Array { - const handle = v8.v8__Object__GetOwnPropertyNames(self.handle, self.ctx.handle).?; + const handle = v8.v8__Object__GetOwnPropertyNames(self.handle, self.local.handle).?; return .{ - .ctx = self.ctx, + .local = self.local, .handle = handle, }; } pub fn getPropertyNames(self: Object) js.Array { - const handle = v8.v8__Object__GetPropertyNames(self.handle, self.ctx.handle).?; + const handle = v8.v8__Object__GetPropertyNames(self.handle, self.local.handle).?; return .{ - .ctx = self.ctx, + .local = self.local, .handle = handle, }; } pub fn nameIterator(self: Object) NameIterator { - const ctx = self.ctx; - - const handle = v8.v8__Object__GetPropertyNames(self.handle, ctx.handle).?; + const handle = v8.v8__Object__GetPropertyNames(self.handle, self.local.handle).?; const count = v8.v8__Array__Length(handle); return .{ - .ctx = ctx, + .local = self.local, .handle = handle, .count = count, }; } pub fn toZig(self: Object, comptime T: type) !T { - const js_value = js.Value{ .ctx = self.ctx, .handle = @ptrCast(self.handle) }; - return self.ctx.jsValueToZig(T, js_value); + const js_value = js.Value{ .local = self.local, .handle = @ptrCast(self.handle) }; + return self.local.jsValueToZig(T, js_value); } pub const Global = struct { handle: v8.Global, - ctx: *js.Context, pub fn deinit(self: *Global) void { v8.v8__Global__Reset(&self.handle); } - pub fn local(self: *const Global) Object { + pub fn local(self: *const Global, l: *const js.Local) Object { return .{ - .ctx = self.ctx, - .handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)), + .local = l, + .handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)), }; } @@ -201,7 +195,7 @@ pub const Global = struct { pub const NameIterator = struct { count: u32, idx: u32 = 0, - ctx: *Context, + local: *const js.Local, handle: *const v8.Array, pub fn next(self: *NameIterator) !?[]const u8 { @@ -211,8 +205,8 @@ pub const NameIterator = struct { } self.idx += 1; - const js_val_handle = v8.v8__Object__GetIndex(@ptrCast(self.handle), self.ctx.handle, idx) orelse return error.JsException; - const js_val = js.Value{ .ctx = self.ctx, .handle = js_val_handle }; - return try self.ctx.valueToString(js_val, .{}); + const js_val_handle = v8.v8__Object__GetIndex(@ptrCast(self.handle), self.local.handle, idx) orelse return error.JsException; + const js_val = js.Value{ .local = self.local, .handle = js_val_handle }; + return try self.local.valueToString(js_val, .{}); } }; diff --git a/src/browser/js/Promise.zig b/src/browser/js/Promise.zig index 437b327a..44be00c7 100644 --- a/src/browser/js/Promise.zig +++ b/src/browser/js/Promise.zig @@ -21,63 +21,51 @@ const v8 = js.v8; const Promise = @This(); -ctx: *js.Context, +local: *const js.Local, handle: *const v8.Promise, pub fn toObject(self: Promise) js.Object { return .{ - .ctx = self.ctx, + .local = self.local, .handle = @ptrCast(self.handle), }; } pub fn toValue(self: Promise) js.Value { return .{ - .ctx = self.ctx, + .local = self.local, .handle = @ptrCast(self.handle), }; } pub fn thenAndCatch(self: Promise, on_fulfilled: js.Function, on_rejected: js.Function) !Promise { - if (v8.v8__Promise__Then2(self.handle, self.ctx.handle, on_fulfilled.handle, on_rejected.handle)) |handle| { + if (v8.v8__Promise__Then2(self.handle, self.local.handle, on_fulfilled.handle, on_rejected.handle)) |handle| { return .{ - .ctx = self.ctx, + .local = self.local, .handle = handle, }; } return error.PromiseChainFailed; } pub fn persist(self: Promise) !Global { - var ctx = self.ctx; + var ctx = self.local.ctx; var global: v8.Global = undefined; v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); try ctx.global_promises.append(ctx.arena, global); - return .{ - .handle = global, - .ctx = ctx, - }; + return .{ .handle = global }; } pub const Global = struct { handle: v8.Global, - ctx: *js.Context, pub fn deinit(self: *Global) void { v8.v8__Global__Reset(&self.handle); } - pub fn local(self: *const Global) Promise { + pub fn local(self: *const Global, l: *const js.Local) Promise { return .{ - .ctx = self.ctx, - .handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)), + .local = l, + .handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)), }; } - - pub fn isEqual(self: *const Global, other: Promise) bool { - return v8.v8__Global__IsEqual(&self.handle, other.handle); - } - - pub fn promise(self: *const Global) Promise { - return self.local(); - } }; diff --git a/src/browser/js/PromiseResolver.zig b/src/browser/js/PromiseResolver.zig index 86f11cac..9e527a88 100644 --- a/src/browser/js/PromiseResolver.zig +++ b/src/browser/js/PromiseResolver.zig @@ -22,19 +22,19 @@ const log = @import("../../log.zig"); const PromiseResolver = @This(); -ctx: *js.Context, +local: *const js.Local, handle: *const v8.PromiseResolver, -pub fn init(ctx: *js.Context) PromiseResolver { +pub fn init(local: *const js.Local) PromiseResolver { return .{ - .ctx = ctx, - .handle = v8.v8__Promise__Resolver__New(ctx.handle).?, + .local = local, + .handle = v8.v8__Promise__Resolver__New(local.handle).?, }; } pub fn promise(self: PromiseResolver) js.Promise { return .{ - .ctx = self.ctx, + .local = self.local, .handle = v8.v8__Promise__Resolver__GetPromise(self.handle).?, }; } @@ -46,15 +46,15 @@ pub fn resolve(self: PromiseResolver, comptime source: []const u8, value: anytyp } fn _resolve(self: PromiseResolver, value: anytype) !void { - const ctx: *js.Context = @constCast(self.ctx); - const js_value = try ctx.zigValueToJs(value, .{}); + const local = self.local; + const js_val = try local.zigValueToJs(value, .{}); var out: v8.MaybeBool = undefined; - v8.v8__Promise__Resolver__Resolve(self.handle, self.ctx.handle, js_value.handle, &out); + v8.v8__Promise__Resolver__Resolve(self.handle, self.local.handle, js_val.handle, &out); if (!out.has_value or !out.value) { return error.FailedToResolvePromise; } - ctx.runMicrotasks(); + local.ctx.runMicrotasks(); } pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype) void { @@ -64,44 +64,36 @@ pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype } fn _reject(self: PromiseResolver, value: anytype) !void { - const ctx = self.ctx; - const js_value = try ctx.zigValueToJs(value, .{}); + const local = self.local; + const js_val = try local.zigValueToJs(value, .{}); var out: v8.MaybeBool = undefined; - v8.v8__Promise__Resolver__Reject(self.handle, ctx.handle, js_value.handle, &out); + v8.v8__Promise__Resolver__Reject(self.handle, local.handle, js_val.handle, &out); if (!out.has_value or !out.value) { return error.FailedToRejectPromise; } - ctx.runMicrotasks(); + local.ctx.runMicrotasks(); } pub fn persist(self: PromiseResolver) !Global { - var ctx = self.ctx; + var ctx = self.local.ctx; var global: v8.Global = undefined; v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); try ctx.global_promise_resolvers.append(ctx.arena, global); - return .{ - .handle = global, - .ctx = ctx, - }; + return .{ .handle = global }; } pub const Global = struct { handle: v8.Global, - ctx: *js.Context, pub fn deinit(self: *Global) void { v8.v8__Global__Reset(&self.handle); } - pub fn local(self: *const Global) PromiseResolver { + pub fn local(self: *const Global, l: *const js.Local) PromiseResolver { return .{ - .ctx = self.ctx, - .handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)), + .local = l, + .handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)), }; } - - pub fn isEqual(self: *const Global, other: PromiseResolver) bool { - return v8.v8__Global__IsEqual(&self.handle, other.handle); - } }; diff --git a/src/browser/js/String.zig b/src/browser/js/String.zig index 4fb9b395..3d0ad6ad 100644 --- a/src/browser/js/String.zig +++ b/src/browser/js/String.zig @@ -25,7 +25,7 @@ const v8 = js.v8; const String = @This(); -ctx: *js.Context, +local: *const js.Local, handle: *const v8.String, pub const ToZigOpts = struct { @@ -41,8 +41,8 @@ pub fn toZigZ(self: String, opts: ToZigOpts) ![:0]u8 { } fn _toZig(self: String, comptime null_terminate: bool, opts: ToZigOpts) !(if (null_terminate) [:0]u8 else []u8) { - const isolate = self.ctx.isolate.handle; - const allocator = opts.allocator orelse self.ctx.call_arena; + const isolate = self.local.isolate.handle; + const allocator = opts.allocator orelse self.local.ctx.call_arena; const len: u32 = @intCast(v8.v8__String__Utf8Length(self.handle, isolate)); const buf = if (null_terminate) try allocator.allocSentinel(u8, len, 0) else try allocator.alloc(u8, len); diff --git a/src/browser/js/TaggedOpaque.zig b/src/browser/js/TaggedOpaque.zig new file mode 100644 index 00000000..68224b64 --- /dev/null +++ b/src/browser/js/TaggedOpaque.zig @@ -0,0 +1,156 @@ +// Copyright (C) 2023-2026 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 js = @import("js.zig"); +const v8 = js.v8; +const bridge = js.bridge; + +// 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. +// +// 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 chain. +const TaggedOpaque = @This(); + +prototype_len: u16, +prototype_chain: [*]const PrototypeChainEntry, + +// 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. +value: *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 js.Value we +// can get a js.Object, and from the js.Object, we can get out TaggedOpaque +// which is where we store the subtype. +subtype: ?bridge.SubType, + +pub const PrototypeChainEntry = struct { + index: bridge.JsApiLookup.BackingInt, + offset: u16, // offset to the _proto field +}; + +// Reverses the mapZigInstanceToJs, making sure that our TaggedOpaque +// contains a ptr to the correct type. +pub fn fromJS(comptime R: type, js_obj_handle: *const v8.Object) !R { + const ti = @typeInfo(R); + if (ti != .pointer) { + @compileError("non-pointer Zig parameter type: " ++ @typeName(R)); + } + + const T = ti.pointer.child; + const JsApi = bridge.Struct(T).JsApi; + + if (@hasDecl(JsApi.Meta, "empty_with_no_proto")) { + // 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, &.{})); + } + + const internal_field_count = v8.v8__Object__InternalFieldCount(js_obj_handle); + // Special case for Window: the global object doesn't have internal fields + // Window instance is stored in context.page.window instead + if (internal_field_count == 0) { + // Normally, this would be an error. All JsObject that map to a Zig type + // are either `empty_with_no_proto` (handled above) or have an + // interalFieldCount. The only exception to that is the Window... + const isolate = v8.v8__Object__GetIsolate(js_obj_handle).?; + const context = js.Context.fromIsolate(.{ .handle = isolate }); + + const Window = @import("../webapi/Window.zig"); + if (T == Window) { + return context.page.window; + } + + // ... Or the window's prototype. + // We could make this all comptime-fancy, but it's easier to hard-code + // the EventTarget + + const EventTarget = @import("../webapi/EventTarget.zig"); + if (T == EventTarget) { + return context.page.window._proto; + } + + // Type not found in Window's prototype chain + return error.InvalidArgument; + } + + // 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 (internal_field_count == 0) { + return error.InvalidArgument; + } + + if (!bridge.JsApiLookup.has(JsApi)) { + @compileError("unknown Zig type: " ++ @typeName(R)); + } + + const internal_field_handle = v8.v8__Object__GetInternalField(js_obj_handle, 0).?; + const tao: *TaggedOpaque = @ptrCast(@alignCast(v8.v8__External__Value(internal_field_handle))); + const expected_type_index = bridge.JsApiLookup.getId(JsApi); + + const prototype_chain = tao.prototype_chain[0..tao.prototype_len]; + if (prototype_chain[0].index == expected_type_index) { + return @ptrCast(@alignCast(tao.value)); + } + + // Ok, let's walk up the chain + var ptr = @intFromPtr(tao.value); + for (prototype_chain[1..]) |proto| { + ptr += proto.offset; // the offset to the _proto field + const proto_ptr: **anyopaque = @ptrFromInt(ptr); + if (proto.index == expected_type_index) { + return @ptrCast(@alignCast(proto_ptr.*)); + } + ptr = @intFromPtr(proto_ptr.*); + } + return error.InvalidArgument; +} diff --git a/src/browser/js/TryCatch.zig b/src/browser/js/TryCatch.zig index 0b50db0f..8f9df5f5 100644 --- a/src/browser/js/TryCatch.zig +++ b/src/browser/js/TryCatch.zig @@ -24,12 +24,12 @@ const Allocator = std.mem.Allocator; const TryCatch = @This(); -ctx: *js.Context, handle: v8.TryCatch, +local: *const js.Local, -pub fn init(self: *TryCatch, ctx: *js.Context) void { - self.ctx = ctx; - v8.v8__TryCatch__CONSTRUCT(&self.handle, ctx.isolate.handle); +pub fn init(self: *TryCatch, l: *const js.Local) void { + self.local = l; + v8.v8__TryCatch__CONSTRUCT(&self.handle, l.isolate.handle); } pub fn hasCaught(self: TryCatch) bool { @@ -41,26 +41,21 @@ pub fn caught(self: TryCatch, allocator: Allocator) ?Caught { return null; } - const ctx = self.ctx; - - var hs: js.HandleScope = undefined; - hs.init(ctx.isolate); - defer hs.deinit(); - + const l = self.local; const line: ?u32 = blk: { const handle = v8.v8__TryCatch__Message(&self.handle) orelse return null; - const l = v8.v8__Message__GetLineNumber(handle, ctx.handle); - break :blk if (l < 0) null else @intCast(l); + const line = v8.v8__Message__GetLineNumber(handle, l.handle); + break :blk if (line < 0) null else @intCast(line); }; const exception: ?[]const u8 = blk: { const handle = v8.v8__TryCatch__Exception(&self.handle) orelse break :blk null; - break :blk ctx.valueToString(.{ .ctx = ctx, .handle = handle }, .{ .allocator = allocator }) catch |err| @errorName(err); + break :blk l.valueHandleToString(@ptrCast(handle), .{ .allocator = allocator }) catch |err| @errorName(err); }; const stack: ?[]const u8 = blk: { - const handle = v8.v8__TryCatch__StackTrace(&self.handle, ctx.handle) orelse break :blk null; - break :blk ctx.valueToString(.{ .ctx = ctx, .handle = handle }, .{ .allocator = allocator }) catch |err| @errorName(err); + const handle = v8.v8__TryCatch__StackTrace(&self.handle, l.handle) orelse break :blk null; + break :blk l.valueHandleToString(@ptrCast(handle), .{ .allocator = allocator }) catch |err| @errorName(err); }; return .{ diff --git a/src/browser/js/Value.zig b/src/browser/js/Value.zig index 6f4c117a..e37594e4 100644 --- a/src/browser/js/Value.zig +++ b/src/browser/js/Value.zig @@ -27,7 +27,7 @@ const Allocator = std.mem.Allocator; const Value = @This(); -ctx: *js.Context, +local: *const js.Local, handle: *const v8.Value, pub fn isObject(self: Value) bool { @@ -155,12 +155,12 @@ pub fn isPromise(self: Value) bool { } pub fn toBool(self: Value) bool { - return v8.v8__Value__BooleanValue(self.handle, self.ctx.isolate.handle); + return v8.v8__Value__BooleanValue(self.handle, self.local.isolate.handle); } 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 }; + const str_handle = v8.v8__Value__TypeOf(self.handle, self.local.isolate.handle).?; + return js.String{ .local = self.local, .handle = str_handle }; } pub fn toF32(self: Value) !f32 { @@ -169,7 +169,7 @@ pub fn toF32(self: Value) !f32 { pub fn toF64(self: Value) !f64 { var maybe: v8.MaybeF64 = undefined; - v8.v8__Value__NumberValue(self.handle, self.ctx.handle, &maybe); + v8.v8__Value__NumberValue(self.handle, self.local.handle, &maybe); if (!maybe.has_value) { return error.JsException; } @@ -178,7 +178,7 @@ pub fn toF64(self: Value) !f64 { pub fn toI32(self: Value) !i32 { var maybe: v8.MaybeI32 = undefined; - v8.v8__Value__Int32Value(self.handle, self.ctx.handle, &maybe); + v8.v8__Value__Int32Value(self.handle, self.local.handle, &maybe); if (!maybe.has_value) { return error.JsException; } @@ -187,7 +187,7 @@ pub fn toI32(self: Value) !i32 { pub fn toU32(self: Value) !u32 { var maybe: v8.MaybeU32 = undefined; - v8.v8__Value__Uint32Value(self.handle, self.ctx.handle, &maybe); + v8.v8__Value__Uint32Value(self.handle, self.local.handle, &maybe); if (!maybe.has_value) { return error.JsException; } @@ -199,7 +199,7 @@ pub fn toPromise(self: Value) js.Promise { std.debug.assert(self.isPromise()); } return .{ - .ctx = self.ctx, + .local = self.local, .handle = @ptrCast(self.handle), }; } @@ -212,53 +212,42 @@ pub fn toStringZ(self: Value, opts: js.String.ToZigOpts) ![:0]u8 { } pub fn toJson(self: Value, allocator: Allocator) ![]u8 { - const json_str_handle = v8.v8__JSON__Stringify(self.ctx.handle, self.handle, null) orelse return error.JsException; - return self.ctx.jsStringToZig(json_str_handle, .{ .allocator = allocator }); + const json_str_handle = v8.v8__JSON__Stringify(self.local.handle, self.handle, null) orelse return error.JsException; + return self.local.jsStringToZig(json_str_handle, .{ .allocator = allocator }); } fn _toString(self: Value, comptime null_terminate: bool, opts: js.String.ToZigOpts) !(if (null_terminate) [:0]u8 else []u8) { - const ctx = self.ctx; + const l = self.local; if (self.isSymbol()) { - const sym_handle = v8.v8__Symbol__Description(@ptrCast(self.handle), ctx.isolate.handle).?; - return _toString(.{ .handle = @ptrCast(sym_handle), .ctx = ctx }, null_terminate, opts); + const sym_handle = v8.v8__Symbol__Description(@ptrCast(self.handle), l.isolate.handle).?; + return _toString(.{ .handle = @ptrCast(sym_handle), .local = l }, null_terminate, opts); } - const str_handle = v8.v8__Value__ToString(self.handle, ctx.handle) orelse { + const str_handle = v8.v8__Value__ToString(self.handle, l.handle) orelse { return error.JsException; }; - const str = js.String{ .ctx = ctx, .handle = str_handle }; + const str = js.String{ .local = l, .handle = str_handle }; if (comptime null_terminate) { return js.String.toZigZ(str, opts); } return js.String.toZig(str, opts); } -pub fn fromJson(ctx: *js.Context, json: []const u8) !Value { - const v8_isolate = v8.Isolate{ .handle = ctx.isolate.handle }; - const json_string = v8.String.initUtf8(v8_isolate, json); - const v8_context = v8.Context{ .handle = ctx.handle }; - const value = try v8.Json.parse(v8_context, json_string); - return .{ .ctx = ctx, .handle = value.handle }; -} - pub fn persist(self: Value) !Global { - var ctx = self.ctx; + var ctx = self.local.ctx; var global: v8.Global = undefined; v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); try ctx.global_values.append(ctx.arena, global); - return .{ - .handle = global, - .ctx = ctx, - }; + return .{ .handle = global }; } pub fn toZig(self: Value, comptime T: type) !T { - return self.ctx.jsValueToZig(T, self); + return self.local.jsValueToZig(T, self); } pub fn toObject(self: Value) js.Object { @@ -267,7 +256,7 @@ pub fn toObject(self: Value) js.Object { } return .{ - .ctx = self.ctx, + .local = self.local, .handle = @ptrCast(self.handle), }; } @@ -278,7 +267,7 @@ pub fn toArray(self: Value) js.Array { } return .{ - .ctx = self.ctx, + .local = self.local, .handle = @ptrCast(self.handle), }; } @@ -295,7 +284,7 @@ pub fn toBigInt(self: Value) js.BigInt { pub fn format(self: Value, writer: *std.Io.Writer) !void { if (comptime IS_DEBUG) { - return self.ctx.debugValue(self, writer); + return self.local.debugValue(self, writer); } const str = self.toString(.{}) catch return error.WriteFailed; return writer.writeAll(str); @@ -303,16 +292,15 @@ pub fn format(self: Value, writer: *std.Io.Writer) !void { pub const Global = struct { handle: v8.Global, - ctx: *js.Context, pub fn deinit(self: *Global) void { v8.v8__Global__Reset(&self.handle); } - pub fn local(self: *const Global) Value { + pub fn local(self: *const Global, l: *const js.Local) Value { return .{ - .ctx = self.ctx, - .handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)), + .local = l, + .handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)), }; } diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 2f7e9f0d..14606178 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -19,555 +19,15 @@ const std = @import("std"); const js = @import("js.zig"); const log = @import("../../log.zig"); +const Page = @import("../Page.zig"); const v8 = js.v8; +const Caller = @import("Caller.zig"); 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; const IS_DEBUG = @import("builtin").mode == .Debug; -// ============================================================================ -// Internal Callback Info Wrappers -// ============================================================================ -// These wrap the raw v8 C API to provide a cleaner interface. -// They are not exported - internal to this module only. - -const Value = struct { - handle: *const v8.Value, - - fn isArray(self: Value) bool { - return v8.v8__Value__IsArray(self.handle); - } - - fn isTypedArray(self: Value) bool { - return v8.v8__Value__IsTypedArray(self.handle); - } - - fn isFunction(self: Value) bool { - return v8.v8__Value__IsFunction(self.handle); - } -}; - -const Name = struct { - handle: *const v8.Name, -}; - -const FunctionCallbackInfo = struct { - handle: *const v8.FunctionCallbackInfo, - - fn length(self: FunctionCallbackInfo) u32 { - return @intCast(v8.v8__FunctionCallbackInfo__Length(self.handle)); - } - - fn getArg(self: FunctionCallbackInfo, index: u32) Value { - return .{ .handle = v8.v8__FunctionCallbackInfo__INDEX(self.handle, @intCast(index)).? }; - } - - fn getThis(self: FunctionCallbackInfo) *const v8.Object { - return v8.v8__FunctionCallbackInfo__This(self.handle).?; - } - - fn getReturnValue(self: FunctionCallbackInfo) ReturnValue { - var rv: v8.ReturnValue = undefined; - v8.v8__FunctionCallbackInfo__GetReturnValue(self.handle, &rv); - return .{ .handle = rv }; - } - - fn isConstructCall(self: FunctionCallbackInfo) bool { - return v8.v8__FunctionCallbackInfo__IsConstructCall(self.handle); - } -}; - -const PropertyCallbackInfo = struct { - handle: *const v8.PropertyCallbackInfo, - - fn getThis(self: PropertyCallbackInfo) *const v8.Object { - return v8.v8__PropertyCallbackInfo__This(self.handle).?; - } - - fn getReturnValue(self: PropertyCallbackInfo) ReturnValue { - var rv: v8.ReturnValue = undefined; - v8.v8__PropertyCallbackInfo__GetReturnValue(self.handle, &rv); - return .{ .handle = rv }; - } -}; - -const ReturnValue = struct { - handle: v8.ReturnValue, - - fn set(self: ReturnValue, value: anytype) void { - const T = @TypeOf(value); - if (T == Value) { - self.setValueHandle(value.handle); - } else if (T == *const v8.Object) { - self.setValueHandle(@ptrCast(value)); - } else if (T == *const v8.Value) { - self.setValueHandle(value); - } else if (T == js.Value) { - self.setValueHandle(value.handle); - } else { - @compileError("Unsupported type for ReturnValue.set: " ++ @typeName(T)); - } - } - - fn setValueHandle(self: ReturnValue, handle: *const v8.Value) void { - v8.v8__ReturnValue__Set(self.handle, handle); - } -}; - -// ============================================================================ -// Caller - Responsible for calling Zig functions from JS invocations -// ============================================================================ - -pub const Caller = struct { - context: *Context, - isolate: js.Isolate, - call_arena: Allocator, - - // Takes the raw v8 isolate and extracts the context from it. - pub fn init(v8_isolate: *v8.Isolate) Caller { - const isolate = js.Isolate{ .handle = v8_isolate }; - const v8_context_handle = v8.v8__Isolate__GetCurrentContext(v8_isolate); - const embedder_data = v8.v8__Context__GetEmbedderData(v8_context_handle, 1); - var lossless: bool = undefined; - const context: *Context = @ptrFromInt(v8.v8__BigInt__Uint64Value(embedder_data, &lossless)); - - 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: 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); - }; - } - - fn _constructor(self: *Caller, func: anytype, info: 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_handle = info.getThis(); - var this = js.Object{ .ctx = self.context, .handle = new_this_handle }; - if (@typeInfo(ReturnType) == .error_union) { - const non_error_res = res catch |err| return err; - this = try self.context.mapZigInstanceToJs(new_this_handle, non_error_res); - } else { - this = try self.context.mapZigInstanceToJs(new_this_handle, res); - } - - // 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 prototype_handle = v8.v8__Object__GetPrototype(new_this_handle).?; - var out: v8.MaybeBool = undefined; - v8.v8__Object__SetPrototype(this.handle, self.context.handle, prototype_handle, &out); - if (comptime IS_DEBUG) { - std.debug.assert(out.has_value and out.value); - } - } - - info.getReturnValue().set(this.handle); - } - - pub fn method(self: *Caller, comptime T: type, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) void { - self._method(T, func, info, opts) catch |err| { - self.handleError(T, @TypeOf(func), err, info, opts); - }; - } - - fn _method(self: *Caller, comptime T: type, func: anytype, info: 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: FunctionCallbackInfo, comptime opts: CallOpts) void { - self._function(func, info, opts) catch |err| { - self.handleError(T, @TypeOf(func), err, info, opts); - }; - } - - fn _function(self: *Caller, func: anytype, info: 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: PropertyCallbackInfo, comptime opts: CallOpts) u8 { - return self._getIndex(T, func, idx, info, opts) catch |err| { - self.handleError(T, @TypeOf(func), err, info, opts); - // not intercepted - return 0; - }; - } - - fn _getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: 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: Name, info: PropertyCallbackInfo, comptime opts: CallOpts) u8 { - return self._getNamedIndex(T, func, name, info, opts) catch |err| { - self.handleError(T, @TypeOf(func), err, info, opts); - // not intercepted - return 0; - }; - } - - fn _getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: Name, info: 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: Name, js_value: Value, info: 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); - // not intercepted - return 0; - }; - } - - fn _setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: Name, js_value: Value, info: 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{ .ctx = self.context, .handle = js_value.handle }); - 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: Name, info: PropertyCallbackInfo, comptime opts: CallOpts) u8 { - return self._deleteNamedIndex(T, func, name, info, opts) catch |err| { - self.handleError(T, @TypeOf(func), err, info, opts); - return 0; - }; - } - - fn _deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: Name, info: 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: 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) { - // not intercepted - return 0; - } - } - self.handleError(T, F, err, info, opts); - // not intercepted - return 0; - }; - }, - else => ret, - }; - - if (comptime getter) { - info.getReturnValue().set(try self.context.zigValueToJs(non_error_ret, opts)); - } - // intercepted - return 1; - } - - 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 nameToString(self: *Caller, name: Name) ![]const u8 { - return self.context.valueToString(js.Value{ .ctx = self.context, .handle = @ptrCast(name.handle) }, .{}); - } - - fn handleError(self: *Caller, comptime T: type, comptime F: type, err: anyerror, info: anytype, comptime opts: CallOpts) void { - const isolate = self.isolate; - - if (comptime @import("builtin").mode == .Debug and @TypeOf(info) == FunctionCallbackInfo) { - if (log.enabled(.js, .warn)) { - self.logFunctionCallError(@typeName(T), @typeName(F), err, info); - } - } - - const js_err: *const v8.Value = switch (err) { - error.InvalidArgument => isolate.createTypeError("invalid argument"), - error.OutOfMemory => isolate.createError("out of memory"), - error.IllegalConstructor => isolate.createError("Illegal Contructor"), - else => blk: { - if (comptime opts.dom_exception) { - const DOMException = @import("../webapi/DOMException.zig"); - if (DOMException.fromError(err)) |ex| { - const value = self.context.zigValueToJs(ex, .{}) catch break :blk isolate.createError("internal error"); - break :blk value.handle; - } - } - break :blk isolate.createError(@errorName(err)); - }, - }; - - const js_exception = isolate.throwException(js_err); - info.getReturnValue().setValueHandle(js_exception); - } - - // 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{ .ctx = context, .handle = js_value.handle }); - } - @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{ .ctx = context, .handle = js_value.handle }) 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: 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: 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 }); - const val = info.getArg(@intCast(i)); - try context.debugValue(js.Value{ .ctx = context, .handle = val.handle }, &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; - } -}; - -// ============================================================================ -// Bridge Builder Functions -// ============================================================================ - pub fn Builder(comptime T: type) type { return struct { pub const @"type" = T; @@ -609,8 +69,9 @@ pub fn Builder(comptime T: type) type { @compileError("Property for " ++ @typeName(@TypeOf(value)) ++ " hasn't been defined yet"); } - pub fn prototypeChain() [prototypeChainLength(T)]js.PrototypeChainEntry { - var entries: [prototypeChainLength(T)]js.PrototypeChainEntry = undefined; + const PrototypeChainEntry = @import("TaggedOpaque.zig").PrototypeChainEntry; + pub fn prototypeChain() [prototypeChainLength(T)]PrototypeChainEntry { + var entries: [prototypeChainLength(T)]PrototypeChainEntry = undefined; entries[0] = .{ .offset = 0, .index = JsApiLookup.getId(T.JsApi) }; @@ -643,11 +104,11 @@ pub const Constructor = struct { return .{ .func = struct { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?; - var caller = Caller.init(v8_isolate); + var caller: Caller = undefined; + caller.init(v8_isolate); defer caller.deinit(); - const info = FunctionCallbackInfo{ .handle = handle.? }; - caller.constructor(T, func, info, .{ + caller.constructor(T, func, handle.?, .{ .dom_exception = opts.dom_exception, }); } @@ -672,18 +133,18 @@ pub const Function = struct { .func = struct { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?; - var caller = Caller.init(v8_isolate); + var caller: Caller = undefined; + caller.init(v8_isolate); defer caller.deinit(); - const info = FunctionCallbackInfo{ .handle = handle.? }; if (comptime opts.static) { - caller.function(T, func, info, .{ + caller.function(T, func, handle.?, .{ .dom_exception = opts.dom_exception, .as_typed_array = opts.as_typed_array, .null_as_undefined = opts.null_as_undefined, }); } else { - caller.method(T, func, info, .{ + caller.method(T, func, handle.?, .{ .dom_exception = opts.dom_exception, .as_typed_array = opts.as_typed_array, .null_as_undefined = opts.null_as_undefined, @@ -716,17 +177,17 @@ pub const Accessor = struct { accessor.getter = struct { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?; - var caller = Caller.init(v8_isolate); + var caller: Caller = undefined; + caller.init(v8_isolate); defer caller.deinit(); - const info = FunctionCallbackInfo{ .handle = handle.? }; if (comptime opts.static) { - caller.function(T, getter, info, .{ + caller.function(T, getter, handle.?, .{ .as_typed_array = opts.as_typed_array, .null_as_undefined = opts.null_as_undefined, }); } else { - caller.method(T, getter, info, .{ + caller.method(T, getter, handle.?, .{ .as_typed_array = opts.as_typed_array, .null_as_undefined = opts.null_as_undefined, }); @@ -739,13 +200,11 @@ pub const Accessor = struct { accessor.setter = struct { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?; - var caller = Caller.init(v8_isolate); + var caller: Caller = undefined; + caller.init(v8_isolate); defer caller.deinit(); - const info = FunctionCallbackInfo{ .handle = handle.? }; - std.debug.assert(info.length() == 1); - - caller.method(T, setter, info, .{ + caller.method(T, setter, handle.?, .{ .as_typed_array = opts.as_typed_array, .null_as_undefined = opts.null_as_undefined, }); @@ -769,11 +228,11 @@ pub const Indexed = struct { return .{ .getter = struct { fn wrap(idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 { const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; - var caller = Caller.init(v8_isolate); + var caller: Caller = undefined; + caller.init(v8_isolate); defer caller.deinit(); - const info = PropertyCallbackInfo{ .handle = handle.? }; - return caller.getIndex(T, getter, idx, info, .{ + return caller.getIndex(T, getter, idx, handle.?, .{ .as_typed_array = opts.as_typed_array, .null_as_undefined = opts.null_as_undefined, }); @@ -796,11 +255,11 @@ pub const NamedIndexed = struct { const getter_fn = struct { fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 { const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; - var caller = Caller.init(v8_isolate); + var caller: Caller = undefined; + caller.init(v8_isolate); defer caller.deinit(); - const info = PropertyCallbackInfo{ .handle = handle.? }; - return caller.getNamedIndex(T, getter, .{ .handle = c_name.? }, info, .{ + return caller.getNamedIndex(T, getter, c_name.?, handle.?, .{ .as_typed_array = opts.as_typed_array, .null_as_undefined = opts.null_as_undefined, }); @@ -810,11 +269,11 @@ pub const NamedIndexed = struct { const setter_fn = if (@typeInfo(@TypeOf(setter)) == .null) null else struct { fn wrap(c_name: ?*const v8.Name, c_value: ?*const v8.Value, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 { const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; - var caller = Caller.init(v8_isolate); + var caller: Caller = undefined; + caller.init(v8_isolate); defer caller.deinit(); - const info = PropertyCallbackInfo{ .handle = handle.? }; - return caller.setNamedIndex(T, setter, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info, .{ + return caller.setNamedIndex(T, setter, c_name.?, c_value.?, handle.?, .{ .as_typed_array = opts.as_typed_array, .null_as_undefined = opts.null_as_undefined, }); @@ -824,11 +283,11 @@ pub const NamedIndexed = struct { const deleter_fn = if (@typeInfo(@TypeOf(deleter)) == .null) null else struct { fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 { const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; - var caller = Caller.init(v8_isolate); + var caller: Caller = undefined; + caller.init(v8_isolate); defer caller.deinit(); - const info = PropertyCallbackInfo{ .handle = handle.? }; - return caller.deleteNamedIndex(T, deleter, .{ .handle = c_name.? }, info, .{ + return caller.deleteNamedIndex(T, deleter, c_name.?, handle.?, .{ .as_typed_array = opts.as_typed_array, .null_as_undefined = opts.null_as_undefined, }); @@ -858,7 +317,7 @@ pub const Iterator = struct { .async = opts.async, .func = struct { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { - const info = FunctionCallbackInfo{ .handle = handle.? }; + const info = Caller.FunctionCallbackInfo{ .handle = handle.? }; info.getReturnValue().set(info.getThis()); } }.wrap, @@ -870,11 +329,10 @@ pub const Iterator = struct { .func = struct { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?; - var caller = Caller.init(v8_isolate); + var caller: Caller = undefined; + caller.init(v8_isolate); defer caller.deinit(); - - const info = FunctionCallbackInfo{ .handle = handle.? }; - caller.method(T, struct_or_func, info, .{}); + caller.method(T, struct_or_func, handle.?, .{}); } }.wrap, }; @@ -892,11 +350,11 @@ pub const Callable = struct { return .{ .func = struct { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?; - var caller = Caller.init(v8_isolate); + var caller: Caller = undefined; + caller.init(v8_isolate); defer caller.deinit(); - const info = FunctionCallbackInfo{ .handle = handle.? }; - caller.method(T, func, info, .{ + caller.method(T, func, handle.?, .{ .null_as_undefined = opts.null_as_undefined, }); } @@ -909,52 +367,52 @@ pub const Property = union(enum) { }; 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 v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; + var caller: Caller = undefined; + caller.init(v8_isolate); + defer caller.deinit(); - const property: []const u8 = context.valueToString(.{ .ctx = context, .handle = c_name.? }, .{}) catch { + const local = caller.local; + const property: []const u8 = local.valueHandleToString(@ptrCast(c_name.?), .{}) catch { return 0; }; - const ignored = std.StaticStringMap(void).initComptime(.{ - .{ "process", {} }, - .{ "ShadyDOM", {} }, - .{ "ShadyCSS", {} }, + const page = local.ctx.page; + const document = page.document; - .{ "litNonce", {} }, - .{ "litHtmlVersions", {} }, - .{ "litElementVersions", {} }, - .{ "litHtmlPolyfillSupport", {} }, - .{ "litElementHydrateSupport", {} }, - .{ "litElementPolyfillSupport", {} }, - .{ "reactiveElementVersions", {} }, + if (document.getElementById(property, page)) |el| { + const js_val = local.zigValueToJs(el, .{}) catch return 0; + var pc = Caller.PropertyCallbackInfo{ .handle = handle.? }; + pc.getReturnValue().set(js_val); + return 1; + } - .{ "recaptcha", {} }, - .{ "grecaptcha", {} }, - .{ "___grecaptcha_cfg", {} }, - .{ "__recaptcha_api", {} }, - .{ "__google_recaptcha_client", {} }, + if (comptime IS_DEBUG) { + const ignored = std.StaticStringMap(void).initComptime(.{ + .{ "process", {} }, + .{ "ShadyDOM", {} }, + .{ "ShadyCSS", {} }, - .{ "CLOSURE_FLAGS", {} }, - }); + .{ "litNonce", {} }, + .{ "litHtmlVersions", {} }, + .{ "litElementVersions", {} }, + .{ "litHtmlPolyfillSupport", {} }, + .{ "litElementHydrateSupport", {} }, + .{ "litElementPolyfillSupport", {} }, + .{ "reactiveElementVersions", {} }, - if (!ignored.has(property)) { - const page = context.page; - const document = page.document; + .{ "recaptcha", {} }, + .{ "grecaptcha", {} }, + .{ "___grecaptcha_cfg", {} }, + .{ "__recaptcha_api", {} }, + .{ "__google_recaptcha_client", {} }, - 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) { + .{ "CLOSURE_FLAGS", {} }, + }); + if (!ignored.has(property)) { log.debug(.unknown_prop, "unknown global property", .{ .info = "but the property can exist in pure JS", - .stack = context.stackTrace() catch "???", + .stack = local.stackTrace() catch "???", .property = property, }); } diff --git a/src/browser/js/global.zig b/src/browser/js/global.zig deleted file mode 100644 index 9bfe782d..00000000 --- a/src/browser/js/global.zig +++ /dev/null @@ -1,48 +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 js = @import("js.zig"); - -const v8 = js.v8; - -pub fn Global(comptime T: type) type { - const H = @FieldType(T, "handle"); - - return struct { - global: v8.Global, - - const Self = @This(); - - pub fn init(isolate: *v8.Isolate, handle: H) Self { - var global: v8.Global = undefined; - v8.v8__Global__New(isolate, handle, &global); - return .{ - .global = global, - }; - } - - pub fn deinit(self: *Self) void { - v8.v8__Global__Reset(&self.global); - } - - pub fn local(self: *const Self) H { - return @ptrCast(@alignCast(@as(*const anyopaque, @ptrFromInt(self.global.data_ptr)))); - } - }; -} diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig index 841d5642..b054ae1f 100644 --- a/src/browser/js/js.zig +++ b/src/browser/js/js.zig @@ -24,7 +24,9 @@ const log = @import("../../log.zig"); pub const Env = @import("Env.zig"); pub const bridge = @import("bridge.zig"); pub const ExecutionWorld = @import("ExecutionWorld.zig"); +pub const Caller = @import("Caller.zig"); pub const Context = @import("Context.zig"); +pub const Local = @import("Local.zig"); pub const Inspector = @import("Inspector.zig"); pub const Snapshot = @import("Snapshot.zig"); pub const Platform = @import("Platform.zig"); @@ -43,7 +45,6 @@ pub const Module = @import("Module.zig"); pub const BigInt = @import("BigInt.zig"); pub const Number = @import("Number.zig"); pub const Integer = @import("Integer.zig"); -pub const Global = @import("global.zig").Global; pub const PromiseResolver = @import("PromiseResolver.zig"); const Allocator = std.mem.Allocator; @@ -78,11 +79,11 @@ pub const ArrayBuffer = struct { }; pub const Exception = struct { - ctx: *const Context, + local: *const Local, handle: *const v8.Value, pub fn exception(self: Exception, allocator: Allocator) ![]const u8 { - return self.context.valueToString(self.inner, .{ .allocator = allocator }); + return self.local.valueToString(self.handel, .{ .allocator = allocator }); } }; @@ -215,61 +216,6 @@ pub fn simpleZigValueToJs(isolate: Isolate, value: anytype, comptime fail: bool, } return null; } -// 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. -// -// 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 chain. -pub const TaggedAnyOpaque = struct { - prototype_len: u16, - prototype_chain: [*]const PrototypeChainEntry, - - // 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. - value: *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 js.Value we - // can get a js.Object, and from the js.Object, we can get out TaggedAnyOpaque - // which is where we store the subtype. - subtype: ?bridge.SubType, -}; - -pub const PrototypeChainEntry = struct { - index: bridge.JsApiLookup.BackingInt, - offset: u16, // offset to the _proto field -}; // These are here, and not in Inspector.zig, because Inspector.zig isn't always // included (e.g. in the wpt build). @@ -281,7 +227,7 @@ pub export fn v8_inspector__Client__IMPL__valueSubtype( _: *v8.InspectorClientImpl, c_value: *const v8.Value, ) callconv(.c) [*c]const u8 { - const external_entry = Inspector.getTaggedAnyOpaque(c_value) orelse return null; + const external_entry = Inspector.getTaggedOpaque(c_value) orelse return null; return if (external_entry.subtype) |st| @tagName(st) else null; } @@ -298,11 +244,11 @@ pub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype( // 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 = Inspector.getTaggedAnyOpaque(c_value) orelse return null; + const external_entry = Inspector.getTaggedOpaque(c_value) orelse return null; return if (external_entry.subtype == null) null else ""; } test "TaggedAnyOpaque" { // If we grow this, fine, but it should be a conscious decision - try std.testing.expectEqual(24, @sizeOf(TaggedAnyOpaque)); + try std.testing.expectEqual(24, @sizeOf(@import("TaggedOpaque.zig"))); } diff --git a/src/browser/tests/history.html b/src/browser/tests/history.html index 90df53bf..1508e232 100644 --- a/src/browser/tests/history.html +++ b/src/browser/tests/history.html @@ -30,7 +30,7 @@ testing.eventually(() => { testing.expectEqual(true, popstateEventFired); - testing.expectEqual(state, popstateEventState); + testing.expectEqual({testInProgress: true }, popstateEventState); }) history.back(); diff --git a/src/browser/tests/history_after_nav.skip.html b/src/browser/tests/history_after_nav.skip.html index 173569a2..0e76edd6 100644 --- a/src/browser/tests/history_after_nav.skip.html +++ b/src/browser/tests/history_after_nav.skip.html @@ -1,6 +1 @@ - - - diff --git a/src/browser/webapi/AbortController.zig b/src/browser/webapi/AbortController.zig index d73de6b4..0c251a47 100644 --- a/src/browser/webapi/AbortController.zig +++ b/src/browser/webapi/AbortController.zig @@ -38,7 +38,7 @@ pub fn getSignal(self: *const AbortController) *AbortSignal { } pub fn abort(self: *AbortController, reason_: ?js.Value.Global, page: *Page) !void { - try self._signal.abort(if (reason_) |r| .{ .js_val = r } else null, page); + try self._signal.abort(if (reason_) |r| .{ .js_val = r } else null, page.js.local.?, page); } pub const JsApi = struct { diff --git a/src/browser/webapi/AbortSignal.zig b/src/browser/webapi/AbortSignal.zig index 9f3298aa..8db60a70 100644 --- a/src/browser/webapi/AbortSignal.zig +++ b/src/browser/webapi/AbortSignal.zig @@ -57,7 +57,7 @@ pub fn asEventTarget(self: *AbortSignal) *EventTarget { return self._proto; } -pub fn abort(self: *AbortSignal, reason_: ?Reason, page: *Page) !void { +pub fn abort(self: *AbortSignal, reason_: ?Reason, local: *const js.Local, page: *Page) !void { if (self._aborted) { return; } @@ -77,11 +77,10 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, page: *Page) !void { // Dispatch abort event const event = try Event.initTrusted("abort", .{}, page); - const func = if (self._on_abort) |*g| g.local() else null; try page._event_manager.dispatchWithFunction( self.asEventTarget(), event, - func, + local.toLocal(self._on_abort), .{ .context = "abort signal" }, ); } @@ -89,7 +88,7 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, page: *Page) !void { // Static method to create an already-aborted signal pub fn createAborted(reason_: ?js.Value.Global, page: *Page) !*AbortSignal { const signal = try init(page); - try signal.abort(if (reason_) |r| .{ .js_val = r } else null, page); + try signal.abort(if (reason_) |r| .{ .js_val = r } else null, page.js.local.?, page); return signal; } @@ -112,11 +111,13 @@ const ThrowIfAborted = union(enum) { undefined: void, }; pub fn throwIfAborted(self: *const AbortSignal, page: *Page) !ThrowIfAborted { + const local = page.js.local.?; + if (self._aborted) { const exception = switch (self._reason) { - .string => |str| page.js.throw(str), - .js_val => |js_val| page.js.throw(try js_val.local().toString(.{ .allocator = page.call_arena })), - .undefined => page.js.throw("AbortError"), + .string => |str| local.throw(str), + .js_val => |js_val| local.throw(try local.toLocal(js_val).toString(.{ .allocator = page.call_arena })), + .undefined => local.throw("AbortError"), }; return .{ .exception = exception }; } @@ -135,7 +136,11 @@ const TimeoutCallback = struct { fn run(ctx: *anyopaque) !?u32 { const self: *TimeoutCallback = @ptrCast(@alignCast(ctx)); - self.signal.abort(.{ .string = "TimeoutError" }, self.page) catch |err| { + var ls: js.Local.Scope = undefined; + self.page.js.localScope(&ls); + defer ls.deinit(); + + self.signal.abort(.{ .string = "TimeoutError" }, &ls.local, self.page) catch |err| { log.warn(.app, "abort signal timeout", .{ .err = err }); }; return null; diff --git a/src/browser/webapi/Blob.zig b/src/browser/webapi/Blob.zig index e21dbbac..ac31560a 100644 --- a/src/browser/webapi/Blob.zig +++ b/src/browser/webapi/Blob.zig @@ -206,7 +206,7 @@ fn writeBlobParts( /// Returns a Promise that resolves with the contents of the blob /// as binary data contained in an ArrayBuffer. pub fn arrayBuffer(self: *const Blob, page: *Page) !js.Promise { - return page.js.resolvePromise(js.ArrayBuffer{ .values = self._slice }); + return page.js.local.?.resolvePromise(js.ArrayBuffer{ .values = self._slice }); } const ReadableStream = @import("streams/ReadableStream.zig"); @@ -219,7 +219,7 @@ pub fn stream(self: *const Blob, page: *Page) !*ReadableStream { /// Returns a Promise that resolves with a string containing /// the contents of the blob, interpreted as UTF-8. pub fn text(self: *const Blob, page: *Page) !js.Promise { - return page.js.resolvePromise(self._slice); + return page.js.local.?.resolvePromise(self._slice); } /// Extension to Blob; works on Firefox and Safari. @@ -227,7 +227,7 @@ pub fn text(self: *const Blob, page: *Page) !js.Promise { /// Returns a Promise that resolves with a Uint8Array containing /// the contents of the blob as an array of bytes. pub fn bytes(self: *const Blob, page: *Page) !js.Promise { - return page.js.resolvePromise(js.TypedArray(u8){ .values = self._slice }); + return page.js.local.?.resolvePromise(js.TypedArray(u8){ .values = self._slice }); } /// Returns a new Blob object which contains data diff --git a/src/browser/webapi/Console.zig b/src/browser/webapi/Console.zig index 1f9b458e..ab1fc12f 100644 --- a/src/browser/webapi/Console.zig +++ b/src/browser/webapi/Console.zig @@ -31,7 +31,7 @@ pub const init: Console = .{}; pub fn trace(_: *const Console, values: []js.Value, page: *Page) !void { logger.debug(.js, "console.trace", .{ - .stack = page.js.stackTrace() catch "???", + .stack = page.js.local.?.stackTrace() catch "???", .args = ValueWriter{ .page = page, .values = values }, }); } @@ -138,7 +138,7 @@ const ValueWriter = struct { try writer.print("\n arg({d}): {f}", .{ i, value }); } if (self.include_stack) { - try writer.print("\n stack: {s}", .{self.page.js.stackTrace() catch |err| @errorName(err) orelse "???"}); + try writer.print("\n stack: {s}", .{self.page.js.local.?.stackTrace() catch |err| @errorName(err) orelse "???"}); } } diff --git a/src/browser/webapi/CustomElementRegistry.zig b/src/browser/webapi/CustomElementRegistry.zig index 956dd8bf..d69ad542 100644 --- a/src/browser/webapi/CustomElementRegistry.zig +++ b/src/browser/webapi/CustomElementRegistry.zig @@ -106,7 +106,7 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu } if (self._when_defined.fetchRemove(name)) |entry| { - entry.value.local().resolve("whenDefined", constructor); + page.js.toLocal(entry.value).resolve("whenDefined", constructor); } } @@ -120,22 +120,23 @@ pub fn upgrade(self: *CustomElementRegistry, root: *Node, page: *Page) !void { } pub fn whenDefined(self: *CustomElementRegistry, name: []const u8, page: *Page) !js.Promise { + const local = page.js.local.?; if (self._definitions.get(name)) |definition| { - return page.js.resolvePromise(definition.constructor); + return local.resolvePromise(definition.constructor); } const gop = try self._when_defined.getOrPut(page.arena, name); if (gop.found_existing) { - return gop.value_ptr.local().promise(); + return local.toLocal(gop.value_ptr.*).promise(); } errdefer _ = self._when_defined.remove(name); const owned_name = try page.dupeString(name); - const resolver = try page.js.createPromiseResolver().persist(); + const resolver = local.createPromiseResolver(); gop.key_ptr.* = owned_name; - gop.value_ptr.* = resolver; + gop.value_ptr.* = try resolver.persist(); - return resolver.local().promise(); + return resolver.promise(); } fn upgradeNode(self: *CustomElementRegistry, node: *Node, page: *Page) !void { @@ -174,8 +175,12 @@ pub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinitio page._upgrading_element = node; defer page._upgrading_element = prev_upgrading; + var ls: js.Local.Scope = undefined; + page.js.localScope(&ls); + defer ls.deinit(); + var caught: js.TryCatch.Caught = undefined; - _ = definition.constructor.local().newInstance(&caught) catch |err| { + _ = ls.toLocal(definition.constructor).newInstance(&caught) catch |err| { log.warn(.js, "custom element upgrade", .{ .name = definition.name, .err = err, .caught = caught }); return error.CustomElementUpgradeFailed; }; diff --git a/src/browser/webapi/DOMNodeIterator.zig b/src/browser/webapi/DOMNodeIterator.zig index 454bbc39..3015951b 100644 --- a/src/browser/webapi/DOMNodeIterator.zig +++ b/src/browser/webapi/DOMNodeIterator.zig @@ -63,14 +63,14 @@ pub fn getFilter(self: *const DOMNodeIterator) ?FilterOpts { return self._filter._original_filter; } -pub fn nextNode(self: *DOMNodeIterator) !?*Node { +pub fn nextNode(self: *DOMNodeIterator, page: *Page) !?*Node { var node = self._reference_node; var before_node = self._pointer_before_reference_node; while (true) { if (before_node) { before_node = false; - const result = try self.filterNode(node); + const result = try self.filterNode(node, page); if (result == NodeFilter.FILTER_ACCEPT) { self._reference_node = node; self._pointer_before_reference_node = false; @@ -84,7 +84,7 @@ pub fn nextNode(self: *DOMNodeIterator) !?*Node { } node = next.?; - const result = try self.filterNode(node); + const result = try self.filterNode(node, page); if (result == NodeFilter.FILTER_ACCEPT) { self._reference_node = node; self._pointer_before_reference_node = false; @@ -94,13 +94,13 @@ pub fn nextNode(self: *DOMNodeIterator) !?*Node { } } -pub fn previousNode(self: *DOMNodeIterator) !?*Node { +pub fn previousNode(self: *DOMNodeIterator, page: *Page) !?*Node { var node = self._reference_node; var before_node = self._pointer_before_reference_node; while (true) { if (!before_node) { - const result = try self.filterNode(node); + const result = try self.filterNode(node, page); if (result == NodeFilter.FILTER_ACCEPT) { self._reference_node = node; self._pointer_before_reference_node = true; @@ -119,7 +119,7 @@ pub fn previousNode(self: *DOMNodeIterator) !?*Node { } } -fn filterNode(self: *const DOMNodeIterator, node: *Node) !i32 { +fn filterNode(self: *const DOMNodeIterator, node: *Node, page: *Page) !i32 { // First check whatToShow if (!NodeFilter.shouldShow(node, self._what_to_show)) { return NodeFilter.FILTER_SKIP; @@ -128,7 +128,7 @@ fn filterNode(self: *const DOMNodeIterator, node: *Node) !i32 { // Then check the filter callback // For NodeIterator, REJECT and SKIP are equivalent - both skip the node // but continue with its descendants - const result = try self._filter.acceptNode(node); + const result = try self._filter.acceptNode(node, page.js.local.?); return result; } diff --git a/src/browser/webapi/DOMTreeWalker.zig b/src/browser/webapi/DOMTreeWalker.zig index 6cae910e..5b9e9a2c 100644 --- a/src/browser/webapi/DOMTreeWalker.zig +++ b/src/browser/webapi/DOMTreeWalker.zig @@ -62,13 +62,13 @@ pub fn setCurrentNode(self: *DOMTreeWalker, node: *Node) void { } // Navigation methods -pub fn parentNode(self: *DOMTreeWalker) !?*Node { +pub fn parentNode(self: *DOMTreeWalker, page: *Page) !?*Node { var node = self._current._parent; while (node) |n| { if (n == self._root._parent) { return null; } - if (try self.acceptNode(n) == NodeFilter.FILTER_ACCEPT) { + if (try self.acceptNode(n, page) == NodeFilter.FILTER_ACCEPT) { self._current = n; return n; } @@ -77,11 +77,11 @@ pub fn parentNode(self: *DOMTreeWalker) !?*Node { return null; } -pub fn firstChild(self: *DOMTreeWalker) !?*Node { +pub fn firstChild(self: *DOMTreeWalker, page: *Page) !?*Node { var node = self._current.firstChild(); while (node) |n| { - const filter_result = try self.acceptNode(n); + const filter_result = try self.acceptNode(n, page); if (filter_result == NodeFilter.FILTER_ACCEPT) { self._current = n; @@ -117,11 +117,11 @@ pub fn firstChild(self: *DOMTreeWalker) !?*Node { return null; } -pub fn lastChild(self: *DOMTreeWalker) !?*Node { +pub fn lastChild(self: *DOMTreeWalker, page: *Page) !?*Node { var node = self._current.lastChild(); while (node) |n| { - const filter_result = try self.acceptNode(n); + const filter_result = try self.acceptNode(n, page); if (filter_result == NodeFilter.FILTER_ACCEPT) { self._current = n; @@ -157,10 +157,10 @@ pub fn lastChild(self: *DOMTreeWalker) !?*Node { return null; } -pub fn previousSibling(self: *DOMTreeWalker) !?*Node { +pub fn previousSibling(self: *DOMTreeWalker, page: *Page) !?*Node { var node = self.previousSiblingOrNull(self._current); while (node) |n| { - if (try self.acceptNode(n) == NodeFilter.FILTER_ACCEPT) { + if (try self.acceptNode(n, page) == NodeFilter.FILTER_ACCEPT) { self._current = n; return n; } @@ -169,10 +169,10 @@ pub fn previousSibling(self: *DOMTreeWalker) !?*Node { return null; } -pub fn nextSibling(self: *DOMTreeWalker) !?*Node { +pub fn nextSibling(self: *DOMTreeWalker, page: *Page) !?*Node { var node = self.nextSiblingOrNull(self._current); while (node) |n| { - if (try self.acceptNode(n) == NodeFilter.FILTER_ACCEPT) { + if (try self.acceptNode(n, page) == NodeFilter.FILTER_ACCEPT) { self._current = n; return n; } @@ -181,7 +181,7 @@ pub fn nextSibling(self: *DOMTreeWalker) !?*Node { return null; } -pub fn previousNode(self: *DOMTreeWalker) !?*Node { +pub fn previousNode(self: *DOMTreeWalker, page: *Page) !?*Node { var node = self._current; while (node != self._root) { var sibling = self.previousSiblingOrNull(node); @@ -189,7 +189,7 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node { node = sib; // Check if this sibling is rejected before descending into it - const sib_result = try self.acceptNode(node); + const sib_result = try self.acceptNode(node, page); if (sib_result == NodeFilter.FILTER_REJECT) { // Skip this sibling and its descendants entirely sibling = self.previousSiblingOrNull(node); @@ -204,7 +204,7 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node { while (child) |c| { if (!self.isInSubtree(c)) break; - const filter_result = try self.acceptNode(c); + const filter_result = try self.acceptNode(c, page); if (filter_result == NodeFilter.FILTER_REJECT) { // Skip this child and try its previous sibling child = self.previousSiblingOrNull(c); @@ -220,7 +220,7 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node { node = child.?; } - if (try self.acceptNode(node) == NodeFilter.FILTER_ACCEPT) { + if (try self.acceptNode(node, page) == NodeFilter.FILTER_ACCEPT) { self._current = node; return node; } @@ -232,7 +232,7 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node { } const parent = node._parent orelse return null; - if (try self.acceptNode(parent) == NodeFilter.FILTER_ACCEPT) { + if (try self.acceptNode(parent, page) == NodeFilter.FILTER_ACCEPT) { self._current = parent; return parent; } @@ -241,14 +241,14 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node { return null; } -pub fn nextNode(self: *DOMTreeWalker) !?*Node { +pub fn nextNode(self: *DOMTreeWalker, page: *Page) !?*Node { var node = self._current; while (true) { // Try children first (depth-first) if (node.firstChild()) |child| { node = child; - const filter_result = try self.acceptNode(node); + const filter_result = try self.acceptNode(node, page); if (filter_result == NodeFilter.FILTER_ACCEPT) { self._current = node; return node; @@ -271,7 +271,7 @@ pub fn nextNode(self: *DOMTreeWalker) !?*Node { if (node.nextSibling()) |sibling| { node = sibling; - const filter_result = try self.acceptNode(node); + const filter_result = try self.acceptNode(node, page); if (filter_result == NodeFilter.FILTER_ACCEPT) { self._current = node; return node; @@ -293,7 +293,7 @@ pub fn nextNode(self: *DOMTreeWalker) !?*Node { } // Helper methods -fn acceptNode(self: *const DOMTreeWalker, node: *Node) !i32 { +fn acceptNode(self: *const DOMTreeWalker, node: *Node, page: *Page) !i32 { // First check whatToShow if (!NodeFilter.shouldShow(node, self._what_to_show)) { return NodeFilter.FILTER_SKIP; @@ -303,7 +303,7 @@ fn acceptNode(self: *const DOMTreeWalker, node: *Node) !i32 { // For TreeWalker, REJECT means reject node and its descendants // SKIP means skip node but check its descendants // ACCEPT means accept the node - return try self._filter.acceptNode(node); + return try self._filter.acceptNode(node, page.js.local.?); } fn isInSubtree(self: *const DOMTreeWalker, node: *Node) bool { diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 57db61fd..2a9ee7ca 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -770,7 +770,7 @@ pub fn getAdoptedStyleSheets(self: *Document, page: *Page) !js.Object.Global { if (self._adopted_style_sheets) |ass| { return ass; } - const js_arr = page.js.newArray(0); + const js_arr = page.js.local.?.newArray(0); const js_obj = js_arr.toObject(); self._adopted_style_sheets = try js_obj.persist(); return self._adopted_style_sheets.?; diff --git a/src/browser/webapi/History.zig b/src/browser/webapi/History.zig index 41464753..825b9464 100644 --- a/src/browser/webapi/History.zig +++ b/src/browser/webapi/History.zig @@ -34,7 +34,7 @@ pub fn getLength(_: *const History, page: *Page) u32 { pub fn getState(_: *const History, page: *Page) !?js.Value { if (page._session.navigation.getCurrentEntry()._state.value) |state| { - const value = try page.js.parseJSON(state); + const value = try page.js.local.?.parseJSON(state); return value; } else return null; } @@ -81,11 +81,10 @@ fn goInner(delta: i32, page: *Page) !void { if (try page.isSameOrigin(url)) { const event = try PopStateEvent.initTrusted("popstate", .{ .state = entry._state.value }, page); - const func = if (page.window._on_popstate) |*g| g.local() else null; try page._event_manager.dispatchWithFunction( page.window.asEventTarget(), event.asEvent(), - func, + page.js.toLocal(page.window._on_popstate), .{ .context = "Pop State" }, ); } diff --git a/src/browser/webapi/IntersectionObserver.zig b/src/browser/webapi/IntersectionObserver.zig index 115430fc..bc793206 100644 --- a/src/browser/webapi/IntersectionObserver.zig +++ b/src/browser/webapi/IntersectionObserver.zig @@ -246,7 +246,12 @@ pub fn deliverEntries(self: *IntersectionObserver, page: *Page) !void { const entries = try self.takeRecords(page); var caught: js.TryCatch.Caught = undefined; - self._callback.local().tryCall(void, .{ entries, self }, &caught) catch |err| { + + var ls: js.Local.Scope = undefined; + page.js.localScope(&ls); + defer ls.deinit(); + + ls.toLocal(self._callback).tryCall(void, .{ entries, self }, &caught) catch |err| { log.err(.page, "IntsctObserver.deliverEntries", .{ .err = err, .caught = caught }); return err; }; diff --git a/src/browser/webapi/MessagePort.zig b/src/browser/webapi/MessagePort.zig index f567199b..15fa5091 100644 --- a/src/browser/webapi/MessagePort.zig +++ b/src/browser/webapi/MessagePort.zig @@ -116,6 +116,7 @@ const PostMessageCallback = struct { fn run(ctx: *anyopaque) !?u32 { const self: *PostMessageCallback = @ptrCast(@alignCast(ctx)); defer self.deinit(); + const page = self.page; if (self.port._closed) { return null; @@ -125,16 +126,19 @@ const PostMessageCallback = struct { .data = self.message, .origin = "", .source = null, - }, self.page) catch |err| { + }, page) catch |err| { log.err(.dom, "MessagePort.postMessage", .{ .err = err }); return null; }; - const func = if (self.port._on_message) |*g| g.local() else null; - self.page._event_manager.dispatchWithFunction( + var ls: js.Local.Scope = undefined; + page.js.localScope(&ls); + defer ls.deinit(); + + page._event_manager.dispatchWithFunction( self.port.asEventTarget(), event.asEvent(), - func, + ls.toLocal(self.port._on_message), .{ .context = "MessagePort message" }, ) catch |err| { log.err(.dom, "MessagePort.postMessage", .{ .err = err }); diff --git a/src/browser/webapi/MutationObserver.zig b/src/browser/webapi/MutationObserver.zig index 81220182..20a1ea7d 100644 --- a/src/browser/webapi/MutationObserver.zig +++ b/src/browser/webapi/MutationObserver.zig @@ -248,8 +248,12 @@ pub fn deliverRecords(self: *MutationObserver, page: *Page) !void { // Take a copy of the records and clear the list before calling callback // This ensures mutations triggered during the callback go into a fresh list const records = try self.takeRecords(page); + var ls: js.Local.Scope = undefined; + page.js.localScope(&ls); + defer ls.deinit(); + var caught: js.TryCatch.Caught = undefined; - self._callback.local().tryCall(void, .{ records, self }, &caught) catch |err| { + ls.toLocal(self._callback).tryCall(void, .{ records, self }, &caught) catch |err| { log.err(.page, "MutObserver.deliverRecords", .{ .err = err, .caught = caught }); return err; }; diff --git a/src/browser/webapi/NodeFilter.zig b/src/browser/webapi/NodeFilter.zig index db0c3bf6..149d547d 100644 --- a/src/browser/webapi/NodeFilter.zig +++ b/src/browser/webapi/NodeFilter.zig @@ -65,9 +65,9 @@ pub const SHOW_DOCUMENT_TYPE: u32 = 0x200; pub const SHOW_DOCUMENT_FRAGMENT: u32 = 0x400; pub const SHOW_NOTATION: u32 = 0x800; -pub fn acceptNode(self: *const NodeFilter, node: *Node) !i32 { +pub fn acceptNode(self: *const NodeFilter, node: *Node, local: *const js.Local) !i32 { const func = self._func orelse return FILTER_ACCEPT; - return func.local().call(i32, .{node}); + return local.toLocal(func).call(i32, .{node}); } pub fn shouldShow(node: *const Node, what_to_show: u32) bool { diff --git a/src/browser/webapi/PerformanceObserver.zig b/src/browser/webapi/PerformanceObserver.zig index 28899e7e..ad130c97 100644 --- a/src/browser/webapi/PerformanceObserver.zig +++ b/src/browser/webapi/PerformanceObserver.zig @@ -153,8 +153,13 @@ pub inline fn hasRecords(self: *const PerformanceObserver) bool { /// Runs the PerformanceObserver's callback with records; emptying it out. pub fn dispatch(self: *PerformanceObserver, page: *Page) !void { const records = try self.takeRecords(page); + + var ls: js.Local.Scope = undefined; + page.js.localScope(&ls); + defer ls.deinit(); + var caught: js.TryCatch.Caught = undefined; - self._callback.local().tryCall(void, .{ EntryList{ ._entries = records }, self }, &caught) catch |err| { + ls.toLocal(self._callback).tryCall(void, .{ EntryList{ ._entries = records }, self }, &caught) catch |err| { log.err(.page, "PerfObserver.dispatch", .{ .err = err, .caught = caught }); return err; }; diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 7b6938e8..ac1aa79e 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -269,10 +269,10 @@ pub fn cancelIdleCallback(self: *Window, id: u32) void { sc.removed = true; } -pub fn reportError(self: *Window, err: js.Value.Global, page: *Page) !void { +pub fn reportError(self: *Window, err: js.Value, page: *Page) !void { const error_event = try ErrorEvent.initTrusted("error", .{ - .@"error" = err, - .message = err.local().toString(.{}) catch "Unknown error", + .@"error" = try err.persist(), + .message = err.toString(.{}) catch "Unknown error", .bubbles = false, .cancelable = true, }, page); @@ -552,20 +552,24 @@ const ScheduleCallback = struct { return null; } + var ls: js.Local.Scope = undefined; + page.js.localScope(&ls); + defer ls.deinit(); + switch (self.mode) { .idle => { const IdleDeadline = @import("IdleDeadline.zig"); - self.cb.local().call(void, .{IdleDeadline{}}) catch |err| { + ls.toLocal(self.cb).call(void, .{IdleDeadline{}}) catch |err| { log.warn(.js, "window.idleCallback", .{ .name = self.name, .err = err }); }; }, .animation_frame => { - self.cb.local().call(void, .{page.window._performance.now()}) catch |err| { + ls.toLocal(self.cb).call(void, .{page.window._performance.now()}) catch |err| { log.warn(.js, "window.RAF", .{ .name = self.name, .err = err }); }; }, .normal => { - self.cb.local().call(void, self.params) catch |err| { + ls.toLocal(self.cb).call(void, self.params) catch |err| { log.warn(.js, "window.timer", .{ .name = self.name, .err = err }); }; }, diff --git a/src/browser/webapi/animation/Animation.zig b/src/browser/webapi/animation/Animation.zig index b6167f7c..b0f1a8cc 100644 --- a/src/browser/webapi/animation/Animation.zig +++ b/src/browser/webapi/animation/Animation.zig @@ -23,8 +23,8 @@ const Animation = @This(); _effect: ?js.Object.Global = null, _timeline: ?js.Object.Global = null, -_ready_resolver: ?js.PromiseResolver = null, -_finished_resolver: ?js.PromiseResolver = null, +_ready_resolver: ?js.PromiseResolver.Global = null, +_finished_resolver: ?js.PromiseResolver.Global = null, pub fn init(page: *Page) !*Animation { return page._factory.create(Animation{}); @@ -46,20 +46,22 @@ pub fn getPending(_: *const Animation) bool { pub fn getFinished(self: *Animation, page: *Page) !js.Promise { if (self._finished_resolver == null) { - const resolver = try page.js.createPromiseResolver().persist(); + const resolver = page.js.local.?.createPromiseResolver(); resolver.resolve("Animation.getFinished", self); - self._finished_resolver = resolver; + self._finished_resolver = try resolver.persist(); + return resolver.promise(); } - return self._finished_resolver.?.promise(); + return page.js.toLocal(self._finished_resolver).?.promise(); } pub fn getReady(self: *Animation, page: *Page) !js.Promise { // never resolved, because we're always "finished" if (self._ready_resolver == null) { - const resolver = try page.js.createPromiseResolver().persist(); - self._ready_resolver = resolver; + const resolver = page.js.local.?.createPromiseResolver(); + self._ready_resolver = try resolver.persist(); + return resolver.promise(); } - return self._ready_resolver.?.promise(); + return page.js.toLocal(self._ready_resolver).?.promise(); } pub fn getEffect(self: *const Animation) ?js.Object.Global { diff --git a/src/browser/webapi/css/CSSStyleSheet.zig b/src/browser/webapi/css/CSSStyleSheet.zig index 2f8b76fb..493e6506 100644 --- a/src/browser/webapi/css/CSSStyleSheet.zig +++ b/src/browser/webapi/css/CSSStyleSheet.zig @@ -66,7 +66,7 @@ pub fn replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise _ = self; _ = text; // TODO: clear self.css_rules - return page.js.resolvePromise({}); + return page.js.local.?.resolvePromise({}); } pub fn replaceSync(self: *CSSStyleSheet, text: []const u8) !void { diff --git a/src/browser/webapi/element/html/Body.zig b/src/browser/webapi/element/html/Body.zig index 6f35b16f..766ef1a3 100644 --- a/src/browser/webapi/element/html/Body.zig +++ b/src/browser/webapi/element/html/Body.zig @@ -50,8 +50,8 @@ pub const Build = struct { pub fn complete(node: *Node, page: *Page) !void { const el = node.as(Element); const on_load = el.getAttributeSafe("onload") orelse return; - if (page.js.stringToFunction(on_load)) |func| { - page.window._on_load = try func.persist(); + if (page.js.stringToPersistedFunction(on_load)) |func| { + page.window._on_load = func; } else |err| { log.err(.js, "body.onload", .{ .err = err, .str = on_load }); } diff --git a/src/browser/webapi/element/html/Custom.zig b/src/browser/webapi/element/html/Custom.zig index c814fe5e..adf23149 100644 --- a/src/browser/webapi/element/html/Custom.zig +++ b/src/browser/webapi/element/html/Custom.zig @@ -160,10 +160,12 @@ pub fn invokeAttributeChangedCallbackOnElement(element: *Element, name: []const fn invokeCallbackOnElement(element: *Element, definition: *CustomElementDefinition, comptime callback_name: [:0]const u8, args: anytype, page: *Page) void { _ = definition; - const ctx = page.js; + var ls: js.Local.Scope = undefined; + page.js.localScope(&ls); + defer ls.deinit(); // Get the JS element object - const js_val = ctx.zigValueToJs(element, .{}) catch return; + const js_val = ls.local.zigValueToJs(element, .{}) catch return; const js_element = js_val.toObject(); // Call the callback method if it exists @@ -195,8 +197,26 @@ pub fn checkAndAttachBuiltIn(element: *Element, page: *Page) !void { page._upgrading_element = node; defer page._upgrading_element = prev_upgrading; + // PERFORMANCE OPTIMIZATION: This pattern is discouraged in general code. + // Used here because: (1) multiple early returns before needing Local, + // (2) called from both V8 callbacks (Local exists) and parser (no Local). + // Prefer either: requiring *const js.Local parameter, OR always creating + // Local.Scope upfront. + var ls: ?js.Local.Scope = null; + var local = blk: { + if (page.js.local) |l| { + break :blk l; + } + ls = undefined; + page.js.localScope(&ls.?); + break :blk &ls.?.local; + }; + defer if (ls) |*_ls| { + _ls.deinit(); + }; + var caught: js.TryCatch.Caught = undefined; - _ = definition.constructor.local().newInstance(&caught) catch |err| { + _ = local.toLocal(definition.constructor).newInstance(&caught) catch |err| { log.warn(.js, "custom builtin ctor", .{ .name = is_value, .err = err, .caught = caught }); return; }; @@ -207,9 +227,11 @@ fn invokeCallback(self: *Custom, comptime callback_name: [:0]const u8, args: any return; } - const ctx = page.js; + var ls: js.Local.Scope = undefined; + page.js.localScope(&ls); + defer ls.deinit(); - const js_val = ctx.zigValueToJs(self, .{}) catch return; + const js_val = ls.local.zigValueToJs(self, .{}) catch return; const js_element = js_val.toObject(); js_element.callMethod(void, callback_name, args) catch return; diff --git a/src/browser/webapi/element/html/Script.zig b/src/browser/webapi/element/html/Script.zig index 6b1d3823..559b2d05 100644 --- a/src/browser/webapi/element/html/Script.zig +++ b/src/browser/webapi/element/html/Script.zig @@ -128,16 +128,16 @@ pub const Build = struct { self._src = element.getAttributeSafe("src") orelse ""; if (element.getAttributeSafe("onload")) |on_load| { - if (page.js.stringToFunction(on_load)) |func| { - self._on_load = try func.persist(); + if (page.js.stringToPersistedFunction(on_load)) |func| { + self._on_load = func; } else |err| { log.err(.js, "script.onload", .{ .err = err, .str = on_load }); } } if (element.getAttributeSafe("onerror")) |on_error| { - if (page.js.stringToFunction(on_error)) |func| { - self._on_error = try func.persist(); + if (page.js.stringToPersistedFunction(on_error)) |func| { + self._on_error = func; } else |err| { log.err(.js, "script.onerror", .{ .err = err, .str = on_error }); } diff --git a/src/browser/webapi/event/PopStateEvent.zig b/src/browser/webapi/event/PopStateEvent.zig index 4d4c710f..78480402 100644 --- a/src/browser/webapi/event/PopStateEvent.zig +++ b/src/browser/webapi/event/PopStateEvent.zig @@ -61,7 +61,7 @@ pub fn asEvent(self: *PopStateEvent) *Event { pub fn getState(self: *PopStateEvent, page: *Page) !?js.Value { const s = self._state orelse return null; - return try page.js.parseJSON(s); + return try page.js.local.?.parseJSON(s); } pub fn hasUAVisualTransition(_: *PopStateEvent) bool { diff --git a/src/browser/webapi/navigation/Navigation.zig b/src/browser/webapi/navigation/Navigation.zig index d2fabff1..d69a1a9c 100644 --- a/src/browser/webapi/navigation/Navigation.zig +++ b/src/browser/webapi/navigation/Navigation.zig @@ -265,8 +265,9 @@ pub fn navigateInner( // // These will only settle on same-origin navigation (mostly intended for SPAs). // It is fine (and expected) for these to not settle on cross-origin requests :) - const committed = try page.js.createPromiseResolver().persist(); - const finished = try page.js.createPromiseResolver().persist(); + const local = page.js.local.?; + const committed = local.createPromiseResolver(); + const finished = local.createPromiseResolver(); const new_url = try URL.resolve(arena, page.url, url, .{}); const is_same_document = URL.eqlDocument(new_url, page.url); @@ -278,9 +279,9 @@ pub fn navigateInner( if (is_same_document) { page.url = new_url; - committed.local().resolve("navigation push", {}); + committed.resolve("navigation push", {}); // todo: Fire navigate event - finished.local().resolve("navigation push", {}); + finished.resolve("navigation push", {}); _ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, true); } else { @@ -291,9 +292,9 @@ pub fn navigateInner( if (is_same_document) { page.url = new_url; - committed.local().resolve("navigation replace", {}); + committed.resolve("navigation replace", {}); // todo: Fire navigate event - finished.local().resolve("navigation replace", {}); + finished.resolve("navigation replace", {}); _ = try self.replaceEntry(url, .{ .source = .navigation, .value = state }, page, true); } else { @@ -306,9 +307,9 @@ pub fn navigateInner( if (is_same_document) { page.url = new_url; - committed.local().resolve("navigation traverse", {}); + committed.resolve("navigation traverse", {}); // todo: Fire navigate event - finished.local().resolve("navigation traverse", {}); + finished.resolve("navigation traverse", {}); } else { try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .script); } @@ -326,9 +327,11 @@ pub fn navigateInner( ); try self._proto.dispatch(.{ .currententrychange = event }, page); + _ = try committed.persist(); + _ = try finished.persist(); return .{ - .committed = try committed.local().promise().persist(), - .finished = try finished.local().promise().persist(), + .committed = try committed.promise().persist(), + .finished = try finished.promise().persist(), }; } diff --git a/src/browser/webapi/navigation/NavigationEventTarget.zig b/src/browser/webapi/navigation/NavigationEventTarget.zig index c5d03d2a..581e06cc 100644 --- a/src/browser/webapi/navigation/NavigationEventTarget.zig +++ b/src/browser/webapi/navigation/NavigationEventTarget.zig @@ -43,11 +43,10 @@ pub fn dispatch(self: *NavigationEventTarget, event_type: DispatchType, page: *P }; }; - const func = if (@field(self, field)) |*g| g.local() else null; return page._event_manager.dispatchWithFunction( self.asEventTarget(), event, - func, + page.js.toLocal(@field(self, field)), .{ .context = "Navigation" }, ); } diff --git a/src/browser/webapi/navigation/NavigationHistoryEntry.zig b/src/browser/webapi/navigation/NavigationHistoryEntry.zig index 6d29df3a..af006702 100644 --- a/src/browser/webapi/navigation/NavigationHistoryEntry.zig +++ b/src/browser/webapi/navigation/NavigationHistoryEntry.zig @@ -81,7 +81,7 @@ pub const StateReturn = union(enum) { value: ?js.Value, undefined: void }; pub fn getState(self: *const NavigationHistoryEntry, page: *Page) !StateReturn { if (self._state.source == .navigation) { if (self._state.value) |value| { - return .{ .value = try page.js.parseJSON(value) }; + return .{ .value = try page.js.local.?.parseJSON(value) }; } } diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index 688f3a26..b5e3b384 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -44,12 +44,14 @@ pub const InitOpts = Request.InitOpts; pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise { const request = try Request.init(input, options, page); + const resolver = page.js.local.?.createPromiseResolver(); + const fetch = try page.arena.create(Fetch); fetch.* = .{ ._page = page, ._buf = .empty, ._url = try page.arena.dupe(u8, request._url), - ._resolver = try page.js.createPromiseResolver().persist(), + ._resolver = try resolver.persist(), ._response = try Response.init(null, .{ .status = 0 }, page), }; @@ -77,7 +79,7 @@ pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise { .done_callback = httpDoneCallback, .error_callback = httpErrorCallback, }); - return fetch._resolver.local().promise(); + return resolver.promise(); } pub fn deinit(self: *Fetch) void { @@ -149,13 +151,22 @@ fn httpDoneCallback(ctx: *anyopaque) !void { .len = self._buf.items.len, }); - return self._resolver.local().resolve("fetch done", self._response); + var ls: js.Local.Scope = undefined; + self._page.js.localScope(&ls); + defer ls.deinit(); + + return ls.toLocal(self._resolver).resolve("fetch done", self._response); } fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void { const self: *Fetch = @ptrCast(@alignCast(ctx)); self._response._type = .@"error"; // Set type to error for network failures - self._resolver.local().reject("fetch error", @errorName(err)); + + var ls: js.Local.Scope = undefined; + self._page.js.localScope(&ls); + defer ls.deinit(); + + ls.toLocal(self._resolver).reject("fetch error", @errorName(err)); } const testing = @import("../../../testing.zig"); diff --git a/src/browser/webapi/net/Response.zig b/src/browser/webapi/net/Response.zig index 2fd38e38..c413afbe 100644 --- a/src/browser/webapi/net/Response.zig +++ b/src/browser/webapi/net/Response.zig @@ -115,15 +115,16 @@ pub fn isOK(self: *const Response) bool { pub fn getText(self: *const Response, page: *Page) !js.Promise { const body = self._body orelse ""; - return page.js.resolvePromise(body); + return page.js.local.?.resolvePromise(body); } pub fn getJson(self: *Response, page: *Page) !js.Promise { const body = self._body orelse ""; - const value = page.js.parseJSON(body) catch |err| { - return page.js.rejectPromise(.{@errorName(err)}); + const local = page.js.local.?; + const value = local.parseJSON(body) catch |err| { + return local.rejectPromise(.{@errorName(err)}); }; - return page.js.resolvePromise(try value.persist()); + return local.resolvePromise(try value.persist()); } pub const JsApi = struct { diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index 519adba4..a2dec743 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -131,7 +131,7 @@ pub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8) !void self._method = try parseMethod(method_); self._url = try URL.resolve(self._arena, self._page.base(), url, .{ .always_dupe = true }); - try self.stateChanged(.opened, self._page); + try self.stateChanged(.opened, self._page.js.local.?, self._page); } pub fn setRequestHeader(self: *XMLHttpRequest, name: []const u8, value: []const u8, page: *Page) !void { @@ -254,7 +254,7 @@ pub fn getResponse(self: *XMLHttpRequest, page: *Page) !?Response { const res: Response = switch (self._response_type) { .text => .{ .text = data }, .json => blk: { - const value = try page.js.parseJSON(data); + const value = try page.js.local.?.parseJSON(data); break :blk .{ .json = try value.persist() }; }, .document => blk: { @@ -322,19 +322,32 @@ fn httpHeaderDoneCallback(transfer: *Http.Transfer) !void { } self._response_url = try self._arena.dupeZ(u8, std.mem.span(header.url)); - try self.stateChanged(.headers_received, self._page); - try self._proto.dispatch(.load_start, .{ .loaded = 0, .total = self._response_len orelse 0 }, self._page); - try self.stateChanged(.loading, self._page); + const page = self._page; + + var ls: js.Local.Scope = undefined; + page.js.localScope(&ls); + defer ls.deinit(); + const local = &ls.local; + + try self.stateChanged(.headers_received, local, page); + try self._proto.dispatch(.load_start, .{ .loaded = 0, .total = self._response_len orelse 0 }, local, page); + try self.stateChanged(.loading, local, page); } fn httpDataCallback(transfer: *Http.Transfer, data: []const u8) !void { const self: *XMLHttpRequest = @ptrCast(@alignCast(transfer.ctx)); try self._response_data.appendSlice(self._arena, data); + const page = self._page; + + var ls: js.Local.Scope = undefined; + page.js.localScope(&ls); + defer ls.deinit(); + try self._proto.dispatch(.progress, .{ .total = self._response_len orelse 0, .loaded = self._response_data.items.len, - }, self._page); + }, &ls.local, page); } fn httpDoneCallback(ctx: *anyopaque) !void { @@ -350,17 +363,25 @@ fn httpDoneCallback(ctx: *anyopaque) !void { // Not that the request is done, the http/client will free the transfer // object. It isn't safe to keep it around. self._transfer = null; - try self.stateChanged(.done, self._page); + + const page = self._page; + + var ls: js.Local.Scope = undefined; + page.js.localScope(&ls); + defer ls.deinit(); + const local = &ls.local; + + try self.stateChanged(.done, local, page); const loaded = self._response_data.items.len; try self._proto.dispatch(.load, .{ .total = loaded, .loaded = loaded, - }, self._page); + }, local, page); try self._proto.dispatch(.load_end, .{ .total = loaded, .loaded = loaded, - }, self._page); + }, local, page); } fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void { @@ -392,12 +413,18 @@ fn _handleError(self: *XMLHttpRequest, err: anyerror) !void { const new_state: ReadyState = if (is_abort) .unsent else .done; if (new_state != self._ready_state) { const page = self._page; - try self.stateChanged(new_state, page); + + var ls: js.Local.Scope = undefined; + page.js.localScope(&ls); + defer ls.deinit(); + const local = &ls.local; + + try self.stateChanged(new_state, local, page); if (is_abort) { - try self._proto.dispatch(.abort, null, page); + try self._proto.dispatch(.abort, null, local, page); } - try self._proto.dispatch(.err, null, page); - try self._proto.dispatch(.load_end, null, page); + try self._proto.dispatch(.err, null, local, page); + try self._proto.dispatch(.load_end, null, local, page); } const level: log.Level = if (err == error.Abort) .debug else .err; @@ -408,7 +435,7 @@ fn _handleError(self: *XMLHttpRequest, err: anyerror) !void { }); } -fn stateChanged(self: *XMLHttpRequest, state: ReadyState, page: *Page) !void { +fn stateChanged(self: *XMLHttpRequest, state: ReadyState, local: *const js.Local, page: *Page) !void { if (state == self._ready_state) { return; } @@ -416,11 +443,10 @@ fn stateChanged(self: *XMLHttpRequest, state: ReadyState, page: *Page) !void { self._ready_state = state; const event = try Event.initTrusted("readystatechange", .{}, page); - const func = if (self._on_ready_state_change) |*g| g.local() else null; try page._event_manager.dispatchWithFunction( self.asEventTarget(), event, - func, + local.toLocal(self._on_ready_state_change), .{ .context = "XHR state change" }, ); } diff --git a/src/browser/webapi/net/XMLHttpRequestEventTarget.zig b/src/browser/webapi/net/XMLHttpRequestEventTarget.zig index a211d30e..2a051f36 100644 --- a/src/browser/webapi/net/XMLHttpRequestEventTarget.zig +++ b/src/browser/webapi/net/XMLHttpRequestEventTarget.zig @@ -43,7 +43,7 @@ pub fn asEventTarget(self: *XMLHttpRequestEventTarget) *EventTarget { return self._proto; } -pub fn dispatch(self: *XMLHttpRequestEventTarget, comptime event_type: DispatchType, progress_: ?Progress, page: *Page) !void { +pub fn dispatch(self: *XMLHttpRequestEventTarget, comptime event_type: DispatchType, progress_: ?Progress, local: *const js.Local, page: *Page) !void { const field, const typ = comptime blk: { break :blk switch (event_type) { .abort => .{ "_on_abort", "abort" }, @@ -63,11 +63,10 @@ pub fn dispatch(self: *XMLHttpRequestEventTarget, comptime event_type: DispatchT page, ); - const func = if (@field(self, field)) |*g| g.local() else null; return page._event_manager.dispatchWithFunction( self.asEventTarget(), event.asEvent(), - func, + local.toLocal(@field(self, field)), .{ .context = "XHR " ++ typ }, ); } diff --git a/src/browser/webapi/streams/ReadableStream.zig b/src/browser/webapi/streams/ReadableStream.zig index af50d8bc..dbb118b2 100644 --- a/src/browser/webapi/streams/ReadableStream.zig +++ b/src/browser/webapi/streams/ReadableStream.zig @@ -137,12 +137,12 @@ pub fn callPullIfNeeded(self: *ReadableStream) !void { self._pulling = true; - const pull_fn = &(self._pull_fn orelse return); + const pull_fn = self._page.js.toLocal(self._pull_fn) orelse return; // Call the pull function // Note: In a complete implementation, we'd handle the promise returned by pull // and set _pulling = false when it resolves - try pull_fn.local().call(void, .{self._controller}); + try pull_fn.call(void, .{self._controller}); self._pulling = false; @@ -167,13 +167,15 @@ fn shouldCallPull(self: *const ReadableStream) bool { } pub fn cancel(self: *ReadableStream, reason: ?[]const u8, page: *Page) !js.Promise { + const local = page.js.local.?; + if (self._state != .readable) { if (self._cancel) |c| { if (c.resolver) |r| { - return r.local().promise(); + return local.toLocal(r).promise(); } } - return page.js.resolvePromise(.{}); + return local.resolvePromise(.{}); } if (self._cancel == null) { @@ -181,16 +183,21 @@ pub fn cancel(self: *ReadableStream, reason: ?[]const u8, page: *Page) !js.Promi } var c = &self._cancel.?; - if (c.resolver == null) { - c.resolver = try page.js.createPromiseResolver().persist(); - } + var resolver = blk: { + if (c.resolver) |r| { + break :blk local.toLocal(r); + } + var temp = local.createPromiseResolver(); + c.resolver = try temp.persist(); + break :blk temp; + }; // Execute the cancel callback if provided - if (c.callback) |*cb| { + if (c.callback) |cb| { if (reason) |r| { - try cb.local().call(void, .{r}); + try local.toLocal(cb).call(void, .{r}); } else { - try cb.local().call(void, .{}); + try local.toLocal(cb).call(void, .{}); } } @@ -201,13 +208,12 @@ pub fn cancel(self: *ReadableStream, reason: ?[]const u8, page: *Page) !js.Promi .done = true, .value = .empty, }; - for (self._controller._pending_reads.items) |*resolver| { - resolver.local().resolve("stream cancelled", result); + for (self._controller._pending_reads.items) |r| { + local.toLocal(r).resolve("stream cancelled", result); } self._controller._pending_reads.clearRetainingCapacity(); - - c.resolver.?.local().resolve("ReadableStream.cancel", {}); - return c.resolver.?.local().promise(); + resolver.resolve("ReadableStream.cancel", {}); + return resolver.promise(); } const Cancel = struct { @@ -250,7 +256,7 @@ pub const AsyncIterator = struct { pub fn @"return"(self: *AsyncIterator, page: *Page) !js.Promise { self._reader.releaseLock(); - return page.js.resolvePromise(.{ .done = true, .value = null }); + return page.js.local.?.resolvePromise(.{ .done = true, .value = null }); } pub const JsApi = struct { diff --git a/src/browser/webapi/streams/ReadableStreamDefaultController.zig b/src/browser/webapi/streams/ReadableStreamDefaultController.zig index da23f77e..f51d5798 100644 --- a/src/browser/webapi/streams/ReadableStreamDefaultController.zig +++ b/src/browser/webapi/streams/ReadableStreamDefaultController.zig @@ -57,9 +57,9 @@ pub fn init(stream: *ReadableStream, high_water_mark: u32, page: *Page) !*Readab } pub fn addPendingRead(self: *ReadableStreamDefaultController, page: *Page) !js.Promise { - const resolver = try page.js.createPromiseResolver().persist(); - try self._pending_reads.append(self._arena, resolver); - return resolver.local().promise(); + const resolver = page.js.local.?.createPromiseResolver(); + try self._pending_reads.append(self._arena, try resolver.persist()); + return resolver.promise(); } pub fn enqueue(self: *ReadableStreamDefaultController, chunk: Chunk) !void { @@ -79,7 +79,7 @@ pub fn enqueue(self: *ReadableStreamDefaultController, chunk: Chunk) !void { .done = false, .value = .fromChunk(chunk), }; - resolver.local().resolve("stream enqueue", result); + self._page.js.toLocal(resolver).resolve("stream enqueue", result); } pub fn close(self: *ReadableStreamDefaultController) !void { @@ -94,8 +94,8 @@ pub fn close(self: *ReadableStreamDefaultController) !void { .done = true, .value = .empty, }; - for (self._pending_reads.items) |*resolver| { - resolver.local().resolve("stream close", result); + for (self._pending_reads.items) |resolver| { + self._page.js.toLocal(resolver).resolve("stream close", result); } self._pending_reads.clearRetainingCapacity(); } @@ -109,8 +109,8 @@ pub fn doError(self: *ReadableStreamDefaultController, err: []const u8) !void { self._stream._stored_error = try self._page.arena.dupe(u8, err); // Reject all pending reads - for (self._pending_reads.items) |*resolver| { - resolver.local().reject("stream errror", err); + for (self._pending_reads.items) |resolver| { + self._page.js.toLocal(resolver).reject("stream errror", err); } self._pending_reads.clearRetainingCapacity(); } diff --git a/src/browser/webapi/streams/ReadableStreamDefaultReader.zig b/src/browser/webapi/streams/ReadableStreamDefaultReader.zig index 16dd0ad4..053080c4 100644 --- a/src/browser/webapi/streams/ReadableStreamDefaultReader.zig +++ b/src/browser/webapi/streams/ReadableStreamDefaultReader.zig @@ -56,12 +56,12 @@ pub const ReadResult = struct { pub fn read(self: *ReadableStreamDefaultReader, page: *Page) !js.Promise { const stream = self._stream orelse { - return page.js.rejectPromise("Reader has been released"); + return page.js.local.?.rejectPromise("Reader has been released"); }; if (stream._state == .errored) { const err = stream._stored_error orelse "Stream errored"; - return page.js.rejectPromise(err); + return page.js.local.?.rejectPromise(err); } if (stream._controller.dequeue()) |chunk| { @@ -69,7 +69,7 @@ pub fn read(self: *ReadableStreamDefaultReader, page: *Page) !js.Promise { .done = false, .value = .fromChunk(chunk), }; - return page.js.resolvePromise(result); + return page.js.local.?.resolvePromise(result); } if (stream._state == .closed) { @@ -77,7 +77,7 @@ pub fn read(self: *ReadableStreamDefaultReader, page: *Page) !js.Promise { .done = true, .value = .empty, }; - return page.js.resolvePromise(result); + return page.js.local.?.resolvePromise(result); } // No data, but not closed. We need to queue the read for any future data @@ -93,7 +93,7 @@ pub fn releaseLock(self: *ReadableStreamDefaultReader) void { pub fn cancel(self: *ReadableStreamDefaultReader, reason_: ?[]const u8, page: *Page) !js.Promise { const stream = self._stream orelse { - return page.js.rejectPromise("Reader has been released"); + return page.js.local.?.rejectPromise("Reader has been released"); }; self.releaseLock(); diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index e11a98fa..d5777da3 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -24,7 +24,6 @@ const Selector = @import("../../browser/webapi/selector/Selector.zig"); const dump = @import("../../browser/dump.zig"); const js = @import("../../browser/js/js.zig"); -const v8 = js.v8; const Allocator = std.mem.Allocator; @@ -273,16 +272,32 @@ fn resolveNode(cmd: anytype) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const page = bc.session.currentPage() orelse return error.PageNotLoaded; - var js_context = page.js; - if (params.executionContextId) |context_id| { - if (js_context.debugContextId() != context_id) { - for (bc.isolated_worlds.items) |*isolated_world| { - js_context = &(isolated_world.executor.context orelse return error.ContextNotFound); - if (js_context.debugContextId() == context_id) { - break; - } - } else return error.ContextNotFound; + var ls: ?js.Local.Scope = null; + defer if (ls) |*_ls| { + _ls.deinit(); + }; + + if (params.executionContextId) |context_id| blk: { + ls = undefined; + page.js.localScope(&ls.?); + if (ls.?.local.debugContextId() == context_id) { + break :blk; } + // not the default scope, check the other ones + for (bc.isolated_worlds.items) |*isolated_world| { + ls.?.deinit(); + ls = null; + + const ctx = &(isolated_world.executor.context orelse return error.ContextNotFound); + ls = undefined; + ctx.localScope(&ls.?); + if (ls.?.local.debugContextId() == context_id) { + break :blk; + } + } else return error.ContextNotFound; + } else { + ls = undefined; + page.js.localScope(&ls.?); } const input_node_id = params.nodeId orelse params.backendNodeId orelse return error.InvalidParam; @@ -291,7 +306,7 @@ fn resolveNode(cmd: anytype) !void { // node._node is a *DOMNode we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement // So we use the Node.Union when retrieve the value from the environment const remote_object = try bc.inspector.getRemoteObject( - js_context, + &ls.?.local, params.objectGroup orelse "", node.dom, ); diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 9ba9db59..4fe99992 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -194,9 +194,12 @@ fn createIsolatedWorld(cmd: anytype) !void { // Create the auxdata json for the contextCreated event // Calling contextCreated will assign a Id to the context and send the contextCreated event const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{params.frameId}); - bc.inspector.contextCreated(js_context, world.name, "", aux_data, false); + var ls: js.Local.Scope = undefined; + js_context.localScope(&ls); + defer ls.deinit(); - return cmd.sendResult(.{ .executionContextId = js_context.debugContextId() }, .{}); + bc.inspector.contextCreated(&ls.local, world.name, "", aux_data, false); + return cmd.sendResult(.{ .executionContextId = ls.local.debugContextId() }, .{}); } fn navigate(cmd: anytype) !void { @@ -351,8 +354,13 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P { const page = bc.session.currentPage() orelse return error.PageNotLoaded; const aux_data = try std.fmt.allocPrint(arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id}); + + var ls: js.Local.Scope = undefined; + page.js.localScope(&ls); + defer ls.deinit(); + bc.inspector.contextCreated( - page.js, + &ls.local, "", try page.getOrigin(arena) orelse "", aux_data, @@ -361,9 +369,15 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P } for (bc.isolated_worlds.items) |*isolated_world| { const aux_json = try std.fmt.allocPrint(arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{target_id}); + // Calling contextCreated will assign a new Id to the context and send the contextCreated event + + var ls: js.Local.Scope = undefined; + (isolated_world.executor.context orelse continue).localScope(&ls); + defer ls.deinit(); + bc.inspector.contextCreated( - &isolated_world.executor.context.?, + &ls.local, isolated_world.name, "://", aux_json, diff --git a/src/cdp/domains/target.zig b/src/cdp/domains/target.zig index 51581caa..1f87eb39 100644 --- a/src/cdp/domains/target.zig +++ b/src/cdp/domains/target.zig @@ -18,6 +18,7 @@ const std = @import("std"); const log = @import("../../log.zig"); +const js = @import("../../browser/js/js.zig"); // TODO: hard coded IDs const LOADER_ID = "LOADERID42AA389647D702B4D805F49A"; @@ -175,9 +176,13 @@ fn createTarget(cmd: anytype) !void { const page = try bc.session.createPage(); { + var ls: js.Local.Scope = undefined; + page.js.localScope(&ls); + defer ls.deinit(); + const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id}); bc.inspector.contextCreated( - page.js, + &ls.local, "", "", // @ZIGDOM // try page.origin(arena), diff --git a/src/testing.zig b/src/testing.zig index 4d05911a..c022d830 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -396,9 +396,12 @@ fn runWebApiTest(test_file: [:0]const u8) !void { 0, ); - const js_context = page.js; + var ls: js.Local.Scope = undefined; + page.js.localScope(&ls); + defer ls.deinit(); + var try_catch: js.TryCatch = undefined; - try_catch.init(js_context); + try_catch.init(&ls.local); defer try_catch.deinit(); try page.navigate(url, .{}); @@ -406,7 +409,7 @@ fn runWebApiTest(test_file: [:0]const u8) !void { test_browser.runMicrotasks(); - js_context.eval("testing.assertOk()", "testing.assertOk()") catch |err| { + ls.local.eval("testing.assertOk()", "testing.assertOk()") catch |err| { const caught = try_catch.caughtOrError(arena_allocator, err); std.debug.print("{s}: test failure\nError: {f}\n", .{ test_file, caught }); return err;