Merge pull request #1374 from lightpanda-io/fix_context_lifetime

Fix context lifetime
This commit is contained in:
Karl Seguin
2026-01-20 16:24:12 +08:00
committed by GitHub
65 changed files with 2969 additions and 2690 deletions

View File

@@ -367,14 +367,18 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
event._target = getAdjustedTarget(original_target, current_target); event._target = getAdjustedTarget(original_target, current_target);
} }
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
switch (listener.function) { 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| { .string => |string| {
const str = try page.call_arena.dupeZ(u8, string.str()); 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| { .object => |obj_global| {
const obj = obj_global.local(); const obj = ls.toLocal(obj_global);
if (try obj.getFunction("handleEvent")) |handleEvent| { if (try obj.getFunction("handleEvent")) |handleEvent| {
try handleEvent.callWithThis(void, obj, .{event}); try handleEvent.callWithThis(void, obj, .{event});
} }

View File

@@ -562,21 +562,24 @@ fn _documentIsComplete(self: *Page) !void {
const event = try Event.initTrusted("load", .{}, self); const event = try Event.initTrusted("load", .{}, self);
// this event is weird, it's dispatched directly on the window, but // this event is weird, it's dispatched directly on the window, but
// with the document as the target // with the document as the target
var ls: JS.Local.Scope = undefined;
self.js.localScope(&ls);
defer ls.deinit();
event._target = self.document.asEventTarget(); event._target = self.document.asEventTarget();
const on_load = if (self.window._on_load) |*g| g.local() else null;
try self._event_manager.dispatchWithFunction( try self._event_manager.dispatchWithFunction(
self.window.asEventTarget(), self.window.asEventTarget(),
event, event,
on_load, ls.toLocal(self.window._on_load),
.{ .inject_target = false, .context = "page load" }, .{ .inject_target = false, .context = "page load" },
); );
const pageshow_event = try PageTransitionEvent.initTrusted("pageshow", .{}, self); 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( try self._event_manager.dispatchWithFunction(
self.window.asEventTarget(), self.window.asEventTarget(),
pageshow_event.asEvent(), pageshow_event.asEvent(),
on_pageshow, ls.toLocal(self.window._on_pageshow),
.{ .context = "page show" }, .{ .context = "page show" },
); );
} }
@@ -748,10 +751,6 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult {
var timer = try std.time.Timer.start(); var timer = try std.time.Timer.start();
var ms_remaining = wait_ms; 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 scheduler = &self.scheduler;
var http_client = self._session.browser.http_client; var http_client = self._session.browser.http_client;
@@ -808,10 +807,6 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult {
// it AFTER. // it AFTER.
const ms_to_next_task = try scheduler.run(); 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 http_active = http_client.active;
const total_network_activity = http_active + http_client.intercepted; const total_network_activity = http_active + http_client.intercepted;
if (self._notified_network_almost_idle.check(total_network_activity <= 2)) { if (self._notified_network_almost_idle.check(total_network_activity <= 2)) {
@@ -2005,8 +2000,12 @@ pub fn createElementNS(self: *Page, namespace: Element.Namespace, name: []const
self._upgrading_element = node; self._upgrading_element = node;
defer self._upgrading_element = prev_upgrading; 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; 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 }); log.warn(.js, "custom element constructor", .{ .name = name, .err = err, .caught = caught });
return node; return node;
}; };

View File

@@ -19,6 +19,7 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const js = @import("js/js.zig");
const log = @import("../log.zig"); const log = @import("../log.zig");
const milliTimestamp = @import("../datetime.zig").milliTimestamp; const milliTimestamp = @import("../datetime.zig").milliTimestamp;

View File

@@ -271,11 +271,15 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
}); });
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
log.debug(.http, "script queue", .{ log.debug(.http, "script queue", .{
.ctx = ctx, .ctx = ctx,
.url = remote_url.?, .url = remote_url.?,
.element = element, .element = element,
.stack = page.js.stackTrace() catch "???", .stack = ls.local.stackTrace() catch "???",
}); });
} }
} }
@@ -357,11 +361,15 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers); try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
var ls: js.Local.Scope = undefined;
self.page.js.localScope(&ls);
defer ls.deinit();
log.debug(.http, "script queue", .{ log.debug(.http, "script queue", .{
.url = url, .url = url,
.ctx = "module", .ctx = "module",
.referrer = referrer, .referrer = referrer,
.stack = self.page.js.stackTrace() catch "???", .stack = ls.local.stackTrace() catch "???",
}); });
} }
@@ -448,11 +456,15 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers); try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
var ls: js.Local.Scope = undefined;
self.page.js.localScope(&ls);
defer ls.deinit();
log.debug(.http, "script queue", .{ log.debug(.http, "script queue", .{
.url = url, .url = url,
.ctx = "dynamic module", .ctx = "dynamic module",
.referrer = referrer, .referrer = referrer,
.stack = self.page.js.stackTrace() catch "???", .stack = ls.local.stackTrace() catch "???",
}); });
} }
@@ -785,6 +797,12 @@ pub const Script = struct {
.cacheable = cacheable, .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 // Handle importmap special case here: the content is a JSON containing
// imports. // imports.
if (self.kind == .importmap) { if (self.kind == .importmap) {
@@ -795,25 +813,24 @@ pub const Script = struct {
.kind = self.kind, .kind = self.kind,
.cacheable = cacheable, .cacheable = cacheable,
}); });
self.executeCallback("error", script_element._on_error, page); self.executeCallback("error", local.toLocal(script_element._on_error), page);
return; return;
}; };
self.executeCallback("load", script_element._on_load, page); self.executeCallback("load", local.toLocal(script_element._on_load), page);
return; return;
} }
const js_context = page.js;
var try_catch: js.TryCatch = undefined; var try_catch: js.TryCatch = undefined;
try_catch.init(js_context); try_catch.init(local);
defer try_catch.deinit(); defer try_catch.deinit();
const success = blk: { const success = blk: {
const content = self.source.content(); const content = self.source.content();
switch (self.kind) { switch (self.kind) {
.javascript => _ = js_context.eval(content, url) catch break :blk false, .javascript => _ = local.eval(content, url) catch break :blk false,
.module => { .module => {
// We don't care about waiting for the evaluation here. // 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. .importmap => unreachable, // handled before the try/catch.
} }
@@ -833,7 +850,7 @@ pub const Script = struct {
} }
if (success) { if (success) {
self.executeCallback("load", script_element._on_load, page); self.executeCallback("load", local.toLocal(script_element._on_load), page);
return; return;
} }
@@ -844,12 +861,11 @@ pub const Script = struct {
.cacheable = cacheable, .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 { fn executeCallback(self: *const Script, comptime typ: []const u8, cb_: ?js.Function, page: *Page) void {
const cb_global = cb_ orelse return; const cb = cb_ orelse return;
const cb = cb_global.local();
const Event = @import("webapi/Event.zig"); const Event = @import("webapi/Event.zig");
const event = Event.initTrusted(typ, .{}, page) catch |err| { const event = Event.initTrusted(typ, .{}, page) catch |err| {

View File

@@ -22,7 +22,7 @@ const v8 = js.v8;
const Array = @This(); const Array = @This();
ctx: *js.Context, local: *const js.Local,
handle: *const v8.Array, handle: *const v8.Array,
pub fn len(self: Array) usize { 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 { 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 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 error.JsException;
}; };
return .{ return .{
.ctx = self.ctx, .local = self.local,
.handle = handle, .handle = handle,
}; };
} }
pub fn set(self: Array, index: u32, value: anytype, comptime opts: js.bridge.Caller.CallOpts) !bool { pub fn set(self: Array, index: u32, value: anytype, comptime opts: js.Caller.CallOpts) !bool {
const ctx = self.ctx; const js_value = try self.local.zigValueToJs(value, opts);
const js_value = try ctx.zigValueToJs(value, opts);
var out: v8.MaybeBool = undefined; 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; return out.has_value;
} }
pub fn toObject(self: Array) js.Object { pub fn toObject(self: Array) js.Object {
return .{ return .{
.ctx = self.ctx, .local = self.local,
.handle = @ptrCast(self.handle), .handle = @ptrCast(self.handle),
}; };
} }
pub fn toValue(self: Array) js.Value { pub fn toValue(self: Array) js.Value {
return .{ return .{
.ctx = self.ctx, .local = self.local,
.handle = @ptrCast(self.handle), .handle = @ptrCast(self.handle),
}; };
} }

569
src/browser/js/Caller.zig Normal file
View File

@@ -0,0 +1,569 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../log.zig");
const 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 {
var hs: js.HandleScope = undefined;
hs.init(self.local.isolate);
defer hs.deinit();
const info = FunctionCallbackInfo{ .handle = handle };
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{ .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 {
var hs: js.HandleScope = undefined;
hs.init(self.local.isolate);
defer hs.deinit();
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 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 {
var hs: js.HandleScope = undefined;
hs.init(self.local.isolate);
defer hs.deinit();
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 {
var hs: js.HandleScope = undefined;
hs.init(self.local.isolate);
defer hs.deinit();
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 {
var hs: js.HandleScope = undefined;
hs.init(self.local.isolate);
defer hs.deinit();
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 {
var hs: js.HandleScope = undefined;
hs.init(self.local.isolate);
defer hs.deinit();
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 {
var hs: js.HandleScope = undefined;
hs.init(self.local.isolate);
defer hs.deinit();
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 };
}
fn isConstructCall(self: FunctionCallbackInfo) bool {
return v8.v8__FunctionCallbackInfo__IsConstructCall(self.handle);
}
};
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);
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -189,19 +189,27 @@ pub fn dumpMemoryStats(self: *Env) void {
fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void { fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void {
const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?; const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?;
const isolate_handle = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?; const v8_isolate = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
const js_isolate = js.Isolate{ .handle = isolate_handle }; const js_isolate = js.Isolate{ .handle = v8_isolate };
const context = Context.fromIsolate(js_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 = const value =
if (v8.v8__PromiseRejectMessage__GetValue(&message_handle)) |v8_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 else
"no value"; "no value";
log.debug(.js, "unhandled rejection", .{ log.debug(.js, "unhandled rejection", .{
.value = value, .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", .note = "This should be updated to call window.unhandledrejection",
}); });
} }

View File

@@ -53,7 +53,6 @@ context_arena: ArenaAllocator,
// does all the work, but having all page-specific data structures // does all the work, but having all page-specific data structures
// grouped together helps keep things clean. // grouped together helps keep things clean.
context: ?Context = null, context: ?Context = null,
persisted_context: ?js.Global(Context) = null,
// no init, must be initialized via env.newExecutionWorld() // no init, must be initialized via env.newExecutionWorld()
@@ -77,43 +76,41 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context
const isolate = env.isolate; const isolate = env.isolate;
const arena = self.context_arena.allocator(); const arena = self.context_arena.allocator();
const persisted_context: js.Global(Context) = blk: { var hs: js.HandleScope = undefined;
var temp_scope: js.HandleScope = undefined; hs.init(isolate);
temp_scope.init(isolate); defer hs.deinit();
defer temp_scope.deinit();
// Getting this into the snapshot is tricky (anything involving the // Getting this into the snapshot is tricky (anything involving the
// global is tricky). Easier to do here // global is tricky). Easier to do here
const global_template = @import("Snapshot.zig").createGlobalTemplate(isolate.handle, env.templates); const global_template = @import("Snapshot.zig").createGlobalTemplate(isolate.handle, env.templates);
v8.v8__ObjectTemplate__SetNamedHandler(global_template, &.{ v8.v8__ObjectTemplate__SetNamedHandler(global_template, &.{
.getter = bridge.unknownPropertyCallback, .getter = bridge.unknownPropertyCallback,
.setter = null, .setter = null,
.query = null, .query = null,
.deleter = null, .deleter = null,
.enumerator = null, .enumerator = null,
.definer = null, .definer = null,
.descriptor = null, .descriptor = null,
.data = null, .data = null,
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking, .flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
}); });
const context_handle = v8.v8__Context__New(isolate.handle, global_template, null).?; const v8_context = v8.v8__Context__New(isolate.handle, global_template, null).?;
break :blk js.Global(Context).init(isolate.handle, context_handle);
}; // Create the v8::Context and wrap it in a v8::Global
var context_global: v8.Global = undefined;
v8.v8__Global__New(isolate.handle, v8_context, &context_global);
// our window wrapped in a v8::Global
const global_obj = v8.v8__Context__Global(v8_context).?;
var global_global: v8.Global = undefined;
v8.v8__Global__New(isolate.handle, global_obj, &global_global);
// 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) { if (enter) {
handle_scope = @as(js.HandleScope, undefined);
handle_scope.?.init(isolate);
v8.v8__Context__Enter(v8_context); v8.v8__Context__Enter(v8_context);
} }
errdefer if (enter) { errdefer if (enter) {
v8.v8__Context__Exit(v8_context); v8.v8__Context__Exit(v8_context);
handle_scope.?.deinit();
}; };
const context_id = env.context_id; const context_id = env.context_id;
@@ -122,24 +119,24 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context
self.context = Context{ self.context = Context{
.page = page, .page = page,
.id = context_id, .id = context_id,
.entered = enter,
.isolate = isolate, .isolate = isolate,
.handle = v8_context, .handle = context_global,
.templates = env.templates, .templates = env.templates,
.handle_scope = handle_scope,
.script_manager = &page._script_manager, .script_manager = &page._script_manager,
.call_arena = page.call_arena, .call_arena = page.call_arena,
.arena = arena, .arena = arena,
}; };
self.persisted_context = persisted_context;
var context = &self.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 // Store a pointer to our context inside the v8 context so that, given
// a v8 context, we can get our context out // a v8 context, we can get our context out
const data = isolate.initBigInt(@intFromPtr(context)); const data = isolate.initBigInt(@intFromPtr(&self.context.?));
v8.v8__Context__SetEmbedderData(context.handle, 1, @ptrCast(data.handle)); v8.v8__Context__SetEmbedderData(v8_context, 1, @ptrCast(data.handle));
try context.setupGlobal(); return &self.context.?;
return context;
} }
pub fn removeContext(self: *ExecutionWorld) void { pub fn removeContext(self: *ExecutionWorld) void {
@@ -147,9 +144,6 @@ pub fn removeContext(self: *ExecutionWorld) void {
context.deinit(); context.deinit();
self.context = null; self.context = null;
self.persisted_context.?.deinit();
self.persisted_context = null;
self.env.isolate.notifyContextDisposed(); self.env.isolate.notifyContextDisposed();
_ = self.context_arena.reset(.{ .retain_with_limit = CONTEXT_ARENA_RETAIN }); _ = self.context_arena.reset(.{ .retain_with_limit = CONTEXT_ARENA_RETAIN });
} }

View File

@@ -24,7 +24,7 @@ const Allocator = std.mem.Allocator;
const Function = @This(); const Function = @This();
ctx: *js.Context, local: *const js.Local,
this: ?*const v8.Object = null, this: ?*const v8.Object = null,
handle: *const v8.Function, handle: *const v8.Function,
@@ -34,34 +34,35 @@ pub const Result = struct {
}; };
pub fn withThis(self: *const Function, value: anytype) !Function { pub fn withThis(self: *const Function, value: anytype) !Function {
const local = self.local;
const this_obj = if (@TypeOf(value) == js.Object) const this_obj = if (@TypeOf(value) == js.Object)
value.handle value.handle
else else
(try self.ctx.zigValueToJs(value, .{})).handle; (try local.zigValueToJs(value, .{})).handle;
return .{ return .{
.ctx = self.ctx, .local = local,
.this = this_obj, .this = this_obj,
.handle = self.handle, .handle = self.handle,
}; };
} }
pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Object { 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; var try_catch: js.TryCatch = undefined;
try_catch.init(ctx); try_catch.init(local);
defer try_catch.deinit(); defer try_catch.deinit();
// This creates a new instance using this Function as a constructor. // This creates a new instance using this Function as a constructor.
// const c_args = @as(?[*]const ?*c.Value, @ptrCast(&.{})); // const c_args = @as(?[*]const ?*c.Value, @ptrCast(&.{}));
const handle = v8.v8__Function__NewInstance(self.handle, ctx.handle, 0, null) orelse { const handle = v8.v8__Function__NewInstance(self.handle, local.handle, 0, null) orelse {
caught.* = try_catch.caughtOrError(ctx.call_arena, error.Unknown); caught.* = try_catch.caughtOrError(local.call_arena, error.Unknown);
return error.JsConstructorFailed; return error.JsConstructorFailed;
}; };
return .{ return .{
.ctx = ctx, .local = local,
.handle = handle, .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 { pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T {
var try_catch: js.TryCatch = undefined; var try_catch: js.TryCatch = undefined;
try_catch.init(self.ctx); try_catch.init(self.local);
defer try_catch.deinit(); defer try_catch.deinit();
return self.callWithThis(T, this, args) catch |err| { 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; return err;
}; };
} }
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T { 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 // When we're calling a function from within JavaScript itself, this isn't
// necessary. We're within a Caller instantiation, which will already have // 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 // 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 // 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. // will be reset after each statement of the function which executes Zig code.
const ctx = local.ctx;
const call_depth = ctx.call_depth; const call_depth = ctx.call_depth;
ctx.call_depth = call_depth + 1; ctx.call_depth = call_depth + 1;
defer ctx.call_depth = call_depth; 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) { if (@TypeOf(this) == js.Object) {
break :blk this; 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; 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; const fields = s.fields;
var js_args: [fields.len]*const v8.Value = undefined; var js_args: [fields.len]*const v8.Value = undefined;
inline for (fields, 0..) |f, i| { 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; const cargs: [fields.len]*const v8.Value = js_args;
break :blk &cargs; break :blk &cargs;
}, },
.pointer => blk: { .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| { for (args, 0..) |a, i| {
values[i] = (try ctx.zigValueToJs(a, .{})).handle; values[i] = (try local.zigValueToJs(a, .{})).handle;
} }
break :blk values; 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 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 "???"}); // std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"});
return error.JSExecCallback; return error.JSExecCallback;
}; };
@@ -140,13 +142,13 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
if (@typeInfo(T) == .void) { if (@typeInfo(T) == .void) {
return {}; return {};
} }
return ctx.jsValueToZig(T, .{ .ctx = ctx, .handle = handle }); return local.jsValueToZig(T, .{ .local = local, .handle = handle });
} }
fn getThis(self: *const Function) js.Object { 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 .{ return .{
.ctx = self.ctx, .local = self.local,
.handle = handle, .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 { pub fn getPropertyValue(self: *const Function, name: []const u8) !?js.Value {
const ctx = self.ctx; const local = self.local;
const key = ctx.isolate.initStringHandle(name); const key = local.isolate.initStringHandle(name);
const handle = v8.v8__Object__Get(self.handle, ctx.handle, key) orelse { const handle = v8.v8__Object__Get(self.handle, self.local.handle, key) orelse {
return error.JsException; return error.JsException;
}; };
return .{ return .{
.ctx = ctx, .local = local,
.handle = handle, .handle = handle,
}; };
} }
pub fn persist(self: *const Function) !Global { pub fn persist(self: *const Function) !Global {
var ctx = self.ctx; var ctx = self.local.ctx;
var global: v8.Global = undefined; var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
try ctx.global_functions.append(ctx.arena, global); try ctx.global_functions.append(ctx.arena, global);
return .{ .handle = global };
return .{
.handle = global,
.ctx = ctx,
};
} }
pub fn persistWithThis(self: *const Function, value: anytype) !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 { pub const Global = struct {
handle: v8.Global, handle: v8.Global,
ctx: *js.Context,
pub fn deinit(self: *Global) void { pub fn deinit(self: *Global) void {
v8.v8__Global__Reset(&self.handle); 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 .{ return .{
.ctx = self.ctx, .local = l,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)), .handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
}; };
} }

View File

@@ -20,7 +20,7 @@ const std = @import("std");
const js = @import("js.zig"); const js = @import("js.zig");
const v8 = js.v8; const v8 = js.v8;
const Context = @import("Context.zig"); const TaggedOpaque = @import("TaggedOpaque.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const RndGen = std.Random.DefaultPrng; const RndGen = std.Random.DefaultPrng;
@@ -36,7 +36,7 @@ client: Client,
channel: Channel, channel: Channel,
session: Session, session: Session,
rnd: RndGen = RndGen.init(0), rnd: RndGen = RndGen.init(0),
default_context: ?*const v8.Context = null, default_context: ?v8.Global,
// We expect allocator to be an arena // We expect allocator to be an arena
// Note: This initializes the pre-allocated inspector in-place // 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 { pub fn deinit(self: *const Inspector) void {
var temp_scope: v8.HandleScope = undefined; var hs: v8.HandleScope = undefined;
v8.v8__HandleScope__CONSTRUCT(&temp_scope, self.isolate); v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate);
defer v8.v8__HandleScope__DESTRUCT(&temp_scope); defer v8.v8__HandleScope__DESTRUCT(&hs);
self.session.deinit(); self.session.deinit();
self.client.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 // - is_default_context: Whether the execution context is default, should match the auxData
pub fn contextCreated( pub fn contextCreated(
self: *Inspector, self: *Inspector,
context: *const Context, local: *const js.Local,
name: []const u8, name: []const u8,
origin: []const u8, origin: []const u8,
aux_data: []const u8, aux_data: []const u8,
@@ -143,11 +143,11 @@ pub fn contextCreated(
aux_data.ptr, aux_data.ptr,
aux_data.len, aux_data.len,
CONTEXT_GROUP_ID, CONTEXT_GROUP_ID,
context.handle, local.handle,
); );
if (is_default_context) { if (is_default_context) {
self.default_context = context.handle; self.default_context = local.ctx.handle;
} }
} }
@@ -158,41 +158,42 @@ pub fn contextCreated(
// we'll create it and track it for cleanup when the context ends. // we'll create it and track it for cleanup when the context ends.
pub fn getRemoteObject( pub fn getRemoteObject(
self: *const Inspector, self: *const Inspector,
context: *Context, local: *const js.Local,
group: []const u8, group: []const u8,
value: anytype, value: anytype,
) !RemoteObject { ) !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 // We do not want to expose this as a parameter for now
const generate_preview = false; const generate_preview = false;
return self.session.wrapObject( return self.session.wrapObject(
context.isolate.handle, local.isolate.handle,
context.handle, local.handle,
js_value.handle, js_val.handle,
group, group,
generate_preview, generate_preview,
); );
} }
// Gets a value by object ID regardless of which context it is in. // Gets a value by object ID regardless of which context it is in.
// Our TaggedAnyOpaque stores the "resolved" ptr value (the most specific _type, // Our TaggedOpaque stores the "resolved" ptr value (the most specific _type,
// e.g. we store the ptr to the Div not the EventTarget). But, this is asking for // e.g. we store the ptr to the Div not the EventTarget). But, this is asking for
// the pointer to the Node, so we need to use the same resolution mechanism which // the pointer to the Node, so we need to use the same resolution mechanism which
// is used when we're calling a function to turn the Div into a Node, which is // is used when we're calling a function to turn the Div into a Node, which is
// what Context.typeTaggedAnyOpaque does. // what TaggedOpaque.fromJS does.
pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []const u8) !*anyopaque { pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []const u8, local: *js.Local) !*anyopaque {
// just to indicate that the caller is responsible for ensure there's a local environment
_ = local;
const unwrapped = try self.session.unwrapObject(allocator, object_id); const unwrapped = try self.session.unwrapObject(allocator, object_id);
// The values context and groupId are not used here // The values context and groupId are not used here
const js_val = unwrapped.value; const js_val = unwrapped.value;
if (!v8.v8__Value__IsObject(js_val)) { if (!v8.v8__Value__IsObject(js_val)) {
return error.ObjectIdIsNotANode; return error.ObjectIdIsNotANode;
} }
const Node = @import("../webapi/Node.zig"); const Node = @import("../webapi/Node.zig");
// Cast to *const v8.Object for typeTaggedAnyOpaque // Cast to *const v8.Object for typeTaggedAnyOpaque
return Context.typeTaggedAnyOpaque(*Node, @ptrCast(js_val)) catch { return TaggedOpaque.fromJS(*Node, @ptrCast(js_val)) catch return error.ObjectIdIsNotANode;
return error.ObjectIdIsNotANode;
};
} }
pub const RemoteObject = struct { pub const RemoteObject = struct {
@@ -399,7 +400,7 @@ fn fromData(data: *anyopaque) *Inspector {
return @ptrCast(@alignCast(data)); 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)) { if (!v8.v8__Value__IsObject(value)) {
return null; return null;
} }
@@ -469,7 +470,8 @@ pub export fn v8_inspector__Client__IMPL__ensureDefaultContextInGroup(
data: *anyopaque, data: *anyopaque,
) callconv(.c) ?*const v8.Context { ) callconv(.c) ?*const v8.Context {
const inspector: *Inspector = @ptrCast(@alignCast(data)); 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( pub export fn v8_inspector__Channel__IMPL__sendResponse(

1326
src/browser/js/Local.zig Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@ const v8 = js.v8;
const Module = @This(); const Module = @This();
ctx: *js.Context, local: *const js.Local,
handle: *const v8.Module, handle: *const v8.Module,
pub const Status = enum(u32) { pub const Status = enum(u32) {
@@ -39,21 +39,21 @@ pub fn getStatus(self: Module) Status {
pub fn getException(self: Module) js.Value { pub fn getException(self: Module) js.Value {
return .{ return .{
.ctx = self.ctx, .local = self.local,
.handle = v8.v8__Module__GetException(self.handle).?, .handle = v8.v8__Module__GetException(self.handle).?,
}; };
} }
pub fn getModuleRequests(self: Module) Requests { pub fn getModuleRequests(self: Module) Requests {
return .{ return .{
.ctx = self.ctx.handle, .context_handle = self.local.handle,
.handle = v8.v8__Module__GetModuleRequests(self.handle).?, .handle = v8.v8__Module__GetModuleRequests(self.handle).?,
}; };
} }
pub fn instantiate(self: Module, cb: v8.ResolveModuleCallback) !bool { pub fn instantiate(self: Module, cb: v8.ResolveModuleCallback) !bool {
var out: v8.MaybeBool = undefined; 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) { if (out.has_value) {
return out.value; return out.value;
} }
@@ -61,15 +61,14 @@ pub fn instantiate(self: Module, cb: v8.ResolveModuleCallback) !bool {
} }
pub fn evaluate(self: Module) !js.Value { pub fn evaluate(self: Module) !js.Value {
const ctx = self.ctx; const res = v8.v8__Module__Evaluate(self.handle, self.local.handle) orelse return error.JsException;
const res = v8.v8__Module__Evaluate(self.handle, ctx.handle) orelse return error.JsException;
if (self.getStatus() == .kErrored) { if (self.getStatus() == .kErrored) {
return error.JsException; return error.JsException;
} }
return .{ return .{
.ctx = ctx, .local = self.local,
.handle = res, .handle = res,
}; };
} }
@@ -80,7 +79,7 @@ pub fn getIdentityHash(self: Module) u32 {
pub fn getModuleNamespace(self: Module) js.Value { pub fn getModuleNamespace(self: Module) js.Value {
return .{ return .{
.ctx = self.ctx, .local = self.local,
.handle = v8.v8__Module__GetModuleNamespace(self.handle).?, .handle = v8.v8__Module__GetModuleNamespace(self.handle).?,
}; };
} }
@@ -90,28 +89,24 @@ pub fn getScriptId(self: Module) u32 {
} }
pub fn persist(self: Module) !Global { pub fn persist(self: Module) !Global {
var ctx = self.ctx; var ctx = self.local.ctx;
var global: v8.Global = undefined; var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
try ctx.global_modules.append(ctx.arena, global); try ctx.global_modules.append(ctx.arena, global);
return .{ return .{ .handle = global };
.handle = global,
.ctx = ctx,
};
} }
pub const Global = struct { pub const Global = struct {
handle: v8.Global, handle: v8.Global,
ctx: *js.Context,
pub fn deinit(self: *Global) void { pub fn deinit(self: *Global) void {
v8.v8__Global__Reset(&self.handle); 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 .{ return .{
.ctx = self.ctx, .local = l,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)), .handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
}; };
} }
@@ -121,15 +116,15 @@ pub const Global = struct {
}; };
const Requests = struct { const Requests = struct {
ctx: *const v8.Context,
handle: *const v8.FixedArray, handle: *const v8.FixedArray,
context_handle: *const v8.Context,
pub fn len(self: Requests) usize { pub fn len(self: Requests) usize {
return @intCast(v8.v8__FixedArray__Length(self.handle)); return @intCast(v8.v8__FixedArray__Length(self.handle));
} }
pub fn get(self: Requests, idx: usize) Request { 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)).? };
} }
}; };

View File

@@ -28,7 +28,7 @@ const Allocator = std.mem.Allocator;
const Object = @This(); const Object = @This();
ctx: *js.Context, local: *const js.Local,
handle: *const v8.Object, handle: *const v8.Object,
pub fn getId(self: Object) u32 { pub fn getId(self: Object) u32 {
@@ -36,11 +36,11 @@ pub fn getId(self: Object) u32 {
} }
pub fn has(self: Object, key: anytype) bool { 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); const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
var out: v8.MaybeBool = undefined; 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) { if (out.has_value) {
return out.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 { 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 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 .{ return .{
.ctx = ctx, .local = self.local,
.handle = js_val_handle, .handle = js_val_handle,
}; };
} }
pub fn set(self: Object, key: anytype, value: anytype, comptime opts: js.bridge.Caller.CallOpts) !bool { pub fn set(self: Object, key: anytype, value: anytype, comptime opts: js.Caller.CallOpts) !bool {
const ctx = self.ctx; 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); const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
var out: v8.MaybeBool = undefined; 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; return out.has_value;
} }
pub fn defineOwnProperty(self: Object, name: []const u8, value: js.Value, attr: v8.PropertyAttribute) ?bool { 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); const name_handle = ctx.isolate.initStringHandle(name);
var out: v8.MaybeBool = undefined; 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) { if (out.has_value) {
return out.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 { 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 { pub fn toValue(self: Object) js.Value {
return .{ return .{
.ctx = self.ctx, .local = self.local,
.handle = @ptrCast(self.handle), .handle = @ptrCast(self.handle),
}; };
} }
pub fn format(self: Object, writer: *std.Io.Writer) !void { pub fn format(self: Object, writer: *std.Io.Writer) !void {
if (comptime IS_DEBUG) { 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; const str = self.toString() catch return error.WriteFailed;
return writer.writeAll(str); return writer.writeAll(str);
} }
pub fn persist(self: Object) !Global { pub fn persist(self: Object) !Global {
var ctx = self.ctx; var ctx = self.local.ctx;
var global: v8.Global = undefined; var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
try ctx.global_objects.append(ctx.arena, global); try ctx.global_objects.append(ctx.arena, global);
return .{ return .{ .handle = global };
.handle = global,
.ctx = ctx,
};
} }
pub fn getFunction(self: Object, name: []const u8) !?js.Function { pub fn getFunction(self: Object, name: []const u8) !?js.Function {
if (self.isNullOrUndefined()) { if (self.isNullOrUndefined()) {
return null; return null;
} }
const ctx = self.ctx; const local = self.local;
const js_name = ctx.isolate.initStringHandle(name); const js_name = local.isolate.initStringHandle(name);
const js_val_handle = v8.v8__Object__Get(self.handle, ctx.handle, js_name) orelse return error.JsException; 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) { if (v8.v8__Value__IsFunction(js_val_handle) == false) {
return null; return null;
} }
return .{ return .{
.ctx = ctx, .local = local,
.handle = @ptrCast(js_val_handle), .handle = @ptrCast(js_val_handle),
}; };
} }
@@ -145,51 +142,48 @@ pub fn isNullOrUndefined(self: Object) bool {
} }
pub fn getOwnPropertyNames(self: Object) js.Array { 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 .{ return .{
.ctx = self.ctx, .local = self.local,
.handle = handle, .handle = handle,
}; };
} }
pub fn getPropertyNames(self: Object) js.Array { 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 .{ return .{
.ctx = self.ctx, .local = self.local,
.handle = handle, .handle = handle,
}; };
} }
pub fn nameIterator(self: Object) NameIterator { pub fn nameIterator(self: Object) NameIterator {
const ctx = self.ctx; const handle = v8.v8__Object__GetPropertyNames(self.handle, self.local.handle).?;
const handle = v8.v8__Object__GetPropertyNames(self.handle, ctx.handle).?;
const count = v8.v8__Array__Length(handle); const count = v8.v8__Array__Length(handle);
return .{ return .{
.ctx = ctx, .local = self.local,
.handle = handle, .handle = handle,
.count = count, .count = count,
}; };
} }
pub fn toZig(self: Object, comptime T: type) !T { pub fn toZig(self: Object, comptime T: type) !T {
const js_value = js.Value{ .ctx = self.ctx, .handle = @ptrCast(self.handle) }; const js_value = js.Value{ .local = self.local, .handle = @ptrCast(self.handle) };
return self.ctx.jsValueToZig(T, js_value); return self.local.jsValueToZig(T, js_value);
} }
pub const Global = struct { pub const Global = struct {
handle: v8.Global, handle: v8.Global,
ctx: *js.Context,
pub fn deinit(self: *Global) void { pub fn deinit(self: *Global) void {
v8.v8__Global__Reset(&self.handle); 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 .{ return .{
.ctx = self.ctx, .local = l,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)), .handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
}; };
} }
@@ -201,7 +195,7 @@ pub const Global = struct {
pub const NameIterator = struct { pub const NameIterator = struct {
count: u32, count: u32,
idx: u32 = 0, idx: u32 = 0,
ctx: *Context, local: *const js.Local,
handle: *const v8.Array, handle: *const v8.Array,
pub fn next(self: *NameIterator) !?[]const u8 { pub fn next(self: *NameIterator) !?[]const u8 {
@@ -211,8 +205,8 @@ pub const NameIterator = struct {
} }
self.idx += 1; 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_handle = v8.v8__Object__GetIndex(@ptrCast(self.handle), self.local.handle, idx) orelse return error.JsException;
const js_val = js.Value{ .ctx = self.ctx, .handle = js_val_handle }; const js_val = js.Value{ .local = self.local, .handle = js_val_handle };
return try self.ctx.valueToString(js_val, .{}); return try self.local.valueToString(js_val, .{});
} }
}; };

View File

@@ -21,63 +21,51 @@ const v8 = js.v8;
const Promise = @This(); const Promise = @This();
ctx: *js.Context, local: *const js.Local,
handle: *const v8.Promise, handle: *const v8.Promise,
pub fn toObject(self: Promise) js.Object { pub fn toObject(self: Promise) js.Object {
return .{ return .{
.ctx = self.ctx, .local = self.local,
.handle = @ptrCast(self.handle), .handle = @ptrCast(self.handle),
}; };
} }
pub fn toValue(self: Promise) js.Value { pub fn toValue(self: Promise) js.Value {
return .{ return .{
.ctx = self.ctx, .local = self.local,
.handle = @ptrCast(self.handle), .handle = @ptrCast(self.handle),
}; };
} }
pub fn thenAndCatch(self: Promise, on_fulfilled: js.Function, on_rejected: js.Function) !Promise { 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 .{ return .{
.ctx = self.ctx, .local = self.local,
.handle = handle, .handle = handle,
}; };
} }
return error.PromiseChainFailed; return error.PromiseChainFailed;
} }
pub fn persist(self: Promise) !Global { pub fn persist(self: Promise) !Global {
var ctx = self.ctx; var ctx = self.local.ctx;
var global: v8.Global = undefined; var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
try ctx.global_promises.append(ctx.arena, global); try ctx.global_promises.append(ctx.arena, global);
return .{ return .{ .handle = global };
.handle = global,
.ctx = ctx,
};
} }
pub const Global = struct { pub const Global = struct {
handle: v8.Global, handle: v8.Global,
ctx: *js.Context,
pub fn deinit(self: *Global) void { pub fn deinit(self: *Global) void {
v8.v8__Global__Reset(&self.handle); 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 .{ return .{
.ctx = self.ctx, .local = l,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)), .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();
}
}; };

View File

@@ -22,19 +22,19 @@ const log = @import("../../log.zig");
const PromiseResolver = @This(); const PromiseResolver = @This();
ctx: *js.Context, local: *const js.Local,
handle: *const v8.PromiseResolver, handle: *const v8.PromiseResolver,
pub fn init(ctx: *js.Context) PromiseResolver { pub fn init(local: *const js.Local) PromiseResolver {
return .{ return .{
.ctx = ctx, .local = local,
.handle = v8.v8__Promise__Resolver__New(ctx.handle).?, .handle = v8.v8__Promise__Resolver__New(local.handle).?,
}; };
} }
pub fn promise(self: PromiseResolver) js.Promise { pub fn promise(self: PromiseResolver) js.Promise {
return .{ return .{
.ctx = self.ctx, .local = self.local,
.handle = v8.v8__Promise__Resolver__GetPromise(self.handle).?, .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 { fn _resolve(self: PromiseResolver, value: anytype) !void {
const ctx: *js.Context = @constCast(self.ctx); const local = self.local;
const js_value = try ctx.zigValueToJs(value, .{}); const js_val = try local.zigValueToJs(value, .{});
var out: v8.MaybeBool = undefined; 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) { if (!out.has_value or !out.value) {
return error.FailedToResolvePromise; return error.FailedToResolvePromise;
} }
ctx.runMicrotasks(); local.ctx.runMicrotasks();
} }
pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype) void { 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 { fn _reject(self: PromiseResolver, value: anytype) !void {
const ctx = self.ctx; const local = self.local;
const js_value = try ctx.zigValueToJs(value, .{}); const js_val = try local.zigValueToJs(value, .{});
var out: v8.MaybeBool = undefined; 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) { if (!out.has_value or !out.value) {
return error.FailedToRejectPromise; return error.FailedToRejectPromise;
} }
ctx.runMicrotasks(); local.ctx.runMicrotasks();
} }
pub fn persist(self: PromiseResolver) !Global { pub fn persist(self: PromiseResolver) !Global {
var ctx = self.ctx; var ctx = self.local.ctx;
var global: v8.Global = undefined; var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
try ctx.global_promise_resolvers.append(ctx.arena, global); try ctx.global_promise_resolvers.append(ctx.arena, global);
return .{ return .{ .handle = global };
.handle = global,
.ctx = ctx,
};
} }
pub const Global = struct { pub const Global = struct {
handle: v8.Global, handle: v8.Global,
ctx: *js.Context,
pub fn deinit(self: *Global) void { pub fn deinit(self: *Global) void {
v8.v8__Global__Reset(&self.handle); 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 .{ return .{
.ctx = self.ctx, .local = l,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)), .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);
}
}; };

View File

@@ -25,7 +25,7 @@ const v8 = js.v8;
const String = @This(); const String = @This();
ctx: *js.Context, local: *const js.Local,
handle: *const v8.String, handle: *const v8.String,
pub const ToZigOpts = struct { 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) { fn _toZig(self: String, comptime null_terminate: bool, opts: ToZigOpts) !(if (null_terminate) [:0]u8 else []u8) {
const isolate = self.ctx.isolate.handle; const isolate = self.local.isolate.handle;
const allocator = opts.allocator orelse self.ctx.call_arena; const allocator = opts.allocator orelse self.local.ctx.call_arena;
const len: u32 = @intCast(v8.v8__String__Utf8Length(self.handle, isolate)); 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); const buf = if (null_terminate) try allocator.allocSentinel(u8, len, 0) else try allocator.alloc(u8, len);

View File

@@ -0,0 +1,156 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const 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;
}

View File

@@ -24,12 +24,12 @@ const Allocator = std.mem.Allocator;
const TryCatch = @This(); const TryCatch = @This();
ctx: *js.Context,
handle: v8.TryCatch, handle: v8.TryCatch,
local: *const js.Local,
pub fn init(self: *TryCatch, ctx: *js.Context) void { pub fn init(self: *TryCatch, l: *const js.Local) void {
self.ctx = ctx; self.local = l;
v8.v8__TryCatch__CONSTRUCT(&self.handle, ctx.isolate.handle); v8.v8__TryCatch__CONSTRUCT(&self.handle, l.isolate.handle);
} }
pub fn hasCaught(self: TryCatch) bool { pub fn hasCaught(self: TryCatch) bool {
@@ -41,26 +41,21 @@ pub fn caught(self: TryCatch, allocator: Allocator) ?Caught {
return null; return null;
} }
const ctx = self.ctx; const l = self.local;
var hs: js.HandleScope = undefined;
hs.init(ctx.isolate);
defer hs.deinit();
const line: ?u32 = blk: { const line: ?u32 = blk: {
const handle = v8.v8__TryCatch__Message(&self.handle) orelse return null; const handle = v8.v8__TryCatch__Message(&self.handle) orelse return null;
const l = v8.v8__Message__GetLineNumber(handle, ctx.handle); const line = v8.v8__Message__GetLineNumber(handle, l.handle);
break :blk if (l < 0) null else @intCast(l); break :blk if (line < 0) null else @intCast(line);
}; };
const exception: ?[]const u8 = blk: { const exception: ?[]const u8 = blk: {
const handle = v8.v8__TryCatch__Exception(&self.handle) orelse break :blk null; 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 stack: ?[]const u8 = blk: {
const handle = v8.v8__TryCatch__StackTrace(&self.handle, ctx.handle) orelse break :blk null; const handle = v8.v8__TryCatch__StackTrace(&self.handle, l.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);
}; };
return .{ return .{

View File

@@ -27,7 +27,7 @@ const Allocator = std.mem.Allocator;
const Value = @This(); const Value = @This();
ctx: *js.Context, local: *const js.Local,
handle: *const v8.Value, handle: *const v8.Value,
pub fn isObject(self: Value) bool { pub fn isObject(self: Value) bool {
@@ -155,12 +155,12 @@ pub fn isPromise(self: Value) bool {
} }
pub fn toBool(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 { pub fn typeOf(self: Value) js.String {
const str_handle = v8.v8__Value__TypeOf(self.handle, self.ctx.isolate.handle).?; const str_handle = v8.v8__Value__TypeOf(self.handle, self.local.isolate.handle).?;
return js.String{ .ctx = self.ctx, .handle = str_handle }; return js.String{ .local = self.local, .handle = str_handle };
} }
pub fn toF32(self: Value) !f32 { pub fn toF32(self: Value) !f32 {
@@ -169,7 +169,7 @@ pub fn toF32(self: Value) !f32 {
pub fn toF64(self: Value) !f64 { pub fn toF64(self: Value) !f64 {
var maybe: v8.MaybeF64 = undefined; 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) { if (!maybe.has_value) {
return error.JsException; return error.JsException;
} }
@@ -178,7 +178,7 @@ pub fn toF64(self: Value) !f64 {
pub fn toI32(self: Value) !i32 { pub fn toI32(self: Value) !i32 {
var maybe: v8.MaybeI32 = undefined; 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) { if (!maybe.has_value) {
return error.JsException; return error.JsException;
} }
@@ -187,7 +187,7 @@ pub fn toI32(self: Value) !i32 {
pub fn toU32(self: Value) !u32 { pub fn toU32(self: Value) !u32 {
var maybe: v8.MaybeU32 = undefined; 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) { if (!maybe.has_value) {
return error.JsException; return error.JsException;
} }
@@ -199,7 +199,7 @@ pub fn toPromise(self: Value) js.Promise {
std.debug.assert(self.isPromise()); std.debug.assert(self.isPromise());
} }
return .{ return .{
.ctx = self.ctx, .local = self.local,
.handle = @ptrCast(self.handle), .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 { 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; const json_str_handle = v8.v8__JSON__Stringify(self.local.handle, self.handle, null) orelse return error.JsException;
return self.ctx.jsStringToZig(json_str_handle, .{ .allocator = allocator }); 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) { 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()) { if (self.isSymbol()) {
const sym_handle = v8.v8__Symbol__Description(@ptrCast(self.handle), ctx.isolate.handle).?; const sym_handle = v8.v8__Symbol__Description(@ptrCast(self.handle), l.isolate.handle).?;
return _toString(.{ .handle = @ptrCast(sym_handle), .ctx = ctx }, null_terminate, opts); 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; 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) { if (comptime null_terminate) {
return js.String.toZigZ(str, opts); return js.String.toZigZ(str, opts);
} }
return js.String.toZig(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 { pub fn persist(self: Value) !Global {
var ctx = self.ctx; var ctx = self.local.ctx;
var global: v8.Global = undefined; var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
try ctx.global_values.append(ctx.arena, global); try ctx.global_values.append(ctx.arena, global);
return .{ return .{ .handle = global };
.handle = global,
.ctx = ctx,
};
} }
pub fn toZig(self: Value, comptime T: type) !T { 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 { pub fn toObject(self: Value) js.Object {
@@ -267,7 +256,7 @@ pub fn toObject(self: Value) js.Object {
} }
return .{ return .{
.ctx = self.ctx, .local = self.local,
.handle = @ptrCast(self.handle), .handle = @ptrCast(self.handle),
}; };
} }
@@ -278,7 +267,7 @@ pub fn toArray(self: Value) js.Array {
} }
return .{ return .{
.ctx = self.ctx, .local = self.local,
.handle = @ptrCast(self.handle), .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 { pub fn format(self: Value, writer: *std.Io.Writer) !void {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
return self.ctx.debugValue(self, writer); return self.local.debugValue(self, writer);
} }
const str = self.toString(.{}) catch return error.WriteFailed; const str = self.toString(.{}) catch return error.WriteFailed;
return writer.writeAll(str); return writer.writeAll(str);
@@ -303,16 +292,15 @@ pub fn format(self: Value, writer: *std.Io.Writer) !void {
pub const Global = struct { pub const Global = struct {
handle: v8.Global, handle: v8.Global,
ctx: *js.Context,
pub fn deinit(self: *Global) void { pub fn deinit(self: *Global) void {
v8.v8__Global__Reset(&self.handle); 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 .{ return .{
.ctx = self.ctx, .local = l,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)), .handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
}; };
} }

View File

@@ -20,555 +20,15 @@ const std = @import("std");
const js = @import("js.zig"); const js = @import("js.zig");
const lp = @import("lightpanda"); const lp = @import("lightpanda");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const Page = @import("../Page.zig");
const v8 = js.v8; const v8 = js.v8;
const Caller = @import("Caller.zig");
const Context = @import("Context.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; 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 { pub fn Builder(comptime T: type) type {
return struct { return struct {
pub const @"type" = T; pub const @"type" = T;
@@ -610,8 +70,9 @@ pub fn Builder(comptime T: type) type {
@compileError("Property for " ++ @typeName(@TypeOf(value)) ++ " hasn't been defined yet"); @compileError("Property for " ++ @typeName(@TypeOf(value)) ++ " hasn't been defined yet");
} }
pub fn prototypeChain() [prototypeChainLength(T)]js.PrototypeChainEntry { const PrototypeChainEntry = @import("TaggedOpaque.zig").PrototypeChainEntry;
var entries: [prototypeChainLength(T)]js.PrototypeChainEntry = undefined; pub fn prototypeChain() [prototypeChainLength(T)]PrototypeChainEntry {
var entries: [prototypeChainLength(T)]PrototypeChainEntry = undefined;
entries[0] = .{ .offset = 0, .index = JsApiLookup.getId(T.JsApi) }; entries[0] = .{ .offset = 0, .index = JsApiLookup.getId(T.JsApi) };
@@ -644,11 +105,11 @@ pub const Constructor = struct {
return .{ .func = struct { return .{ .func = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?; 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(); defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? }; caller.constructor(T, func, handle.?, .{
caller.constructor(T, func, info, .{
.dom_exception = opts.dom_exception, .dom_exception = opts.dom_exception,
}); });
} }
@@ -673,18 +134,18 @@ pub const Function = struct {
.func = struct { .func = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?; 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(); defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
if (comptime opts.static) { if (comptime opts.static) {
caller.function(T, func, info, .{ caller.function(T, func, handle.?, .{
.dom_exception = opts.dom_exception, .dom_exception = opts.dom_exception,
.as_typed_array = opts.as_typed_array, .as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined, .null_as_undefined = opts.null_as_undefined,
}); });
} else { } else {
caller.method(T, func, info, .{ caller.method(T, func, handle.?, .{
.dom_exception = opts.dom_exception, .dom_exception = opts.dom_exception,
.as_typed_array = opts.as_typed_array, .as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined, .null_as_undefined = opts.null_as_undefined,
@@ -717,17 +178,17 @@ pub const Accessor = struct {
accessor.getter = struct { accessor.getter = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?; 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(); defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
if (comptime opts.static) { if (comptime opts.static) {
caller.function(T, getter, info, .{ caller.function(T, getter, handle.?, .{
.as_typed_array = opts.as_typed_array, .as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined, .null_as_undefined = opts.null_as_undefined,
}); });
} else { } else {
caller.method(T, getter, info, .{ caller.method(T, getter, handle.?, .{
.as_typed_array = opts.as_typed_array, .as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined, .null_as_undefined = opts.null_as_undefined,
}); });
@@ -740,15 +201,11 @@ pub const Accessor = struct {
accessor.setter = struct { accessor.setter = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?; 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(); defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? }; caller.method(T, setter, handle.?, .{
if (comptime IS_DEBUG) {
lp.assert(info.length() == 1, "bridge.setter", .{ .len = info.length() });
}
caller.method(T, setter, info, .{
.as_typed_array = opts.as_typed_array, .as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined, .null_as_undefined = opts.null_as_undefined,
}); });
@@ -772,11 +229,11 @@ pub const Indexed = struct {
return .{ .getter = struct { return .{ .getter = struct {
fn wrap(idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 { fn wrap(idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; 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(); defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? }; return caller.getIndex(T, getter, idx, handle.?, .{
return caller.getIndex(T, getter, idx, info, .{
.as_typed_array = opts.as_typed_array, .as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined, .null_as_undefined = opts.null_as_undefined,
}); });
@@ -799,11 +256,11 @@ pub const NamedIndexed = struct {
const getter_fn = struct { const getter_fn = struct {
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 { fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; 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(); defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? }; return caller.getNamedIndex(T, getter, c_name.?, handle.?, .{
return caller.getNamedIndex(T, getter, .{ .handle = c_name.? }, info, .{
.as_typed_array = opts.as_typed_array, .as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined, .null_as_undefined = opts.null_as_undefined,
}); });
@@ -813,11 +270,11 @@ pub const NamedIndexed = struct {
const setter_fn = if (@typeInfo(@TypeOf(setter)) == .null) null else 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 { 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).?; 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(); defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? }; return caller.setNamedIndex(T, setter, c_name.?, c_value.?, handle.?, .{
return caller.setNamedIndex(T, setter, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info, .{
.as_typed_array = opts.as_typed_array, .as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined, .null_as_undefined = opts.null_as_undefined,
}); });
@@ -827,11 +284,11 @@ pub const NamedIndexed = struct {
const deleter_fn = if (@typeInfo(@TypeOf(deleter)) == .null) null else 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 { fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; 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(); defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? }; return caller.deleteNamedIndex(T, deleter, c_name.?, handle.?, .{
return caller.deleteNamedIndex(T, deleter, .{ .handle = c_name.? }, info, .{
.as_typed_array = opts.as_typed_array, .as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined, .null_as_undefined = opts.null_as_undefined,
}); });
@@ -861,7 +318,7 @@ pub const Iterator = struct {
.async = opts.async, .async = opts.async,
.func = struct { .func = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { 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()); info.getReturnValue().set(info.getThis());
} }
}.wrap, }.wrap,
@@ -873,11 +330,10 @@ pub const Iterator = struct {
.func = struct { .func = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?; 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(); defer caller.deinit();
caller.method(T, struct_or_func, handle.?, .{});
const info = FunctionCallbackInfo{ .handle = handle.? };
caller.method(T, struct_or_func, info, .{});
} }
}.wrap, }.wrap,
}; };
@@ -895,11 +351,11 @@ pub const Callable = struct {
return .{ .func = struct { return .{ .func = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?; 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(); defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? }; caller.method(T, func, handle.?, .{
caller.method(T, func, info, .{
.null_as_undefined = opts.null_as_undefined, .null_as_undefined = opts.null_as_undefined,
}); });
} }
@@ -912,52 +368,57 @@ pub const Property = union(enum) {
}; };
pub fn unknownPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 { pub fn unknownPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const isolate_handle = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
const context = Context.fromIsolate(.{ .handle = isolate_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;
var hs: js.HandleScope = undefined;
hs.init(local.isolate);
defer hs.deinit();
const property: []const u8 = local.valueHandleToString(@ptrCast(c_name.?), .{}) catch {
return 0; return 0;
}; };
const ignored = std.StaticStringMap(void).initComptime(.{ const page = local.ctx.page;
.{ "process", {} }, const document = page.document;
.{ "ShadyDOM", {} },
.{ "ShadyCSS", {} },
.{ "litNonce", {} }, if (document.getElementById(property, page)) |el| {
.{ "litHtmlVersions", {} }, const js_val = local.zigValueToJs(el, .{}) catch return 0;
.{ "litElementVersions", {} }, var pc = Caller.PropertyCallbackInfo{ .handle = handle.? };
.{ "litHtmlPolyfillSupport", {} }, pc.getReturnValue().set(js_val);
.{ "litElementHydrateSupport", {} }, return 1;
.{ "litElementPolyfillSupport", {} }, }
.{ "reactiveElementVersions", {} },
.{ "recaptcha", {} }, if (comptime IS_DEBUG) {
.{ "grecaptcha", {} }, const ignored = std.StaticStringMap(void).initComptime(.{
.{ "___grecaptcha_cfg", {} }, .{ "process", {} },
.{ "__recaptcha_api", {} }, .{ "ShadyDOM", {} },
.{ "__google_recaptcha_client", {} }, .{ "ShadyCSS", {} },
.{ "CLOSURE_FLAGS", {} }, .{ "litNonce", {} },
}); .{ "litHtmlVersions", {} },
.{ "litElementVersions", {} },
.{ "litHtmlPolyfillSupport", {} },
.{ "litElementHydrateSupport", {} },
.{ "litElementPolyfillSupport", {} },
.{ "reactiveElementVersions", {} },
if (!ignored.has(property)) { .{ "recaptcha", {} },
const page = context.page; .{ "grecaptcha", {} },
const document = page.document; .{ "___grecaptcha_cfg", {} },
.{ "__recaptcha_api", {} },
.{ "__google_recaptcha_client", {} },
if (document.getElementById(property, page)) |el| { .{ "CLOSURE_FLAGS", {} },
const js_value = context.zigValueToJs(el, .{}) catch { });
return 0; if (!ignored.has(property)) {
};
var pc = PropertyCallbackInfo{ .handle = handle.? };
pc.getReturnValue().set(js_value);
return 1;
}
if (comptime IS_DEBUG) {
log.debug(.unknown_prop, "unknown global property", .{ log.debug(.unknown_prop, "unknown global property", .{
.info = "but the property can exist in pure JS", .info = "but the property can exist in pure JS",
.stack = context.stackTrace() catch "???", .stack = local.stackTrace() catch "???",
.property = property, .property = property,
}); });
} }

View File

@@ -1,48 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const 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))));
}
};
}

View File

@@ -24,7 +24,9 @@ const log = @import("../../log.zig");
pub const Env = @import("Env.zig"); pub const Env = @import("Env.zig");
pub const bridge = @import("bridge.zig"); pub const bridge = @import("bridge.zig");
pub const ExecutionWorld = @import("ExecutionWorld.zig"); pub const ExecutionWorld = @import("ExecutionWorld.zig");
pub const Caller = @import("Caller.zig");
pub const Context = @import("Context.zig"); pub const Context = @import("Context.zig");
pub const Local = @import("Local.zig");
pub const Inspector = @import("Inspector.zig"); pub const Inspector = @import("Inspector.zig");
pub const Snapshot = @import("Snapshot.zig"); pub const Snapshot = @import("Snapshot.zig");
pub const Platform = @import("Platform.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 BigInt = @import("BigInt.zig");
pub const Number = @import("Number.zig"); pub const Number = @import("Number.zig");
pub const Integer = @import("Integer.zig"); pub const Integer = @import("Integer.zig");
pub const Global = @import("global.zig").Global;
pub const PromiseResolver = @import("PromiseResolver.zig"); pub const PromiseResolver = @import("PromiseResolver.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
@@ -78,11 +79,11 @@ pub const ArrayBuffer = struct {
}; };
pub const Exception = struct { pub const Exception = struct {
ctx: *const Context, local: *const Local,
handle: *const v8.Value, handle: *const v8.Value,
pub fn exception(self: Exception, allocator: Allocator) ![]const u8 { 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; 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 // These are here, and not in Inspector.zig, because Inspector.zig isn't always
// included (e.g. in the wpt build). // included (e.g. in the wpt build).
@@ -281,7 +227,7 @@ pub export fn v8_inspector__Client__IMPL__valueSubtype(
_: *v8.InspectorClientImpl, _: *v8.InspectorClientImpl,
c_value: *const v8.Value, c_value: *const v8.Value,
) callconv(.c) [*c]const u8 { ) 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; 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 // 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 // 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 ""; return if (external_entry.subtype == null) null else "";
} }
test "TaggedAnyOpaque" { test "TaggedAnyOpaque" {
// If we grow this, fine, but it should be a conscious decision // 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")));
} }

View File

@@ -30,7 +30,7 @@
testing.eventually(() => { testing.eventually(() => {
testing.expectEqual(true, popstateEventFired); testing.expectEqual(true, popstateEventFired);
testing.expectEqual(state, popstateEventState); testing.expectEqual({testInProgress: true }, popstateEventState);
}) })
history.back(); history.back();

View File

@@ -1,6 +1 @@
<!DOCTYPE html> <!DOCTYPE html>
<script src="testing.js"></script>
<script id=history-after-nav>
testing.expectEqual(true, history.state && history.state.testInProgress);
</script>

View File

@@ -38,7 +38,7 @@ pub fn getSignal(self: *const AbortController) *AbortSignal {
} }
pub fn abort(self: *AbortController, reason_: ?js.Value.Global, page: *Page) !void { 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 { pub const JsApi = struct {

View File

@@ -57,7 +57,7 @@ pub fn asEventTarget(self: *AbortSignal) *EventTarget {
return self._proto; 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) { if (self._aborted) {
return; return;
} }
@@ -77,11 +77,10 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, page: *Page) !void {
// Dispatch abort event // Dispatch abort event
const event = try Event.initTrusted("abort", .{}, page); const event = try Event.initTrusted("abort", .{}, page);
const func = if (self._on_abort) |*g| g.local() else null;
try page._event_manager.dispatchWithFunction( try page._event_manager.dispatchWithFunction(
self.asEventTarget(), self.asEventTarget(),
event, event,
func, local.toLocal(self._on_abort),
.{ .context = "abort signal" }, .{ .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 // Static method to create an already-aborted signal
pub fn createAborted(reason_: ?js.Value.Global, page: *Page) !*AbortSignal { pub fn createAborted(reason_: ?js.Value.Global, page: *Page) !*AbortSignal {
const signal = try init(page); 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; return signal;
} }
@@ -112,11 +111,13 @@ const ThrowIfAborted = union(enum) {
undefined: void, undefined: void,
}; };
pub fn throwIfAborted(self: *const AbortSignal, page: *Page) !ThrowIfAborted { pub fn throwIfAborted(self: *const AbortSignal, page: *Page) !ThrowIfAborted {
const local = page.js.local.?;
if (self._aborted) { if (self._aborted) {
const exception = switch (self._reason) { const exception = switch (self._reason) {
.string => |str| page.js.throw(str), .string => |str| local.throw(str),
.js_val => |js_val| page.js.throw(try js_val.local().toString(.{ .allocator = page.call_arena })), .js_val => |js_val| local.throw(try local.toLocal(js_val).toString(.{ .allocator = page.call_arena })),
.undefined => page.js.throw("AbortError"), .undefined => local.throw("AbortError"),
}; };
return .{ .exception = exception }; return .{ .exception = exception };
} }
@@ -135,7 +136,11 @@ const TimeoutCallback = struct {
fn run(ctx: *anyopaque) !?u32 { fn run(ctx: *anyopaque) !?u32 {
const self: *TimeoutCallback = @ptrCast(@alignCast(ctx)); 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 }); log.warn(.app, "abort signal timeout", .{ .err = err });
}; };
return null; return null;

View File

@@ -206,7 +206,7 @@ fn writeBlobParts(
/// Returns a Promise that resolves with the contents of the blob /// Returns a Promise that resolves with the contents of the blob
/// as binary data contained in an ArrayBuffer. /// as binary data contained in an ArrayBuffer.
pub fn arrayBuffer(self: *const Blob, page: *Page) !js.Promise { 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"); 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 /// Returns a Promise that resolves with a string containing
/// the contents of the blob, interpreted as UTF-8. /// the contents of the blob, interpreted as UTF-8.
pub fn text(self: *const Blob, page: *Page) !js.Promise { 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. /// 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 /// Returns a Promise that resolves with a Uint8Array containing
/// the contents of the blob as an array of bytes. /// the contents of the blob as an array of bytes.
pub fn bytes(self: *const Blob, page: *Page) !js.Promise { 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 /// Returns a new Blob object which contains data

View File

@@ -31,7 +31,7 @@ pub const init: Console = .{};
pub fn trace(_: *const Console, values: []js.Value, page: *Page) !void { pub fn trace(_: *const Console, values: []js.Value, page: *Page) !void {
logger.debug(.js, "console.trace", .{ logger.debug(.js, "console.trace", .{
.stack = page.js.stackTrace() catch "???", .stack = page.js.local.?.stackTrace() catch "???",
.args = ValueWriter{ .page = page, .values = values }, .args = ValueWriter{ .page = page, .values = values },
}); });
} }
@@ -138,7 +138,7 @@ const ValueWriter = struct {
try writer.print("\n arg({d}): {f}", .{ i, value }); try writer.print("\n arg({d}): {f}", .{ i, value });
} }
if (self.include_stack) { 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 "???"});
} }
} }

View File

@@ -106,7 +106,7 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu
} }
if (self._when_defined.fetchRemove(name)) |entry| { 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 { pub fn whenDefined(self: *CustomElementRegistry, name: []const u8, page: *Page) !js.Promise {
const local = page.js.local.?;
if (self._definitions.get(name)) |definition| { 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); const gop = try self._when_defined.getOrPut(page.arena, name);
if (gop.found_existing) { if (gop.found_existing) {
return gop.value_ptr.local().promise(); return local.toLocal(gop.value_ptr.*).promise();
} }
errdefer _ = self._when_defined.remove(name); errdefer _ = self._when_defined.remove(name);
const owned_name = try page.dupeString(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.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 { fn upgradeNode(self: *CustomElementRegistry, node: *Node, page: *Page) !void {
@@ -174,8 +175,12 @@ pub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinitio
page._upgrading_element = node; page._upgrading_element = node;
defer page._upgrading_element = prev_upgrading; 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; 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 }); log.warn(.js, "custom element upgrade", .{ .name = definition.name, .err = err, .caught = caught });
return error.CustomElementUpgradeFailed; return error.CustomElementUpgradeFailed;
}; };

View File

@@ -63,14 +63,14 @@ pub fn getFilter(self: *const DOMNodeIterator) ?FilterOpts {
return self._filter._original_filter; 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 node = self._reference_node;
var before_node = self._pointer_before_reference_node; var before_node = self._pointer_before_reference_node;
while (true) { while (true) {
if (before_node) { if (before_node) {
before_node = false; before_node = false;
const result = try self.filterNode(node); const result = try self.filterNode(node, page);
if (result == NodeFilter.FILTER_ACCEPT) { if (result == NodeFilter.FILTER_ACCEPT) {
self._reference_node = node; self._reference_node = node;
self._pointer_before_reference_node = false; self._pointer_before_reference_node = false;
@@ -84,7 +84,7 @@ pub fn nextNode(self: *DOMNodeIterator) !?*Node {
} }
node = next.?; node = next.?;
const result = try self.filterNode(node); const result = try self.filterNode(node, page);
if (result == NodeFilter.FILTER_ACCEPT) { if (result == NodeFilter.FILTER_ACCEPT) {
self._reference_node = node; self._reference_node = node;
self._pointer_before_reference_node = false; 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 node = self._reference_node;
var before_node = self._pointer_before_reference_node; var before_node = self._pointer_before_reference_node;
while (true) { while (true) {
if (!before_node) { if (!before_node) {
const result = try self.filterNode(node); const result = try self.filterNode(node, page);
if (result == NodeFilter.FILTER_ACCEPT) { if (result == NodeFilter.FILTER_ACCEPT) {
self._reference_node = node; self._reference_node = node;
self._pointer_before_reference_node = true; 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 // First check whatToShow
if (!NodeFilter.shouldShow(node, self._what_to_show)) { if (!NodeFilter.shouldShow(node, self._what_to_show)) {
return NodeFilter.FILTER_SKIP; return NodeFilter.FILTER_SKIP;
@@ -128,7 +128,7 @@ fn filterNode(self: *const DOMNodeIterator, node: *Node) !i32 {
// Then check the filter callback // Then check the filter callback
// For NodeIterator, REJECT and SKIP are equivalent - both skip the node // For NodeIterator, REJECT and SKIP are equivalent - both skip the node
// but continue with its descendants // but continue with its descendants
const result = try self._filter.acceptNode(node); const result = try self._filter.acceptNode(node, page.js.local.?);
return result; return result;
} }

View File

@@ -62,13 +62,13 @@ pub fn setCurrentNode(self: *DOMTreeWalker, node: *Node) void {
} }
// Navigation methods // Navigation methods
pub fn parentNode(self: *DOMTreeWalker) !?*Node { pub fn parentNode(self: *DOMTreeWalker, page: *Page) !?*Node {
var node = self._current._parent; var node = self._current._parent;
while (node) |n| { while (node) |n| {
if (n == self._root._parent) { if (n == self._root._parent) {
return null; return null;
} }
if (try self.acceptNode(n) == NodeFilter.FILTER_ACCEPT) { if (try self.acceptNode(n, page) == NodeFilter.FILTER_ACCEPT) {
self._current = n; self._current = n;
return n; return n;
} }
@@ -77,11 +77,11 @@ pub fn parentNode(self: *DOMTreeWalker) !?*Node {
return null; return null;
} }
pub fn firstChild(self: *DOMTreeWalker) !?*Node { pub fn firstChild(self: *DOMTreeWalker, page: *Page) !?*Node {
var node = self._current.firstChild(); var node = self._current.firstChild();
while (node) |n| { while (node) |n| {
const filter_result = try self.acceptNode(n); const filter_result = try self.acceptNode(n, page);
if (filter_result == NodeFilter.FILTER_ACCEPT) { if (filter_result == NodeFilter.FILTER_ACCEPT) {
self._current = n; self._current = n;
@@ -117,11 +117,11 @@ pub fn firstChild(self: *DOMTreeWalker) !?*Node {
return null; return null;
} }
pub fn lastChild(self: *DOMTreeWalker) !?*Node { pub fn lastChild(self: *DOMTreeWalker, page: *Page) !?*Node {
var node = self._current.lastChild(); var node = self._current.lastChild();
while (node) |n| { while (node) |n| {
const filter_result = try self.acceptNode(n); const filter_result = try self.acceptNode(n, page);
if (filter_result == NodeFilter.FILTER_ACCEPT) { if (filter_result == NodeFilter.FILTER_ACCEPT) {
self._current = n; self._current = n;
@@ -157,10 +157,10 @@ pub fn lastChild(self: *DOMTreeWalker) !?*Node {
return null; return null;
} }
pub fn previousSibling(self: *DOMTreeWalker) !?*Node { pub fn previousSibling(self: *DOMTreeWalker, page: *Page) !?*Node {
var node = self.previousSiblingOrNull(self._current); var node = self.previousSiblingOrNull(self._current);
while (node) |n| { while (node) |n| {
if (try self.acceptNode(n) == NodeFilter.FILTER_ACCEPT) { if (try self.acceptNode(n, page) == NodeFilter.FILTER_ACCEPT) {
self._current = n; self._current = n;
return n; return n;
} }
@@ -169,10 +169,10 @@ pub fn previousSibling(self: *DOMTreeWalker) !?*Node {
return null; return null;
} }
pub fn nextSibling(self: *DOMTreeWalker) !?*Node { pub fn nextSibling(self: *DOMTreeWalker, page: *Page) !?*Node {
var node = self.nextSiblingOrNull(self._current); var node = self.nextSiblingOrNull(self._current);
while (node) |n| { while (node) |n| {
if (try self.acceptNode(n) == NodeFilter.FILTER_ACCEPT) { if (try self.acceptNode(n, page) == NodeFilter.FILTER_ACCEPT) {
self._current = n; self._current = n;
return n; return n;
} }
@@ -181,7 +181,7 @@ pub fn nextSibling(self: *DOMTreeWalker) !?*Node {
return null; return null;
} }
pub fn previousNode(self: *DOMTreeWalker) !?*Node { pub fn previousNode(self: *DOMTreeWalker, page: *Page) !?*Node {
var node = self._current; var node = self._current;
while (node != self._root) { while (node != self._root) {
var sibling = self.previousSiblingOrNull(node); var sibling = self.previousSiblingOrNull(node);
@@ -189,7 +189,7 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node {
node = sib; node = sib;
// Check if this sibling is rejected before descending into it // 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) { if (sib_result == NodeFilter.FILTER_REJECT) {
// Skip this sibling and its descendants entirely // Skip this sibling and its descendants entirely
sibling = self.previousSiblingOrNull(node); sibling = self.previousSiblingOrNull(node);
@@ -204,7 +204,7 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node {
while (child) |c| { while (child) |c| {
if (!self.isInSubtree(c)) break; 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) { if (filter_result == NodeFilter.FILTER_REJECT) {
// Skip this child and try its previous sibling // Skip this child and try its previous sibling
child = self.previousSiblingOrNull(c); child = self.previousSiblingOrNull(c);
@@ -220,7 +220,7 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node {
node = child.?; node = child.?;
} }
if (try self.acceptNode(node) == NodeFilter.FILTER_ACCEPT) { if (try self.acceptNode(node, page) == NodeFilter.FILTER_ACCEPT) {
self._current = node; self._current = node;
return node; return node;
} }
@@ -232,7 +232,7 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node {
} }
const parent = node._parent orelse return null; 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; self._current = parent;
return parent; return parent;
} }
@@ -241,14 +241,14 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node {
return null; return null;
} }
pub fn nextNode(self: *DOMTreeWalker) !?*Node { pub fn nextNode(self: *DOMTreeWalker, page: *Page) !?*Node {
var node = self._current; var node = self._current;
while (true) { while (true) {
// Try children first (depth-first) // Try children first (depth-first)
if (node.firstChild()) |child| { if (node.firstChild()) |child| {
node = child; node = child;
const filter_result = try self.acceptNode(node); const filter_result = try self.acceptNode(node, page);
if (filter_result == NodeFilter.FILTER_ACCEPT) { if (filter_result == NodeFilter.FILTER_ACCEPT) {
self._current = node; self._current = node;
return node; return node;
@@ -271,7 +271,7 @@ pub fn nextNode(self: *DOMTreeWalker) !?*Node {
if (node.nextSibling()) |sibling| { if (node.nextSibling()) |sibling| {
node = sibling; node = sibling;
const filter_result = try self.acceptNode(node); const filter_result = try self.acceptNode(node, page);
if (filter_result == NodeFilter.FILTER_ACCEPT) { if (filter_result == NodeFilter.FILTER_ACCEPT) {
self._current = node; self._current = node;
return node; return node;
@@ -293,7 +293,7 @@ pub fn nextNode(self: *DOMTreeWalker) !?*Node {
} }
// Helper methods // Helper methods
fn acceptNode(self: *const DOMTreeWalker, node: *Node) !i32 { fn acceptNode(self: *const DOMTreeWalker, node: *Node, page: *Page) !i32 {
// First check whatToShow // First check whatToShow
if (!NodeFilter.shouldShow(node, self._what_to_show)) { if (!NodeFilter.shouldShow(node, self._what_to_show)) {
return NodeFilter.FILTER_SKIP; 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 // For TreeWalker, REJECT means reject node and its descendants
// SKIP means skip node but check its descendants // SKIP means skip node but check its descendants
// ACCEPT means accept the node // 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 { fn isInSubtree(self: *const DOMTreeWalker, node: *Node) bool {

View File

@@ -770,7 +770,7 @@ pub fn getAdoptedStyleSheets(self: *Document, page: *Page) !js.Object.Global {
if (self._adopted_style_sheets) |ass| { if (self._adopted_style_sheets) |ass| {
return ass; return ass;
} }
const js_arr = page.js.newArray(0); const js_arr = page.js.local.?.newArray(0);
const js_obj = js_arr.toObject(); const js_obj = js_arr.toObject();
self._adopted_style_sheets = try js_obj.persist(); self._adopted_style_sheets = try js_obj.persist();
return self._adopted_style_sheets.?; return self._adopted_style_sheets.?;

View File

@@ -34,7 +34,7 @@ pub fn getLength(_: *const History, page: *Page) u32 {
pub fn getState(_: *const History, page: *Page) !?js.Value { pub fn getState(_: *const History, page: *Page) !?js.Value {
if (page._session.navigation.getCurrentEntry()._state.value) |state| { 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; return value;
} else return null; } else return null;
} }
@@ -81,11 +81,10 @@ fn goInner(delta: i32, page: *Page) !void {
if (try page.isSameOrigin(url)) { if (try page.isSameOrigin(url)) {
const event = try PopStateEvent.initTrusted("popstate", .{ .state = entry._state.value }, page); 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( try page._event_manager.dispatchWithFunction(
page.window.asEventTarget(), page.window.asEventTarget(),
event.asEvent(), event.asEvent(),
func, page.js.toLocal(page.window._on_popstate),
.{ .context = "Pop State" }, .{ .context = "Pop State" },
); );
} }

View File

@@ -246,7 +246,12 @@ pub fn deliverEntries(self: *IntersectionObserver, page: *Page) !void {
const entries = try self.takeRecords(page); const entries = try self.takeRecords(page);
var caught: js.TryCatch.Caught = undefined; 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 }); log.err(.page, "IntsctObserver.deliverEntries", .{ .err = err, .caught = caught });
return err; return err;
}; };

View File

@@ -116,6 +116,7 @@ const PostMessageCallback = struct {
fn run(ctx: *anyopaque) !?u32 { fn run(ctx: *anyopaque) !?u32 {
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx)); const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
defer self.deinit(); defer self.deinit();
const page = self.page;
if (self.port._closed) { if (self.port._closed) {
return null; return null;
@@ -125,16 +126,19 @@ const PostMessageCallback = struct {
.data = self.message, .data = self.message,
.origin = "", .origin = "",
.source = null, .source = null,
}, self.page) catch |err| { }, page) catch |err| {
log.err(.dom, "MessagePort.postMessage", .{ .err = err }); log.err(.dom, "MessagePort.postMessage", .{ .err = err });
return null; return null;
}; };
const func = if (self.port._on_message) |*g| g.local() else null; var ls: js.Local.Scope = undefined;
self.page._event_manager.dispatchWithFunction( page.js.localScope(&ls);
defer ls.deinit();
page._event_manager.dispatchWithFunction(
self.port.asEventTarget(), self.port.asEventTarget(),
event.asEvent(), event.asEvent(),
func, ls.toLocal(self.port._on_message),
.{ .context = "MessagePort message" }, .{ .context = "MessagePort message" },
) catch |err| { ) catch |err| {
log.err(.dom, "MessagePort.postMessage", .{ .err = err }); log.err(.dom, "MessagePort.postMessage", .{ .err = err });

View File

@@ -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 // 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 // This ensures mutations triggered during the callback go into a fresh list
const records = try self.takeRecords(page); 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; 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 }); log.err(.page, "MutObserver.deliverRecords", .{ .err = err, .caught = caught });
return err; return err;
}; };

View File

@@ -65,9 +65,9 @@ pub const SHOW_DOCUMENT_TYPE: u32 = 0x200;
pub const SHOW_DOCUMENT_FRAGMENT: u32 = 0x400; pub const SHOW_DOCUMENT_FRAGMENT: u32 = 0x400;
pub const SHOW_NOTATION: u32 = 0x800; 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; 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 { pub fn shouldShow(node: *const Node, what_to_show: u32) bool {

View File

@@ -362,10 +362,10 @@ pub const Mark = struct {
pub const Measure = struct { pub const Measure = struct {
_proto: *Entry, _proto: *Entry,
_detail: ?js.Object.Global, _detail: ?js.Value.Global,
const Options = struct { const Options = struct {
detail: ?js.Object = null, detail: ?js.Value = null,
start: ?TimestampOrMark, start: ?TimestampOrMark,
end: ?TimestampOrMark, end: ?TimestampOrMark,
duration: ?f64 = null, duration: ?f64 = null,
@@ -378,7 +378,7 @@ pub const Measure = struct {
pub fn init( pub fn init(
name: []const u8, name: []const u8,
maybe_detail: ?js.Object, maybe_detail: ?js.Value,
start_timestamp: f64, start_timestamp: f64,
end_timestamp: f64, end_timestamp: f64,
maybe_duration: ?f64, maybe_duration: ?f64,
@@ -405,7 +405,7 @@ pub const Measure = struct {
return m; return m;
} }
pub fn getDetail(self: *const Measure) ?js.Object.Global { pub fn getDetail(self: *const Measure) ?js.Value.Global {
return self._detail; return self._detail;
} }

View File

@@ -153,8 +153,13 @@ pub inline fn hasRecords(self: *const PerformanceObserver) bool {
/// Runs the PerformanceObserver's callback with records; emptying it out. /// Runs the PerformanceObserver's callback with records; emptying it out.
pub fn dispatch(self: *PerformanceObserver, page: *Page) !void { pub fn dispatch(self: *PerformanceObserver, page: *Page) !void {
const records = try self.takeRecords(page); 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; 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 }); log.err(.page, "PerfObserver.dispatch", .{ .err = err, .caught = caught });
return err; return err;
}; };

View File

@@ -96,10 +96,10 @@ pub fn generateKey(
page: *Page, page: *Page,
) !js.Promise { ) !js.Promise {
const key_or_pair = CryptoKey.init(algorithm, extractable, key_usages, page) catch |err| { const key_or_pair = CryptoKey.init(algorithm, extractable, key_usages, page) catch |err| {
return page.js.rejectPromise(@errorName(err)); return page.js.local.?.rejectPromise(@errorName(err));
}; };
return page.js.resolvePromise(key_or_pair); return page.js.local.?.resolvePromise(key_or_pair);
} }
/// Exports a key: that is, it takes as input a CryptoKey object and gives you /// Exports a key: that is, it takes as input a CryptoKey object and gives you
@@ -115,7 +115,7 @@ pub fn exportKey(
} }
if (std.mem.eql(u8, format, "raw")) { if (std.mem.eql(u8, format, "raw")) {
return page.js.resolvePromise(js.ArrayBuffer{ .values = key._key }); return page.js.local.?.resolvePromise(js.ArrayBuffer{ .values = key._key });
} }
const is_unsupported = std.mem.eql(u8, format, "pkcs8") or const is_unsupported = std.mem.eql(u8, format, "pkcs8") or
@@ -125,7 +125,7 @@ pub fn exportKey(
log.warn(.not_implemented, "SubtleCrypto.exportKey", .{ .format = format }); log.warn(.not_implemented, "SubtleCrypto.exportKey", .{ .format = format });
} }
return page.js.rejectPromise(@errorName(error.NotSupported)); return page.js.local.?.rejectPromise(@errorName(error.NotSupported));
} }
/// Derive a secret key from a master key. /// Derive a secret key from a master key.
@@ -140,14 +140,14 @@ pub fn deriveBits(
.ecdh_or_x25519 => |p| { .ecdh_or_x25519 => |p| {
const name = p.name; const name = p.name;
if (std.mem.eql(u8, name, "X25519")) { if (std.mem.eql(u8, name, "X25519")) {
return page.js.resolvePromise(base_key.deriveBitsX25519(p.public, length, page)); return page.js.local.?.resolvePromise(base_key.deriveBitsX25519(p.public, length, page));
} }
if (std.mem.eql(u8, name, "ECDH")) { if (std.mem.eql(u8, name, "ECDH")) {
log.warn(.not_implemented, "SubtleCrypto.deriveBits", .{ .name = name }); log.warn(.not_implemented, "SubtleCrypto.deriveBits", .{ .name = name });
} }
return page.js.rejectPromise(@errorName(error.NotSupported)); return page.js.local.?.rejectPromise(@errorName(error.NotSupported));
}, },
}; };
} }
@@ -184,19 +184,19 @@ pub fn sign(
.hmac => { .hmac => {
// Verify algorithm. // Verify algorithm.
if (!algorithm.isHMAC()) { if (!algorithm.isHMAC()) {
return page.js.rejectPromise(@errorName(error.InvalidAccessError)); return page.js.local.?.rejectPromise(@errorName(error.InvalidAccessError));
} }
// Call sign for HMAC. // Call sign for HMAC.
const result = key.signHMAC(data, page) catch |err| { const result = key.signHMAC(data, page) catch |err| {
return page.js.rejectPromise(@errorName(err)); return page.js.local.?.rejectPromise(@errorName(err));
}; };
return page.js.resolvePromise(result); return page.js.local.?.resolvePromise(result);
}, },
else => { else => {
log.warn(.not_implemented, "SubtleCrypto.sign", .{ .key_type = key._type }); log.warn(.not_implemented, "SubtleCrypto.sign", .{ .key_type = key._type });
return page.js.rejectPromise(@errorName(error.InvalidAccessError)); return page.js.local.?.rejectPromise(@errorName(error.InvalidAccessError));
}, },
}; };
} }
@@ -454,10 +454,10 @@ pub const CryptoKey = struct {
if (signed != null) { if (signed != null) {
// CRYPTO_memcmp compare in constant time so prohibits time-based attacks. // CRYPTO_memcmp compare in constant time so prohibits time-based attacks.
const res = crypto.CRYPTO_memcmp(signed, @ptrCast(signature.ptr), signature.len); const res = crypto.CRYPTO_memcmp(signed, @ptrCast(signature.ptr), signature.len);
return page.js.resolvePromise(res == 0); return page.js.local.?.resolvePromise(res == 0);
} }
return page.js.resolvePromise(false); return page.js.local.?.resolvePromise(false);
} }
// X25519. // X25519.

View File

@@ -269,10 +269,10 @@ pub fn cancelIdleCallback(self: *Window, id: u32) void {
sc.removed = true; 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", .{ const error_event = try ErrorEvent.initTrusted("error", .{
.@"error" = err, .@"error" = try err.persist(),
.message = err.local().toString(.{}) catch "Unknown error", .message = err.toString(.{}) catch "Unknown error",
.bubbles = false, .bubbles = false,
.cancelable = true, .cancelable = true,
}, page); }, page);
@@ -552,20 +552,24 @@ const ScheduleCallback = struct {
return null; return null;
} }
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
switch (self.mode) { switch (self.mode) {
.idle => { .idle => {
const IdleDeadline = @import("IdleDeadline.zig"); 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 }); log.warn(.js, "window.idleCallback", .{ .name = self.name, .err = err });
}; };
}, },
.animation_frame => { .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 }); log.warn(.js, "window.RAF", .{ .name = self.name, .err = err });
}; };
}, },
.normal => { .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 }); log.warn(.js, "window.timer", .{ .name = self.name, .err = err });
}; };
}, },

View File

@@ -46,20 +46,22 @@ pub fn getPending(_: *const Animation) bool {
pub fn getFinished(self: *Animation, page: *Page) !js.Promise { pub fn getFinished(self: *Animation, page: *Page) !js.Promise {
if (self._finished_resolver == null) { if (self._finished_resolver == null) {
const resolver = try page.js.createPromiseResolver().persist(); const resolver = page.js.local.?.createPromiseResolver();
resolver.local().resolve("Animation.getFinished", self); resolver.resolve("Animation.getFinished", self);
self._finished_resolver = resolver; self._finished_resolver = try resolver.persist();
return resolver.promise();
} }
return self._finished_resolver.?.local().promise(); return page.js.toLocal(self._finished_resolver).?.promise();
} }
pub fn getReady(self: *Animation, page: *Page) !js.Promise { pub fn getReady(self: *Animation, page: *Page) !js.Promise {
// never resolved, because we're always "finished" // never resolved, because we're always "finished"
if (self._ready_resolver == null) { if (self._ready_resolver == null) {
const resolver = try page.js.createPromiseResolver().persist(); const resolver = page.js.local.?.createPromiseResolver();
self._ready_resolver = resolver; self._ready_resolver = try resolver.persist();
return resolver.promise();
} }
return self._ready_resolver.?.local().promise(); return page.js.toLocal(self._ready_resolver).?.promise();
} }
pub fn getEffect(self: *const Animation) ?js.Object.Global { pub fn getEffect(self: *const Animation) ?js.Object.Global {

View File

@@ -66,7 +66,7 @@ pub fn replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise
_ = self; _ = self;
_ = text; _ = text;
// TODO: clear self.css_rules // TODO: clear self.css_rules
return page.js.resolvePromise({}); return page.js.local.?.resolvePromise({});
} }
pub fn replaceSync(self: *CSSStyleSheet, text: []const u8) !void { pub fn replaceSync(self: *CSSStyleSheet, text: []const u8) !void {

View File

@@ -50,8 +50,8 @@ pub const Build = struct {
pub fn complete(node: *Node, page: *Page) !void { pub fn complete(node: *Node, page: *Page) !void {
const el = node.as(Element); const el = node.as(Element);
const on_load = el.getAttributeSafe("onload") orelse return; const on_load = el.getAttributeSafe("onload") orelse return;
if (page.js.stringToFunction(on_load)) |func| { if (page.js.stringToPersistedFunction(on_load)) |func| {
page.window._on_load = try func.persist(); page.window._on_load = func;
} else |err| { } else |err| {
log.err(.js, "body.onload", .{ .err = err, .str = on_load }); log.err(.js, "body.onload", .{ .err = err, .str = on_load });
} }

View File

@@ -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 { fn invokeCallbackOnElement(element: *Element, definition: *CustomElementDefinition, comptime callback_name: [:0]const u8, args: anytype, page: *Page) void {
_ = definition; _ = definition;
const ctx = page.js; var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
// Get the JS element object // 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(); const js_element = js_val.toObject();
// Call the callback method if it exists // Call the callback method if it exists
@@ -195,8 +197,26 @@ pub fn checkAndAttachBuiltIn(element: *Element, page: *Page) !void {
page._upgrading_element = node; page._upgrading_element = node;
defer page._upgrading_element = prev_upgrading; 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; 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 }); log.warn(.js, "custom builtin ctor", .{ .name = is_value, .err = err, .caught = caught });
return; return;
}; };
@@ -207,9 +227,11 @@ fn invokeCallback(self: *Custom, comptime callback_name: [:0]const u8, args: any
return; 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(); const js_element = js_val.toObject();
js_element.callMethod(void, callback_name, args) catch return; js_element.callMethod(void, callback_name, args) catch return;

View File

@@ -130,16 +130,16 @@ pub const Build = struct {
self._src = element.getAttributeSafe("src") orelse ""; self._src = element.getAttributeSafe("src") orelse "";
if (element.getAttributeSafe("onload")) |on_load| { if (element.getAttributeSafe("onload")) |on_load| {
if (page.js.stringToFunction(on_load)) |func| { if (page.js.stringToPersistedFunction(on_load)) |func| {
self._on_load = try func.persist(); self._on_load = func;
} else |err| { } else |err| {
log.err(.js, "script.onload", .{ .err = err, .str = on_load }); log.err(.js, "script.onload", .{ .err = err, .str = on_load });
} }
} }
if (element.getAttributeSafe("onerror")) |on_error| { if (element.getAttributeSafe("onerror")) |on_error| {
if (page.js.stringToFunction(on_error)) |func| { if (page.js.stringToPersistedFunction(on_error)) |func| {
self._on_error = try func.persist(); self._on_error = func;
} else |err| { } else |err| {
log.err(.js, "script.onerror", .{ .err = err, .str = on_error }); log.err(.js, "script.onerror", .{ .err = err, .str = on_error });
} }

View File

@@ -27,8 +27,8 @@ const Allocator = std.mem.Allocator;
const CustomEvent = @This(); const CustomEvent = @This();
_proto: *Event, _proto: *Event,
_arena: Allocator,
_detail: ?js.Value.Global = null, _detail: ?js.Value.Global = null,
_arena: Allocator,
const CustomEventOptions = struct { const CustomEventOptions = struct {
detail: ?js.Value.Global = null, detail: ?js.Value.Global = null,

View File

@@ -61,7 +61,7 @@ pub fn asEvent(self: *PopStateEvent) *Event {
pub fn getState(self: *PopStateEvent, page: *Page) !?js.Value { pub fn getState(self: *PopStateEvent, page: *Page) !?js.Value {
const s = self._state orelse return null; 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 { pub fn hasUAVisualTransition(_: *PopStateEvent) bool {

View File

@@ -267,8 +267,9 @@ pub fn navigateInner(
// //
// These will only settle on same-origin navigation (mostly intended for SPAs). // 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 :) // It is fine (and expected) for these to not settle on cross-origin requests :)
const committed = try page.js.createPromiseResolver().persist(); const local = page.js.local.?;
const finished = try page.js.createPromiseResolver().persist(); const committed = local.createPromiseResolver();
const finished = local.createPromiseResolver();
const new_url = try URL.resolve(arena, page.url, url, .{}); const new_url = try URL.resolve(arena, page.url, url, .{});
const is_same_document = URL.eqlDocument(new_url, page.url); const is_same_document = URL.eqlDocument(new_url, page.url);
@@ -280,9 +281,9 @@ pub fn navigateInner(
if (is_same_document) { if (is_same_document) {
page.url = new_url; page.url = new_url;
committed.local().resolve("navigation push", {}); committed.resolve("navigation push", {});
// todo: Fire navigate event // todo: Fire navigate event
finished.local().resolve("navigation push", {}); finished.resolve("navigation push", {});
_ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, true); _ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, true);
} else { } else {
@@ -293,9 +294,9 @@ pub fn navigateInner(
if (is_same_document) { if (is_same_document) {
page.url = new_url; page.url = new_url;
committed.local().resolve("navigation replace", {}); committed.resolve("navigation replace", {});
// todo: Fire navigate event // todo: Fire navigate event
finished.local().resolve("navigation replace", {}); finished.resolve("navigation replace", {});
_ = try self.replaceEntry(url, .{ .source = .navigation, .value = state }, page, true); _ = try self.replaceEntry(url, .{ .source = .navigation, .value = state }, page, true);
} else { } else {
@@ -308,9 +309,9 @@ pub fn navigateInner(
if (is_same_document) { if (is_same_document) {
page.url = new_url; page.url = new_url;
committed.local().resolve("navigation traverse", {}); committed.resolve("navigation traverse", {});
// todo: Fire navigate event // todo: Fire navigate event
finished.local().resolve("navigation traverse", {}); finished.resolve("navigation traverse", {});
} else { } else {
try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .script); try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .script);
} }
@@ -328,9 +329,11 @@ pub fn navigateInner(
); );
try self._proto.dispatch(.{ .currententrychange = event }, page); try self._proto.dispatch(.{ .currententrychange = event }, page);
_ = try committed.persist();
_ = try finished.persist();
return .{ return .{
.committed = try committed.local().promise().persist(), .committed = try committed.promise().persist(),
.finished = try finished.local().promise().persist(), .finished = try finished.promise().persist(),
}; };
} }

View File

@@ -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( return page._event_manager.dispatchWithFunction(
self.asEventTarget(), self.asEventTarget(),
event, event,
func, page.js.toLocal(@field(self, field)),
.{ .context = "Navigation" }, .{ .context = "Navigation" },
); );
} }

View File

@@ -81,7 +81,7 @@ pub const StateReturn = union(enum) { value: ?js.Value, undefined: void };
pub fn getState(self: *const NavigationHistoryEntry, page: *Page) !StateReturn { pub fn getState(self: *const NavigationHistoryEntry, page: *Page) !StateReturn {
if (self._state.source == .navigation) { if (self._state.source == .navigation) {
if (self._state.value) |value| { if (self._state.value) |value| {
return .{ .value = try page.js.parseJSON(value) }; return .{ .value = try page.js.local.?.parseJSON(value) };
} }
} }

View File

@@ -44,12 +44,14 @@ pub const InitOpts = Request.InitOpts;
pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise { pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
const request = try Request.init(input, options, page); const request = try Request.init(input, options, page);
const resolver = page.js.local.?.createPromiseResolver();
const fetch = try page.arena.create(Fetch); const fetch = try page.arena.create(Fetch);
fetch.* = .{ fetch.* = .{
._page = page, ._page = page,
._buf = .empty, ._buf = .empty,
._url = try page.arena.dupe(u8, request._url), ._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), ._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, .done_callback = httpDoneCallback,
.error_callback = httpErrorCallback, .error_callback = httpErrorCallback,
}); });
return fetch._resolver.local().promise(); return resolver.promise();
} }
pub fn deinit(self: *Fetch) void { pub fn deinit(self: *Fetch) void {
@@ -149,13 +151,22 @@ fn httpDoneCallback(ctx: *anyopaque) !void {
.len = self._buf.items.len, .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 { fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
const self: *Fetch = @ptrCast(@alignCast(ctx)); const self: *Fetch = @ptrCast(@alignCast(ctx));
self._response._type = .@"error"; // Set type to error for network failures 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"); const testing = @import("../../../testing.zig");

View File

@@ -115,15 +115,16 @@ pub fn isOK(self: *const Response) bool {
pub fn getText(self: *const Response, page: *Page) !js.Promise { pub fn getText(self: *const Response, page: *Page) !js.Promise {
const body = self._body orelse ""; 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 { pub fn getJson(self: *Response, page: *Page) !js.Promise {
const body = self._body orelse ""; const body = self._body orelse "";
const value = page.js.parseJSON(body) catch |err| { const local = page.js.local.?;
return page.js.rejectPromise(.{@errorName(err)}); 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 { pub const JsApi = struct {

View File

@@ -131,7 +131,7 @@ pub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8) !void
self._method = try parseMethod(method_); self._method = try parseMethod(method_);
self._url = try URL.resolve(self._arena, self._page.base(), url, .{ .always_dupe = true }); 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 { 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) { const res: Response = switch (self._response_type) {
.text => .{ .text = data }, .text => .{ .text = data },
.json => blk: { .json => blk: {
const value = try page.js.parseJSON(data); const value = try page.js.local.?.parseJSON(data);
break :blk .{ .json = try value.persist() }; break :blk .{ .json = try value.persist() };
}, },
.document => blk: { .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)); self._response_url = try self._arena.dupeZ(u8, std.mem.span(header.url));
try self.stateChanged(.headers_received, self._page); const page = self._page;
try self._proto.dispatch(.load_start, .{ .loaded = 0, .total = self._response_len orelse 0 }, self._page);
try self.stateChanged(.loading, 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 { fn httpDataCallback(transfer: *Http.Transfer, data: []const u8) !void {
const self: *XMLHttpRequest = @ptrCast(@alignCast(transfer.ctx)); const self: *XMLHttpRequest = @ptrCast(@alignCast(transfer.ctx));
try self._response_data.appendSlice(self._arena, data); 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, .{ try self._proto.dispatch(.progress, .{
.total = self._response_len orelse 0, .total = self._response_len orelse 0,
.loaded = self._response_data.items.len, .loaded = self._response_data.items.len,
}, self._page); }, &ls.local, page);
} }
fn httpDoneCallback(ctx: *anyopaque) !void { 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 // Not that the request is done, the http/client will free the transfer
// object. It isn't safe to keep it around. // object. It isn't safe to keep it around.
self._transfer = null; 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; const loaded = self._response_data.items.len;
try self._proto.dispatch(.load, .{ try self._proto.dispatch(.load, .{
.total = loaded, .total = loaded,
.loaded = loaded, .loaded = loaded,
}, self._page); }, local, page);
try self._proto.dispatch(.load_end, .{ try self._proto.dispatch(.load_end, .{
.total = loaded, .total = loaded,
.loaded = loaded, .loaded = loaded,
}, self._page); }, local, page);
} }
fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void { 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; const new_state: ReadyState = if (is_abort) .unsent else .done;
if (new_state != self._ready_state) { if (new_state != self._ready_state) {
const page = self._page; 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) { 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(.err, null, local, page);
try self._proto.dispatch(.load_end, null, page); try self._proto.dispatch(.load_end, null, local, page);
} }
const level: log.Level = if (err == error.Abort) .debug else .err; 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) { if (state == self._ready_state) {
return; return;
} }
@@ -416,11 +443,10 @@ fn stateChanged(self: *XMLHttpRequest, state: ReadyState, page: *Page) !void {
self._ready_state = state; self._ready_state = state;
const event = try Event.initTrusted("readystatechange", .{}, page); 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( try page._event_manager.dispatchWithFunction(
self.asEventTarget(), self.asEventTarget(),
event, event,
func, local.toLocal(self._on_ready_state_change),
.{ .context = "XHR state change" }, .{ .context = "XHR state change" },
); );
} }

View File

@@ -43,7 +43,7 @@ pub fn asEventTarget(self: *XMLHttpRequestEventTarget) *EventTarget {
return self._proto; 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: { const field, const typ = comptime blk: {
break :blk switch (event_type) { break :blk switch (event_type) {
.abort => .{ "_on_abort", "abort" }, .abort => .{ "_on_abort", "abort" },
@@ -63,11 +63,10 @@ pub fn dispatch(self: *XMLHttpRequestEventTarget, comptime event_type: DispatchT
page, page,
); );
const func = if (@field(self, field)) |*g| g.local() else null;
return page._event_manager.dispatchWithFunction( return page._event_manager.dispatchWithFunction(
self.asEventTarget(), self.asEventTarget(),
event.asEvent(), event.asEvent(),
func, local.toLocal(@field(self, field)),
.{ .context = "XHR " ++ typ }, .{ .context = "XHR " ++ typ },
); );
} }

View File

@@ -137,12 +137,12 @@ pub fn callPullIfNeeded(self: *ReadableStream) !void {
self._pulling = true; 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 // Call the pull function
// Note: In a complete implementation, we'd handle the promise returned by pull // Note: In a complete implementation, we'd handle the promise returned by pull
// and set _pulling = false when it resolves // 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; 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 { pub fn cancel(self: *ReadableStream, reason: ?[]const u8, page: *Page) !js.Promise {
const local = page.js.local.?;
if (self._state != .readable) { if (self._state != .readable) {
if (self._cancel) |c| { if (self._cancel) |c| {
if (c.resolver) |r| { if (c.resolver) |r| {
return r.local().promise(); return local.toLocal(r).promise();
} }
} }
return page.js.resolvePromise(.{}); return local.resolvePromise(.{});
} }
if (self._cancel == null) { if (self._cancel == null) {
@@ -181,16 +183,21 @@ pub fn cancel(self: *ReadableStream, reason: ?[]const u8, page: *Page) !js.Promi
} }
var c = &self._cancel.?; var c = &self._cancel.?;
if (c.resolver == null) { var resolver = blk: {
c.resolver = try page.js.createPromiseResolver().persist(); 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 // Execute the cancel callback if provided
if (c.callback) |*cb| { if (c.callback) |cb| {
if (reason) |r| { if (reason) |r| {
try cb.local().call(void, .{r}); try local.toLocal(cb).call(void, .{r});
} else { } 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, .done = true,
.value = .empty, .value = .empty,
}; };
for (self._controller._pending_reads.items) |*resolver| { for (self._controller._pending_reads.items) |r| {
resolver.local().resolve("stream cancelled", result); local.toLocal(r).resolve("stream cancelled", result);
} }
self._controller._pending_reads.clearRetainingCapacity(); self._controller._pending_reads.clearRetainingCapacity();
resolver.resolve("ReadableStream.cancel", {});
c.resolver.?.local().resolve("ReadableStream.cancel", {}); return resolver.promise();
return c.resolver.?.local().promise();
} }
const Cancel = struct { const Cancel = struct {
@@ -250,7 +256,7 @@ pub const AsyncIterator = struct {
pub fn @"return"(self: *AsyncIterator, page: *Page) !js.Promise { pub fn @"return"(self: *AsyncIterator, page: *Page) !js.Promise {
self._reader.releaseLock(); self._reader.releaseLock();
return page.js.resolvePromise(.{ .done = true, .value = null }); return page.js.local.?.resolvePromise(.{ .done = true, .value = null });
} }
pub const JsApi = struct { pub const JsApi = struct {

View File

@@ -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 { pub fn addPendingRead(self: *ReadableStreamDefaultController, page: *Page) !js.Promise {
const resolver = try page.js.createPromiseResolver().persist(); const resolver = page.js.local.?.createPromiseResolver();
try self._pending_reads.append(self._arena, resolver); try self._pending_reads.append(self._arena, try resolver.persist());
return resolver.local().promise(); return resolver.promise();
} }
pub fn enqueue(self: *ReadableStreamDefaultController, chunk: Chunk) !void { pub fn enqueue(self: *ReadableStreamDefaultController, chunk: Chunk) !void {
@@ -79,7 +79,7 @@ pub fn enqueue(self: *ReadableStreamDefaultController, chunk: Chunk) !void {
.done = false, .done = false,
.value = .fromChunk(chunk), .value = .fromChunk(chunk),
}; };
resolver.local().resolve("stream enqueue", result); self._page.js.toLocal(resolver).resolve("stream enqueue", result);
} }
pub fn close(self: *ReadableStreamDefaultController) !void { pub fn close(self: *ReadableStreamDefaultController) !void {
@@ -94,8 +94,8 @@ pub fn close(self: *ReadableStreamDefaultController) !void {
.done = true, .done = true,
.value = .empty, .value = .empty,
}; };
for (self._pending_reads.items) |*resolver| { for (self._pending_reads.items) |resolver| {
resolver.local().resolve("stream close", result); self._page.js.toLocal(resolver).resolve("stream close", result);
} }
self._pending_reads.clearRetainingCapacity(); 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); self._stream._stored_error = try self._page.arena.dupe(u8, err);
// Reject all pending reads // Reject all pending reads
for (self._pending_reads.items) |*resolver| { for (self._pending_reads.items) |resolver| {
resolver.local().reject("stream errror", err); self._page.js.toLocal(resolver).reject("stream errror", err);
} }
self._pending_reads.clearRetainingCapacity(); self._pending_reads.clearRetainingCapacity();
} }

View File

@@ -56,12 +56,12 @@ pub const ReadResult = struct {
pub fn read(self: *ReadableStreamDefaultReader, page: *Page) !js.Promise { pub fn read(self: *ReadableStreamDefaultReader, page: *Page) !js.Promise {
const stream = self._stream orelse { 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) { if (stream._state == .errored) {
const err = stream._stored_error orelse "Stream 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| { if (stream._controller.dequeue()) |chunk| {
@@ -69,7 +69,7 @@ pub fn read(self: *ReadableStreamDefaultReader, page: *Page) !js.Promise {
.done = false, .done = false,
.value = .fromChunk(chunk), .value = .fromChunk(chunk),
}; };
return page.js.resolvePromise(result); return page.js.local.?.resolvePromise(result);
} }
if (stream._state == .closed) { if (stream._state == .closed) {
@@ -77,7 +77,7 @@ pub fn read(self: *ReadableStreamDefaultReader, page: *Page) !js.Promise {
.done = true, .done = true,
.value = .empty, .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 // 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 { pub fn cancel(self: *ReadableStreamDefaultReader, reason_: ?[]const u8, page: *Page) !js.Promise {
const stream = self._stream orelse { const stream = self._stream orelse {
return page.js.rejectPromise("Reader has been released"); return page.js.local.?.rejectPromise("Reader has been released");
}; };
self.releaseLock(); self.releaseLock();

View File

@@ -24,7 +24,6 @@ const Selector = @import("../../browser/webapi/selector/Selector.zig");
const dump = @import("../../browser/dump.zig"); const dump = @import("../../browser/dump.zig");
const js = @import("../../browser/js/js.zig"); const js = @import("../../browser/js/js.zig");
const v8 = js.v8;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
@@ -273,16 +272,32 @@ fn resolveNode(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const page = bc.session.currentPage() orelse return error.PageNotLoaded; const page = bc.session.currentPage() orelse return error.PageNotLoaded;
var js_context = page.js; var ls: ?js.Local.Scope = null;
if (params.executionContextId) |context_id| { defer if (ls) |*_ls| {
if (js_context.debugContextId() != context_id) { _ls.deinit();
for (bc.isolated_worlds.items) |*isolated_world| { };
js_context = &(isolated_world.executor.context orelse return error.ContextNotFound);
if (js_context.debugContextId() == context_id) { if (params.executionContextId) |context_id| blk: {
break; ls = undefined;
} page.js.localScope(&ls.?);
} else return error.ContextNotFound; 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; 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 // 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 // So we use the Node.Union when retrieve the value from the environment
const remote_object = try bc.inspector.getRemoteObject( const remote_object = try bc.inspector.getRemoteObject(
js_context, &ls.?.local,
params.objectGroup orelse "", params.objectGroup orelse "",
node.dom, node.dom,
); );
@@ -377,15 +392,20 @@ fn scrollIntoViewIfNeeded(cmd: anytype) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
fn getNode(arena: Allocator, browser_context: anytype, node_id: ?Node.Id, backend_node_id: ?Node.Id, object_id: ?[]const u8) !*Node { fn getNode(arena: Allocator, bc: anytype, node_id: ?Node.Id, backend_node_id: ?Node.Id, object_id: ?[]const u8) !*Node {
const input_node_id = node_id orelse backend_node_id; const input_node_id = node_id orelse backend_node_id;
if (input_node_id) |input_node_id_| { if (input_node_id) |input_node_id_| {
return browser_context.node_registry.lookup_by_id.get(input_node_id_) orelse return error.NodeNotFound; return bc.node_registry.lookup_by_id.get(input_node_id_) orelse return error.NodeNotFound;
} }
if (object_id) |object_id_| { if (object_id) |object_id_| {
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
// Retrieve the object from which ever context it is in. // Retrieve the object from which ever context it is in.
const parser_node = try browser_context.inspector.getNodePtr(arena, object_id_); const parser_node = try bc.inspector.getNodePtr(arena, object_id_, &ls.local);
return try browser_context.node_registry.register(@ptrCast(@alignCast(parser_node))); return try bc.node_registry.register(@ptrCast(@alignCast(parser_node)));
} }
return error.MissingParams; return error.MissingParams;
} }

View File

@@ -196,9 +196,12 @@ fn createIsolatedWorld(cmd: anytype) !void {
// Create the auxdata json for the contextCreated event // Create the auxdata json for the contextCreated event
// Calling contextCreated will assign a Id to the context and send 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}); 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 { fn navigate(cmd: anytype) !void {
@@ -353,8 +356,13 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
{ {
const page = bc.session.currentPage() orelse return error.PageNotLoaded; 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}); 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( bc.inspector.contextCreated(
page.js, &ls.local,
"", "",
try page.getOrigin(arena) orelse "", try page.getOrigin(arena) orelse "",
aux_data, aux_data,
@@ -363,9 +371,15 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
} }
for (bc.isolated_worlds.items) |*isolated_world| { for (bc.isolated_worlds.items) |*isolated_world| {
const aux_json = try std.fmt.allocPrint(arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{target_id}); 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 // 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( bc.inspector.contextCreated(
&isolated_world.executor.context.?, &ls.local,
isolated_world.name, isolated_world.name,
"://", "://",
aux_json, aux_json,

View File

@@ -19,6 +19,7 @@
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda"); const lp = @import("lightpanda");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const js = @import("../../browser/js/js.zig");
// TODO: hard coded IDs // TODO: hard coded IDs
const LOADER_ID = "LOADERID42AA389647D702B4D805F49A"; const LOADER_ID = "LOADERID42AA389647D702B4D805F49A";
@@ -176,9 +177,13 @@ fn createTarget(cmd: anytype) !void {
const page = try bc.session.createPage(); 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}); const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
bc.inspector.contextCreated( bc.inspector.contextCreated(
page.js, &ls.local,
"", "",
"", // @ZIGDOM "", // @ZIGDOM
// try page.origin(arena), // try page.origin(arena),

View File

@@ -85,15 +85,18 @@ pub fn run(allocator: Allocator, file: []const u8, session: *lp.Session) !void {
const page = try session.createPage(); const page = try session.createPage();
defer session.removePage(); defer session.removePage();
const js_context = page.js; var ls: lp.js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
var try_catch: lp.js.TryCatch = undefined; var try_catch: lp.js.TryCatch = undefined;
try_catch.init(js_context); try_catch.init(&ls.local);
defer try_catch.deinit(); defer try_catch.deinit();
try page.navigate(url, .{}); try page.navigate(url, .{});
_ = session.wait(2000); _ = session.wait(2000);
js_context.eval("testing.assertOk()", "testing.assertOk()") catch |err| { ls.local.eval("testing.assertOk()", "testing.assertOk()") catch |err| {
const caught = try_catch.caughtOrError(allocator, err); const caught = try_catch.caughtOrError(allocator, err);
std.debug.print("{s}: test failure\nError: {f}\n", .{ file, caught }); std.debug.print("{s}: test failure\nError: {f}\n", .{ file, caught });
return err; return err;

View File

@@ -118,20 +118,23 @@ fn run(
_ = page.wait(2000); _ = page.wait(2000);
const js_context = page.js; var ls: lp.js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
var try_catch: lp.js.TryCatch = undefined; var try_catch: lp.js.TryCatch = undefined;
try_catch.init(js_context); try_catch.init(&ls.local);
defer try_catch.deinit(); defer try_catch.deinit();
// Check the final test status. // Check the final test status.
js_context.eval("report.status", "teststatus") catch |err| { ls.local.eval("report.status", "teststatus") catch |err| {
const caught = try_catch.caughtOrError(arena, err); const caught = try_catch.caughtOrError(arena, err);
err_out.* = caught.exception; err_out.* = caught.exception;
return err; return err;
}; };
// return the detailed result. // return the detailed result.
const value = js_context.exec("report.log", "report") catch |err| { const value = ls.local.exec("report.log", "report") catch |err| {
const caught = try_catch.caughtOrError(arena, err); const caught = try_catch.caughtOrError(arena, err);
err_out.* = caught.exception; err_out.* = caught.exception;
return err; return err;

View File

@@ -396,9 +396,12 @@ fn runWebApiTest(test_file: [:0]const u8) !void {
0, 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; var try_catch: js.TryCatch = undefined;
try_catch.init(js_context); try_catch.init(&ls.local);
defer try_catch.deinit(); defer try_catch.deinit();
try page.navigate(url, .{}); try page.navigate(url, .{});
@@ -406,7 +409,7 @@ fn runWebApiTest(test_file: [:0]const u8) !void {
test_browser.runMicrotasks(); 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); const caught = try_catch.caughtOrError(arena_allocator, err);
std.debug.print("{s}: test failure\nError: {f}\n", .{ test_file, caught }); std.debug.print("{s}: test failure\nError: {f}\n", .{ test_file, caught });
return err; return err;