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 Element = @import("element.zig").Element;
const Union = @import("element.zig").Union; 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 Walker = @import("walker.zig").Walker;
const WalkerDepthFirst = @import("walker.zig").WalkerDepthFirst; const WalkerDepthFirst = @import("walker.zig").WalkerDepthFirst;
@@ -443,15 +443,15 @@ pub const HTMLCollection = struct {
return null; 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(); const len = try self.get_length();
for (0..len) |i| { for (0..len) |i| {
const node = try self.item(@intCast(i)) orelse unreachable; const node = try self.item(@intCast(i)) orelse unreachable;
const e = @as(*parser.Element, @ptrCast(node)); 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| { 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 SessionState = @import("../env.zig").SessionState;
const Env = @import("../env.zig").Env; const Env = @import("../env.zig").Env;
const JsObject = @import("../env.zig").JsObject; const JsThis = @import("../env.zig").JsThis;
const NodeList = @import("nodelist.zig").NodeList; const NodeList = @import("nodelist.zig").NodeList;
pub const Interfaces = .{ pub const Interfaces = .{
@@ -184,9 +184,9 @@ pub const MutationRecords = struct {
return null; 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| { 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 parser = @import("../netsurf.zig");
const JsObject = @import("../env.zig").JsObject; const JsThis = @import("../env.zig").JsThis;
const Callback = @import("../env.zig").Callback; const Callback = @import("../env.zig").Callback;
const SessionState = @import("../env.zig").SessionState; 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 // 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(); const len = self.get_length();
for (0..len) |i| { for (0..len) |i| {
const node = try self._item(@intCast(i)) orelse unreachable; 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 iterator = @import("../iterator/iterator.zig");
const Callback = @import("../env.zig").Callback; const Callback = @import("../env.zig").Callback;
const JsObject = @import("../env.zig").JsObject;
const SessionState = @import("../env.zig").SessionState; const SessionState = @import("../env.zig").SessionState;
const DOMException = @import("exceptions.zig").DOMException; const DOMException = @import("exceptions.zig").DOMException;
@@ -138,11 +139,11 @@ pub const DOMTokenList = struct {
} }
// TODO handle thisArg // 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); var entries = _entries(self);
while (try entries._next()) |entry| { while (try entries._next()) |entry| {
var result: Callback.Result = undefined; 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.err("callback error: {s}", .{result.exception});
log.debug("stack:\n{s}", .{result.stack orelse "???"}); log.debug("stack:\n{s}", .{result.stack orelse "???"});
}; };

View File

@@ -21,6 +21,7 @@ const Interfaces = generate.Tuple(.{
@import("xmlserializer/xmlserializer.zig").Interfaces, @import("xmlserializer/xmlserializer.zig").Interfaces,
}); });
pub const JsThis = Env.JsThis;
pub const JsObject = Env.JsObject; pub const JsObject = Env.JsObject;
pub const Callback = Env.Callback; pub const Callback = Env.Callback;
pub const Env = js.Env(*SessionState, Interfaces{}); 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 }); _ = 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 // Wrap a v8.Value, largely so that we can provide a convenient
// toString function // toString function
fn createValue(self: *const Executor, value: v8.Value) Value { 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; gop.value_ptr.* = js_persistent;
if (@hasDecl(ptr.child, "postAttach")) { 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) { switch (@typeInfo(@TypeOf(ptr.child.postAttach)).@"fn".params.len) {
2 => try value.postAttach(obj_wrap), 2 => try value.postAttach(obj_wrap),
3 => try value.postAttach(self.state, 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 { pub const Callback = struct {
id: usize, id: usize,
executor: *Executor, executor: *Executor,
this: ?v8.Object = null, _this: ?v8.Object = null,
func: PersistentFunction, func: PersistentFunction,
// We use this when mapping a JS value to a Zig object. We can't // 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 { pub fn setThis(self: *Callback, value: anytype) !void {
const persistent_object = self.executor.scope.?.identity_map.get(@intFromPtr(value)) orelse { self._this = try self.executor.valueToExistingObject(value);
return error.InvalidThisForCallback;
};
self.this = persistent_object.castToObject();
} }
pub fn call(self: *const Callback, args: anytype) !void { 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 { 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; var try_catch: TryCatch = undefined;
try_catch.init(self.executor); try_catch.init(self.executor);
defer try_catch.deinit(); defer try_catch.deinit();
self.call(args) catch |err| { self.callWithThis(this, args) catch |err| {
if (try_catch.hasCaught()) { if (try_catch.hasCaught()) {
const allocator = self.executor.scope.?.call_arena; const allocator = self.executor.scope.?.call_arena;
result.stack = try_catch.stack(allocator) catch null; 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 executor = self.executor;
const js_this = try executor.valueToExistingObject(this);
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args; const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
const fields = @typeInfo(@TypeOf(aargs)).@"struct".fields; const fields = @typeInfo(@TypeOf(aargs)).@"struct".fields;
var js_args: [fields.len]v8.Value = undefined; 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 // 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 executor = self.executor;
const value = self.func.castToFunction().toValue(); const value = self.func.castToFunction().toValue();
const src = try valueToString(executor.call_arena.allocator(), value, executor.isolate, executor.context); 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 { pub const TryCatch = struct {
inner: v8.TryCatch, inner: v8.TryCatch,
executor: *const Executor, executor: *const Executor,
@@ -1761,14 +1811,14 @@ fn Caller(comptime E: type) type {
break :blk params[0 .. params.len - 1]; 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 // from our params slice, because we don't want to bind it to
// a JS argument // a JS argument
if (comptime isJsObject(params[params.len - 1].type.?)) { if (comptime isJsThis(params[params.len - 1].type.?)) {
@field(args, std.fmt.comptimePrint("{d}", .{params.len - 1 + offset})) = .{ @field(args, std.fmt.comptimePrint("{d}", .{params.len - 1 + offset})) = .{ .obj = .{
.handle = info.getThis(), .js_obj = info.getThis(),
.executor = self.executor, .executor = self.executor,
}; } };
// AND the 2nd last parameter is state // AND the 2nd last parameter is state
if (params.len > 1 and comptime isState(params[params.len - 2].type.?)) { 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.?)) { if (comptime isState(param.type.?)) {
@compileError("State must be the last parameter (or 2nd last if there's a JsObject): " ++ 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 isJsObject(param.type.?)) { } else if (comptime isJsThis(param.type.?)) {
@compileError("JsObject must be the last parameter: " ++ named_function.full_name); @compileError("JsThis must be the last parameter: " ++ named_function.full_name);
} else if (i >= js_parameter_count) { } else if (i >= js_parameter_count) {
if (@typeInfo(param.type.?) != .optional) { if (@typeInfo(param.type.?) != .optional) {
return error.InvalidArgument; return error.InvalidArgument;
@@ -1929,9 +1979,20 @@ fn Caller(comptime E: type) type {
if (!js_value.isObject()) { if (!js_value.isObject()) {
return error.InvalidArgument; 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 context = self.context;
const isolate = self.isolate; const isolate = self.isolate;
const js_obj = js_value.castTo(v8.Object);
var value: T = undefined; var value: T = undefined;
inline for (s.fields) |field| { inline for (s.fields) |field| {
@@ -2017,6 +2078,10 @@ fn Caller(comptime E: type) type {
fn isJsObject(comptime T: type) bool { fn isJsObject(comptime T: type) bool {
return @typeInfo(T) == .@"struct" and @hasDecl(T, "_JSOBJECT_ID_KLUDGE"); 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");
}
}; };
} }