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;