Remove Global v8::Local<V8::Context>

When we create a js.Context, we create the underlying v8.Context and store it
for the duration of the page lifetime. This works because we have a global
HandleScope - the v8.Context (which is really a v8::Local<v8::Context>) is that
to the global HandleScope, effectively making it a global.

If we want to remove our global HandleScope, then we can no longer pin the
v8.Context in our js.Context. Our js.Context now only holds a v8.Global of the
v8.Context (v8::Global<v8::Context).

This PR introduces a new type, js.Local, which takes over a lot of the
functionality previously found in either js.Caller or js.Context. The simplest
way to think about it is:

1 - For v8 -> zig calls, we create a js.Caller (as always)
2 - For zig -> v8 calls, we go through the js.Context (as always)
3 - The shared functionality, which works on a v8.Context, now belongs to js.Local

For #1 (v8 -> zig), creating a js.Local for a js.Caller is really simple and
centralized. v8 largely gives us everything we need from the
FunctionCallbackInfo or PropertyCallbackInfo.  For #2, it's messier, because we
can only create a local v8::Context if we have a HandleScope, which we may or
may not.

Unfortunately, in many cases, what to do becomes the responsibility of the caller
and much of the code has to become aware of this local-ness. What does it means
for our code? The impact is on WebAPIs that store .Global. Because the global
can't do anything. You always need to convert that .Global to a local
(e.g. js.Function.Global -> js.Function).

If you're 100% sure the WebAPI is only being invoked by a v8 callback, you can
use `page.js.local.?.toLocal(some_global).call(...)` to get the local value.

If you're 100% sure the WebAPI is only being invoked by Zig, you need to create
 `js.Local.Scope` to get access to a local:

```zig
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
ls.toLocal(some_global).call(...)
// can also access `&ls.local` for APIs that require a *const js.Local
```
For functions that can be invoked by either V8 or Zig, you should generally push
the responsibility to the caller by accepting a `local: *const js.Local`. If the
caller is a v8 callback, it can pass `page.js.local.?`. If the caller is a Zig
callback, it can create a `Local.Scope`.

As an alternative, it is possible to simply pass the *Page, and check
`if page.js.local == null` and, if so, create a Local.Scope. But this should only
be done for performance reasons. We currently only do this in 1 place, and it's
because the Zig caller doesn't know whether a Local will actually be needed and
it's potentially called on every element creating from the parser.
This commit is contained in:
Karl Seguin
2026-01-15 08:52:13 +08:00
parent 798ee4a4d5
commit 62aa564df1
60 changed files with 2878 additions and 2613 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1,535 @@
// 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 {
const info = FunctionCallbackInfo{ .handle = handle };
self._constructor(func, info) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
};
}
fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo) !void {
const F = @TypeOf(func);
const args = try self.getArgs(F, 0, info);
const res = @call(.auto, func, args);
const ReturnType = @typeInfo(F).@"fn".return_type orelse {
@compileError(@typeName(F) ++ " has a constructor without a return type");
};
const new_this_handle = info.getThis();
var this = js.Object{ .local = &self.local, .handle = new_this_handle };
if (@typeInfo(ReturnType) == .error_union) {
const non_error_res = res catch |err| return err;
this = try self.local.mapZigInstanceToJs(new_this_handle, non_error_res);
} else {
this = try self.local.mapZigInstanceToJs(new_this_handle, res);
}
// If we got back a different object (existing wrapper), copy the prototype
// from new object. (this happens when we're upgrading an CustomElement)
if (this.handle != new_this_handle) {
const prototype_handle = v8.v8__Object__GetPrototype(new_this_handle).?;
var out: v8.MaybeBool = undefined;
v8.v8__Object__SetPrototype(this.handle, self.local.handle, prototype_handle, &out);
if (comptime IS_DEBUG) {
std.debug.assert(out.has_value and out.value);
}
}
info.getReturnValue().set(this.handle);
}
pub fn method(self: *Caller, comptime T: type, func: anytype, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
const info = FunctionCallbackInfo{ .handle = handle };
self._method(T, func, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
};
}
fn _method(self: *Caller, comptime T: type, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) !void {
const F = @TypeOf(func);
var handle_scope: js.HandleScope = undefined;
handle_scope.init(self.local.isolate);
defer handle_scope.deinit();
var args = try self.getArgs(F, 1, info);
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
const res = @call(.auto, func, args);
info.getReturnValue().set(try self.local.zigValueToJs(res, opts));
}
pub fn function(self: *Caller, comptime T: type, func: anytype, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
const info = FunctionCallbackInfo{ .handle = handle };
self._function(func, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
};
}
fn _function(self: *Caller, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) !void {
const F = @TypeOf(func);
const args = try self.getArgs(F, 0, info);
const res = @call(.auto, func, args);
info.getReturnValue().set(try self.local.zigValueToJs(res, opts));
}
pub fn getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
const info = PropertyCallbackInfo{ .handle = handle };
return self._getIndex(T, func, idx, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
// not intercepted
return 0;
};
}
fn _getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args = try self.getArgs(F, 2, info);
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
@field(args, "1") = idx;
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, true, ret, info, opts);
}
pub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
const info = PropertyCallbackInfo{ .handle = handle };
return self._getNamedIndex(T, func, name, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
// not intercepted
return 0;
};
}
fn _getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args = try self.getArgs(F, 2, info);
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
@field(args, "1") = try self.nameToString(name);
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, true, ret, info, opts);
}
pub fn setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, js_value: *const v8.Value, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
const info = PropertyCallbackInfo{ .handle = handle };
return self._setNamedIndex(T, func, name, .{ .local = &self.local, .handle = js_value }, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
// not intercepted
return 0;
};
}
fn _setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, js_value: js.Value, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args: ParameterTypes(F) = undefined;
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
@field(args, "1") = try self.nameToString(name);
@field(args, "2") = try self.local.jsValueToZig(@TypeOf(@field(args, "2")), js_value);
if (@typeInfo(F).@"fn".params.len == 4) {
@field(args, "3") = self.local.ctx.page;
}
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, false, ret, info, opts);
}
pub fn deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
const info = PropertyCallbackInfo{ .handle = handle };
return self._deleteNamedIndex(T, func, name, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
return 0;
};
}
fn _deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args: ParameterTypes(F) = undefined;
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
@field(args, "1") = try self.nameToString(name);
if (@typeInfo(F).@"fn".params.len == 3) {
@field(args, "2") = self.local.ctx.page;
}
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, false, ret, info, opts);
}
fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, comptime getter: bool, ret: anytype, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
// need to unwrap this error immediately for when opts.null_as_undefined == true
// and we need to compare it to null;
const non_error_ret = switch (@typeInfo(@TypeOf(ret))) {
.error_union => |eu| blk: {
break :blk ret catch |err| {
// We can't compare err == error.NotHandled if error.NotHandled
// isn't part of the possible error set. So we first need to check
// if error.NotHandled is part of the error set.
if (isInErrorSet(error.NotHandled, eu.error_set)) {
if (err == error.NotHandled) {
// not intercepted
return 0;
}
}
self.handleError(T, F, err, info, opts);
// not intercepted
return 0;
};
},
else => ret,
};
if (comptime getter) {
info.getReturnValue().set(try self.local.zigValueToJs(non_error_ret, opts));
}
// intercepted
return 1;
}
fn isInErrorSet(err: anyerror, comptime T: type) bool {
inline for (@typeInfo(T).error_set.?) |e| {
if (err == @field(anyerror, e.name)) return true;
}
return false;
}
fn nameToString(self: *const Caller, name: *const v8.Name) ![]const u8 {
return self.local.valueHandleToString(@ptrCast(name), .{});
}
fn handleError(self: *Caller, comptime T: type, comptime F: type, err: anyerror, info: anytype, comptime opts: CallOpts) void {
const isolate = self.local.isolate;
if (comptime @import("builtin").mode == .Debug and @TypeOf(info) == FunctionCallbackInfo) {
if (log.enabled(.js, .warn)) {
self.logFunctionCallError(@typeName(T), @typeName(F), err, info);
}
}
const js_err: *const v8.Value = switch (err) {
error.InvalidArgument => isolate.createTypeError("invalid argument"),
error.OutOfMemory => isolate.createError("out of memory"),
error.IllegalConstructor => isolate.createError("Illegal Contructor"),
else => blk: {
if (comptime opts.dom_exception) {
const DOMException = @import("../webapi/DOMException.zig");
if (DOMException.fromError(err)) |ex| {
const value = self.local.zigValueToJs(ex, .{}) catch break :blk isolate.createError("internal error");
break :blk value.handle;
}
}
break :blk isolate.createError(@errorName(err));
},
};
const js_exception = isolate.throwException(js_err);
info.getReturnValue().setValueHandle(js_exception);
}
// If we call a method in javascript: cat.lives('nine');
//
// Then we'd expect a Zig function with 2 parameters: a self and the string.
// In this case, offset == 1. Offset is always 1 for setters or methods.
//
// Offset is always 0 for constructors.
//
// For constructors, setters and methods, we can further increase offset + 1
// if the first parameter is an instance of Page.
//
// Finally, if the JS function is called with _more_ parameters and
// the last parameter in Zig is an array, we'll try to slurp the additional
// parameters into the array.
fn getArgs(self: *const Caller, comptime F: type, comptime offset: usize, info: anytype) !ParameterTypes(F) {
const local = &self.local;
var args: ParameterTypes(F) = undefined;
const params = @typeInfo(F).@"fn".params[offset..];
// Except for the constructor, the first parameter is always `self`
// This isn't something we'll bind from JS, so skip it.
const params_to_map = blk: {
if (params.len == 0) {
return args;
}
// If the last parameter is the Page, set it, and exclude it
// from our params slice, because we don't want to bind it to
// a JS argument
if (comptime isPage(params[params.len - 1].type.?)) {
@field(args, tupleFieldName(params.len - 1 + offset)) = local.ctx.page;
break :blk params[0 .. params.len - 1];
}
// we have neither a Page nor a JsObject. All params must be
// bound to a JavaScript value.
break :blk params;
};
if (params_to_map.len == 0) {
return args;
}
const js_parameter_count = info.length();
const last_js_parameter = params_to_map.len - 1;
var is_variadic = false;
{
// This is going to get complicated. If the last Zig parameter
// is a slice AND the corresponding javascript parameter is
// NOT an an array, then we'll treat it as a variadic.
const last_parameter_type = params_to_map[params_to_map.len - 1].type.?;
const last_parameter_type_info = @typeInfo(last_parameter_type);
if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) {
const slice_type = last_parameter_type_info.pointer.child;
const corresponding_js_value = info.getArg(@intCast(last_js_parameter), local);
if (corresponding_js_value.isArray() == false and corresponding_js_value.isTypedArray() == false and slice_type != u8) {
is_variadic = true;
if (js_parameter_count == 0) {
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
} else if (js_parameter_count >= params_to_map.len) {
const arr = try local.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
for (arr, last_js_parameter..) |*a, i| {
a.* = try local.jsValueToZig(slice_type, info.getArg(@intCast(i), local));
}
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;
} else {
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
}
}
}
}
inline for (params_to_map, 0..) |param, i| {
const field_index = comptime i + offset;
if (comptime i == params_to_map.len - 1) {
if (is_variadic) {
break;
}
}
if (comptime isPage(param.type.?)) {
@compileError("Page must be the last parameter (or 2nd last if there's a JsThis): " ++ @typeName(F));
} else if (i >= js_parameter_count) {
if (@typeInfo(param.type.?) != .optional) {
return error.InvalidArgument;
}
@field(args, tupleFieldName(field_index)) = null;
} else {
const js_val = info.getArg(@intCast(i), local);
@field(args, tupleFieldName(field_index)) = local.jsValueToZig(param.type.?, js_val) catch {
return error.InvalidArgument;
};
}
}
return args;
}
// This is extracted to speed up compilation. When left inlined in handleError,
// this can add as much as 10 seconds of compilation time.
fn logFunctionCallError(self: *Caller, type_name: []const u8, func: []const u8, err: anyerror, info: FunctionCallbackInfo) void {
const args_dump = self.serializeFunctionArgs(info) catch "failed to serialize args";
log.info(.js, "function call error", .{
.type = type_name,
.func = func,
.err = err,
.args = args_dump,
.stack = self.local.stackTrace() catch |err1| @errorName(err1),
});
}
fn serializeFunctionArgs(self: *Caller, info: FunctionCallbackInfo) ![]const u8 {
const local = &self.local;
var buf = std.Io.Writer.Allocating.init(local.call_arena);
const separator = log.separator();
for (0..info.length()) |i| {
try buf.writer.print("{s}{d} - ", .{ separator, i + 1 });
const js_value = info.getArg(@intCast(i), local);
try local.debugValue(js_value, &buf.writer);
}
return buf.written();
}
// Takes a function, and returns a tuple for its argument. Used when we
// @call a function
fn ParameterTypes(comptime F: type) type {
const params = @typeInfo(F).@"fn".params;
var fields: [params.len]std.builtin.Type.StructField = undefined;
inline for (params, 0..) |param, i| {
fields[i] = .{
.name = tupleFieldName(i),
.type = param.type.?,
.default_value_ptr = null,
.is_comptime = false,
.alignment = @alignOf(param.type.?),
};
}
return @Type(.{ .@"struct" = .{
.layout = .auto,
.decls = &.{},
.fields = &fields,
.is_tuple = true,
} });
}
fn tupleFieldName(comptime i: usize) [:0]const u8 {
return switch (i) {
0 => "0",
1 => "1",
2 => "2",
3 => "3",
4 => "4",
5 => "5",
6 => "6",
7 => "7",
8 => "8",
9 => "9",
else => std.fmt.comptimePrint("{d}", .{i}),
};
}
fn isPage(comptime T: type) bool {
return T == *Page or T == *const Page;
}
// These wrap the raw v8 C API to provide a cleaner interface.
pub const FunctionCallbackInfo = struct {
handle: *const v8.FunctionCallbackInfo,
pub fn length(self: FunctionCallbackInfo) u32 {
return @intCast(v8.v8__FunctionCallbackInfo__Length(self.handle));
}
pub fn getArg(self: FunctionCallbackInfo, index: u32, local: *const js.Local) js.Value {
return .{ .local = local, .handle = v8.v8__FunctionCallbackInfo__INDEX(self.handle, @intCast(index)).? };
}
pub fn getThis(self: FunctionCallbackInfo) *const v8.Object {
return v8.v8__FunctionCallbackInfo__This(self.handle).?;
}
pub fn getReturnValue(self: FunctionCallbackInfo) ReturnValue {
var rv: v8.ReturnValue = undefined;
v8.v8__FunctionCallbackInfo__GetReturnValue(self.handle, &rv);
return .{ .handle = rv };
}
};
pub const PropertyCallbackInfo = struct {
handle: *const v8.PropertyCallbackInfo,
pub fn getThis(self: PropertyCallbackInfo) *const v8.Object {
return v8.v8__PropertyCallbackInfo__This(self.handle).?;
}
pub fn getReturnValue(self: PropertyCallbackInfo) ReturnValue {
var rv: v8.ReturnValue = undefined;
v8.v8__PropertyCallbackInfo__GetReturnValue(self.handle, &rv);
return .{ .handle = rv };
}
};
const ReturnValue = struct {
handle: v8.ReturnValue,
pub fn set(self: ReturnValue, value: anytype) void {
const T = @TypeOf(value);
if (T == *const v8.Object) {
self.setValueHandle(@ptrCast(value));
} else if (T == *const v8.Value) {
self.setValueHandle(value);
} else if (T == js.Value) {
self.setValueHandle(value.handle);
} else {
@compileError("Unsupported type for ReturnValue.set: " ++ @typeName(T));
}
}
pub fn setValueHandle(self: ReturnValue, handle: *const v8.Value) void {
v8.v8__ReturnValue__Set(self.handle, handle);
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -187,19 +187,27 @@ pub fn dumpMemoryStats(self: *Env) void {
fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void { fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void {
const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?; const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?;
const isolate_handle = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?; const v8_isolate = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
const js_isolate = js.Isolate{ .handle = isolate_handle }; const js_isolate = js.Isolate{ .handle = v8_isolate };
const context = Context.fromIsolate(js_isolate); const ctx = Context.fromIsolate(js_isolate);
const local = js.Local{
.ctx = ctx,
.isolate = js_isolate,
.handle = v8.v8__Isolate__GetCurrentContext(v8_isolate).?,
.call_arena = ctx.call_arena,
};
const value = const value =
if (v8.v8__PromiseRejectMessage__GetValue(&message_handle)) |v8_value| if (v8.v8__PromiseRejectMessage__GetValue(&message_handle)) |v8_value|
context.valueToString(.{ .ctx = context, .handle = v8_value }, .{}) catch |err| @errorName(err) // @HandleScope - no reason to create a js.Context here
local.valueHandleToString(v8_value, .{}) catch |err| @errorName(err)
else else
"no value"; "no value";
log.debug(.js, "unhandled rejection", .{ log.debug(.js, "unhandled rejection", .{
.value = value, .value = value,
.stack = context.stackTrace() catch |err| @errorName(err) orelse "???", .stack = local.stackTrace() catch |err| @errorName(err) orelse "???",
.note = "This should be updated to call window.unhandledrejection", .note = "This should be updated to call window.unhandledrejection",
}); });
} }

View File

@@ -53,7 +53,6 @@ context_arena: ArenaAllocator,
// does all the work, but having all page-specific data structures // does all the work, but having all page-specific data structures
// grouped together helps keep things clean. // grouped together helps keep things clean.
context: ?Context = null, context: ?Context = null,
persisted_context: ?js.Global(Context) = null,
// no init, must be initialized via env.newExecutionWorld() // no init, must be initialized via env.newExecutionWorld()
@@ -77,7 +76,12 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context
const isolate = env.isolate; const isolate = env.isolate;
const arena = self.context_arena.allocator(); const arena = self.context_arena.allocator();
const persisted_context: js.Global(Context) = blk: { // our window wrapped in a v8::Global
var global_global: v8.Global = undefined;
// Create the v8::Context and wrap it in a v8::Global
var context_global: v8.Global = undefined;
const v8_context = blk: {
var temp_scope: js.HandleScope = undefined; var temp_scope: js.HandleScope = undefined;
temp_scope.init(isolate); temp_scope.init(isolate);
defer temp_scope.deinit(); defer temp_scope.deinit();
@@ -97,14 +101,15 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking, .flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
}); });
const context_handle = v8.v8__Context__New(isolate.handle, global_template, null).?; const local_context = v8.v8__Context__New(isolate.handle, global_template, null).?;
break :blk js.Global(Context).init(isolate.handle, context_handle); v8.v8__Global__New(isolate.handle, local_context, &context_global);
const global_obj = v8.v8__Context__Global(local_context).?;
v8.v8__Global__New(isolate.handle, global_obj, &global_global);
break :blk local_context;
}; };
// For a Page we only create one HandleScope, it is stored in the main World (enter==true). A page can have multple contexts, 1 for each World.
// The main Context that enters and holds the HandleScope should therefore always be created first. Following other worlds for this page
// like isolated Worlds, will thereby place their objects on the main page's HandleScope. Note: In the furure the number of context will multiply multiple frames support
const v8_context = persisted_context.local();
var handle_scope: ?js.HandleScope = null; var handle_scope: ?js.HandleScope = null;
if (enter) { if (enter) {
handle_scope = @as(js.HandleScope, undefined); handle_scope = @as(js.HandleScope, undefined);
@@ -123,23 +128,24 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context
.page = page, .page = page,
.id = context_id, .id = context_id,
.isolate = isolate, .isolate = isolate,
.handle = v8_context, .handle = context_global,
.global_global = global_global,
.templates = env.templates, .templates = env.templates,
.handle_scope = handle_scope, .handle_scope = handle_scope,
.script_manager = &page._script_manager, .script_manager = &page._script_manager,
.call_arena = page.call_arena, .call_arena = page.call_arena,
.arena = arena, .arena = arena,
}; };
self.persisted_context = persisted_context;
var context = &self.context.?; var context = &self.context.?;
try context.identity_map.putNoClobber(arena, @intFromPtr(page.window), global_global);
// Store a pointer to our context inside the v8 context so that, given // Store a pointer to our context inside the v8 context so that, given
// a v8 context, we can get our context out // a v8 context, we can get our context out
const data = isolate.initBigInt(@intFromPtr(context)); const data = isolate.initBigInt(@intFromPtr(&self.context.?));
v8.v8__Context__SetEmbedderData(context.handle, 1, @ptrCast(data.handle)); v8.v8__Context__SetEmbedderData(v8_context, 1, @ptrCast(data.handle));
try context.setupGlobal(); return &self.context.?;
return context;
} }
pub fn removeContext(self: *ExecutionWorld) void { pub fn removeContext(self: *ExecutionWorld) void {
@@ -147,9 +153,6 @@ pub fn removeContext(self: *ExecutionWorld) void {
context.deinit(); context.deinit();
self.context = null; self.context = null;
self.persisted_context.?.deinit();
self.persisted_context = null;
self.env.isolate.notifyContextDisposed(); self.env.isolate.notifyContextDisposed();
_ = self.context_arena.reset(.{ .retain_with_limit = CONTEXT_ARENA_RETAIN }); _ = self.context_arena.reset(.{ .retain_with_limit = CONTEXT_ARENA_RETAIN });
} }

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,156 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("js.zig");
const v8 = js.v8;
const bridge = js.bridge;
// When we return a Zig object to V8, we put it on the heap and pass it into
// v8 as an *anyopaque (i.e. void *). When V8 gives us back the value, say, as a
// function parameter, we know what type it _should_ be.
//
// In a simple/perfect world, we could use this knowledge to cast the *anyopaque
// to the parameter type:
// const arg: @typeInfo(@TypeOf(function)).@"fn".params[0] = @ptrCast(v8_data);
//
// But there are 2 reasons we can't do that.
//
// == Reason 1 ==
// The JS code might pass the wrong type:
//
// var cat = new Cat();
// cat.setOwner(new Cat());
//
// The zig_setOwner method expects the 2nd parameter to be an *Owner, but
// the JS code passed a *Cat.
//
// To solve this issue, we tag every returned value so that we can check what
// type it is. In the above case, we'd expect an *Owner, but the tag would tell
// us that we got a *Cat. We use the type index in our Types lookup as the tag.
//
// == Reason 2 ==
// Because of prototype inheritance, even "correct" code can be a challenge. For
// example, say the above JavaScript is fixed:
//
// var cat = new Cat();
// cat.setOwner(new Owner("Leto"));
//
// The issue is that setOwner might not expect an *Owner, but rather a
// *Person, which is the prototype for Owner. Now our Zig code is expecting
// a *Person, but it was (correctly) given an *Owner.
// For this reason, we also store the prototype chain.
const TaggedOpaque = @This();
prototype_len: u16,
prototype_chain: [*]const PrototypeChainEntry,
// Ptr to the Zig instance. Between the context where it's called (i.e.
// we have the comptime parameter info for all functions), and the index field
// we can figure out what type this is.
value: *anyopaque,
// When we're asked to describe an object via the Inspector, we _must_ include
// the proper subtype (and description) fields in the returned JSON.
// V8 will give us a Value and ask us for the subtype. From the js.Value we
// can get a js.Object, and from the js.Object, we can get out TaggedOpaque
// which is where we store the subtype.
subtype: ?bridge.SubType,
pub const PrototypeChainEntry = struct {
index: bridge.JsApiLookup.BackingInt,
offset: u16, // offset to the _proto field
};
// Reverses the mapZigInstanceToJs, making sure that our TaggedOpaque
// contains a ptr to the correct type.
pub fn fromJS(comptime R: type, js_obj_handle: *const v8.Object) !R {
const ti = @typeInfo(R);
if (ti != .pointer) {
@compileError("non-pointer Zig parameter type: " ++ @typeName(R));
}
const T = ti.pointer.child;
const JsApi = bridge.Struct(T).JsApi;
if (@hasDecl(JsApi.Meta, "empty_with_no_proto")) {
// Empty structs aren't stored as TOAs and there's no data
// stored in the JSObject's IntenrnalField. Why bother when
// we can just return an empty struct here?
return @constCast(@as(*const T, &.{}));
}
const internal_field_count = v8.v8__Object__InternalFieldCount(js_obj_handle);
// Special case for Window: the global object doesn't have internal fields
// Window instance is stored in context.page.window instead
if (internal_field_count == 0) {
// Normally, this would be an error. All JsObject that map to a Zig type
// are either `empty_with_no_proto` (handled above) or have an
// interalFieldCount. The only exception to that is the Window...
const isolate = v8.v8__Object__GetIsolate(js_obj_handle).?;
const context = js.Context.fromIsolate(.{ .handle = isolate });
const Window = @import("../webapi/Window.zig");
if (T == Window) {
return context.page.window;
}
// ... Or the window's prototype.
// We could make this all comptime-fancy, but it's easier to hard-code
// the EventTarget
const EventTarget = @import("../webapi/EventTarget.zig");
if (T == EventTarget) {
return context.page.window._proto;
}
// Type not found in Window's prototype chain
return error.InvalidArgument;
}
// if it isn't an empty struct, then the v8.Object should have an
// InternalFieldCount > 0, since our toa pointer should be embedded
// at index 0 of the internal field count.
if (internal_field_count == 0) {
return error.InvalidArgument;
}
if (!bridge.JsApiLookup.has(JsApi)) {
@compileError("unknown Zig type: " ++ @typeName(R));
}
const internal_field_handle = v8.v8__Object__GetInternalField(js_obj_handle, 0).?;
const tao: *TaggedOpaque = @ptrCast(@alignCast(v8.v8__External__Value(internal_field_handle)));
const expected_type_index = bridge.JsApiLookup.getId(JsApi);
const prototype_chain = tao.prototype_chain[0..tao.prototype_len];
if (prototype_chain[0].index == expected_type_index) {
return @ptrCast(@alignCast(tao.value));
}
// Ok, let's walk up the chain
var ptr = @intFromPtr(tao.value);
for (prototype_chain[1..]) |proto| {
ptr += proto.offset; // the offset to the _proto field
const proto_ptr: **anyopaque = @ptrFromInt(ptr);
if (proto.index == expected_type_index) {
return @ptrCast(@alignCast(proto_ptr.*));
}
ptr = @intFromPtr(proto_ptr.*);
}
return error.InvalidArgument;
}

View File

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

View File

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

View File

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

View File

@@ -1,48 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
pub fn Global(comptime T: type) type {
const H = @FieldType(T, "handle");
return struct {
global: v8.Global,
const Self = @This();
pub fn init(isolate: *v8.Isolate, handle: H) Self {
var global: v8.Global = undefined;
v8.v8__Global__New(isolate, handle, &global);
return .{
.global = global,
};
}
pub fn deinit(self: *Self) void {
v8.v8__Global__Reset(&self.global);
}
pub fn local(self: *const Self) H {
return @ptrCast(@alignCast(@as(*const anyopaque, @ptrFromInt(self.global.data_ptr))));
}
};
}

View File

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

View File

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

View File

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

View File

@@ -38,7 +38,7 @@ pub fn getSignal(self: *const AbortController) *AbortSignal {
} }
pub fn abort(self: *AbortController, reason_: ?js.Value.Global, page: *Page) !void { pub fn abort(self: *AbortController, reason_: ?js.Value.Global, page: *Page) !void {
try self._signal.abort(if (reason_) |r| .{ .js_val = r } else null, page); try self._signal.abort(if (reason_) |r| .{ .js_val = r } else null, page.js.local.?, page);
} }
pub const JsApi = struct { pub const JsApi = struct {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -248,8 +248,12 @@ pub fn deliverRecords(self: *MutationObserver, page: *Page) !void {
// Take a copy of the records and clear the list before calling callback // Take a copy of the records and clear the list before calling callback
// This ensures mutations triggered during the callback go into a fresh list // This ensures mutations triggered during the callback go into a fresh list
const records = try self.takeRecords(page); const records = try self.takeRecords(page);
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
var caught: js.TryCatch.Caught = undefined; var caught: js.TryCatch.Caught = undefined;
self._callback.local().tryCall(void, .{ records, self }, &caught) catch |err| { ls.toLocal(self._callback).tryCall(void, .{ records, self }, &caught) catch |err| {
log.err(.page, "MutObserver.deliverRecords", .{ .err = err, .caught = caught }); log.err(.page, "MutObserver.deliverRecords", .{ .err = err, .caught = caught });
return err; return err;
}; };

View File

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

View File

@@ -153,8 +153,13 @@ pub inline fn hasRecords(self: *const PerformanceObserver) bool {
/// Runs the PerformanceObserver's callback with records; emptying it out. /// Runs the PerformanceObserver's callback with records; emptying it out.
pub fn dispatch(self: *PerformanceObserver, page: *Page) !void { pub fn dispatch(self: *PerformanceObserver, page: *Page) !void {
const records = try self.takeRecords(page); const records = try self.takeRecords(page);
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
var caught: js.TryCatch.Caught = undefined; var caught: js.TryCatch.Caught = undefined;
self._callback.local().tryCall(void, .{ EntryList{ ._entries = records }, self }, &caught) catch |err| { ls.toLocal(self._callback).tryCall(void, .{ EntryList{ ._entries = records }, self }, &caught) catch |err| {
log.err(.page, "PerfObserver.dispatch", .{ .err = err, .caught = caught }); log.err(.page, "PerfObserver.dispatch", .{ .err = err, .caught = caught });
return err; return err;
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -61,7 +61,7 @@ pub fn asEvent(self: *PopStateEvent) *Event {
pub fn getState(self: *PopStateEvent, page: *Page) !?js.Value { pub fn getState(self: *PopStateEvent, page: *Page) !?js.Value {
const s = self._state orelse return null; const s = self._state orelse return null;
return try page.js.parseJSON(s); return try page.js.local.?.parseJSON(s);
} }
pub fn hasUAVisualTransition(_: *PopStateEvent) bool { pub fn hasUAVisualTransition(_: *PopStateEvent) bool {

View File

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

View File

@@ -43,11 +43,10 @@ pub fn dispatch(self: *NavigationEventTarget, event_type: DispatchType, page: *P
}; };
}; };
const func = if (@field(self, field)) |*g| g.local() else null;
return page._event_manager.dispatchWithFunction( return page._event_manager.dispatchWithFunction(
self.asEventTarget(), self.asEventTarget(),
event, event,
func, page.js.toLocal(@field(self, field)),
.{ .context = "Navigation" }, .{ .context = "Navigation" },
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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