Allow this argument to TokenList forEach

JsObject can now be used as a normal parameter. It'll receive the opaque value.
This is largely useful when a Zig function takes an argument which it needs
to pass back into a callback.

JsThis is now a thin wrapper around JsObject for functions that was the JsObject
of the receiver. This is for advanced usage where the Zig function wants to
manipulate the v8.Object that represents the zig value. postAttach is an example
of such usage.
This commit is contained in:
Karl Seguin
2025-04-18 20:38:52 +08:00
parent a2291b0713
commit 9b4d1d442e
6 changed files with 98 additions and 31 deletions

View File

@@ -24,7 +24,7 @@ const utils = @import("utils.z");
const Element = @import("element.zig").Element;
const Union = @import("element.zig").Union;
const JsObject = @import("../env.zig").JsObject;
const JsThis = @import("../env.zig").JsThis;
const Walker = @import("walker.zig").Walker;
const WalkerDepthFirst = @import("walker.zig").WalkerDepthFirst;
@@ -443,15 +443,15 @@ pub const HTMLCollection = struct {
return null;
}
pub fn postAttach(self: *HTMLCollection, js_obj: JsObject) !void {
pub fn postAttach(self: *HTMLCollection, js_this: JsThis) !void {
const len = try self.get_length();
for (0..len) |i| {
const node = try self.item(@intCast(i)) orelse unreachable;
const e = @as(*parser.Element, @ptrCast(node));
try js_obj.setIndex(@intCast(i), e);
try js_this.setIndex(@intCast(i), e);
if (try item_name(e)) |name| {
try js_obj.set(name, e);
try js_this.set(name, e);
}
}
}

View File

@@ -22,7 +22,7 @@ const parser = @import("../netsurf.zig");
const SessionState = @import("../env.zig").SessionState;
const Env = @import("../env.zig").Env;
const JsObject = @import("../env.zig").JsObject;
const JsThis = @import("../env.zig").JsThis;
const NodeList = @import("nodelist.zig").NodeList;
pub const Interfaces = .{
@@ -184,9 +184,9 @@ pub const MutationRecords = struct {
return null;
};
}
pub fn postAttach(self: *const MutationRecords, js_obj: JsObject) !void {
pub fn postAttach(self: *const MutationRecords, js_this: JsThis) !void {
if (self.first) |mr| {
try js_obj.set("0", mr);
try js_this.set("0", mr);
}
}
};

View File

@@ -20,7 +20,7 @@ const std = @import("std");
const parser = @import("../netsurf.zig");
const JsObject = @import("../env.zig").JsObject;
const JsThis = @import("../env.zig").JsThis;
const Callback = @import("../env.zig").Callback;
const SessionState = @import("../env.zig").SessionState;
@@ -177,11 +177,11 @@ pub const NodeList = struct {
}
// TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries
pub fn postAttach(self: *NodeList, js_obj: JsObject) !void {
pub fn postAttach(self: *NodeList, js_this: JsThis) !void {
const len = self.get_length();
for (0..len) |i| {
const node = try self._item(@intCast(i)) orelse unreachable;
try js_obj.setIndex(i, node);
try js_this.setIndex(i, node);
}
}
};

View File

@@ -22,6 +22,7 @@ const parser = @import("../netsurf.zig");
const iterator = @import("../iterator/iterator.zig");
const Callback = @import("../env.zig").Callback;
const JsObject = @import("../env.zig").JsObject;
const SessionState = @import("../env.zig").SessionState;
const DOMException = @import("exceptions.zig").DOMException;
@@ -138,11 +139,11 @@ pub const DOMTokenList = struct {
}
// TODO handle thisArg
pub fn _forEach(self: *parser.TokenList, cbk: Callback) !void {
pub fn _forEach(self: *parser.TokenList, cbk: Callback, this_arg: JsObject) !void {
var entries = _entries(self);
while (try entries._next()) |entry| {
var result: Callback.Result = undefined;
cbk.tryCall(.{ entry.@"1", entry.@"0", self }, &result) catch {
cbk.tryCallWithThis(this_arg, .{ entry.@"1", entry.@"0", self }, &result) catch {
log.err("callback error: {s}", .{result.exception});
log.debug("stack:\n{s}", .{result.stack orelse "???"});
};

View File

@@ -21,6 +21,7 @@ const Interfaces = generate.Tuple(.{
@import("xmlserializer/xmlserializer.zig").Interfaces,
});
pub const JsThis = Env.JsThis;
pub const JsObject = Env.JsObject;
pub const Callback = Env.Callback;
pub const Env = js.Env(*SessionState, Interfaces{});

View File

@@ -902,6 +902,27 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
_ = self.scope_arena.reset(.{ .retain_with_limit = 1024 * 64 });
}
// Given an anytype, turns it into a v8.Object. The anytype could be:
// 1 - A V8.object already
// 2 - Our this JsObject wrapper around a V8.Object
// 3 - A zig instance that has previously been given to V8
// (i.e., the value has to be known to the executor)
fn valueToExistingObject(self: *const Executor, value: anytype) !v8.Object {
if (@TypeOf(value) == v8.Object) {
return value;
}
if (@TypeOf(value) == JsObject) {
return value.js_obj;
}
const persistent_object = self.scope.?.identity_map.get(@intFromPtr(value)) orelse {
return error.InvalidThisForCallback;
};
return persistent_object.castToObject();
}
// Wrap a v8.Value, largely so that we can provide a convenient
// toString function
fn createValue(self: *const Executor, value: v8.Value) Value {
@@ -1003,7 +1024,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
gop.value_ptr.* = js_persistent;
if (@hasDecl(ptr.child, "postAttach")) {
const obj_wrap = JsObject{ .js_obj = js_obj, .executor = self };
const obj_wrap = JsThis{ .obj = .{ .js_obj = js_obj, .executor = self } };
switch (@typeInfo(@TypeOf(ptr.child.postAttach)).@"fn".params.len) {
2 => try value.postAttach(obj_wrap),
3 => try value.postAttach(self.state, obj_wrap),
@@ -1094,7 +1115,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
pub const Callback = struct {
id: usize,
executor: *Executor,
this: ?v8.Object = null,
_this: ?v8.Object = null,
func: PersistentFunction,
// We use this when mapping a JS value to a Zig object. We can't
@@ -1110,22 +1131,23 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
};
pub fn setThis(self: *Callback, value: anytype) !void {
const persistent_object = self.executor.scope.?.identity_map.get(@intFromPtr(value)) orelse {
return error.InvalidThisForCallback;
};
self.this = persistent_object.castToObject();
self._this = try self.executor.valueToExistingObject(value);
}
pub fn call(self: *const Callback, args: anytype) !void {
return self.callWithThis(self.this orelse self.executor.context.getGlobal(), args);
return self.callWithThis(self.getThis(), args);
}
pub fn tryCall(self: *const Callback, args: anytype, result: *Result) !void {
return self.tryCallWithThis(self.getThis(), args, result);
}
pub fn tryCallWithThis(self: *const Callback, this: anytype, args: anytype, result: *Result) !void {
var try_catch: TryCatch = undefined;
try_catch.init(self.executor);
defer try_catch.deinit();
self.call(args) catch |err| {
self.callWithThis(this, args) catch |err| {
if (try_catch.hasCaught()) {
const allocator = self.executor.scope.?.call_arena;
result.stack = try_catch.stack(allocator) catch null;
@@ -1138,9 +1160,11 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
};
}
fn callWithThis(self: *const @This(), js_this: v8.Object, args: anytype) !void {
pub fn callWithThis(self: *const Callback, this: anytype, args: anytype) !void {
const executor = self.executor;
const js_this = try executor.valueToExistingObject(this);
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
const fields = @typeInfo(@TypeOf(aargs)).@"struct".fields;
var js_args: [fields.len]v8.Value = undefined;
@@ -1154,8 +1178,12 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
}
}
fn getThis(self: *const Callback) v8.Object {
return self._this orelse self.executor.context.getGlobal();
}
// debug/helper to print the source of the JS callback
fn printFunc(self: *const @This()) !void {
fn printFunc(self: Callback) !void {
const executor = self.executor;
const value = self.func.castToFunction().toValue();
const src = try valueToString(executor.call_arena.allocator(), value, executor.isolate, executor.context);
@@ -1198,6 +1226,28 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
}
};
// This only exists so that we know whether a function wants the opaque
// JS argument (JsObject), or if it wants the receiver as an opaque
// value.
// JsObject is normally used when a method wants an opaque JS object
// that it'll pass into a callback.
// JsThis is used when the function wants to do advanced manipulation
// of the v8.Object bound to the instance. For example, postAttach is an
// example of using JsThis.
pub const JsThis = struct {
obj: JsObject,
const _JSTHIS_ID_KLUDGE = true;
pub fn setIndex(self: JsThis, index: usize, value: anytype) !void {
return self.obj.setIndex(index, value);
}
pub fn set(self: JsThis, key: []const u8, value: anytype) !void {
return self.obj.set(key, value);
}
};
pub const TryCatch = struct {
inner: v8.TryCatch,
executor: *const Executor,
@@ -1761,14 +1811,14 @@ fn Caller(comptime E: type) type {
break :blk params[0 .. params.len - 1];
}
// If the last parameter is a JsObject, set it, and exclude it
// If the last parameter is a special JsThis, set it, and exclude it
// from our params slice, because we don't want to bind it to
// a JS argument
if (comptime isJsObject(params[params.len - 1].type.?)) {
@field(args, std.fmt.comptimePrint("{d}", .{params.len - 1 + offset})) = .{
.handle = info.getThis(),
if (comptime isJsThis(params[params.len - 1].type.?)) {
@field(args, std.fmt.comptimePrint("{d}", .{params.len - 1 + offset})) = .{ .obj = .{
.js_obj = info.getThis(),
.executor = self.executor,
};
} };
// AND the 2nd last parameter is state
if (params.len > 1 and comptime isState(params[params.len - 2].type.?)) {
@@ -1827,9 +1877,9 @@ fn Caller(comptime E: type) type {
}
if (comptime isState(param.type.?)) {
@compileError("State must be the last parameter (or 2nd last if there's a JsObject): " ++ named_function.full_name);
} else if (comptime isJsObject(param.type.?)) {
@compileError("JsObject must be the last parameter: " ++ named_function.full_name);
@compileError("State must be the last parameter (or 2nd last if there's a JsThis): " ++ named_function.full_name);
} else if (comptime isJsThis(param.type.?)) {
@compileError("JsThis must be the last parameter: " ++ named_function.full_name);
} else if (i >= js_parameter_count) {
if (@typeInfo(param.type.?) != .optional) {
return error.InvalidArgument;
@@ -1929,9 +1979,20 @@ fn Caller(comptime E: type) type {
if (!js_value.isObject()) {
return error.InvalidArgument;
}
const js_obj = js_value.castTo(v8.Object);
if (comptime isJsObject(T)) {
// Caller wants an opaque JsObject. Probably a parameter
// that it needs to pass back into a callback
return E.JsObject{
.js_obj = js_obj,
.executor = self.executor,
};
}
const context = self.context;
const isolate = self.isolate;
const js_obj = js_value.castTo(v8.Object);
var value: T = undefined;
inline for (s.fields) |field| {
@@ -2017,6 +2078,10 @@ fn Caller(comptime E: type) type {
fn isJsObject(comptime T: type) bool {
return @typeInfo(T) == .@"struct" and @hasDecl(T, "_JSOBJECT_ID_KLUDGE");
}
fn isJsThis(comptime T: type) bool {
return @typeInfo(T) == .@"struct" and @hasDecl(T, "_JSTHIS_ID_KLUDGE");
}
};
}