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);
}
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
switch (listener.function) {
.value => |value| try value.local().callWithThis(void, current_target, .{event}),
.value => |value| try ls.toLocal(value).callWithThis(void, current_target, .{event}),
.string => |string| {
const str = try page.call_arena.dupeZ(u8, string.str());
try self.page.js.eval(str, null);
try ls.local.eval(str, null);
},
.object => |*obj_global| {
const obj = obj_global.local();
.object => |obj_global| {
const obj = ls.toLocal(obj_global);
if (try obj.getFunction("handleEvent")) |handleEvent| {
try handleEvent.callWithThis(void, obj, .{event});
}

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ const v8 = js.v8;
const Array = @This();
ctx: *js.Context,
local: *const js.Local,
handle: *const v8.Array,
pub fn len(self: Array) usize {
@@ -30,39 +30,37 @@ pub fn len(self: Array) usize {
}
pub fn get(self: Array, index: u32) !js.Value {
const ctx = self.ctx;
const ctx = self.local.ctx;
const idx = js.Integer.init(ctx.isolate.handle, index);
const handle = v8.v8__Object__Get(@ptrCast(self.handle), ctx.handle, idx.handle) orelse {
const handle = v8.v8__Object__Get(@ptrCast(self.handle), self.local.handle, idx.handle) orelse {
return error.JsException;
};
return .{
.ctx = self.ctx,
.local = self.local,
.handle = handle,
};
}
pub fn set(self: Array, index: u32, value: anytype, comptime opts: js.bridge.Caller.CallOpts) !bool {
const ctx = self.ctx;
const js_value = try ctx.zigValueToJs(value, opts);
pub fn set(self: Array, index: u32, value: anytype, comptime opts: js.Caller.CallOpts) !bool {
const js_value = try self.local.zigValueToJs(value, opts);
var out: v8.MaybeBool = undefined;
v8.v8__Object__SetAtIndex(@ptrCast(self.handle), ctx.handle, index, js_value.handle, &out);
v8.v8__Object__SetAtIndex(@ptrCast(self.handle), self.local.handle, index, js_value.handle, &out);
return out.has_value;
}
pub fn toObject(self: Array) js.Object {
return .{
.ctx = self.ctx,
.local = self.local,
.handle = @ptrCast(self.handle),
};
}
pub fn toValue(self: Array) js.Value {
return .{
.ctx = self.ctx,
.local = self.local,
.handle = @ptrCast(self.handle),
};
}

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 {
const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?;
const isolate_handle = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
const js_isolate = js.Isolate{ .handle = isolate_handle };
const context = Context.fromIsolate(js_isolate);
const v8_isolate = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
const js_isolate = js.Isolate{ .handle = v8_isolate };
const ctx = Context.fromIsolate(js_isolate);
const local = js.Local{
.ctx = ctx,
.isolate = js_isolate,
.handle = v8.v8__Isolate__GetCurrentContext(v8_isolate).?,
.call_arena = ctx.call_arena,
};
const value =
if (v8.v8__PromiseRejectMessage__GetValue(&message_handle)) |v8_value|
context.valueToString(.{ .ctx = context, .handle = v8_value }, .{}) catch |err| @errorName(err)
// @HandleScope - no reason to create a js.Context here
local.valueHandleToString(v8_value, .{}) catch |err| @errorName(err)
else
"no value";
log.debug(.js, "unhandled rejection", .{
.value = value,
.stack = context.stackTrace() catch |err| @errorName(err) orelse "???",
.stack = local.stackTrace() catch |err| @errorName(err) orelse "???",
.note = "This should be updated to call window.unhandledrejection",
});
}

View File

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

View File

@@ -24,7 +24,7 @@ const Allocator = std.mem.Allocator;
const Function = @This();
ctx: *js.Context,
local: *const js.Local,
this: ?*const v8.Object = null,
handle: *const v8.Function,
@@ -34,34 +34,35 @@ pub const Result = struct {
};
pub fn withThis(self: *const Function, value: anytype) !Function {
const local = self.local;
const this_obj = if (@TypeOf(value) == js.Object)
value.handle
else
(try self.ctx.zigValueToJs(value, .{})).handle;
(try local.zigValueToJs(value, .{})).handle;
return .{
.ctx = self.ctx,
.local = local,
.this = this_obj,
.handle = self.handle,
};
}
pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Object {
const ctx = self.ctx;
const local = self.local;
var try_catch: js.TryCatch = undefined;
try_catch.init(ctx);
try_catch.init(local);
defer try_catch.deinit();
// This creates a new instance using this Function as a constructor.
// const c_args = @as(?[*]const ?*c.Value, @ptrCast(&.{}));
const handle = v8.v8__Function__NewInstance(self.handle, ctx.handle, 0, null) orelse {
caught.* = try_catch.caughtOrError(ctx.call_arena, error.Unknown);
const handle = v8.v8__Function__NewInstance(self.handle, local.handle, 0, null) orelse {
caught.* = try_catch.caughtOrError(local.call_arena, error.Unknown);
return error.JsConstructorFailed;
};
return .{
.ctx = ctx,
.local = local,
.handle = handle,
};
}
@@ -77,17 +78,17 @@ pub fn tryCall(self: *const Function, comptime T: type, args: anytype, caught: *
pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T {
var try_catch: js.TryCatch = undefined;
try_catch.init(self.ctx);
try_catch.init(self.local);
defer try_catch.deinit();
return self.callWithThis(T, this, args) catch |err| {
caught.* = try_catch.caughtOrError(self.ctx.call_arena, err);
caught.* = try_catch.caughtOrError(self.local.ctx.call_arena, err);
return err;
};
}
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
const ctx = self.ctx;
const local = self.local;
// When we're calling a function from within JavaScript itself, this isn't
// necessary. We're within a Caller instantiation, which will already have
@@ -98,6 +99,7 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
// need to increase the call_depth so that the call_arena remains valid for
// the duration of the function call. If we don't do this, the call_arena
// will be reset after each statement of the function which executes Zig code.
const ctx = local.ctx;
const call_depth = ctx.call_depth;
ctx.call_depth = call_depth + 1;
defer ctx.call_depth = call_depth;
@@ -106,7 +108,7 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
if (@TypeOf(this) == js.Object) {
break :blk this;
}
break :blk try ctx.zigValueToJs(this, .{});
break :blk try local.zigValueToJs(this, .{});
};
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
@@ -116,15 +118,15 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
const fields = s.fields;
var js_args: [fields.len]*const v8.Value = undefined;
inline for (fields, 0..) |f, i| {
js_args[i] = (try ctx.zigValueToJs(@field(aargs, f.name), .{})).handle;
js_args[i] = (try local.zigValueToJs(@field(aargs, f.name), .{})).handle;
}
const cargs: [fields.len]*const v8.Value = js_args;
break :blk &cargs;
},
.pointer => blk: {
var values = try ctx.call_arena.alloc(*const v8.Value, args.len);
var values = try local.call_arena.alloc(*const v8.Value, args.len);
for (args, 0..) |a, i| {
values[i] = (try ctx.zigValueToJs(a, .{})).handle;
values[i] = (try local.zigValueToJs(a, .{})).handle;
}
break :blk values;
},
@@ -132,7 +134,7 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
};
const c_args = @as(?[*]const ?*v8.Value, @ptrCast(js_args.ptr));
const handle = v8.v8__Function__Call(self.handle, ctx.handle, js_this.handle, @as(c_int, @intCast(js_args.len)), c_args) orelse {
const handle = v8.v8__Function__Call(self.handle, local.handle, js_this.handle, @as(c_int, @intCast(js_args.len)), c_args) orelse {
// std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"});
return error.JSExecCallback;
};
@@ -140,13 +142,13 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
if (@typeInfo(T) == .void) {
return {};
}
return ctx.jsValueToZig(T, .{ .ctx = ctx, .handle = handle });
return local.jsValueToZig(T, .{ .local = local, .handle = handle });
}
fn getThis(self: *const Function) js.Object {
const handle = if (self.this) |t| t else v8.v8__Context__Global(self.ctx.handle).?;
const handle = if (self.this) |t| t else v8.v8__Context__Global(self.local.handle).?;
return .{
.ctx = self.ctx,
.local = self.local,
.handle = handle,
};
}
@@ -156,30 +158,24 @@ pub fn src(self: *const Function) ![]const u8 {
}
pub fn getPropertyValue(self: *const Function, name: []const u8) !?js.Value {
const ctx = self.ctx;
const key = ctx.isolate.initStringHandle(name);
const handle = v8.v8__Object__Get(self.handle, ctx.handle, key) orelse {
const local = self.local;
const key = local.isolate.initStringHandle(name);
const handle = v8.v8__Object__Get(self.handle, self.local.handle, key) orelse {
return error.JsException;
};
return .{
.ctx = ctx,
.local = local,
.handle = handle,
};
}
pub fn persist(self: *const Function) !Global {
var ctx = self.ctx;
var ctx = self.local.ctx;
var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
try ctx.global_functions.append(ctx.arena, global);
return .{
.handle = global,
.ctx = ctx,
};
return .{ .handle = global };
}
pub fn persistWithThis(self: *const Function, value: anytype) !Global {
@@ -189,16 +185,15 @@ pub fn persistWithThis(self: *const Function, value: anytype) !Global {
pub const Global = struct {
handle: v8.Global,
ctx: *js.Context,
pub fn deinit(self: *Global) void {
v8.v8__Global__Reset(&self.handle);
}
pub fn local(self: *const Global) Function {
pub fn local(self: *const Global, l: *const js.Local) Function {
return .{
.ctx = self.ctx,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)),
.local = l,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
};
}

View File

@@ -20,7 +20,7 @@ const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const Context = @import("Context.zig");
const TaggedOpaque = @import("TaggedOpaque.zig");
const Allocator = std.mem.Allocator;
const RndGen = std.Random.DefaultPrng;
@@ -36,7 +36,7 @@ client: Client,
channel: Channel,
session: Session,
rnd: RndGen = RndGen.init(0),
default_context: ?*const v8.Context = null,
default_context: ?v8.Global,
// We expect allocator to be an arena
// Note: This initializes the pre-allocated inspector in-place
@@ -96,9 +96,9 @@ pub fn init(self: *Inspector, isolate: *v8.Isolate, ctx: anytype) !void {
}
pub fn deinit(self: *const Inspector) void {
var temp_scope: v8.HandleScope = undefined;
v8.v8__HandleScope__CONSTRUCT(&temp_scope, self.isolate);
defer v8.v8__HandleScope__DESTRUCT(&temp_scope);
var hs: v8.HandleScope = undefined;
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate);
defer v8.v8__HandleScope__DESTRUCT(&hs);
self.session.deinit();
self.client.deinit();
@@ -128,7 +128,7 @@ pub fn send(self: *const Inspector, msg: []const u8) void {
// - is_default_context: Whether the execution context is default, should match the auxData
pub fn contextCreated(
self: *Inspector,
context: *const Context,
local: *const js.Local,
name: []const u8,
origin: []const u8,
aux_data: []const u8,
@@ -143,11 +143,11 @@ pub fn contextCreated(
aux_data.ptr,
aux_data.len,
CONTEXT_GROUP_ID,
context.handle,
local.handle,
);
if (is_default_context) {
self.default_context = context.handle;
self.default_context = local.ctx.handle;
}
}
@@ -158,41 +158,42 @@ pub fn contextCreated(
// we'll create it and track it for cleanup when the context ends.
pub fn getRemoteObject(
self: *const Inspector,
context: *Context,
local: *const js.Local,
group: []const u8,
value: anytype,
) !RemoteObject {
const js_value = try context.zigValueToJs(value, .{});
const js_val = try local.zigValueToJs(value, .{});
// We do not want to expose this as a parameter for now
const generate_preview = false;
return self.session.wrapObject(
context.isolate.handle,
context.handle,
js_value.handle,
local.isolate.handle,
local.handle,
js_val.handle,
group,
generate_preview,
);
}
// 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
// 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
// what Context.typeTaggedAnyOpaque does.
pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []const u8) !*anyopaque {
// what TaggedOpaque.fromJS does.
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);
// The values context and groupId are not used here
const js_val = unwrapped.value;
if (!v8.v8__Value__IsObject(js_val)) {
return error.ObjectIdIsNotANode;
}
const Node = @import("../webapi/Node.zig");
// Cast to *const v8.Object for typeTaggedAnyOpaque
return Context.typeTaggedAnyOpaque(*Node, @ptrCast(js_val)) catch {
return error.ObjectIdIsNotANode;
};
return TaggedOpaque.fromJS(*Node, @ptrCast(js_val)) catch return error.ObjectIdIsNotANode;
}
pub const RemoteObject = struct {
@@ -399,7 +400,7 @@ fn fromData(data: *anyopaque) *Inspector {
return @ptrCast(@alignCast(data));
}
pub fn getTaggedAnyOpaque(value: *const v8.Value) ?*js.TaggedAnyOpaque {
pub fn getTaggedOpaque(value: *const v8.Value) ?*TaggedOpaque {
if (!v8.v8__Value__IsObject(value)) {
return null;
}
@@ -469,7 +470,8 @@ pub export fn v8_inspector__Client__IMPL__ensureDefaultContextInGroup(
data: *anyopaque,
) callconv(.c) ?*const v8.Context {
const inspector: *Inspector = @ptrCast(@alignCast(data));
return inspector.default_context;
const global_handle = inspector.default_context orelse return null;
return v8.v8__Global__Get(&global_handle, inspector.isolate);
}
pub export fn v8_inspector__Channel__IMPL__sendResponse(

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -27,7 +27,7 @@ const Allocator = std.mem.Allocator;
const Value = @This();
ctx: *js.Context,
local: *const js.Local,
handle: *const v8.Value,
pub fn isObject(self: Value) bool {
@@ -155,12 +155,12 @@ pub fn isPromise(self: Value) bool {
}
pub fn toBool(self: Value) bool {
return v8.v8__Value__BooleanValue(self.handle, self.ctx.isolate.handle);
return v8.v8__Value__BooleanValue(self.handle, self.local.isolate.handle);
}
pub fn typeOf(self: Value) js.String {
const str_handle = v8.v8__Value__TypeOf(self.handle, self.ctx.isolate.handle).?;
return js.String{ .ctx = self.ctx, .handle = str_handle };
const str_handle = v8.v8__Value__TypeOf(self.handle, self.local.isolate.handle).?;
return js.String{ .local = self.local, .handle = str_handle };
}
pub fn toF32(self: Value) !f32 {
@@ -169,7 +169,7 @@ pub fn toF32(self: Value) !f32 {
pub fn toF64(self: Value) !f64 {
var maybe: v8.MaybeF64 = undefined;
v8.v8__Value__NumberValue(self.handle, self.ctx.handle, &maybe);
v8.v8__Value__NumberValue(self.handle, self.local.handle, &maybe);
if (!maybe.has_value) {
return error.JsException;
}
@@ -178,7 +178,7 @@ pub fn toF64(self: Value) !f64 {
pub fn toI32(self: Value) !i32 {
var maybe: v8.MaybeI32 = undefined;
v8.v8__Value__Int32Value(self.handle, self.ctx.handle, &maybe);
v8.v8__Value__Int32Value(self.handle, self.local.handle, &maybe);
if (!maybe.has_value) {
return error.JsException;
}
@@ -187,7 +187,7 @@ pub fn toI32(self: Value) !i32 {
pub fn toU32(self: Value) !u32 {
var maybe: v8.MaybeU32 = undefined;
v8.v8__Value__Uint32Value(self.handle, self.ctx.handle, &maybe);
v8.v8__Value__Uint32Value(self.handle, self.local.handle, &maybe);
if (!maybe.has_value) {
return error.JsException;
}
@@ -199,7 +199,7 @@ pub fn toPromise(self: Value) js.Promise {
std.debug.assert(self.isPromise());
}
return .{
.ctx = self.ctx,
.local = self.local,
.handle = @ptrCast(self.handle),
};
}
@@ -212,53 +212,42 @@ pub fn toStringZ(self: Value, opts: js.String.ToZigOpts) ![:0]u8 {
}
pub fn toJson(self: Value, allocator: Allocator) ![]u8 {
const json_str_handle = v8.v8__JSON__Stringify(self.ctx.handle, self.handle, null) orelse return error.JsException;
return self.ctx.jsStringToZig(json_str_handle, .{ .allocator = allocator });
const json_str_handle = v8.v8__JSON__Stringify(self.local.handle, self.handle, null) orelse return error.JsException;
return self.local.jsStringToZig(json_str_handle, .{ .allocator = allocator });
}
fn _toString(self: Value, comptime null_terminate: bool, opts: js.String.ToZigOpts) !(if (null_terminate) [:0]u8 else []u8) {
const ctx = self.ctx;
const l = self.local;
if (self.isSymbol()) {
const sym_handle = v8.v8__Symbol__Description(@ptrCast(self.handle), ctx.isolate.handle).?;
return _toString(.{ .handle = @ptrCast(sym_handle), .ctx = ctx }, null_terminate, opts);
const sym_handle = v8.v8__Symbol__Description(@ptrCast(self.handle), l.isolate.handle).?;
return _toString(.{ .handle = @ptrCast(sym_handle), .local = l }, null_terminate, opts);
}
const str_handle = v8.v8__Value__ToString(self.handle, ctx.handle) orelse {
const str_handle = v8.v8__Value__ToString(self.handle, l.handle) orelse {
return error.JsException;
};
const str = js.String{ .ctx = ctx, .handle = str_handle };
const str = js.String{ .local = l, .handle = str_handle };
if (comptime null_terminate) {
return js.String.toZigZ(str, opts);
}
return js.String.toZig(str, opts);
}
pub fn fromJson(ctx: *js.Context, json: []const u8) !Value {
const v8_isolate = v8.Isolate{ .handle = ctx.isolate.handle };
const json_string = v8.String.initUtf8(v8_isolate, json);
const v8_context = v8.Context{ .handle = ctx.handle };
const value = try v8.Json.parse(v8_context, json_string);
return .{ .ctx = ctx, .handle = value.handle };
}
pub fn persist(self: Value) !Global {
var ctx = self.ctx;
var ctx = self.local.ctx;
var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
try ctx.global_values.append(ctx.arena, global);
return .{
.handle = global,
.ctx = ctx,
};
return .{ .handle = global };
}
pub fn toZig(self: Value, comptime T: type) !T {
return self.ctx.jsValueToZig(T, self);
return self.local.jsValueToZig(T, self);
}
pub fn toObject(self: Value) js.Object {
@@ -267,7 +256,7 @@ pub fn toObject(self: Value) js.Object {
}
return .{
.ctx = self.ctx,
.local = self.local,
.handle = @ptrCast(self.handle),
};
}
@@ -278,7 +267,7 @@ pub fn toArray(self: Value) js.Array {
}
return .{
.ctx = self.ctx,
.local = self.local,
.handle = @ptrCast(self.handle),
};
}
@@ -295,7 +284,7 @@ pub fn toBigInt(self: Value) js.BigInt {
pub fn format(self: Value, writer: *std.Io.Writer) !void {
if (comptime IS_DEBUG) {
return self.ctx.debugValue(self, writer);
return self.local.debugValue(self, writer);
}
const str = self.toString(.{}) catch return error.WriteFailed;
return writer.writeAll(str);
@@ -303,16 +292,15 @@ pub fn format(self: Value, writer: *std.Io.Writer) !void {
pub const Global = struct {
handle: v8.Global,
ctx: *js.Context,
pub fn deinit(self: *Global) void {
v8.v8__Global__Reset(&self.handle);
}
pub fn local(self: *const Global) Value {
pub fn local(self: *const Global, l: *const js.Local) Value {
return .{
.ctx = self.ctx,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)),
.local = l,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
};
}

View File

@@ -20,555 +20,15 @@ const std = @import("std");
const js = @import("js.zig");
const lp = @import("lightpanda");
const log = @import("../../log.zig");
const Page = @import("../Page.zig");
const v8 = js.v8;
const Caller = @import("Caller.zig");
const Context = @import("Context.zig");
const Page = @import("../Page.zig");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const CALL_ARENA_RETAIN = 1024 * 16;
const IS_DEBUG = @import("builtin").mode == .Debug;
// ============================================================================
// Internal Callback Info Wrappers
// ============================================================================
// These wrap the raw v8 C API to provide a cleaner interface.
// They are not exported - internal to this module only.
const Value = struct {
handle: *const v8.Value,
fn isArray(self: Value) bool {
return v8.v8__Value__IsArray(self.handle);
}
fn isTypedArray(self: Value) bool {
return v8.v8__Value__IsTypedArray(self.handle);
}
fn isFunction(self: Value) bool {
return v8.v8__Value__IsFunction(self.handle);
}
};
const Name = struct {
handle: *const v8.Name,
};
const FunctionCallbackInfo = struct {
handle: *const v8.FunctionCallbackInfo,
fn length(self: FunctionCallbackInfo) u32 {
return @intCast(v8.v8__FunctionCallbackInfo__Length(self.handle));
}
fn getArg(self: FunctionCallbackInfo, index: u32) Value {
return .{ .handle = v8.v8__FunctionCallbackInfo__INDEX(self.handle, @intCast(index)).? };
}
fn getThis(self: FunctionCallbackInfo) *const v8.Object {
return v8.v8__FunctionCallbackInfo__This(self.handle).?;
}
fn getReturnValue(self: FunctionCallbackInfo) ReturnValue {
var rv: v8.ReturnValue = undefined;
v8.v8__FunctionCallbackInfo__GetReturnValue(self.handle, &rv);
return .{ .handle = rv };
}
fn isConstructCall(self: FunctionCallbackInfo) bool {
return v8.v8__FunctionCallbackInfo__IsConstructCall(self.handle);
}
};
const PropertyCallbackInfo = struct {
handle: *const v8.PropertyCallbackInfo,
fn getThis(self: PropertyCallbackInfo) *const v8.Object {
return v8.v8__PropertyCallbackInfo__This(self.handle).?;
}
fn getReturnValue(self: PropertyCallbackInfo) ReturnValue {
var rv: v8.ReturnValue = undefined;
v8.v8__PropertyCallbackInfo__GetReturnValue(self.handle, &rv);
return .{ .handle = rv };
}
};
const ReturnValue = struct {
handle: v8.ReturnValue,
fn set(self: ReturnValue, value: anytype) void {
const T = @TypeOf(value);
if (T == Value) {
self.setValueHandle(value.handle);
} else if (T == *const v8.Object) {
self.setValueHandle(@ptrCast(value));
} else if (T == *const v8.Value) {
self.setValueHandle(value);
} else if (T == js.Value) {
self.setValueHandle(value.handle);
} else {
@compileError("Unsupported type for ReturnValue.set: " ++ @typeName(T));
}
}
fn setValueHandle(self: ReturnValue, handle: *const v8.Value) void {
v8.v8__ReturnValue__Set(self.handle, handle);
}
};
// ============================================================================
// Caller - Responsible for calling Zig functions from JS invocations
// ============================================================================
pub const Caller = struct {
context: *Context,
isolate: js.Isolate,
call_arena: Allocator,
// Takes the raw v8 isolate and extracts the context from it.
pub fn init(v8_isolate: *v8.Isolate) Caller {
const isolate = js.Isolate{ .handle = v8_isolate };
const v8_context_handle = v8.v8__Isolate__GetCurrentContext(v8_isolate);
const embedder_data = v8.v8__Context__GetEmbedderData(v8_context_handle, 1);
var lossless: bool = undefined;
const context: *Context = @ptrFromInt(v8.v8__BigInt__Uint64Value(embedder_data, &lossless));
context.call_depth += 1;
return .{
.context = context,
.isolate = isolate,
.call_arena = context.call_arena,
};
}
pub fn deinit(self: *Caller) void {
const context = self.context;
const call_depth = context.call_depth - 1;
// Because of callbacks, calls can be nested. Because of this, we
// can't clear the call_arena after _every_ call. Imagine we have
// arr.forEach((i) => { console.log(i); }
//
// First we call forEach. Inside of our forEach call,
// we call console.log. If we reset the call_arena after this call,
// it'll reset it for the `forEach` call after, which might still
// need the data.
//
// Therefore, we keep a call_depth, and only reset the call_arena
// when a top-level (call_depth == 0) function ends.
if (call_depth == 0) {
const arena: *ArenaAllocator = @ptrCast(@alignCast(context.call_arena.ptr));
_ = arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
}
context.call_depth = call_depth;
}
pub const CallOpts = struct {
dom_exception: bool = false,
null_as_undefined: bool = false,
as_typed_array: bool = false,
};
pub fn constructor(self: *Caller, comptime T: type, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) void {
if (!info.isConstructCall()) {
self.handleError(T, @TypeOf(func), error.InvalidArgument, info, opts);
return;
}
self._constructor(func, info) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
};
}
fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo) !void {
const F = @TypeOf(func);
const args = try self.getArgs(F, 0, info);
const res = @call(.auto, func, args);
const ReturnType = @typeInfo(F).@"fn".return_type orelse {
@compileError(@typeName(F) ++ " has a constructor without a return type");
};
const new_this_handle = info.getThis();
var this = js.Object{ .ctx = self.context, .handle = new_this_handle };
if (@typeInfo(ReturnType) == .error_union) {
const non_error_res = res catch |err| return err;
this = try self.context.mapZigInstanceToJs(new_this_handle, non_error_res);
} else {
this = try self.context.mapZigInstanceToJs(new_this_handle, res);
}
// If we got back a different object (existing wrapper), copy the prototype
// from new object. (this happens when we're upgrading an CustomElement)
if (this.handle != new_this_handle) {
const prototype_handle = v8.v8__Object__GetPrototype(new_this_handle).?;
var out: v8.MaybeBool = undefined;
v8.v8__Object__SetPrototype(this.handle, self.context.handle, prototype_handle, &out);
if (comptime IS_DEBUG) {
std.debug.assert(out.has_value and out.value);
}
}
info.getReturnValue().set(this.handle);
}
pub fn method(self: *Caller, comptime T: type, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) void {
self._method(T, func, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
};
}
fn _method(self: *Caller, comptime T: type, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) !void {
const F = @TypeOf(func);
var handle_scope: js.HandleScope = undefined;
handle_scope.init(self.isolate);
defer handle_scope.deinit();
var args = try self.getArgs(F, 1, info);
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
const res = @call(.auto, func, args);
info.getReturnValue().set(try self.context.zigValueToJs(res, opts));
}
pub fn function(self: *Caller, comptime T: type, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) void {
self._function(func, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
};
}
fn _function(self: *Caller, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) !void {
const F = @TypeOf(func);
const context = self.context;
const args = try self.getArgs(F, 0, info);
const res = @call(.auto, func, args);
info.getReturnValue().set(try context.zigValueToJs(res, opts));
}
pub fn getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._getIndex(T, func, idx, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
// not intercepted
return 0;
};
}
fn _getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args = try self.getArgs(F, 2, info);
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = idx;
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, true, ret, info, opts);
}
pub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: Name, info: PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._getNamedIndex(T, func, name, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
// not intercepted
return 0;
};
}
fn _getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args = try self.getArgs(F, 2, info);
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = try self.nameToString(name);
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, true, ret, info, opts);
}
pub fn setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: Name, js_value: Value, info: PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._setNamedIndex(T, func, name, js_value, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
// not intercepted
return 0;
};
}
fn _setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: Name, js_value: Value, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args: ParameterTypes(F) = undefined;
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = try self.nameToString(name);
@field(args, "2") = try self.context.jsValueToZig(@TypeOf(@field(args, "2")), js.Value{ .ctx = self.context, .handle = js_value.handle });
if (@typeInfo(F).@"fn".params.len == 4) {
@field(args, "3") = self.context.page;
}
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, false, ret, info, opts);
}
pub fn deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: Name, info: PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._deleteNamedIndex(T, func, name, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
return 0;
};
}
fn _deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args: ParameterTypes(F) = undefined;
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = try self.nameToString(name);
if (@typeInfo(F).@"fn".params.len == 3) {
@field(args, "2") = self.context.page;
}
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, false, ret, info, opts);
}
fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, comptime getter: bool, ret: anytype, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
// need to unwrap this error immediately for when opts.null_as_undefined == true
// and we need to compare it to null;
const non_error_ret = switch (@typeInfo(@TypeOf(ret))) {
.error_union => |eu| blk: {
break :blk ret catch |err| {
// We can't compare err == error.NotHandled if error.NotHandled
// isn't part of the possible error set. So we first need to check
// if error.NotHandled is part of the error set.
if (isInErrorSet(error.NotHandled, eu.error_set)) {
if (err == error.NotHandled) {
// not intercepted
return 0;
}
}
self.handleError(T, F, err, info, opts);
// not intercepted
return 0;
};
},
else => ret,
};
if (comptime getter) {
info.getReturnValue().set(try self.context.zigValueToJs(non_error_ret, opts));
}
// intercepted
return 1;
}
fn isInErrorSet(err: anyerror, comptime T: type) bool {
inline for (@typeInfo(T).error_set.?) |e| {
if (err == @field(anyerror, e.name)) return true;
}
return false;
}
fn nameToString(self: *Caller, name: Name) ![]const u8 {
return self.context.valueToString(js.Value{ .ctx = self.context, .handle = @ptrCast(name.handle) }, .{});
}
fn handleError(self: *Caller, comptime T: type, comptime F: type, err: anyerror, info: anytype, comptime opts: CallOpts) void {
const isolate = self.isolate;
if (comptime @import("builtin").mode == .Debug and @TypeOf(info) == FunctionCallbackInfo) {
if (log.enabled(.js, .warn)) {
self.logFunctionCallError(@typeName(T), @typeName(F), err, info);
}
}
const js_err: *const v8.Value = switch (err) {
error.InvalidArgument => isolate.createTypeError("invalid argument"),
error.OutOfMemory => isolate.createError("out of memory"),
error.IllegalConstructor => isolate.createError("Illegal Contructor"),
else => blk: {
if (comptime opts.dom_exception) {
const DOMException = @import("../webapi/DOMException.zig");
if (DOMException.fromError(err)) |ex| {
const value = self.context.zigValueToJs(ex, .{}) catch break :blk isolate.createError("internal error");
break :blk value.handle;
}
}
break :blk isolate.createError(@errorName(err));
},
};
const js_exception = isolate.throwException(js_err);
info.getReturnValue().setValueHandle(js_exception);
}
// If we call a method in javascript: cat.lives('nine');
//
// Then we'd expect a Zig function with 2 parameters: a self and the string.
// In this case, offset == 1. Offset is always 1 for setters or methods.
//
// Offset is always 0 for constructors.
//
// For constructors, setters and methods, we can further increase offset + 1
// if the first parameter is an instance of Page.
//
// Finally, if the JS function is called with _more_ parameters and
// the last parameter in Zig is an array, we'll try to slurp the additional
// parameters into the array.
fn getArgs(self: *const Caller, comptime F: type, comptime offset: usize, info: anytype) !ParameterTypes(F) {
const context = self.context;
var args: ParameterTypes(F) = undefined;
const params = @typeInfo(F).@"fn".params[offset..];
// Except for the constructor, the first parameter is always `self`
// This isn't something we'll bind from JS, so skip it.
const params_to_map = blk: {
if (params.len == 0) {
return args;
}
// If the last parameter is the Page, set it, and exclude it
// from our params slice, because we don't want to bind it to
// a JS argument
if (comptime isPage(params[params.len - 1].type.?)) {
@field(args, tupleFieldName(params.len - 1 + offset)) = self.context.page;
break :blk params[0 .. params.len - 1];
}
// we have neither a Page nor a JsObject. All params must be
// bound to a JavaScript value.
break :blk params;
};
if (params_to_map.len == 0) {
return args;
}
const js_parameter_count = info.length();
const last_js_parameter = params_to_map.len - 1;
var is_variadic = false;
{
// This is going to get complicated. If the last Zig parameter
// is a slice AND the corresponding javascript parameter is
// NOT an an array, then we'll treat it as a variadic.
const last_parameter_type = params_to_map[params_to_map.len - 1].type.?;
const last_parameter_type_info = @typeInfo(last_parameter_type);
if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) {
const slice_type = last_parameter_type_info.pointer.child;
const corresponding_js_value = info.getArg(@as(u32, @intCast(last_js_parameter)));
if (corresponding_js_value.isArray() == false and corresponding_js_value.isTypedArray() == false and slice_type != u8) {
is_variadic = true;
if (js_parameter_count == 0) {
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
} else if (js_parameter_count >= params_to_map.len) {
const arr = try self.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
for (arr, last_js_parameter..) |*a, i| {
const js_value = info.getArg(@as(u32, @intCast(i)));
a.* = try context.jsValueToZig(slice_type, js.Value{ .ctx = context, .handle = js_value.handle });
}
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;
} else {
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
}
}
}
}
inline for (params_to_map, 0..) |param, i| {
const field_index = comptime i + offset;
if (comptime i == params_to_map.len - 1) {
if (is_variadic) {
break;
}
}
if (comptime isPage(param.type.?)) {
@compileError("Page must be the last parameter (or 2nd last if there's a JsThis): " ++ @typeName(F));
} else if (i >= js_parameter_count) {
if (@typeInfo(param.type.?) != .optional) {
return error.InvalidArgument;
}
@field(args, tupleFieldName(field_index)) = null;
} else {
const js_value = info.getArg(@as(u32, @intCast(i)));
@field(args, tupleFieldName(field_index)) = context.jsValueToZig(param.type.?, js.Value{ .ctx = context, .handle = js_value.handle }) catch {
return error.InvalidArgument;
};
}
}
return args;
}
// This is extracted to speed up compilation. When left inlined in handleError,
// this can add as much as 10 seconds of compilation time.
fn logFunctionCallError(self: *Caller, type_name: []const u8, func: []const u8, err: anyerror, info: FunctionCallbackInfo) void {
const args_dump = self.serializeFunctionArgs(info) catch "failed to serialize args";
log.info(.js, "function call error", .{
.type = type_name,
.func = func,
.err = err,
.args = args_dump,
.stack = self.context.stackTrace() catch |err1| @errorName(err1),
});
}
fn serializeFunctionArgs(self: *Caller, info: FunctionCallbackInfo) ![]const u8 {
const context = self.context;
var buf = std.Io.Writer.Allocating.init(context.call_arena);
const separator = log.separator();
for (0..info.length()) |i| {
try buf.writer.print("{s}{d} - ", .{ separator, i + 1 });
const val = info.getArg(@intCast(i));
try context.debugValue(js.Value{ .ctx = context, .handle = val.handle }, &buf.writer);
}
return buf.written();
}
// Takes a function, and returns a tuple for its argument. Used when we
// @call a function
fn ParameterTypes(comptime F: type) type {
const params = @typeInfo(F).@"fn".params;
var fields: [params.len]std.builtin.Type.StructField = undefined;
inline for (params, 0..) |param, i| {
fields[i] = .{
.name = tupleFieldName(i),
.type = param.type.?,
.default_value_ptr = null,
.is_comptime = false,
.alignment = @alignOf(param.type.?),
};
}
return @Type(.{ .@"struct" = .{
.layout = .auto,
.decls = &.{},
.fields = &fields,
.is_tuple = true,
} });
}
fn tupleFieldName(comptime i: usize) [:0]const u8 {
return switch (i) {
0 => "0",
1 => "1",
2 => "2",
3 => "3",
4 => "4",
5 => "5",
6 => "6",
7 => "7",
8 => "8",
9 => "9",
else => std.fmt.comptimePrint("{d}", .{i}),
};
}
fn isPage(comptime T: type) bool {
return T == *Page or T == *const Page;
}
};
// ============================================================================
// Bridge Builder Functions
// ============================================================================
pub fn Builder(comptime T: type) type {
return struct {
pub const @"type" = T;
@@ -610,8 +70,9 @@ pub fn Builder(comptime T: type) type {
@compileError("Property for " ++ @typeName(@TypeOf(value)) ++ " hasn't been defined yet");
}
pub fn prototypeChain() [prototypeChainLength(T)]js.PrototypeChainEntry {
var entries: [prototypeChainLength(T)]js.PrototypeChainEntry = undefined;
const PrototypeChainEntry = @import("TaggedOpaque.zig").PrototypeChainEntry;
pub fn prototypeChain() [prototypeChainLength(T)]PrototypeChainEntry {
var entries: [prototypeChainLength(T)]PrototypeChainEntry = undefined;
entries[0] = .{ .offset = 0, .index = JsApiLookup.getId(T.JsApi) };
@@ -644,11 +105,11 @@ pub const Constructor = struct {
return .{ .func = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
var caller: Caller = undefined;
caller.init(v8_isolate);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
caller.constructor(T, func, info, .{
caller.constructor(T, func, handle.?, .{
.dom_exception = opts.dom_exception,
});
}
@@ -673,18 +134,18 @@ pub const Function = struct {
.func = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
var caller: Caller = undefined;
caller.init(v8_isolate);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
if (comptime opts.static) {
caller.function(T, func, info, .{
caller.function(T, func, handle.?, .{
.dom_exception = opts.dom_exception,
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
} else {
caller.method(T, func, info, .{
caller.method(T, func, handle.?, .{
.dom_exception = opts.dom_exception,
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
@@ -717,17 +178,17 @@ pub const Accessor = struct {
accessor.getter = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
var caller: Caller = undefined;
caller.init(v8_isolate);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
if (comptime opts.static) {
caller.function(T, getter, info, .{
caller.function(T, getter, handle.?, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
} else {
caller.method(T, getter, info, .{
caller.method(T, getter, handle.?, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
@@ -740,15 +201,11 @@ pub const Accessor = struct {
accessor.setter = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
var caller: Caller = undefined;
caller.init(v8_isolate);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
if (comptime IS_DEBUG) {
lp.assert(info.length() == 1, "bridge.setter", .{ .len = info.length() });
}
caller.method(T, setter, info, .{
caller.method(T, setter, handle.?, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
@@ -772,11 +229,11 @@ pub const Indexed = struct {
return .{ .getter = struct {
fn wrap(idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
var caller: Caller = undefined;
caller.init(v8_isolate);
defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? };
return caller.getIndex(T, getter, idx, info, .{
return caller.getIndex(T, getter, idx, handle.?, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
@@ -799,11 +256,11 @@ pub const NamedIndexed = struct {
const getter_fn = struct {
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
var caller: Caller = undefined;
caller.init(v8_isolate);
defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? };
return caller.getNamedIndex(T, getter, .{ .handle = c_name.? }, info, .{
return caller.getNamedIndex(T, getter, c_name.?, handle.?, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
@@ -813,11 +270,11 @@ pub const NamedIndexed = struct {
const setter_fn = if (@typeInfo(@TypeOf(setter)) == .null) null else struct {
fn wrap(c_name: ?*const v8.Name, c_value: ?*const v8.Value, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
var caller: Caller = undefined;
caller.init(v8_isolate);
defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? };
return caller.setNamedIndex(T, setter, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info, .{
return caller.setNamedIndex(T, setter, c_name.?, c_value.?, handle.?, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
@@ -827,11 +284,11 @@ pub const NamedIndexed = struct {
const deleter_fn = if (@typeInfo(@TypeOf(deleter)) == .null) null else struct {
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
var caller: Caller = undefined;
caller.init(v8_isolate);
defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? };
return caller.deleteNamedIndex(T, deleter, .{ .handle = c_name.? }, info, .{
return caller.deleteNamedIndex(T, deleter, c_name.?, handle.?, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
@@ -861,7 +318,7 @@ pub const Iterator = struct {
.async = opts.async,
.func = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const info = FunctionCallbackInfo{ .handle = handle.? };
const info = Caller.FunctionCallbackInfo{ .handle = handle.? };
info.getReturnValue().set(info.getThis());
}
}.wrap,
@@ -873,11 +330,10 @@ pub const Iterator = struct {
.func = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
var caller: Caller = undefined;
caller.init(v8_isolate);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
caller.method(T, struct_or_func, info, .{});
caller.method(T, struct_or_func, handle.?, .{});
}
}.wrap,
};
@@ -895,11 +351,11 @@ pub const Callable = struct {
return .{ .func = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
var caller: Caller = undefined;
caller.init(v8_isolate);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
caller.method(T, func, info, .{
caller.method(T, func, handle.?, .{
.null_as_undefined = opts.null_as_undefined,
});
}
@@ -912,52 +368,57 @@ pub const Property = union(enum) {
};
pub fn unknownPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const isolate_handle = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
const context = Context.fromIsolate(.{ .handle = isolate_handle });
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller: Caller = undefined;
caller.init(v8_isolate);
defer caller.deinit();
const property: []const u8 = context.valueToString(.{ .ctx = context, .handle = c_name.? }, .{}) catch {
const local = &caller.local;
var hs: js.HandleScope = undefined;
hs.init(local.isolate);
defer hs.deinit();
const property: []const u8 = local.valueHandleToString(@ptrCast(c_name.?), .{}) catch {
return 0;
};
const ignored = std.StaticStringMap(void).initComptime(.{
.{ "process", {} },
.{ "ShadyDOM", {} },
.{ "ShadyCSS", {} },
const page = local.ctx.page;
const document = page.document;
.{ "litNonce", {} },
.{ "litHtmlVersions", {} },
.{ "litElementVersions", {} },
.{ "litHtmlPolyfillSupport", {} },
.{ "litElementHydrateSupport", {} },
.{ "litElementPolyfillSupport", {} },
.{ "reactiveElementVersions", {} },
if (document.getElementById(property, page)) |el| {
const js_val = local.zigValueToJs(el, .{}) catch return 0;
var pc = Caller.PropertyCallbackInfo{ .handle = handle.? };
pc.getReturnValue().set(js_val);
return 1;
}
.{ "recaptcha", {} },
.{ "grecaptcha", {} },
.{ "___grecaptcha_cfg", {} },
.{ "__recaptcha_api", {} },
.{ "__google_recaptcha_client", {} },
if (comptime IS_DEBUG) {
const ignored = std.StaticStringMap(void).initComptime(.{
.{ "process", {} },
.{ "ShadyDOM", {} },
.{ "ShadyCSS", {} },
.{ "CLOSURE_FLAGS", {} },
});
.{ "litNonce", {} },
.{ "litHtmlVersions", {} },
.{ "litElementVersions", {} },
.{ "litHtmlPolyfillSupport", {} },
.{ "litElementHydrateSupport", {} },
.{ "litElementPolyfillSupport", {} },
.{ "reactiveElementVersions", {} },
if (!ignored.has(property)) {
const page = context.page;
const document = page.document;
.{ "recaptcha", {} },
.{ "grecaptcha", {} },
.{ "___grecaptcha_cfg", {} },
.{ "__recaptcha_api", {} },
.{ "__google_recaptcha_client", {} },
if (document.getElementById(property, page)) |el| {
const js_value = context.zigValueToJs(el, .{}) catch {
return 0;
};
var pc = PropertyCallbackInfo{ .handle = handle.? };
pc.getReturnValue().set(js_value);
return 1;
}
if (comptime IS_DEBUG) {
.{ "CLOSURE_FLAGS", {} },
});
if (!ignored.has(property)) {
log.debug(.unknown_prop, "unknown global property", .{
.info = "but the property can exist in pure JS",
.stack = context.stackTrace() catch "???",
.stack = local.stackTrace() catch "???",
.property = property,
});
}

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 bridge = @import("bridge.zig");
pub const ExecutionWorld = @import("ExecutionWorld.zig");
pub const Caller = @import("Caller.zig");
pub const Context = @import("Context.zig");
pub const Local = @import("Local.zig");
pub const Inspector = @import("Inspector.zig");
pub const Snapshot = @import("Snapshot.zig");
pub const Platform = @import("Platform.zig");
@@ -43,7 +45,6 @@ pub const Module = @import("Module.zig");
pub const BigInt = @import("BigInt.zig");
pub const Number = @import("Number.zig");
pub const Integer = @import("Integer.zig");
pub const Global = @import("global.zig").Global;
pub const PromiseResolver = @import("PromiseResolver.zig");
const Allocator = std.mem.Allocator;
@@ -78,11 +79,11 @@ pub const ArrayBuffer = struct {
};
pub const Exception = struct {
ctx: *const Context,
local: *const Local,
handle: *const v8.Value,
pub fn exception(self: Exception, allocator: Allocator) ![]const u8 {
return self.context.valueToString(self.inner, .{ .allocator = allocator });
return self.local.valueToString(self.handel, .{ .allocator = allocator });
}
};
@@ -215,61 +216,6 @@ pub fn simpleZigValueToJs(isolate: Isolate, value: anytype, comptime fail: bool,
}
return null;
}
// When we return a Zig object to V8, we put it on the heap and pass it into
// v8 as an *anyopaque (i.e. void *). When V8 gives us back the value, say, as a
// function parameter, we know what type it _should_ be.
//
// In a simple/perfect world, we could use this knowledge to cast the *anyopaque
// to the parameter type:
// const arg: @typeInfo(@TypeOf(function)).@"fn".params[0] = @ptrCast(v8_data);
//
// But there are 2 reasons we can't do that.
//
// == Reason 1 ==
// The JS code might pass the wrong type:
//
// var cat = new Cat();
// cat.setOwner(new Cat());
//
// The zig_setOwner method expects the 2nd parameter to be an *Owner, but
// the JS code passed a *Cat.
//
// To solve this issue, we tag every returned value so that we can check what
// type it is. In the above case, we'd expect an *Owner, but the tag would tell
// us that we got a *Cat. We use the type index in our Types lookup as the tag.
//
// == Reason 2 ==
// Because of prototype inheritance, even "correct" code can be a challenge. For
// example, say the above JavaScript is fixed:
//
// var cat = new Cat();
// cat.setOwner(new Owner("Leto"));
//
// The issue is that setOwner might not expect an *Owner, but rather a
// *Person, which is the prototype for Owner. Now our Zig code is expecting
// a *Person, but it was (correctly) given an *Owner.
// For this reason, we also store the prototype chain.
pub const TaggedAnyOpaque = struct {
prototype_len: u16,
prototype_chain: [*]const PrototypeChainEntry,
// Ptr to the Zig instance. Between the context where it's called (i.e.
// we have the comptime parameter info for all functions), and the index field
// we can figure out what type this is.
value: *anyopaque,
// When we're asked to describe an object via the Inspector, we _must_ include
// the proper subtype (and description) fields in the returned JSON.
// V8 will give us a Value and ask us for the subtype. From the js.Value we
// can get a js.Object, and from the js.Object, we can get out TaggedAnyOpaque
// which is where we store the subtype.
subtype: ?bridge.SubType,
};
pub const PrototypeChainEntry = struct {
index: bridge.JsApiLookup.BackingInt,
offset: u16, // offset to the _proto field
};
// These are here, and not in Inspector.zig, because Inspector.zig isn't always
// included (e.g. in the wpt build).
@@ -281,7 +227,7 @@ pub export fn v8_inspector__Client__IMPL__valueSubtype(
_: *v8.InspectorClientImpl,
c_value: *const v8.Value,
) callconv(.c) [*c]const u8 {
const external_entry = Inspector.getTaggedAnyOpaque(c_value) orelse return null;
const external_entry = Inspector.getTaggedOpaque(c_value) orelse return null;
return if (external_entry.subtype) |st| @tagName(st) else null;
}
@@ -298,11 +244,11 @@ pub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype(
// We _must_ include a non-null description in order for the subtype value
// to be included. Besides that, I don't know if the value has any meaning
const external_entry = Inspector.getTaggedAnyOpaque(c_value) orelse return null;
const external_entry = Inspector.getTaggedOpaque(c_value) orelse return null;
return if (external_entry.subtype == null) null else "";
}
test "TaggedAnyOpaque" {
// If we grow this, fine, but it should be a conscious decision
try std.testing.expectEqual(24, @sizeOf(TaggedAnyOpaque));
try std.testing.expectEqual(24, @sizeOf(@import("TaggedOpaque.zig")));
}

View File

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

View File

@@ -1,6 +1 @@
<!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 {
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 {

View File

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

View File

@@ -206,7 +206,7 @@ fn writeBlobParts(
/// Returns a Promise that resolves with the contents of the blob
/// as binary data contained in an ArrayBuffer.
pub fn arrayBuffer(self: *const Blob, page: *Page) !js.Promise {
return page.js.resolvePromise(js.ArrayBuffer{ .values = self._slice });
return page.js.local.?.resolvePromise(js.ArrayBuffer{ .values = self._slice });
}
const ReadableStream = @import("streams/ReadableStream.zig");
@@ -219,7 +219,7 @@ pub fn stream(self: *const Blob, page: *Page) !*ReadableStream {
/// Returns a Promise that resolves with a string containing
/// the contents of the blob, interpreted as UTF-8.
pub fn text(self: *const Blob, page: *Page) !js.Promise {
return page.js.resolvePromise(self._slice);
return page.js.local.?.resolvePromise(self._slice);
}
/// Extension to Blob; works on Firefox and Safari.
@@ -227,7 +227,7 @@ pub fn text(self: *const Blob, page: *Page) !js.Promise {
/// Returns a Promise that resolves with a Uint8Array containing
/// the contents of the blob as an array of bytes.
pub fn bytes(self: *const Blob, page: *Page) !js.Promise {
return page.js.resolvePromise(js.TypedArray(u8){ .values = self._slice });
return page.js.local.?.resolvePromise(js.TypedArray(u8){ .values = self._slice });
}
/// Returns a new Blob object which contains data

View File

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

View File

@@ -106,7 +106,7 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu
}
if (self._when_defined.fetchRemove(name)) |entry| {
entry.value.local().resolve("whenDefined", constructor);
page.js.toLocal(entry.value).resolve("whenDefined", constructor);
}
}
@@ -120,22 +120,23 @@ pub fn upgrade(self: *CustomElementRegistry, root: *Node, page: *Page) !void {
}
pub fn whenDefined(self: *CustomElementRegistry, name: []const u8, page: *Page) !js.Promise {
const local = page.js.local.?;
if (self._definitions.get(name)) |definition| {
return page.js.resolvePromise(definition.constructor);
return local.resolvePromise(definition.constructor);
}
const gop = try self._when_defined.getOrPut(page.arena, name);
if (gop.found_existing) {
return gop.value_ptr.local().promise();
return local.toLocal(gop.value_ptr.*).promise();
}
errdefer _ = self._when_defined.remove(name);
const owned_name = try page.dupeString(name);
const resolver = try page.js.createPromiseResolver().persist();
const resolver = local.createPromiseResolver();
gop.key_ptr.* = owned_name;
gop.value_ptr.* = resolver;
gop.value_ptr.* = try resolver.persist();
return resolver.local().promise();
return resolver.promise();
}
fn upgradeNode(self: *CustomElementRegistry, node: *Node, page: *Page) !void {
@@ -174,8 +175,12 @@ pub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinitio
page._upgrading_element = node;
defer page._upgrading_element = prev_upgrading;
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
var caught: js.TryCatch.Caught = undefined;
_ = definition.constructor.local().newInstance(&caught) catch |err| {
_ = ls.toLocal(definition.constructor).newInstance(&caught) catch |err| {
log.warn(.js, "custom element upgrade", .{ .name = definition.name, .err = err, .caught = caught });
return error.CustomElementUpgradeFailed;
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -246,7 +246,12 @@ pub fn deliverEntries(self: *IntersectionObserver, page: *Page) !void {
const entries = try self.takeRecords(page);
var caught: js.TryCatch.Caught = undefined;
self._callback.local().tryCall(void, .{ entries, self }, &caught) catch |err| {
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
ls.toLocal(self._callback).tryCall(void, .{ entries, self }, &caught) catch |err| {
log.err(.page, "IntsctObserver.deliverEntries", .{ .err = err, .caught = caught });
return err;
};

View File

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

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
// This ensures mutations triggered during the callback go into a fresh list
const records = try self.takeRecords(page);
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
var caught: js.TryCatch.Caught = undefined;
self._callback.local().tryCall(void, .{ records, self }, &caught) catch |err| {
ls.toLocal(self._callback).tryCall(void, .{ records, self }, &caught) catch |err| {
log.err(.page, "MutObserver.deliverRecords", .{ .err = err, .caught = caught });
return err;
};

View File

@@ -65,9 +65,9 @@ pub const SHOW_DOCUMENT_TYPE: u32 = 0x200;
pub const SHOW_DOCUMENT_FRAGMENT: u32 = 0x400;
pub const SHOW_NOTATION: u32 = 0x800;
pub fn acceptNode(self: *const NodeFilter, node: *Node) !i32 {
pub fn acceptNode(self: *const NodeFilter, node: *Node, local: *const js.Local) !i32 {
const func = self._func orelse return FILTER_ACCEPT;
return func.local().call(i32, .{node});
return local.toLocal(func).call(i32, .{node});
}
pub fn shouldShow(node: *const Node, what_to_show: u32) bool {

View File

@@ -362,10 +362,10 @@ pub const Mark = struct {
pub const Measure = struct {
_proto: *Entry,
_detail: ?js.Object.Global,
_detail: ?js.Value.Global,
const Options = struct {
detail: ?js.Object = null,
detail: ?js.Value = null,
start: ?TimestampOrMark,
end: ?TimestampOrMark,
duration: ?f64 = null,
@@ -378,7 +378,7 @@ pub const Measure = struct {
pub fn init(
name: []const u8,
maybe_detail: ?js.Object,
maybe_detail: ?js.Value,
start_timestamp: f64,
end_timestamp: f64,
maybe_duration: ?f64,
@@ -405,7 +405,7 @@ pub const Measure = struct {
return m;
}
pub fn getDetail(self: *const Measure) ?js.Object.Global {
pub fn getDetail(self: *const Measure) ?js.Value.Global {
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.
pub fn dispatch(self: *PerformanceObserver, page: *Page) !void {
const records = try self.takeRecords(page);
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
var caught: js.TryCatch.Caught = undefined;
self._callback.local().tryCall(void, .{ EntryList{ ._entries = records }, self }, &caught) catch |err| {
ls.toLocal(self._callback).tryCall(void, .{ EntryList{ ._entries = records }, self }, &caught) catch |err| {
log.err(.page, "PerfObserver.dispatch", .{ .err = err, .caught = caught });
return err;
};

View File

@@ -96,10 +96,10 @@ pub fn generateKey(
page: *Page,
) !js.Promise {
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
@@ -115,7 +115,7 @@ pub fn exportKey(
}
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
@@ -125,7 +125,7 @@ pub fn exportKey(
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.
@@ -140,14 +140,14 @@ pub fn deriveBits(
.ecdh_or_x25519 => |p| {
const name = p.name;
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")) {
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 => {
// Verify algorithm.
if (!algorithm.isHMAC()) {
return page.js.rejectPromise(@errorName(error.InvalidAccessError));
return page.js.local.?.rejectPromise(@errorName(error.InvalidAccessError));
}
// Call sign for HMAC.
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 => {
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) {
// CRYPTO_memcmp compare in constant time so prohibits time-based attacks.
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.

View File

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

View File

@@ -46,20 +46,22 @@ pub fn getPending(_: *const Animation) bool {
pub fn getFinished(self: *Animation, page: *Page) !js.Promise {
if (self._finished_resolver == null) {
const resolver = try page.js.createPromiseResolver().persist();
resolver.local().resolve("Animation.getFinished", self);
self._finished_resolver = resolver;
const resolver = page.js.local.?.createPromiseResolver();
resolver.resolve("Animation.getFinished", self);
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 {
// never resolved, because we're always "finished"
if (self._ready_resolver == null) {
const resolver = try page.js.createPromiseResolver().persist();
self._ready_resolver = resolver;
const resolver = page.js.local.?.createPromiseResolver();
self._ready_resolver = try resolver.persist();
return resolver.promise();
}
return self._ready_resolver.?.local().promise();
return page.js.toLocal(self._ready_resolver).?.promise();
}
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;
_ = text;
// TODO: clear self.css_rules
return page.js.resolvePromise({});
return page.js.local.?.resolvePromise({});
}
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 {
const el = node.as(Element);
const on_load = el.getAttributeSafe("onload") orelse return;
if (page.js.stringToFunction(on_load)) |func| {
page.window._on_load = try func.persist();
if (page.js.stringToPersistedFunction(on_load)) |func| {
page.window._on_load = func;
} else |err| {
log.err(.js, "body.onload", .{ .err = err, .str = on_load });
}

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 {
_ = definition;
const ctx = page.js;
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
// Get the JS element object
const js_val = ctx.zigValueToJs(element, .{}) catch return;
const js_val = ls.local.zigValueToJs(element, .{}) catch return;
const js_element = js_val.toObject();
// Call the callback method if it exists
@@ -195,8 +197,26 @@ pub fn checkAndAttachBuiltIn(element: *Element, page: *Page) !void {
page._upgrading_element = node;
defer page._upgrading_element = prev_upgrading;
// PERFORMANCE OPTIMIZATION: This pattern is discouraged in general code.
// Used here because: (1) multiple early returns before needing Local,
// (2) called from both V8 callbacks (Local exists) and parser (no Local).
// Prefer either: requiring *const js.Local parameter, OR always creating
// Local.Scope upfront.
var ls: ?js.Local.Scope = null;
var local = blk: {
if (page.js.local) |l| {
break :blk l;
}
ls = undefined;
page.js.localScope(&ls.?);
break :blk &ls.?.local;
};
defer if (ls) |*_ls| {
_ls.deinit();
};
var caught: js.TryCatch.Caught = undefined;
_ = definition.constructor.local().newInstance(&caught) catch |err| {
_ = local.toLocal(definition.constructor).newInstance(&caught) catch |err| {
log.warn(.js, "custom builtin ctor", .{ .name = is_value, .err = err, .caught = caught });
return;
};
@@ -207,9 +227,11 @@ fn invokeCallback(self: *Custom, comptime callback_name: [:0]const u8, args: any
return;
}
const ctx = page.js;
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
const js_val = ctx.zigValueToJs(self, .{}) catch return;
const js_val = ls.local.zigValueToJs(self, .{}) catch return;
const js_element = js_val.toObject();
js_element.callMethod(void, callback_name, args) catch return;

View File

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

View File

@@ -27,8 +27,8 @@ const Allocator = std.mem.Allocator;
const CustomEvent = @This();
_proto: *Event,
_arena: Allocator,
_detail: ?js.Value.Global = null,
_arena: Allocator,
const CustomEventOptions = struct {
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 {
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 {

View File

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

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(
self.asEventTarget(),
event,
func,
page.js.toLocal(@field(self, field)),
.{ .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 {
if (self._state.source == .navigation) {
if (self._state.value) |value| {
return .{ .value = try page.js.parseJSON(value) };
return .{ .value = try page.js.local.?.parseJSON(value) };
}
}

View File

@@ -44,12 +44,14 @@ pub const InitOpts = Request.InitOpts;
pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
const request = try Request.init(input, options, page);
const resolver = page.js.local.?.createPromiseResolver();
const fetch = try page.arena.create(Fetch);
fetch.* = .{
._page = page,
._buf = .empty,
._url = try page.arena.dupe(u8, request._url),
._resolver = try page.js.createPromiseResolver().persist(),
._resolver = try resolver.persist(),
._response = try Response.init(null, .{ .status = 0 }, page),
};
@@ -77,7 +79,7 @@ pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
.done_callback = httpDoneCallback,
.error_callback = httpErrorCallback,
});
return fetch._resolver.local().promise();
return resolver.promise();
}
pub fn deinit(self: *Fetch) void {
@@ -149,13 +151,22 @@ fn httpDoneCallback(ctx: *anyopaque) !void {
.len = self._buf.items.len,
});
return self._resolver.local().resolve("fetch done", self._response);
var ls: js.Local.Scope = undefined;
self._page.js.localScope(&ls);
defer ls.deinit();
return ls.toLocal(self._resolver).resolve("fetch done", self._response);
}
fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
const self: *Fetch = @ptrCast(@alignCast(ctx));
self._response._type = .@"error"; // Set type to error for network failures
self._resolver.local().reject("fetch error", @errorName(err));
var ls: js.Local.Scope = undefined;
self._page.js.localScope(&ls);
defer ls.deinit();
ls.toLocal(self._resolver).reject("fetch error", @errorName(err));
}
const testing = @import("../../../testing.zig");

View File

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

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

View File

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

View File

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

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

View File

@@ -56,12 +56,12 @@ pub const ReadResult = struct {
pub fn read(self: *ReadableStreamDefaultReader, page: *Page) !js.Promise {
const stream = self._stream orelse {
return page.js.rejectPromise("Reader has been released");
return page.js.local.?.rejectPromise("Reader has been released");
};
if (stream._state == .errored) {
const err = stream._stored_error orelse "Stream errored";
return page.js.rejectPromise(err);
return page.js.local.?.rejectPromise(err);
}
if (stream._controller.dequeue()) |chunk| {
@@ -69,7 +69,7 @@ pub fn read(self: *ReadableStreamDefaultReader, page: *Page) !js.Promise {
.done = false,
.value = .fromChunk(chunk),
};
return page.js.resolvePromise(result);
return page.js.local.?.resolvePromise(result);
}
if (stream._state == .closed) {
@@ -77,7 +77,7 @@ pub fn read(self: *ReadableStreamDefaultReader, page: *Page) !js.Promise {
.done = true,
.value = .empty,
};
return page.js.resolvePromise(result);
return page.js.local.?.resolvePromise(result);
}
// No data, but not closed. We need to queue the read for any future data
@@ -93,7 +93,7 @@ pub fn releaseLock(self: *ReadableStreamDefaultReader) void {
pub fn cancel(self: *ReadableStreamDefaultReader, reason_: ?[]const u8, page: *Page) !js.Promise {
const stream = self._stream orelse {
return page.js.rejectPromise("Reader has been released");
return page.js.local.?.rejectPromise("Reader has been released");
};
self.releaseLock();

View File

@@ -24,7 +24,6 @@ const Selector = @import("../../browser/webapi/selector/Selector.zig");
const dump = @import("../../browser/dump.zig");
const js = @import("../../browser/js/js.zig");
const v8 = js.v8;
const Allocator = std.mem.Allocator;
@@ -273,16 +272,32 @@ fn resolveNode(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
var js_context = page.js;
if (params.executionContextId) |context_id| {
if (js_context.debugContextId() != context_id) {
for (bc.isolated_worlds.items) |*isolated_world| {
js_context = &(isolated_world.executor.context orelse return error.ContextNotFound);
if (js_context.debugContextId() == context_id) {
break;
}
} else return error.ContextNotFound;
var ls: ?js.Local.Scope = null;
defer if (ls) |*_ls| {
_ls.deinit();
};
if (params.executionContextId) |context_id| blk: {
ls = undefined;
page.js.localScope(&ls.?);
if (ls.?.local.debugContextId() == context_id) {
break :blk;
}
// not the default scope, check the other ones
for (bc.isolated_worlds.items) |*isolated_world| {
ls.?.deinit();
ls = null;
const ctx = &(isolated_world.executor.context orelse return error.ContextNotFound);
ls = undefined;
ctx.localScope(&ls.?);
if (ls.?.local.debugContextId() == context_id) {
break :blk;
}
} else return error.ContextNotFound;
} else {
ls = undefined;
page.js.localScope(&ls.?);
}
const input_node_id = params.nodeId orelse params.backendNodeId orelse return error.InvalidParam;
@@ -291,7 +306,7 @@ fn resolveNode(cmd: anytype) !void {
// node._node is a *DOMNode we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement
// So we use the Node.Union when retrieve the value from the environment
const remote_object = try bc.inspector.getRemoteObject(
js_context,
&ls.?.local,
params.objectGroup orelse "",
node.dom,
);
@@ -377,15 +392,20 @@ fn scrollIntoViewIfNeeded(cmd: anytype) !void {
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;
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_| {
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.
const parser_node = try browser_context.inspector.getNodePtr(arena, object_id_);
return try browser_context.node_registry.register(@ptrCast(@alignCast(parser_node)));
const parser_node = try bc.inspector.getNodePtr(arena, object_id_, &ls.local);
return try bc.node_registry.register(@ptrCast(@alignCast(parser_node)));
}
return error.MissingParams;
}

View File

@@ -196,9 +196,12 @@ fn createIsolatedWorld(cmd: anytype) !void {
// Create the auxdata json for the contextCreated event
// Calling contextCreated will assign a Id to the context and send the contextCreated event
const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{params.frameId});
bc.inspector.contextCreated(js_context, world.name, "", aux_data, false);
var ls: js.Local.Scope = undefined;
js_context.localScope(&ls);
defer ls.deinit();
return cmd.sendResult(.{ .executionContextId = js_context.debugContextId() }, .{});
bc.inspector.contextCreated(&ls.local, world.name, "", aux_data, false);
return cmd.sendResult(.{ .executionContextId = ls.local.debugContextId() }, .{});
}
fn navigate(cmd: anytype) !void {
@@ -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 aux_data = try std.fmt.allocPrint(arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
bc.inspector.contextCreated(
page.js,
&ls.local,
"",
try page.getOrigin(arena) orelse "",
aux_data,
@@ -363,9 +371,15 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
}
for (bc.isolated_worlds.items) |*isolated_world| {
const aux_json = try std.fmt.allocPrint(arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{target_id});
// Calling contextCreated will assign a new Id to the context and send the contextCreated event
var ls: js.Local.Scope = undefined;
(isolated_world.executor.context orelse continue).localScope(&ls);
defer ls.deinit();
bc.inspector.contextCreated(
&isolated_world.executor.context.?,
&ls.local,
isolated_world.name,
"://",
aux_json,

View File

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

View File

@@ -85,15 +85,18 @@ pub fn run(allocator: Allocator, file: []const u8, session: *lp.Session) !void {
const page = try session.createPage();
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;
try_catch.init(js_context);
try_catch.init(&ls.local);
defer try_catch.deinit();
try page.navigate(url, .{});
_ = 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);
std.debug.print("{s}: test failure\nError: {f}\n", .{ file, caught });
return err;

View File

@@ -118,20 +118,23 @@ fn run(
_ = 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;
try_catch.init(js_context);
try_catch.init(&ls.local);
defer try_catch.deinit();
// 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);
err_out.* = caught.exception;
return err;
};
// 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);
err_out.* = caught.exception;
return err;

View File

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