Files
browser/src/browser/js/Local.zig
Karl Seguin 01ecb296e5 Rework finalizers
This commit involves a number of changes to finalizers, all aimed towards
better consistency and reliability.

A big part of this has to do with v8::Inspector's ability to move objects
across IsolatedWorlds. There has been a few previous efforts on this, the most
significant being https://github.com/lightpanda-io/browser/pull/1901. To recap,
a Zig instance can map to 0-N v8::Objects. Where N is the total number of
IsolatedWorlds. Generally, IsolatedWorlds between origins are...isolated...but
the v8::Inspector isn't bound by this. So a Zig instance cannot be tied to a
Context/Identity/IsolatedWorld...it has to live until all references, possibly
from different IsolatedWorlds, are released (or the page is reset).

Finalizers could previously be managed via reference counting or explicitly
toggling the instance as weak/strong. Now, only reference counting is supported.
weak/strong can essentially be seen as an acquireRef (rc += 1) and
releaseRef (rc -= 1). Explicit setting did make some things easier, like not
having to worry so much about double-releasing (e.g. XHR abort being called
multiple times), but it was only used in a few places AND it simply doesn't work
with objects shared between IsolatedWorlds. It is never a boolean now, as 3
different IsolatedWorlds can each hold a reference.

Temps and Globals are tracked on the Session. Previously, they were tracked on
the Identity, but that makes no sense. If a Zig instance can outlive an Identity,
then any of its Temp references can too. This hasn't been a problem because we've
only seen MutationObserver and IntersectionObserver be used cross-origin,
but the right CDP script can make this crash with a use-after-free (e.g.
`MessageEvent.data` is released when the Identity is done, but `MessageEvent` is
still referenced by a different IsolateWorld).

Rather than deinit with a `comptime shutdown: bool`, there is now an explicit
`releaseRef` and `deinit`.

Bridge registration has been streamlined. Previously, types had to register
their finalizer AND acquireRef/releaseRef/deinit had to be declared on the entire
prototype chain, even if these methods just delegated to their proto. Finalizers
are now automatically enabled if a type has a `acquireRef` function. If a type
has an `acquireRef`, then it must have a `releaseRef` and a `deinit`. So if
there's custom cleanup to do in `deinit`, then you also have to define
`acquireRef` and `releaseRef` which will just delegate to the _proto.

Furthermore these finalizer methods can be defined anywhere on the chain.

Previously:

```zig
const KeywboardEvent = struct {
  _proto: *Event,
  ...

  pub fn deinit(self: *KeyboardEvent, session: *Session) void {
    self._proto.deinit(session);
  }

  pub fn releaseRef(self: *KeyboardEvent, session: *Session) void {
    self._proto.releaseRef(session);
  }
}
```

```zig
const KeyboardEvent = struct {
  _proto: *Event,
  ...
  // no deinit, releaseRef, acquireref
}
```

Since the `KeyboardEvent` doesn't participate in finalization directly, it
doesn't have to define anything. The bridge will detect the most specific place
they are defined and call them there.
2026-03-28 21:11:23 +08:00

1513 lines
59 KiB
Zig

// 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 string = @import("../../string.zig");
const Session = @import("../Session.zig");
const js = @import("js.zig");
const bridge = @import("bridge.zig");
const Caller = @import("Caller.zig");
const Context = @import("Context.zig");
const Isolate = @import("Isolate.zig");
const TaggedOpaque = @import("TaggedOpaque.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
const v8 = js.v8;
const CallOpts = Caller.CallOpts;
// Where js.Context has a lifetime tied to the page, and holds the
// v8::Global<v8::Context>, this has a much shorter lifetime and holds a
// v8::Local<v8::Context>. In V8, you need a Local<v8::Context> or get anything
// done, but the local only exists for the lifetime of the HandleScope it was
// created on. When V8 calls into Zig, things are pretty straightforward, since
// that callback gives us the currenty-entered V8::Local<Context>. But when Zig
// has to call into V8, it's a bit more messy.
// As a general rule, think of it this way:
// 1 - Caller.zig is for V8 -> Zig
// 2 - Context.zig is for Zig -> V8
// The Local is encapsulates the data and logic they both need. It just happens
// that it's easier to use Local from Caller than from Context.
const Local = @This();
ctx: *Context,
handle: *const v8.Context,
// available on ctx, but accessed often, so pushed into the Local
isolate: Isolate,
call_arena: std.mem.Allocator,
pub fn newString(self: *const Local, str: []const u8) js.String {
return .{
.local = self,
.handle = self.isolate.initStringHandle(str),
};
}
pub fn newObject(self: *const Local) js.Object {
return .{
.local = self,
.handle = v8.v8__Object__New(self.isolate.handle).?,
};
}
pub fn newArray(self: *const Local, len: u32) js.Array {
return .{
.local = self,
.handle = v8.v8__Array__New(self.isolate.handle, @intCast(len)).?,
};
}
/// Creates a new typed array. Memory is owned by JS context.
/// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Typed_arrays
pub fn createTypedArray(self: *const Local, comptime array_type: js.ArrayType, size: usize) js.ArrayBufferRef(array_type) {
return .init(self, size);
}
pub fn newCallback(
self: *const Local,
callback: anytype,
data: anytype,
) js.Function {
const external = self.isolate.createExternal(data);
const handle = v8.v8__Function__New__DEFAULT2(self.handle, struct {
fn wrap(info_handle: ?*const js.v8.FunctionCallbackInfo) callconv(.c) void {
Caller.Function.call(@TypeOf(data), info_handle.?, callback, .{ .embedded_receiver = true });
}
}.wrap, @ptrCast(external)).?;
return .{ .local = self, .handle = handle };
}
pub fn runMacrotasks(self: *const Local) void {
const env = self.ctx.env;
env.pumpMessageLoop();
env.runMicrotasks(); // macrotasks can cause microtasks to queue
}
pub fn runMicrotasks(self: *const Local) void {
self.ctx.env.runMicrotasks();
}
// == Executors ==
pub fn eval(self: *const Local, src: []const u8, name: ?[]const u8) !void {
_ = try self.exec(src, name);
}
pub fn exec(self: *const Local, src: []const u8, name: ?[]const u8) !js.Value {
return self.compileAndRun(src, name);
}
/// Compiles a function body as function.
///
/// https://v8.github.io/api/head/classv8_1_1ScriptCompiler.html#a3a15bb5a7dfc3f998e6ac789e6b4646a
pub fn compileFunction(
self: *const Local,
function_body: []const u8,
/// We tend to know how many params we'll pass; can remove the comptime if necessary.
comptime parameter_names: []const []const u8,
extensions: []const v8.Object,
) !js.Function {
// TODO: Make configurable.
const script_name = self.isolate.initStringHandle("anonymous");
const script_source = self.isolate.initStringHandle(function_body);
var parameter_list: [parameter_names.len]*const v8.String = undefined;
inline for (0..parameter_names.len) |i| {
parameter_list[i] = self.isolate.initStringHandle(parameter_names[i]);
}
// Create `ScriptOrigin`.
var origin: v8.ScriptOrigin = undefined;
v8.v8__ScriptOrigin__CONSTRUCT(&origin, script_name);
// Create `ScriptCompilerSource`.
var script_compiler_source: v8.ScriptCompilerSource = undefined;
v8.v8__ScriptCompiler__Source__CONSTRUCT2(script_source, &origin, null, &script_compiler_source);
defer v8.v8__ScriptCompiler__Source__DESTRUCT(&script_compiler_source);
// Compile the function.
const result = v8.v8__ScriptCompiler__CompileFunction(
self.handle,
&script_compiler_source,
parameter_list.len,
&parameter_list,
extensions.len,
@ptrCast(&extensions),
v8.kNoCompileOptions,
v8.kNoCacheNoReason,
) orelse return error.CompilationError;
return .{ .local = self, .handle = result };
}
pub fn compileAndRun(self: *const Local, src: []const u8, name: ?[]const u8) !js.Value {
const script_name = self.isolate.initStringHandle(name orelse "anonymous");
const script_source = self.isolate.initStringHandle(src);
// Create ScriptOrigin
var origin: v8.ScriptOrigin = undefined;
v8.v8__ScriptOrigin__CONSTRUCT(&origin, @ptrCast(script_name));
// Create ScriptCompilerSource
var script_comp_source: v8.ScriptCompilerSource = undefined;
v8.v8__ScriptCompiler__Source__CONSTRUCT2(script_source, &origin, null, &script_comp_source);
defer v8.v8__ScriptCompiler__Source__DESTRUCT(&script_comp_source);
// Compile the script
const v8_script = v8.v8__ScriptCompiler__Compile(
self.handle,
&script_comp_source,
v8.kNoCompileOptions,
v8.kNoCacheNoReason,
) orelse return error.CompilationError;
// Run the script
const result = v8.v8__Script__Run(v8_script, self.handle) orelse return error.JsException;
return .{ .local = self, .handle = result };
}
// == Zig -> JS ==
// To turn a Zig instance into a v8 object, we need to do a number of things.
// First, if it's a struct, we need to put it on the heap.
// Second, if we've already returned this instance, we should return
// the same object. Hence, our executor maintains a map of Zig objects
// to v8.Global(js.Object) (the "identity_map").
// Finally, if this is the first time we've seen this instance, we need to:
// 1 - get the FunctionTemplate (from our templates slice)
// 2 - Create the TaggedAnyOpaque so that, if needed, we can do the reverse
// (i.e. js -> zig)
// 3 - Create a v8.Global(js.Object) (because Zig owns this object, not v8)
// 4 - Store our TaggedAnyOpaque into the persistent object
// 5 - Update our identity_map (so that, if we return this same instance again,
// we can just grab it from the identity_map)
pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object, value: anytype) !js.Object {
const ctx = self.ctx;
const context_arena = ctx.arena;
const T = @TypeOf(value);
switch (@typeInfo(T)) {
.@"struct" => {
// Struct, has to be placed on the heap
const heap = try context_arena.create(T);
heap.* = value;
return self.mapZigInstanceToJs(js_obj_handle, heap);
},
.pointer => |ptr| {
const resolved = resolveValue(value);
const resolved_ptr_id = @intFromPtr(resolved.ptr);
const gop = try ctx.addIdentity(resolved_ptr_id);
if (gop.found_existing) {
// we've seen this instance before, return the same object
return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self);
}
const isolate = self.isolate;
const JsApi = bridge.Struct(ptr.child).JsApi;
// Sometimes we're creating a new Object, like when
// we're returning a value from a function. In those cases
// we have to get the object template, and we can get an object
// by calling initInstance its InstanceTemplate.
// Sometimes though we already have the Object to bind to
// for example, when we're executing a constructor, v8 has
// already created the "this" object.
const js_obj = js.Object{
.local = self,
.handle = js_obj_handle orelse blk: {
const function_template_handle = ctx.templates[resolved.class_id];
const object_template_handle = v8.v8__FunctionTemplate__InstanceTemplate(function_template_handle).?;
break :blk v8.v8__ObjectTemplate__NewInstance(object_template_handle, self.handle).?;
},
};
if (!@hasDecl(JsApi.Meta, "empty_with_no_proto")) {
// The TAO contains the pointer to our Zig instance as
// well as any meta data we'll need to use it later.
// See the TaggedOpaque struct for more details.
const tao = try context_arena.create(TaggedOpaque);
tao.* = .{
.value = resolved.ptr,
.prototype_chain = resolved.prototype_chain.ptr,
.prototype_len = @intCast(resolved.prototype_chain.len),
.subtype = if (@hasDecl(JsApi.Meta, "subtype")) JsApi.Meta.subype else .node,
};
v8.v8__Object__SetAlignedPointerInInternalField(js_obj.handle, 0, tao);
} else {
// If the struct is empty, we don't need to do all
// the TOA stuff and setting the internal data.
// When we try to map this from JS->Zig, in
// TaggedOpaque, we'll also know there that
// the type is empty and can create an empty instance.
}
// dont' use js_obj.persist(), because we don't want to track this in
// context.global_objects, we want to track it in context.identity_map.
v8.v8__Global__New(isolate.handle, js_obj.handle, gop.value_ptr);
if (resolved.finalizer) |finalizer| {
const finalizer_ptr_id = finalizer.ptr_id;
finalizer.acquireRef(finalizer_ptr_id);
const session = ctx.session;
const finalizer_gop = try session.finalizer_callbacks.getOrPut(session.page_arena, finalizer_ptr_id);
if (finalizer_gop.found_existing == false) {
// This is the first context (and very likely only one) to
// see this Zig instance. We need to create the FinalizerCallback
// so that we can cleanup on page reset if v8 doesn't finalize.
errdefer _ = session.finalizer_callbacks.remove(finalizer_ptr_id);
finalizer_gop.value_ptr.* = try self.createFinalizerCallback(resolved_ptr_id, finalizer_ptr_id, finalizer.deinit);
}
const fc = finalizer_gop.value_ptr.*;
const identity_finalizer = try fc.arena.create(Session.FinalizerCallback.Identity);
identity_finalizer.* = .{
.fc = fc,
.identity = ctx.identity,
};
v8.v8__Global__SetWeakFinalizer(gop.value_ptr, identity_finalizer, finalizer.release, v8.kParameter);
}
return js_obj;
},
else => @compileError("Expected a struct or pointer, got " ++ @typeName(T) ++ " (constructors must return struct or pointers)"),
}
}
pub fn zigValueToJs(self: *const Local, value: anytype, comptime opts: CallOpts) !js.Value {
const isolate = self.isolate;
// Check if it's a "simple" type. This is extracted so that it can be
// reused by other parts of the code. "simple" types only require an
// isolate to create (specifically, they don't our templates array)
if (js.simpleZigValueToJs(isolate, value, false, opts.null_as_undefined)) |js_value_handle| {
return .{ .local = self, .handle = js_value_handle };
}
const T = @TypeOf(value);
switch (@typeInfo(T)) {
.void, .bool, .int, .comptime_int, .float, .comptime_float, .@"enum", .null => {
// Need to do this to keep the compiler happy
// simpleZigValueToJs handles all of these cases.
unreachable;
},
.array => {
var js_arr = self.newArray(value.len);
for (value, 0..) |v, i| {
if (try js_arr.set(@intCast(i), v, opts) == false) {
return error.FailedToCreateArray;
}
}
return js_arr.toValue();
},
.pointer => |ptr| switch (ptr.size) {
.one => {
if (@typeInfo(ptr.child) == .@"struct" and @hasDecl(ptr.child, "JsApi")) {
if (bridge.JsApiLookup.has(ptr.child.JsApi)) {
const js_obj = try self.mapZigInstanceToJs(null, value);
return js_obj.toValue();
}
}
if (@typeInfo(ptr.child) == .@"struct" and @hasDecl(ptr.child, "runtimeGenericWrap")) {
const wrap = try value.runtimeGenericWrap(self.ctx.page);
return self.zigValueToJs(wrap, opts);
}
const one_info = @typeInfo(ptr.child);
if (one_info == .array and one_info.array.child == u8) {
// Need to do this to keep the compiler happy
// If this was the case, simpleZigValueToJs would
// have handled it
unreachable;
}
},
.slice => {
if (ptr.child == u8) {
// Need to do this to keep the compiler happy
// If this was the case, simpleZigValueToJs would
// have handled it
unreachable;
}
var js_arr = self.newArray(@intCast(value.len));
for (value, 0..) |v, i| {
if (try js_arr.set(@intCast(i), v, opts) == false) {
return error.FailedToCreateArray;
}
}
return js_arr.toValue();
},
else => {},
},
.@"struct" => |s| {
if (@hasDecl(T, "JsApi")) {
if (bridge.JsApiLookup.has(T.JsApi)) {
const js_obj = try self.mapZigInstanceToJs(null, value);
return js_obj.toValue();
}
}
if (T == string.String or T == string.Global) {
// would have been handled by simpleZigValueToJs
unreachable;
}
// zig fmt: off
switch (T) {
js.Value => return value,
js.Exception => return .{ .local = self, .handle = isolate.throwException(value.handle) },
js.ArrayBufferRef(.int8).Global, js.ArrayBufferRef(.uint8).Global,
js.ArrayBufferRef(.uint8_clamped).Global, js.ArrayBufferRef(.int16).Global,
js.ArrayBufferRef(.uint16).Global, js.ArrayBufferRef(.int32).Global,
js.ArrayBufferRef(.uint32).Global, js.ArrayBufferRef(.float16).Global,
js.ArrayBufferRef(.float32).Global, js.ArrayBufferRef(.float64).Global,
=> {
return .{ .local = self, .handle = value.local(self).handle };
},
inline
js.Array,
js.Function,
js.Object,
js.Promise,
js.String => return .{ .local = self, .handle = @ptrCast(value.handle) },
inline
js.Function.Global,
js.Function.Temp,
js.Value.Global,
js.Value.Temp,
js.Object.Global,
js.Promise.Global,
js.Promise.Temp,
js.PromiseResolver.Global,
js.Module.Global => return .{ .local = self, .handle = @ptrCast(value.local(self).handle) },
else => {}
}
// zig fmt: on
if (@hasDecl(T, "runtimeGenericWrap")) {
const wrap = try value.runtimeGenericWrap(self.ctx.page);
return self.zigValueToJs(wrap, opts);
}
if (s.is_tuple) {
// return the tuple struct as an array
var js_arr = self.newArray(@intCast(s.fields.len));
inline for (s.fields, 0..) |f, i| {
if (try js_arr.set(@intCast(i), @field(value, f.name), opts) == false) {
return error.FailedToCreateArray;
}
}
return js_arr.toValue();
}
const js_obj = self.newObject();
inline for (s.fields) |f| {
if (try js_obj.set(f.name, @field(value, f.name), opts) == false) {
return error.CreateObjectFailure;
}
}
return js_obj.toValue();
},
.@"union" => |un| {
if (T == std.json.Value) {
return self.zigJsonToJs(value);
}
if (un.tag_type) |UnionTagType| {
inline for (un.fields) |field| {
if (value == @field(UnionTagType, field.name)) {
return self.zigValueToJs(@field(value, field.name), opts);
}
}
unreachable;
}
@compileError("Cannot use untagged union: " ++ @typeName(T));
},
.optional => {
if (value) |v| {
return self.zigValueToJs(v, opts);
}
// would be handled by simpleZigValueToJs
unreachable;
},
.error_union => return self.zigValueToJs(try value, opts),
else => {},
}
@compileError("A function returns an unsupported type: " ++ @typeName(T));
}
fn zigJsonToJs(self: *const Local, value: std.json.Value) !js.Value {
const isolate = self.isolate;
switch (value) {
.bool => |v| return .{ .local = self, .handle = js.simpleZigValueToJs(isolate, v, true, false) },
.float => |v| return .{ .local = self, .handle = js.simpleZigValueToJs(isolate, v, true, false) },
.integer => |v| return .{ .local = self, .handle = js.simpleZigValueToJs(isolate, v, true, false) },
.string => |v| return .{ .local = self, .handle = js.simpleZigValueToJs(isolate, v, true, false) },
.null => return .{ .local = self, .handle = isolate.initNull() },
// TODO handle number_string.
// It is used to represent too big numbers.
.number_string => return error.TODO,
.array => |v| {
const js_arr = self.newArray(@intCast(v.items.len));
for (v.items, 0..) |array_value, i| {
if (try js_arr.set(@intCast(i), array_value, .{}) == false) {
return error.JSObjectSetValue;
}
}
return js_arr.toArray();
},
.object => |v| {
var js_obj = self.newObject();
var it = v.iterator();
while (it.next()) |kv| {
if (try js_obj.set(kv.key_ptr.*, kv.value_ptr.*, .{}) == false) {
return error.JSObjectSetValue;
}
}
return .{ .local = self, .handle = @ptrCast(js_obj.handle) };
},
}
}
// == JS -> Zig ==
pub fn jsValueToZig(self: *const Local, comptime T: type, js_val: js.Value) !T {
switch (@typeInfo(T)) {
.optional => |o| {
// If type type is a ?js.Value or a ?js.Object, then we want to pass
// a js.Object, not null. Consider a function,
// _doSomething(arg: ?Env.JsObjet) void { ... }
//
// And then these two calls:
// doSomething();
// doSomething(null);
//
// In the first case, we'll pass `null`. But in the
// second, we'll pass a js.Object which represents
// null.
// If we don't have this code, both cases will
// pass in `null` and the the doSomething won't
// be able to tell if `null` was explicitly passed
// or whether no parameter was passed.
if (comptime o.child == js.Value) {
return js_val;
}
if (comptime o.child == js.NullableString) {
if (js_val.isUndefined()) {
return null;
}
return .{ .value = try js_val.toStringSlice() };
}
if (comptime o.child == js.Object) {
return js.Object{
.local = self,
.handle = @ptrCast(js_val.handle),
};
}
if (js_val.isNullOrUndefined()) {
return null;
}
return try self.jsValueToZig(o.child, js_val);
},
.float => |f| switch (f.bits) {
0...32 => return js_val.toF32(),
33...64 => return js_val.toF64(),
else => {},
},
.int => return jsIntToZig(T, js_val),
.bool => return js_val.toBool(),
.pointer => |ptr| switch (ptr.size) {
.one => {
if (!js_val.isObject()) {
return error.InvalidArgument;
}
if (@hasDecl(ptr.child, "JsApi")) {
std.debug.assert(bridge.JsApiLookup.has(ptr.child.JsApi));
return TaggedOpaque.fromJS(*ptr.child, @ptrCast(js_val.handle));
}
},
.slice => {
if (ptr.sentinel() == null) {
if (try jsValueToTypedArray(ptr.child, js_val)) |value| {
return value;
}
}
if (ptr.child == u8) {
if (ptr.sentinel()) |s| {
if (comptime s == 0) {
return try js_val.toStringSliceZ();
}
} else {
return try js_val.toStringSlice();
}
}
if (!js_val.isArray()) {
return error.InvalidArgument;
}
const js_arr = js_val.toArray();
const arr = try self.call_arena.alloc(ptr.child, js_arr.len());
for (arr, 0..) |*a, i| {
const item_value = try js_arr.get(@intCast(i));
a.* = try self.jsValueToZig(ptr.child, item_value);
}
return arr;
},
else => {},
},
.array => |arr| {
// Retrieve fixed-size array as slice
const slice_type = []arr.child;
const slice_value = try self.jsValueToZig(slice_type, js_val);
if (slice_value.len != arr.len) {
// Exact length match, we could allow smaller arrays, but we would not be able to communicate how many were written
return error.InvalidArgument;
}
return @as(*T, @ptrCast(slice_value.ptr)).*;
},
.@"struct" => {
return try (self.jsValueToStruct(T, js_val)) orelse {
return error.InvalidArgument;
};
},
.@"union" => |u| {
// see probeJsValueToZig for some explanation of what we're
// trying to do
// the first field that we find which the js_val could be
// coerced to.
var coerce_index: ?usize = null;
// the first field that we find which the js_val is
// compatible with. A compatible field has higher precedence
// than a coercible, but still isn't a perfect match.
var compatible_index: ?usize = null;
inline for (u.fields, 0..) |field, i| {
switch (try self.probeJsValueToZig(field.type, js_val)) {
.value => |v| return @unionInit(T, field.name, v),
.ok => {
// a perfect match like above case, except the probing
// didn't get the value for us.
return @unionInit(T, field.name, try self.jsValueToZig(field.type, js_val));
},
.coerce => if (coerce_index == null) {
coerce_index = i;
},
.compatible => if (compatible_index == null) {
compatible_index = i;
},
.invalid => {},
}
}
// We didn't find a perfect match.
const closest = compatible_index orelse coerce_index orelse return error.InvalidArgument;
inline for (u.fields, 0..) |field, i| {
if (i == closest) {
return @unionInit(T, field.name, try self.jsValueToZig(field.type, js_val));
}
}
unreachable;
},
.@"enum" => |e| {
if (@hasDecl(T, "js_enum_from_string")) {
const js_str = js_val.isString() orelse return error.InvalidArgument;
return std.meta.stringToEnum(T, try js_str.toSlice()) orelse return error.InvalidArgument;
}
switch (@typeInfo(e.tag_type)) {
.int => return std.meta.intToEnum(T, try jsIntToZig(e.tag_type, js_val)),
else => @compileError("unsupported enum parameter type: " ++ @typeName(T)),
}
},
else => {},
}
@compileError("has an unsupported parameter type: " ++ @typeName(T));
}
// Extracted so that it can be used in both jsValueToZig and in
// probeJsValueToZig. Avoids having to duplicate this logic when probing.
fn jsValueToStruct(self: *const Local, comptime T: type, js_val: js.Value) !?T {
return switch (T) {
js.Function, js.Function.Global, js.Function.Temp => {
if (!js_val.isFunction()) {
return null;
}
const js_func = js.Function{ .local = self, .handle = @ptrCast(js_val.handle) };
return switch (T) {
js.Function => js_func,
js.Function.Temp => try js_func.temp(),
js.Function.Global => try js_func.persist(),
else => unreachable,
};
},
// zig fmt: off
js.TypedArray(u8), js.TypedArray(u16), js.TypedArray(u32), js.TypedArray(u64),
js.TypedArray(i8), js.TypedArray(i16), js.TypedArray(i32), js.TypedArray(i64),
js.TypedArray(f32), js.TypedArray(f64),
// zig fmt: on
=> {
const ValueType = @typeInfo(std.meta.fieldInfo(T, .values).type).pointer.child;
const arr = (try jsValueToTypedArray(ValueType, js_val)) orelse return null;
return .{ .values = arr };
},
js.Value => js_val,
js.Value.Global => return try js_val.persist(),
js.Value.Temp => return try js_val.temp(),
js.Object => {
if (!js_val.isObject()) {
return null;
}
return js.Object{
.local = self,
.handle = @ptrCast(js_val.handle),
};
},
js.Object.Global => {
if (!js_val.isObject()) {
return null;
}
const obj = js.Object{
.local = self,
.handle = @ptrCast(js_val.handle),
};
return try obj.persist();
},
js.Promise.Global, js.Promise.Temp => {
if (!js_val.isPromise()) {
return null;
}
const js_promise = js.Promise{
.local = self,
.handle = @ptrCast(js_val.handle),
};
return switch (T) {
js.Promise.Temp => try js_promise.temp(),
js.Promise.Global => try js_promise.persist(),
else => unreachable,
};
},
string.String => {
const js_str = js_val.isString() orelse return null;
return try js_str.toSSO(false);
},
string.Global => {
const js_str = js_val.isString() orelse return null;
return try js_str.toSSO(true);
},
else => {
if (!js_val.isObject()) {
return null;
}
const isolate = self.isolate;
const js_obj = js_val.toObject();
var value: T = undefined;
inline for (@typeInfo(T).@"struct".fields) |field| {
const name = field.name;
const key = isolate.initStringHandle(name);
if (js_obj.has(key)) {
@field(value, name) = try self.jsValueToZig(field.type, try js_obj.get(key));
} else if (@typeInfo(field.type) == .optional) {
@field(value, name) = null;
} else {
const dflt = field.defaultValue() orelse return null;
@field(value, name) = dflt;
}
}
return value;
},
};
}
fn jsValueToTypedArray(comptime T: type, js_val: js.Value) !?[]T {
var force_u8 = false;
var array_buffer: ?*const v8.ArrayBuffer = null;
var byte_len: usize = undefined;
var byte_offset: usize = undefined;
if (js_val.isTypedArray()) {
const buffer_handle: *const v8.ArrayBufferView = @ptrCast(js_val.handle);
byte_len = v8.v8__ArrayBufferView__ByteLength(buffer_handle);
byte_offset = v8.v8__ArrayBufferView__ByteOffset(buffer_handle);
array_buffer = v8.v8__ArrayBufferView__Buffer(buffer_handle).?;
} else if (js_val.isArrayBufferView()) {
force_u8 = true;
const buffer_handle: *const v8.ArrayBufferView = @ptrCast(js_val.handle);
byte_len = v8.v8__ArrayBufferView__ByteLength(buffer_handle);
byte_offset = v8.v8__ArrayBufferView__ByteOffset(buffer_handle);
array_buffer = v8.v8__ArrayBufferView__Buffer(buffer_handle).?;
} else if (js_val.isArrayBuffer()) {
force_u8 = true;
array_buffer = @ptrCast(js_val.handle);
byte_len = v8.v8__ArrayBuffer__ByteLength(array_buffer);
byte_offset = 0;
}
const backing_store_ptr = v8.v8__ArrayBuffer__GetBackingStore(array_buffer orelse return null);
const backing_store_handle = v8.std__shared_ptr__v8__BackingStore__get(&backing_store_ptr).?;
const data = v8.v8__BackingStore__Data(backing_store_handle);
switch (T) {
u8 => {
if (force_u8 or js_val.isUint8Array() or js_val.isUint8ClampedArray()) {
if (byte_len == 0) return &[_]u8{};
const arr_ptr = @as([*]u8, @ptrCast(@alignCast(data)));
return arr_ptr[byte_offset .. byte_offset + byte_len];
}
},
i8 => {
if (js_val.isInt8Array()) {
if (byte_len == 0) return &[_]i8{};
const arr_ptr = @as([*]i8, @ptrCast(@alignCast(data)));
return arr_ptr[byte_offset .. byte_offset + byte_len];
}
},
u16 => {
if (js_val.isUint16Array()) {
if (byte_len == 0) return &[_]u16{};
const arr_ptr = @as([*]u16, @ptrCast(@alignCast(data)));
return arr_ptr[byte_offset .. byte_offset + byte_len / 2];
}
},
i16 => {
if (js_val.isInt16Array()) {
if (byte_len == 0) return &[_]i16{};
const arr_ptr = @as([*]i16, @ptrCast(@alignCast(data)));
return arr_ptr[byte_offset .. byte_offset + byte_len / 2];
}
},
u32 => {
if (js_val.isUint32Array()) {
if (byte_len == 0) return &[_]u32{};
const arr_ptr = @as([*]u32, @ptrCast(@alignCast(data)));
return arr_ptr[byte_offset .. byte_offset + byte_len / 4];
}
},
i32 => {
if (js_val.isInt32Array()) {
if (byte_len == 0) return &[_]i32{};
const arr_ptr = @as([*]i32, @ptrCast(@alignCast(data)));
return arr_ptr[byte_offset .. byte_offset + byte_len / 4];
}
},
u64 => {
if (js_val.isBigUint64Array()) {
if (byte_len == 0) return &[_]u64{};
const arr_ptr = @as([*]u64, @ptrCast(@alignCast(data)));
return arr_ptr[byte_offset .. byte_offset + byte_len / 8];
}
},
i64 => {
if (js_val.isBigInt64Array()) {
if (byte_len == 0) return &[_]i64{};
const arr_ptr = @as([*]i64, @ptrCast(@alignCast(data)));
return arr_ptr[byte_offset .. byte_offset + byte_len / 8];
}
},
else => {},
}
return error.InvalidArgument;
}
// Probing is part of trying to map a JS value to a Zig union. There's
// a lot of ambiguity in this process, in part because some JS values
// can almost always be coerced. For example, anything can be coerced
// into an integer (it just becomes 0), or a float (becomes NaN) or a
// string.
//
// The way we'll do this is that, if there's a direct match, we'll use it
// If there's a potential match, we'll keep looking for a direct match
// and only use the (first) potential match as a fallback.
//
// Finally, I considered adding this probing directly into jsValueToZig
// but I decided doing this separately was better. However, the goal is
// obviously that probing is consistent with jsValueToZig.
fn ProbeResult(comptime T: type) type {
return union(enum) {
// The js_value maps directly to T
value: T,
// The value is a T. This is almost the same as returning value: T,
// but the caller still has to get T by calling jsValueToZig.
// We prefer returning .{.ok => {}}, to avoid reducing duplication
// with jsValueToZig, but in some cases where probing has a cost
// AND yields the value anyways, we'll use .{.value = T}.
ok: void,
// the js_value is compatible with T (i.e. a int -> float),
compatible: void,
// the js_value can be coerced to T (this is a lower precedence
// than compatible)
coerce: void,
// the js_value cannot be turned into T
invalid: void,
};
}
fn probeJsValueToZig(self: *const Local, comptime T: type, js_val: js.Value) !ProbeResult(T) {
switch (@typeInfo(T)) {
.optional => |o| {
if (js_val.isNullOrUndefined()) {
return .{ .value = null };
}
return self.probeJsValueToZig(o.child, js_val);
},
.float => {
if (js_val.isNumber() or js_val.isNumberObject()) {
if (js_val.isInt32() or js_val.isUint32() or js_val.isBigInt() or js_val.isBigIntObject()) {
// int => float is a reasonable match
return .{ .compatible = {} };
}
return .{ .ok = {} };
}
// anything can be coerced into a float, it becomes NaN
return .{ .coerce = {} };
},
.int => {
if (js_val.isNumber() or js_val.isNumberObject()) {
if (js_val.isInt32() or js_val.isUint32() or js_val.isBigInt() or js_val.isBigIntObject()) {
return .{ .ok = {} };
}
// float => int is kind of reasonable, I guess
return .{ .compatible = {} };
}
// anything can be coerced into a int, it becomes 0
return .{ .coerce = {} };
},
.bool => {
if (js_val.isBoolean() or js_val.isBooleanObject()) {
return .{ .ok = {} };
}
// anything can be coerced into a boolean, it will become
// true or false based on..some complex rules I don't know.
return .{ .coerce = {} };
},
.pointer => |ptr| switch (ptr.size) {
.one => {
if (!js_val.isObject()) {
return .{ .invalid = {} };
}
if (bridge.JsApiLookup.has(ptr.child.JsApi)) {
// There's a bit of overhead in doing this, so instead
// of having a version of TaggedOpaque which
// returns a boolean or an optional, we rely on the
// main implementation and just handle the error.
const attempt = TaggedOpaque.fromJS(*ptr.child, @ptrCast(js_val.handle));
if (attempt) |value| {
return .{ .value = value };
} else |_| {
return .{ .invalid = {} };
}
}
// probably an error, but not for us to deal with
return .{ .invalid = {} };
},
.slice => {
if (js_val.isTypedArray()) {
switch (ptr.child) {
u8 => if (ptr.sentinel() == null) {
if (js_val.isUint8Array() or js_val.isUint8ClampedArray()) {
return .{ .ok = {} };
}
},
i8 => if (js_val.isInt8Array()) {
return .{ .ok = {} };
},
u16 => if (js_val.isUint16Array()) {
return .{ .ok = {} };
},
i16 => if (js_val.isInt16Array()) {
return .{ .ok = {} };
},
u32 => if (js_val.isUint32Array()) {
return .{ .ok = {} };
},
i32 => if (js_val.isInt32Array()) {
return .{ .ok = {} };
},
u64 => if (js_val.isBigUint64Array()) {
return .{ .ok = {} };
},
i64 => if (js_val.isBigInt64Array()) {
return .{ .ok = {} };
},
else => {},
}
return .{ .invalid = {} };
}
if (ptr.child == u8) {
if (v8.v8__Value__IsString(js_val.handle)) {
return .{ .ok = {} };
}
// anything can be coerced into a string
return .{ .coerce = {} };
}
if (!js_val.isArray()) {
return .{ .invalid = {} };
}
// This can get tricky.
const js_arr = js_val.toArray();
if (js_arr.len() == 0) {
// not so tricky in this case.
return .{ .value = &.{} };
}
// We settle for just probing the first value. Ok, actually
// not tricky in this case either.
const first_val = try js_arr.get(0);
switch (try self.probeJsValueToZig(ptr.child, first_val)) {
.value, .ok => return .{ .ok = {} },
.compatible => return .{ .compatible = {} },
.coerce => return .{ .coerce = {} },
.invalid => return .{ .invalid = {} },
}
},
else => {},
},
.array => |arr| {
// Retrieve fixed-size array as slice then probe
const slice_type = []arr.child;
switch (try self.probeJsValueToZig(slice_type, js_val)) {
.value => |slice_value| {
if (slice_value.len == arr.len) {
return .{ .value = @as(*T, @ptrCast(slice_value.ptr)).* };
}
return .{ .invalid = {} };
},
.ok => {
// Exact length match, we could allow smaller arrays as .compatible, but we would not be able to communicate how many were written
if (js_val.isArray()) {
const js_arr = js_val.toArray();
if (js_arr.len() == arr.len) {
return .{ .ok = {} };
}
} else if (arr.child == u8) {
if (js_val.isString()) |js_str| {
if (js_str.lenUtf8(self.isolate) == arr.len) {
return .{ .ok = {} };
}
}
}
return .{ .invalid = {} };
},
.compatible => return .{ .compatible = {} },
.coerce => return .{ .coerce = {} },
.invalid => return .{ .invalid = {} },
}
},
.@"struct" => {
// Handle string.String and string.Global specially
if (T == string.String or T == string.Global) {
if (v8.v8__Value__IsString(js_val.handle)) {
return .{ .ok = {} };
}
// Anything can be coerced to a string
return .{ .coerce = {} };
}
// We don't want to duplicate the code for this, so we call
// the actual conversion function.
const value = (try self.jsValueToStruct(T, js_val)) orelse {
return .{ .invalid = {} };
};
return .{ .value = value };
},
else => {},
}
return .{ .invalid = {} };
}
fn jsIntToZig(comptime T: type, js_value: js.Value) !T {
const n = @typeInfo(T).int;
switch (n.signedness) {
.signed => switch (n.bits) {
8 => return jsSignedIntToZig(i8, -128, 127, try js_value.toI32()),
16 => return jsSignedIntToZig(i16, -32_768, 32_767, try js_value.toI32()),
32 => return jsSignedIntToZig(i32, -2_147_483_648, 2_147_483_647, try js_value.toI32()),
64 => {
if (js_value.isBigInt()) {
const v = js_value.toBigInt();
return v.getInt64();
}
return jsSignedIntToZig(i64, -2_147_483_648, 2_147_483_647, try js_value.toI32());
},
else => {},
},
.unsigned => switch (n.bits) {
8 => return jsUnsignedIntToZig(u8, 255, try js_value.toU32()),
16 => return jsUnsignedIntToZig(u16, 65_535, try js_value.toU32()),
32 => {
if (js_value.isBigInt()) {
const v = js_value.toBigInt();
const large = v.getUint64();
if (large <= 4_294_967_295) {
return @intCast(large);
}
return error.InvalidArgument;
}
return jsUnsignedIntToZig(u32, 4_294_967_295, try js_value.toU32());
},
64 => {
if (js_value.isBigInt()) {
const v = js_value.toBigInt();
return v.getUint64();
}
return jsUnsignedIntToZig(u64, 4_294_967_295, try js_value.toU32());
},
else => {},
},
}
@compileError("Only i8, i16, i32, i64, u8, u16, u32 and u64 are supported");
}
fn jsSignedIntToZig(comptime T: type, comptime min: comptime_int, max: comptime_int, maybe: i32) !T {
if (maybe >= min and maybe <= max) {
return @intCast(maybe);
}
return error.InvalidArgument;
}
fn jsUnsignedIntToZig(comptime T: type, max: comptime_int, maybe: u32) !T {
if (maybe <= max) {
return @intCast(maybe);
}
return error.InvalidArgument;
}
// Every WebApi type has a class_id as T.JsApi.Meta.class_id. We use this to create
// a JSValue class of the correct type. However, given a Node, we don't want
// to create a Node class, we want to create a class of the most specific type.
// In other words, given a Node{._type = .{.document .{}}}, we want to create
// a Document, not a Node.
// This function recursively walks the _type union field (if there is one) to
// get the most specific class_id possible.
const Resolved = struct {
ptr: *anyopaque,
class_id: u16,
prototype_chain: []const @import("TaggedOpaque.zig").PrototypeChainEntry,
finalizer: ?Finalizer,
const Finalizer = struct {
// Resolved.ptr is the most specific value in a chain (e.g. IFrame, not EventTarget, Node, ...)
// Finalizer.ptr_id is the most specific value in a chain that defines an acquireRef
ptr_id: usize,
deinit: *const fn (ptr_id: usize, session: *Session) void,
acquireRef: *const fn (ptr_id: usize) void,
release: *const fn (handle: ?*const v8.WeakCallbackInfo) callconv(.c) void,
};
};
pub fn resolveValue(value: anytype) Resolved {
const T = bridge.Struct(@TypeOf(value));
if (!@hasField(T, "_type") or @typeInfo(@TypeOf(value._type)) != .@"union") {
return resolveT(T, value);
}
const U = @typeInfo(@TypeOf(value._type)).@"union";
inline for (U.fields) |field| {
if (value._type == @field(U.tag_type.?, field.name)) {
const child = switch (@typeInfo(field.type)) {
.pointer => @field(value._type, field.name),
.@"struct" => &@field(value._type, field.name),
.void => {
// Unusual case, but the Event (and maybe others) can be
// returned as-is. In that case, it has a dummy void type.
return resolveT(T, value);
},
else => @compileError(@typeName(field.type) ++ " has an unsupported _type field"),
};
return resolveValue(child);
}
}
unreachable;
}
fn resolveT(comptime T: type, value: *T) Resolved {
const Meta = T.JsApi.Meta;
return .{
.ptr = value,
.class_id = Meta.class_id,
.prototype_chain = &Meta.prototype_chain,
.finalizer = blk: {
const FT = (comptime findFinalizerType(T)) orelse break :blk null;
const getFinalizerPtr = comptime finalizerPtrGetter(T, FT);
const finalizer_ptr = getFinalizerPtr(value);
const Wrap = struct {
fn deinit(ptr_id: usize, session: *Session) void {
FT.deinit(@ptrFromInt(ptr_id), session);
}
fn acquireRef(ptr_id: usize) void {
FT.acquireRef(@ptrFromInt(ptr_id));
}
fn release(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
const identity_finalizer: *Session.FinalizerCallback.Identity = @ptrCast(@alignCast(ptr));
const fc = identity_finalizer.fc;
if (identity_finalizer.identity.identity_map.fetchRemove(fc.resolved_ptr_id)) |kv| {
var global = kv.value;
v8.v8__Global__Reset(&global);
}
FT.releaseRef(@ptrFromInt(fc.finalizer_ptr_id), fc.session);
}
};
break :blk .{
.ptr_id = @intFromPtr(finalizer_ptr),
.deinit = Wrap.deinit,
.acquireRef = Wrap.acquireRef,
.release = Wrap.release,
};
},
};
}
// Start at the "resolved" type (the most specific) and work our way up the
// prototype chain looking for the type that defines acquireRef
fn findFinalizerType(comptime T: type) ?type {
const S = bridge.Struct(T);
if (@hasDecl(S, "acquireRef")) {
return S;
}
if (@hasField(S, "_proto")) {
const ProtoPtr = std.meta.fieldInfo(S, ._proto).type;
const ProtoChild = @typeInfo(ProtoPtr).pointer.child;
return findFinalizerType(ProtoChild);
}
return null;
}
// Generate a function that follows the _proto pointer chain to get to the finalizer type
fn finalizerPtrGetter(comptime T: type, comptime FT: type) *const fn (*T) *FT {
const S = bridge.Struct(T);
if (S == FT) {
return struct {
fn get(v: *T) *FT {
return v;
}
}.get;
}
if (@hasField(S, "_proto")) {
const ProtoPtr = std.meta.fieldInfo(S, ._proto).type;
const ProtoChild = @typeInfo(ProtoPtr).pointer.child;
const childGetter = comptime finalizerPtrGetter(ProtoChild, FT);
return struct {
fn get(v: *T) *FT {
return childGetter(v._proto);
}
}.get;
}
@compileError("Cannot find path from " ++ @typeName(T) ++ " to " ++ @typeName(FT));
}
pub fn stackTrace(self: *const Local) !?[]const u8 {
const isolate = self.isolate;
const separator = log.separator();
var buf: std.ArrayList(u8) = .empty;
var writer = buf.writer(self.call_arena);
const stack_trace_handle = v8.v8__StackTrace__CurrentStackTrace__STATIC(isolate.handle, 30).?;
const frame_count = v8.v8__StackTrace__GetFrameCount(stack_trace_handle);
if (v8.v8__StackTrace__CurrentScriptNameOrSourceURL__STATIC(isolate.handle)) |script| {
const stack = js.String{ .local = self, .handle = script };
try writer.print("{s}<{f}>", .{ separator, stack });
}
for (0..@intCast(frame_count)) |i| {
const frame_handle = v8.v8__StackTrace__GetFrame(stack_trace_handle, isolate.handle, @intCast(i)).?;
if (v8.v8__StackFrame__GetFunctionName(frame_handle)) |name| {
const script = js.String{ .local = self, .handle = name };
try writer.print("{s}{f}:{d}", .{ separator, script, v8.v8__StackFrame__GetLineNumber(frame_handle) });
} else {
try writer.print("{s}<anonymous>:{d}", .{ separator, v8.v8__StackFrame__GetLineNumber(frame_handle) });
}
}
return buf.items;
}
// == Promise Helpers ==
pub fn rejectPromise(self: *const Local, err: js.PromiseResolver.RejectError) js.Promise {
var resolver = js.PromiseResolver.init(self);
resolver.rejectError("Local.rejectPromise", err);
return resolver.promise();
}
pub fn rejectErrorPromise(self: *const Local, value: js.PromiseResolver.RejectError) !js.Promise {
var resolver = js.PromiseResolver.init(self);
resolver.rejectError("Local.rejectPromise", value);
return resolver.promise();
}
pub fn resolvePromise(self: *const Local, value: anytype) !js.Promise {
var resolver = js.PromiseResolver.init(self);
resolver.resolve("Local.resolvePromise", value);
return resolver.promise();
}
pub fn createPromiseResolver(self: *const Local) js.PromiseResolver {
return js.PromiseResolver.init(self);
}
pub fn debugValue(self: *const Local, js_val: js.Value, writer: *std.Io.Writer) !void {
var seen: std.AutoHashMapUnmanaged(u32, void) = .empty;
return self._debugValue(js_val, &seen, 0, writer) catch error.WriteFailed;
}
fn _debugValue(self: *const Local, js_val: js.Value, seen: *std.AutoHashMapUnmanaged(u32, void), depth: usize, writer: *std.Io.Writer) !void {
if (js_val.isNull()) {
// I think null can sometimes appear as an object, so check this and
// handle it first.
return writer.writeAll("null");
}
if (!js_val.isObject()) {
// handle these explicitly, so we don't include the type (we only want to include
// it when there's some ambiguity, e.g. the string "true")
if (js_val.isUndefined()) {
return writer.writeAll("undefined");
}
if (js_val.isTrue()) {
return writer.writeAll("true");
}
if (js_val.isFalse()) {
return writer.writeAll("false");
}
if (js_val.isSymbol()) {
const symbol_handle = v8.v8__Symbol__Description(@ptrCast(js_val.handle), self.isolate.handle).?;
if (v8.v8__Value__IsUndefined(symbol_handle)) {
return writer.writeAll("undefined (symbol)");
}
return writer.print("{f} (symbol)", .{js.String{ .local = self, .handle = @ptrCast(symbol_handle) }});
}
const js_val_str = try js_val.toStringSlice();
if (js_val_str.len > 2000) {
try writer.writeAll(js_val_str[0..2000]);
try writer.writeAll(" ... (truncated)");
} else {
try writer.writeAll(js_val_str);
}
return writer.print(" ({f})", .{js_val.typeOf()});
}
const js_obj = js_val.toObject();
{
// explicit scope because gop will become invalid in recursive call
const obj_id: u32 = @bitCast(v8.v8__Object__GetIdentityHash(js_obj.handle));
const gop = try seen.getOrPut(self.call_arena, obj_id);
if (gop.found_existing) {
return writer.writeAll("<circular>\n");
}
gop.value_ptr.* = {};
}
if (depth > 20) {
return writer.writeAll("...deeply nested object...");
}
const names_arr = js_obj.getOwnPropertyNames() catch {
return writer.writeAll("...invalid object...");
};
const len = names_arr.len();
const own_len = blk: {
const own_names = js_obj.getOwnPropertyNames() catch break :blk 0;
break :blk own_names.len();
};
if (own_len == 0) {
const js_val_str = try js_val.toStringSlice();
if (js_val_str.len > 2000) {
try writer.writeAll(js_val_str[0..2000]);
return writer.writeAll(" ... (truncated)");
}
return writer.writeAll(js_val_str);
}
const all_len = js_obj.getPropertyNames().len();
try writer.print("({d}/{d})", .{ own_len, all_len });
for (0..len) |i| {
if (i == 0) {
try writer.writeByte('\n');
}
const field_name = try names_arr.get(@intCast(i));
const name = try field_name.toStringSlice();
try writer.splatByteAll(' ', depth);
try writer.writeAll(name);
try writer.writeAll(": ");
const field_val = try js_obj.get(name);
try self._debugValue(field_val, seen, depth + 1, writer);
if (i != len - 1) {
try writer.writeByte('\n');
}
}
}
// == Misc ==
pub fn parseJSON(self: *const Local, json: []const u8) !js.Value {
const string_handle = self.isolate.initStringHandle(json);
const value_handle = v8.v8__JSON__Parse(self.handle, string_handle) orelse return error.JsException;
return .{
.local = self,
.handle = value_handle,
};
}
pub fn throw(self: *const Local, err: []const u8) js.Exception {
const handle = self.isolate.createError(err);
return .{
.local = self,
.handle = handle,
};
}
// Convert a Global (or optional Global) to a Local (or optional Local).
// Meant to be used from either page.js.toLocal, where the context must have an
// non-null local (orelse panic), or from a LocalScope
pub fn toLocal(self: *const Local, global: anytype) ToLocalReturnType(@TypeOf(global)) {
const T = @TypeOf(global);
if (@typeInfo(T) == .optional) {
const unwrapped = global orelse return null;
return unwrapped.local(self);
}
return global.local(self);
}
pub fn ToLocalReturnType(comptime T: type) type {
if (@typeInfo(T) == .optional) {
const GlobalType = @typeInfo(T).optional.child;
const struct_info = @typeInfo(GlobalType).@"struct";
inline for (struct_info.decls) |decl| {
if (std.mem.eql(u8, decl.name, "local")) {
const Fn = @TypeOf(@field(GlobalType, "local"));
const fn_info = @typeInfo(Fn).@"fn";
return ?fn_info.return_type.?;
}
}
@compileError("Type does not have local method");
} else {
const struct_info = @typeInfo(T).@"struct";
inline for (struct_info.decls) |decl| {
if (std.mem.eql(u8, decl.name, "local")) {
const Fn = @TypeOf(@field(T, "local"));
const fn_info = @typeInfo(Fn).@"fn";
return fn_info.return_type.?;
}
}
@compileError("Type does not have local method");
}
}
pub fn debugContextId(self: *const Local) i32 {
return v8.v8__Context__DebugContextId(self.handle);
}
fn createFinalizerCallback(
self: *const Local,
// Key in identity map
// The most specific value (KeyboardEvent, not Event)
resolved_ptr_id: usize,
// The most specific value where finalizers are defined
// What actually gets acquired / released / deinit
finalizer_ptr_id: usize,
deinit: *const fn (ptr_id: usize, session: *Session) void,
) !*Session.FinalizerCallback {
const session = self.ctx.session;
const arena = try session.getArena(.{ .debug = "FinalizerCallback" });
errdefer session.releaseArena(arena);
const fc = try arena.create(Session.FinalizerCallback);
fc.* = .{
.arena = arena,
.session = session,
._deinit = deinit,
.resolved_ptr_id = resolved_ptr_id,
.finalizer_ptr_id = finalizer_ptr_id,
};
return fc;
}
// Encapsulates a Local and a HandleScope. When we're going from V8->Zig
// we easily get both a Local and a HandleScope via Caller.init.
// But when we're going from Zig -> V8, things are more complicated.
// 1 - In some cases, we're going from Zig -> V8, but the origin is actually V8,
// so it's really V8 -> Zig -> V8. For example, when element.click() is called,
// V8 will call the Element.click method, which could then call back into V8 for
// a click handler.
//
// 2 - In other cases, it's always initiated from Zig, e.g. window.setTimeout or
// window.onload.
//
// 3 - Yet in other cases, it might could be either. Event dispatching can both be
// initiated from Zig and from V8.
//
// When JS execution is Zig initiated (or if we aren't sure whether it's Zig
// initiated or not), we need to create a Local.Scope:
//
// var ls: js.Local.Scope = udnefined;
// page.js.localScope(&ls);
// defer ls.deinit();
// // can use ls.local as needed.
//
// Note: Zig code that is 100% guaranteed to be v8-initiated can get a local via:
// page.js.local.?
pub const Scope = struct {
local: Local,
handle_scope: js.HandleScope,
pub fn deinit(self: *Scope) void {
v8.v8__Context__Exit(self.local.handle);
self.handle_scope.deinit();
}
pub fn toLocal(self: *Scope, global: anytype) ToLocalReturnType(@TypeOf(global)) {
return self.local.toLocal(global);
}
};