From a2291b0713b1e99c36db3a09a026778c105d8f88 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 18 Apr 2025 19:10:37 +0800 Subject: [PATCH 1/2] Add missing TokenList APIs Add value setter, keys(), values(), entries() and forEach(). Like nodelist, forEach still doesn't support `this` arg (gotta think about how to do this). I think these iterable methods are missing in a few places, so I added a generic Entries iterator and a generic Iterable. jsruntime will now map a Zig tuple to a JS array. --- src/browser/dom/dom.zig | 4 +- src/browser/dom/token_list.zig | 92 +++++++++++++- src/browser/iterator/iterator.zig | 197 ++++++++++++++++++++++++++++-- src/browser/netsurf.zig | 5 + src/runtime/js.zig | 13 ++ 5 files changed, 298 insertions(+), 13 deletions(-) diff --git a/src/browser/dom/dom.zig b/src/browser/dom/dom.zig index 07e57c06..c0099a04 100644 --- a/src/browser/dom/dom.zig +++ b/src/browser/dom/dom.zig @@ -20,7 +20,7 @@ const DOMException = @import("exceptions.zig").DOMException; const EventTarget = @import("event_target.zig").EventTarget; const DOMImplementation = @import("implementation.zig").DOMImplementation; const NamedNodeMap = @import("namednodemap.zig").NamedNodeMap; -const DOMTokenList = @import("token_list.zig").DOMTokenList; +const DOMTokenList = @import("token_list.zig"); const NodeList = @import("nodelist.zig"); const Node = @import("node.zig"); const MutationObserver = @import("mutation_observer.zig"); @@ -30,7 +30,7 @@ pub const Interfaces = .{ EventTarget, DOMImplementation, NamedNodeMap, - DOMTokenList, + DOMTokenList.Interfaces, NodeList.Interfaces, Node.Node, Node.Interfaces, diff --git a/src/browser/dom/token_list.zig b/src/browser/dom/token_list.zig index 0472e81e..361ef047 100644 --- a/src/browser/dom/token_list.zig +++ b/src/browser/dom/token_list.zig @@ -19,9 +19,21 @@ const std = @import("std"); const parser = @import("../netsurf.zig"); +const iterator = @import("../iterator/iterator.zig"); +const Callback = @import("../env.zig").Callback; +const SessionState = @import("../env.zig").SessionState; const DOMException = @import("exceptions.zig").DOMException; +const log = std.log.scoped(.token_list); + +pub const Interfaces = .{ + DOMTokenList, + DOMTokenListIterable, + TokenListEntriesIterator, + TokenListEntriesIterator.Iterable, +}; + // https://dom.spec.whatwg.org/#domtokenlist pub const DOMTokenList = struct { pub const Self = parser.TokenList; @@ -98,7 +110,60 @@ pub const DOMTokenList = struct { } pub fn get_value(self: *parser.TokenList) !?[]const u8 { - return try parser.tokenListGetValue(self); + return (try parser.tokenListGetValue(self)) orelse ""; + } + + pub fn set_value(self: *parser.TokenList, value: []const u8) !void { + return parser.tokenListSetValue(self, value); + } + + pub fn _toString(self: *parser.TokenList) ![]const u8 { + return (try get_value(self)) orelse ""; + } + + pub fn _keys(self: *parser.TokenList) !iterator.U32Iterator { + return .{ .length = try get_length(self) }; + } + + pub fn _values(self: *parser.TokenList) DOMTokenListIterable { + return DOMTokenListIterable.init(.{ .token_list = self }); + } + + pub fn _entries(self: *parser.TokenList) TokenListEntriesIterator { + return TokenListEntriesIterator.init(.{ .token_list = self }); + } + + pub fn _symbol_iterator(self: *parser.TokenList) DOMTokenListIterable { + return _values(self); + } + + // TODO handle thisArg + pub fn _forEach(self: *parser.TokenList, cbk: Callback) !void { + var entries = _entries(self); + while (try entries._next()) |entry| { + var result: Callback.Result = undefined; + cbk.tryCall(.{ entry.@"1", entry.@"0", self }, &result) catch { + log.err("callback error: {s}", .{result.exception}); + log.debug("stack:\n{s}", .{result.stack orelse "???"}); + }; + } + } +}; + +const DOMTokenListIterable = iterator.Iterable(Iterator, "DOMTokenListIterable"); +const TokenListEntriesIterator = iterator.NumericEntries(Iterator, "TokenListEntriesIterator"); + +pub const Iterator = struct { + index: u32 = 0, + token_list: *parser.TokenList, + + // used when wrapped in an iterator.NumericEntries + pub const Error = parser.DOMError; + + pub fn _next(self: *Iterator) !?[]const u8 { + const index = self.index; + self.index = index + 1; + return DOMTokenList._item(self.token_list, index); } }; @@ -150,4 +215,29 @@ test "Browser.DOM.TokenList" { .{ "cl4.replace('nok', 'ok')", "true" }, .{ "cl4.value", "empty ok" }, }, .{}); + + try runner.testCases(&.{ + .{ "let cl5 = gs.classList", "undefined" }, + .{ "let keys = [...cl5.keys()]", "undefined" }, + .{ "keys.length", "2" }, + .{ "keys[0]", "0" }, + .{ "keys[1]", "1" }, + + .{ "let values = [...cl5.values()]", "undefined" }, + .{ "values.length", "2" }, + .{ "values[0]", "empty" }, + .{ "values[1]", "ok" }, + + .{ "let entries = [...cl5.entries()]", "undefined" }, + .{ "entries.length", "2" }, + .{ "entries[0]", "0,empty" }, + .{ "entries[1]", "1,ok" }, + }, .{}); + + try runner.testCases(&.{ + .{ "let cl6 = gs.classList", "undefined" }, + .{ "cl6.value = 'a b ccc'", "a b ccc" }, + .{ "cl6.value", "a b ccc" }, + .{ "cl6.toString()", "a b ccc" }, + }, .{}); } diff --git a/src/browser/iterator/iterator.zig b/src/browser/iterator/iterator.zig index aa248509..cb7a1c04 100644 --- a/src/browser/iterator/iterator.zig +++ b/src/browser/iterator/iterator.zig @@ -28,24 +28,201 @@ pub const U32Iterator = struct { .done = false, }; } + + // Iterators should be iterable. There's a [JS] example on MDN that + // suggests this is the correct approach: + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterator_protocol + pub fn _symbol_iterator(self: *U32Iterator) *U32Iterator { + return self; + } }; -const testing = std.testing; -test "U32Iterator" { - const Return = U32Iterator.Return; +// A wrapper around an iterator that emits an Iterable result +// An iterable has a next() which emits a {done: bool, value: T} result +pub fn Iterable(comptime T: type, comptime JsName: []const u8) type { + // The inner iterator's return type. + // Maybe an error union. + // Definitely an optional + const RawValue = @typeInfo(@TypeOf(T._next)).@"fn".return_type.?; + const CanError = @typeInfo(RawValue) == .error_union; + const Value = blk: { + // Unwrap the RawValue + var V = RawValue; + if (CanError) { + V = @typeInfo(V).error_union.payload; + } + break :blk @typeInfo(V).optional.child; + }; + + const Result = struct { + done: bool, + // todo, technically, we should return undefined when done = true + // or even omit the value; + value: ?Value, + }; + + const ReturnType = if (CanError) T.Error!Result else Result; + + return struct { + // the inner value iterator + inner: T, + + // Generics don't generate clean names. Can't just take the resulting + // type name and use that as a the JS class name. So we always ask for + // an explicit JS class name + pub const js_name = JsName; + + const Self = @This(); + + pub fn init(inner: T) Self { + return .{ .inner = inner }; + } + + pub fn _next(self: *Self) ReturnType { + const value = if (comptime CanError) try self.inner._next() else self.inner._next(); + return .{ .done = value == null, .value = value }; + } + + pub fn _symbol_iterator(self: *Self) *Self { + return self; + } + }; +} + +// A wrapper around an iterator that emits integer/index keyed entries. +pub fn NumericEntries(comptime T: type, comptime JsName: []const u8) type { + // The inner iterator's return type. + // Maybe an error union. + // Definitely an optional + const RawValue = @typeInfo(@TypeOf(T._next)).@"fn".return_type.?; + const CanError = @typeInfo(RawValue) == .error_union; + + const Value = blk: { + // Unwrap the RawValue + var V = RawValue; + if (CanError) { + V = @typeInfo(V).error_union.payload; + } + break :blk @typeInfo(V).optional.child; + }; + + const ReturnType = if (CanError) T.Error!?struct { u32, Value } else ?struct { u32, Value }; + + // Avoid ambiguity. We want to expose a NumericEntries(T).Iterable, so we + // need a declartion inside here for an "Iterable", but that will conflict + // with the above Iterable generic function we have. + const BaseIterable = Iterable; + + return struct { + // the inner value iterator + inner: T, + index: u32, + + const Self = @This(); + + // Generics don't generate clean names. Can't just take the resulting + // type name and use that as a the JS class name. So we always ask for + // an explicit JS class name + pub const js_name = JsName; + + // re-exposed for when/if we compose this type into an Iterable + pub const Error = T.Error; + + // This iterator as an iterable + pub const Iterable = BaseIterable(Self, JsName ++ "Iterable"); + + pub fn init(inner: T) Self { + return .{ .inner = inner, .index = 0 }; + } + + pub fn _next(self: *Self) ReturnType { + const value_ = if (comptime CanError) try self.inner._next() else self.inner._next(); + const value = value_ orelse return null; + + const index = self.index; + self.index = index + 1; + return .{ index, value }; + } + + // make the iterator, iterable + pub fn _symbol_iterator(self: *Self) Self.Iterable { + return Self.Iterable.init(self.*); + } + }; +} + +const testing = @import("../../testing.zig"); +test "U32Iterator" { { var it = U32Iterator{ .length = 0 }; - try testing.expectEqual(Return{ .value = 0, .done = true }, it._next()); - try testing.expectEqual(Return{ .value = 0, .done = true }, it._next()); + try testing.expectEqual(.{ .value = 0, .done = true }, it._next()); + try testing.expectEqual(.{ .value = 0, .done = true }, it._next()); } { var it = U32Iterator{ .length = 3 }; - try testing.expectEqual(Return{ .value = 0, .done = false }, it._next()); - try testing.expectEqual(Return{ .value = 1, .done = false }, it._next()); - try testing.expectEqual(Return{ .value = 2, .done = false }, it._next()); - try testing.expectEqual(Return{ .value = 0, .done = true }, it._next()); - try testing.expectEqual(Return{ .value = 0, .done = true }, it._next()); + try testing.expectEqual(.{ .value = 0, .done = false }, it._next()); + try testing.expectEqual(.{ .value = 1, .done = false }, it._next()); + try testing.expectEqual(.{ .value = 2, .done = false }, it._next()); + try testing.expectEqual(.{ .value = 0, .done = true }, it._next()); + try testing.expectEqual(.{ .value = 0, .done = true }, it._next()); } } + +test "NumericEntries" { + const it = DummyIterator{}; + var entries = NumericEntries(DummyIterator, "DummyIterator").init(it); + + const v1 = entries._next().?; + try testing.expectEqual(0, v1.@"0"); + try testing.expectEqual("it's", v1.@"1"); + + const v2 = entries._next().?; + try testing.expectEqual(1, v2.@"0"); + try testing.expectEqual("over", v2.@"1"); + + const v3 = entries._next().?; + try testing.expectEqual(2, v3.@"0"); + try testing.expectEqual("9000!!", v3.@"1"); + + try testing.expectEqual(null, entries._next()); + try testing.expectEqual(null, entries._next()); + try testing.expectEqual(null, entries._next()); +} + +test "Iterable" { + const it = DummyIterator{}; + var entries = Iterable(DummyIterator, "DummyIterator").init(it); + + const v1 = entries._next(); + try testing.expectEqual(false, v1.done); + try testing.expectEqual("it's", v1.value.?); + + const v2 = entries._next(); + try testing.expectEqual(false, v2.done); + try testing.expectEqual("over", v2.value.?); + + const v3 = entries._next(); + try testing.expectEqual(false, v3.done); + try testing.expectEqual("9000!!", v3.value.?); + + try testing.expectEqual(true, entries._next().done); + try testing.expectEqual(true, entries._next().done); + try testing.expectEqual(true, entries._next().done); +} + +const DummyIterator = struct { + index: u32 = 0, + + pub fn _next(self: *DummyIterator) ?[]const u8 { + const index = self.index; + self.index = index + 1; + return switch (index) { + 0 => "it's", + 1 => "over", + 2 => "9000!!", + else => null, + }; + } +}; diff --git a/src/browser/netsurf.zig b/src/browser/netsurf.zig index eef41155..3a7decdd 100644 --- a/src/browser/netsurf.zig +++ b/src/browser/netsurf.zig @@ -1674,6 +1674,11 @@ pub fn tokenListGetValue(l: *TokenList) !?[]const u8 { return strToData(res.?); } +pub fn tokenListSetValue(l: *TokenList, value: []const u8) !void { + const err = c.dom_tokenlist_set_value(l, try strFromData(value)); + try DOMErr(err); +} + // ElementHTML pub const ElementHTML = c.dom_html_element; diff --git a/src/runtime/js.zig b/src/runtime/js.zig index e7233966..327cd579 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -691,6 +691,19 @@ pub fn Env(comptime S: type, comptime types: anytype) type { return value.func.toValue(); } + if (s.is_tuple) { + // return the tuple struct as an array + var js_arr = v8.Array.init(isolate, @intCast(s.fields.len)); + var js_obj = js_arr.castTo(v8.Object); + inline for (s.fields, 0..) |f, i| { + const js_val = try zigValueToJs(templates, isolate, context, @field(value, f.name)); + if (js_obj.setValueAtIndex(context, @intCast(i), js_val) == false) { + return error.FailedToCreateArray; + } + } + return js_obj.toValue(); + } + // return the struct as a JS object const js_obj = v8.Object.init(isolate); inline for (s.fields) |f| { From 9b4d1d442e3b5eb531b3f92f099cfcd52d48b476 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 18 Apr 2025 20:38:52 +0800 Subject: [PATCH 2/2] 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. --- src/browser/dom/html_collection.zig | 8 +- src/browser/dom/mutation_observer.zig | 6 +- src/browser/dom/nodelist.zig | 6 +- src/browser/dom/token_list.zig | 5 +- src/browser/env.zig | 1 + src/runtime/js.zig | 103 +++++++++++++++++++++----- 6 files changed, 98 insertions(+), 31 deletions(-) diff --git a/src/browser/dom/html_collection.zig b/src/browser/dom/html_collection.zig index 3a7aa8fa..812b6931 100644 --- a/src/browser/dom/html_collection.zig +++ b/src/browser/dom/html_collection.zig @@ -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); } } } diff --git a/src/browser/dom/mutation_observer.zig b/src/browser/dom/mutation_observer.zig index 0324399e..d9b9bc09 100644 --- a/src/browser/dom/mutation_observer.zig +++ b/src/browser/dom/mutation_observer.zig @@ -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); } } }; diff --git a/src/browser/dom/nodelist.zig b/src/browser/dom/nodelist.zig index 2d27f4d0..37ef3b31 100644 --- a/src/browser/dom/nodelist.zig +++ b/src/browser/dom/nodelist.zig @@ -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); } } }; diff --git a/src/browser/dom/token_list.zig b/src/browser/dom/token_list.zig index 361ef047..538c4156 100644 --- a/src/browser/dom/token_list.zig +++ b/src/browser/dom/token_list.zig @@ -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 "???"}); }; diff --git a/src/browser/env.zig b/src/browser/env.zig index 0fd4370d..67e7207e 100644 --- a/src/browser/env.zig +++ b/src/browser/env.zig @@ -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{}); diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 327cd579..0387eace 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -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"); + } }; }