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

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