From f4e8bb6c6652bf7a39ebe7edcbac710cefc8a118 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 17 Apr 2025 09:26:37 +0800 Subject: [PATCH] Re-introduce `postAttach` index_get seems to be ~1000x slower than setting the value directly on the v8.Object. There's a lot of information on "v8 fast properties", and values set directly on objects seem to be heavily optimized. Still, I can't imagine indexed properties are always _that_ slow, so I must be doing something wrong. Still, for now, this brings back the original functionality / behavior / perf. Introduce the ability for Zig functions to take a Env.JsObject parameter. While this isn't currently being used, it aligns with bringing back the postAttach / JSObject functionality in main. Moved function *State to the end of the function list (making it consistent with getters and setters). The optional Env.JsObject parameter comes after the optional state. Removed some uncessary arena deinits from a few webapis. --- src/browser/dom/comment.zig | 2 +- src/browser/dom/document.zig | 8 +- src/browser/dom/element.zig | 8 +- src/browser/dom/event_target.zig | 4 +- src/browser/dom/html_collection.zig | 35 ++--- src/browser/dom/implementation.zig | 4 +- src/browser/dom/mutation_observer.zig | 8 +- src/browser/dom/node.zig | 5 - src/browser/dom/nodelist.zig | 37 ++++-- src/browser/dom/text.zig | 2 +- src/browser/env.zig | 1 + src/browser/html/document.zig | 2 +- src/browser/html/elements.zig | 29 ++--- src/browser/html/history.zig | 4 - src/browser/html/location.zig | 4 - src/browser/html/navigator.zig | 4 - src/browser/html/window.zig | 4 +- src/browser/storage/storage.zig | 3 - src/browser/url/url.zig | 51 ++------ src/browser/xhr/xhr.zig | 2 +- src/browser/xmlserializer/xmlserializer.zig | 2 +- src/runtime/js.zig | 135 ++++++++++++++++---- src/runtime/test_complex_types.zig | 2 +- src/testing.zig | 2 +- 24 files changed, 201 insertions(+), 157 deletions(-) diff --git a/src/browser/dom/comment.zig b/src/browser/dom/comment.zig index b03ddea6..25734253 100644 --- a/src/browser/dom/comment.zig +++ b/src/browser/dom/comment.zig @@ -28,7 +28,7 @@ pub const Comment = struct { pub const Self = parser.Comment; pub const prototype = *CharacterData; - pub fn constructor(state: *const SessionState, data: ?[]const u8) !*parser.Comment { + pub fn constructor(data: ?[]const u8, state: *const SessionState) !*parser.Comment { return parser.documentCreateComment( parser.documentHTMLToDocument(state.document.?), data orelse "", diff --git a/src/browser/dom/document.zig b/src/browser/dom/document.zig index b3975b68..1ac56df4 100644 --- a/src/browser/dom/document.zig +++ b/src/browser/dom/document.zig @@ -138,16 +138,16 @@ pub const Document = struct { // HTMLCollection in zig here. pub fn _getElementsByTagName( self: *parser.Document, - state: *SessionState, tag_name: []const u8, + state: *SessionState, ) !collection.HTMLCollection { return try collection.HTMLCollectionByTagName(state.arena, parser.documentToNode(self), tag_name, true); } pub fn _getElementsByClassName( self: *parser.Document, - state: *SessionState, classNames: []const u8, + state: *SessionState, ) !collection.HTMLCollection { const allocator = state.arena; return try collection.HTMLCollectionByClassName(allocator, parser.documentToNode(self), classNames, true); @@ -212,7 +212,7 @@ pub const Document = struct { return 1; } - pub fn _querySelector(self: *parser.Document, state: *SessionState, selector: []const u8) !?ElementUnion { + pub fn _querySelector(self: *parser.Document, selector: []const u8, state: *SessionState) !?ElementUnion { if (selector.len == 0) return null; const allocator = state.arena; @@ -223,7 +223,7 @@ pub const Document = struct { return try Element.toInterface(parser.nodeToElement(n.?)); } - pub fn _querySelectorAll(self: *parser.Document, state: *SessionState, selector: []const u8) !NodeList { + pub fn _querySelectorAll(self: *parser.Document, selector: []const u8, state: *SessionState) !NodeList { const allocator = state.arena; return css.querySelectorAll(allocator, parser.documentToNode(self), selector); } diff --git a/src/browser/dom/element.zig b/src/browser/dom/element.zig index a7591ebf..ffd937e9 100644 --- a/src/browser/dom/element.zig +++ b/src/browser/dom/element.zig @@ -226,8 +226,8 @@ pub const Element = struct { pub fn _getElementsByTagName( self: *parser.Element, - state: *SessionState, tag_name: []const u8, + state: *SessionState, ) !collection.HTMLCollection { return try collection.HTMLCollectionByTagName( state.arena, @@ -239,8 +239,8 @@ pub const Element = struct { pub fn _getElementsByClassName( self: *parser.Element, - state: *SessionState, classNames: []const u8, + state: *SessionState, ) !collection.HTMLCollection { return try collection.HTMLCollectionByClassName( state.arena, @@ -306,7 +306,7 @@ pub const Element = struct { } } - pub fn _querySelector(self: *parser.Element, state: *SessionState, selector: []const u8) !?Union { + pub fn _querySelector(self: *parser.Element, selector: []const u8, state: *SessionState) !?Union { if (selector.len == 0) return null; const n = try css.querySelector(state.arena, parser.elementToNode(self), selector); @@ -316,7 +316,7 @@ pub const Element = struct { return try toInterface(parser.nodeToElement(n.?)); } - pub fn _querySelectorAll(self: *parser.Element, state: *SessionState, selector: []const u8) !NodeList { + pub fn _querySelectorAll(self: *parser.Element, selector: []const u8, state: *SessionState) !NodeList { return css.querySelectorAll(state.arena, parser.elementToNode(self), selector); } diff --git a/src/browser/dom/event_target.zig b/src/browser/dom/event_target.zig index 51bdcf7b..959a35ca 100644 --- a/src/browser/dom/event_target.zig +++ b/src/browser/dom/event_target.zig @@ -46,10 +46,10 @@ pub const EventTarget = struct { pub fn _addEventListener( self: *parser.EventTarget, - state: *SessionState, eventType: []const u8, cbk: Env.Callback, capture: ?bool, + state: *SessionState, // TODO: hanle EventListenerOptions // see #https://github.com/lightpanda-io/jsruntime-lib/issues/114 ) !void { @@ -76,10 +76,10 @@ pub const EventTarget = struct { pub fn _removeEventListener( self: *parser.EventTarget, - state: *SessionState, eventType: []const u8, cbk: Env.Callback, capture: ?bool, + state: *SessionState, // TODO: hanle EventListenerOptions // see #https://github.com/lightpanda-io/jsruntime-lib/issues/114 ) !void { diff --git a/src/browser/dom/html_collection.zig b/src/browser/dom/html_collection.zig index 568a53bb..3a7aa8fa 100644 --- a/src/browser/dom/html_collection.zig +++ b/src/browser/dom/html_collection.zig @@ -24,6 +24,8 @@ const utils = @import("utils.z"); const Element = @import("element.zig").Element; const Union = @import("element.zig").Union; +const JsObject = @import("../env.zig").JsObject; + const Walker = @import("walker.zig").Walker; const WalkerDepthFirst = @import("walker.zig").WalkerDepthFirst; const WalkerChildren = @import("walker.zig").WalkerChildren; @@ -318,10 +320,6 @@ pub const HTMLCollection = struct { cur_idx: ?u32 = undefined, cur_node: ?*parser.Node = undefined, - // array_like_keys is used to keep reference to array like interface implementation. - // the collection generates keys string which must be free on deinit. - array_like_keys: std.ArrayListUnmanaged([]u8) = .{}, - // start returns the first node to walk on. fn start(self: HTMLCollection) !?*parser.Node { if (self.root == null) return null; @@ -403,13 +401,6 @@ pub const HTMLCollection = struct { return try Element.toInterface(e); } - pub fn indexed_get(self: *HTMLCollection, index: u32, has_value: *bool) !?Union { - return (try self._item(index)) orelse { - has_value.* = false; - return null; - }; - } - pub fn _namedItem(self: *const HTMLCollection, name: []const u8) !?Union { if (self.root == null) return null; if (name.len == 0) return null; @@ -441,13 +432,6 @@ pub const HTMLCollection = struct { return null; } - pub fn named_get(self: *HTMLCollection, name: []const u8, has_value: *bool) !?Union { - return (try self._namedItem(name)) orelse { - has_value.* = false; - return null; - }; - } - fn item_name(elt: *parser.Element) !?[]const u8 { if (try parser.elementGetAttribute(elt, "id")) |v| { return v; @@ -459,10 +443,17 @@ pub const HTMLCollection = struct { return null; } - pub fn deinit(self: *HTMLCollection, alloc: std.mem.Allocator) void { - for (self.array_like_keys_) |k| alloc.free(k); - self.array_like_keys.deinit(alloc); - self.matcher.deinit(alloc); + pub fn postAttach(self: *HTMLCollection, js_obj: JsObject) !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); + + if (try item_name(e)) |name| { + try js_obj.set(name, e); + } + } } }; diff --git a/src/browser/dom/implementation.zig b/src/browser/dom/implementation.zig index 7681674f..b97b37e5 100644 --- a/src/browser/dom/implementation.zig +++ b/src/browser/dom/implementation.zig @@ -31,10 +31,10 @@ pub const DOMImplementation = struct { pub fn _createDocumentType( _: *DOMImplementation, - state: *SessionState, qname: []const u8, publicId: []const u8, systemId: []const u8, + state: *SessionState, ) !*parser.DocumentType { const allocator = state.arena; const cqname = try allocator.dupeZ(u8, qname); @@ -51,10 +51,10 @@ pub const DOMImplementation = struct { pub fn _createDocument( _: *DOMImplementation, - state: *SessionState, namespace: ?[]const u8, qname: ?[]const u8, doctype: ?*parser.DocumentType, + state: *SessionState, ) !*parser.Document { const allocator = state.arena; var cnamespace: ?[:0]const u8 = null; diff --git a/src/browser/dom/mutation_observer.zig b/src/browser/dom/mutation_observer.zig index a3df4a03..0324399e 100644 --- a/src/browser/dom/mutation_observer.zig +++ b/src/browser/dom/mutation_observer.zig @@ -22,6 +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 NodeList = @import("nodelist.zig").NodeList; pub const Interfaces = .{ @@ -84,7 +85,7 @@ pub const MutationObserver = struct { return opt orelse .{}; } - pub fn _observe(self: *MutationObserver, state: *SessionState, node: *parser.Node, options: ?MutationObserverInit) !void { + pub fn _observe(self: *MutationObserver, node: *parser.Node, options: ?MutationObserverInit, state: *SessionState) !void { const arena = state.arena; const o = try arena.create(Observer); o.* = .{ @@ -183,6 +184,11 @@ pub const MutationRecords = struct { return null; }; } + pub fn postAttach(self: *const MutationRecords, js_obj: JsObject) !void { + if (self.first) |mr| { + try js_obj.set("0", mr); + } + } }; pub const MutationRecord = struct { diff --git a/src/browser/dom/node.zig b/src/browser/dom/node.zig index 4359e07f..edee957f 100644 --- a/src/browser/dom/node.zig +++ b/src/browser/dom/node.zig @@ -18,11 +18,6 @@ const std = @import("std"); -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; -const runScript = jsruntime.test_utils.runScript; - const parser = @import("../netsurf.zig"); const generate = @import("../../runtime/generate.zig"); diff --git a/src/browser/dom/nodelist.zig b/src/browser/dom/nodelist.zig index 4c231274..2d27f4d0 100644 --- a/src/browser/dom/nodelist.zig +++ b/src/browser/dom/nodelist.zig @@ -20,10 +20,9 @@ const std = @import("std"); const parser = @import("../netsurf.zig"); -const jsruntime = @import("jsruntime"); +const JsObject = @import("../env.zig").JsObject; const Callback = @import("../env.zig").Callback; -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; +const SessionState = @import("../env.zig").SessionState; const NodeUnion = @import("node.zig").Union; const Node = @import("node.zig").Node; @@ -133,13 +132,22 @@ pub const NodeList = struct { return try Node.toInterface(n); } - // TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries - pub fn indexed_get(self: *const NodeList, index: u32, has_value: *bool) !?NodeUnion { - return (try self._item(index)) orelse { - has_value.* = false; - return null; - }; - } + // This code works, but it's _MUCH_ slower than using postAttach. The benefit + // of this version, is that it's "live"..but we're talking many orders of + // magnitude slower. + // + // You can test it by commenting out `postAttach`, uncommenting this and + // running: + // zig build wpt -- tests/wpt/dom/nodes/NodeList-static-length-getter-tampered-indexOf-1.html + // + // I think this _is_ the right way to do it, but I must be doing something + // wrong to make it so slow. + // pub fn indexed_get(self: *const NodeList, index: u32, has_value: *bool) !?NodeUnion { + // return (try self._item(index)) orelse { + // has_value.* = false; + // return null; + // }; + // } pub fn _forEach(self: *NodeList, cbk: Callback) !void { // TODO handle thisArg for (self.nodes.items, 0..) |n, i| { @@ -167,6 +175,15 @@ pub const NodeList = struct { pub fn _symbol_iterator(self: *NodeList) NodeListIterator { return self._values(); } + + // TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries + pub fn postAttach(self: *NodeList, js_obj: JsObject) !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); + } + } }; const testing = @import("../../testing.zig"); diff --git a/src/browser/dom/text.zig b/src/browser/dom/text.zig index 2c920178..aa99deca 100644 --- a/src/browser/dom/text.zig +++ b/src/browser/dom/text.zig @@ -33,7 +33,7 @@ pub const Text = struct { pub const Self = parser.Text; pub const prototype = *CharacterData; - pub fn constructor(state: *const SessionState, data: ?[]const u8) !*parser.Text { + pub fn constructor(data: ?[]const u8, state: *const SessionState) !*parser.Text { return parser.documentCreateTextNode( parser.documentHTMLToDocument(state.document.?), data orelse "", diff --git a/src/browser/env.zig b/src/browser/env.zig index 6df3fc71..0fd4370d 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 JsObject = Env.JsObject; pub const Callback = Env.Callback; pub const Env = js.Env(*SessionState, Interfaces{}); diff --git a/src/browser/html/document.zig b/src/browser/html/document.zig index 3ab251c3..d434ee21 100644 --- a/src/browser/html/document.zig +++ b/src/browser/html/document.zig @@ -102,7 +102,7 @@ pub const HTMLDocument = struct { return v; } - pub fn _getElementsByName(self: *parser.DocumentHTML, state: *SessionState, name: []const u8) !NodeList { + pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, state: *SessionState) !NodeList { const arena = state.arena; var list = NodeList.init(); errdefer list.deinit(arena); diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index 9ce0855d..0cee0cca 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -211,7 +211,7 @@ pub const HTMLAnchorElement = struct { inline fn url(self: *parser.Anchor, state: *SessionState) !URL { const href = try parser.anchorGetHref(self); - return URL.constructor(state, href, null); // TODO inject base url + return URL.constructor(href, null, state); // TODO inject base url } // TODO return a disposable string @@ -231,9 +231,7 @@ pub const HTMLAnchorElement = struct { var u = try url(self, state); u.uri.scheme = v; - const href = try u.format(arena); - defer arena.free(href); - + const href = try u.toString(arena); try parser.anchorSetHref(self, href); } @@ -266,8 +264,7 @@ pub const HTMLAnchorElement = struct { u.uri.port = null; } - const href = try u.format(arena); - defer arena.free(href); + const href = try u.toString(arena); try parser.anchorSetHref(self, href); } @@ -281,7 +278,7 @@ pub const HTMLAnchorElement = struct { const arena = state.arena; var u = try url(self, state); u.uri.host = .{ .raw = v }; - const href = try u.format(arena); + const href = try u.toString(arena); try parser.anchorSetHref(self, href); } @@ -301,8 +298,7 @@ pub const HTMLAnchorElement = struct { u.uri.port = null; } - const href = try u.format(arena); - defer arena.free(href); + const href = try u.toString(arena); try parser.anchorSetHref(self, href); } @@ -321,8 +317,7 @@ pub const HTMLAnchorElement = struct { } else { u.uri.user = null; } - const href = try u.format(arena); - defer arena.free(href); + const href = try u.toString(arena); try parser.anchorSetHref(self, href); } @@ -342,8 +337,7 @@ pub const HTMLAnchorElement = struct { } else { u.uri.password = null; } - const href = try u.format(arena); - defer arena.free(href); + const href = try u.toString(arena); try parser.anchorSetHref(self, href); } @@ -358,8 +352,7 @@ pub const HTMLAnchorElement = struct { const arena = state.arena; var u = try url(self, state); u.uri.path = .{ .raw = v }; - const href = try u.format(arena); - defer arena.free(href); + const href = try u.toString(arena); try parser.anchorSetHref(self, href); } @@ -379,8 +372,7 @@ pub const HTMLAnchorElement = struct { } else { u.uri.query = null; } - const href = try u.format(arena); - defer arena.free(href); + const href = try u.toString(arena); try parser.anchorSetHref(self, href); } @@ -400,8 +392,7 @@ pub const HTMLAnchorElement = struct { } else { u.uri.fragment = null; } - const href = try u.format(arena); - defer arena.free(href); + const href = try u.toString(arena); try parser.anchorSetHref(self, href); } diff --git a/src/browser/html/history.zig b/src/browser/html/history.zig index 75cdc46f..0bd07f9c 100644 --- a/src/browser/html/history.zig +++ b/src/browser/html/history.zig @@ -19,10 +19,6 @@ const std = @import("std"); const builtin = @import("builtin"); -const jsruntime = @import("jsruntime"); - -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; // https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface pub const History = struct { diff --git a/src/browser/html/location.zig b/src/browser/html/location.zig index 294f25ab..08f6aa4c 100644 --- a/src/browser/html/location.zig +++ b/src/browser/html/location.zig @@ -21,13 +21,9 @@ const std = @import("std"); const SessionState = @import("../env.zig").SessionState; const builtin = @import("builtin"); -const jsruntime = @import("jsruntime"); const URL = @import("../url/url.zig").URL; -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; - // https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-location-interface pub const Location = struct { url: ?URL = null, diff --git a/src/browser/html/navigator.zig b/src/browser/html/navigator.zig index a501c2f6..e09f6f1a 100644 --- a/src/browser/html/navigator.zig +++ b/src/browser/html/navigator.zig @@ -19,10 +19,6 @@ const std = @import("std"); const builtin = @import("builtin"); -const jsruntime = @import("jsruntime"); - -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; // https://html.spec.whatwg.org/multipage/system-state.html#navigator pub const Navigator = struct { diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig index 7ae30cf1..45aa7673 100644 --- a/src/browser/html/window.zig +++ b/src/browser/html/window.zig @@ -116,7 +116,7 @@ pub const Window = struct { } // TODO handle callback arguments. - pub fn _setTimeout(self: *Window, state: *SessionState, cbk: Callback, delay: ?u32) !u32 { + pub fn _setTimeout(self: *Window, cbk: Callback, delay: ?u32, state: *SessionState) !u32 { if (self.timeoutid >= self.timeoutids.len) return error.TooMuchTimeout; const ddelay: u63 = delay orelse 0; @@ -128,7 +128,7 @@ pub const Window = struct { return self.timeoutid; } - pub fn _clearTimeout(self: *Window, state: *SessionState, id: u32) !void { + pub fn _clearTimeout(self: *Window, id: u32, state: *SessionState) !void { // I do would prefer return an error in this case, but it seems some JS // uses invalid id, in particular id 0. // So we silently ignore invalid id for now. diff --git a/src/browser/storage/storage.zig b/src/browser/storage/storage.zig index 616d1bf9..90a15584 100644 --- a/src/browser/storage/storage.zig +++ b/src/browser/storage/storage.zig @@ -18,9 +18,6 @@ const std = @import("std"); -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; const DOMError = @import("../netsurf.zig").DOMError; const log = std.log.scoped(.storage); diff --git a/src/browser/url/url.zig b/src/browser/url/url.zig index d967b95d..7c7bf0a9 100644 --- a/src/browser/url/url.zig +++ b/src/browser/url/url.zig @@ -44,7 +44,11 @@ pub const URL = struct { uri: std.Uri, search_params: URLSearchParams, - pub fn constructor(state: *SessionState, url: []const u8, base: ?[]const u8) !URL { + pub fn constructor( + url: []const u8, + base: ?[]const u8, + state: *SessionState, + ) !URL { const arena = state.arena; const raw = try std.mem.concat(arena, u8, &[_][]const u8{ url, base orelse "" }); errdefer arena.free(raw); @@ -63,13 +67,8 @@ pub const URL = struct { }; } - // the caller must free the returned string. - // TODO return a disposable string - // https://github.com/lightpanda-io/jsruntime-lib/issues/195 pub fn get_origin(self: *URL, state: *SessionState) ![]const u8 { var buf = std.ArrayList(u8).init(state.arena); - defer buf.deinit(); - try self.uri.writeToStream(.{ .scheme = true, .authentication = false, @@ -78,32 +77,27 @@ pub const URL = struct { .query = false, .fragment = false, }, buf.writer()); - return try buf.toOwnedSlice(); + return buf.items; } // get_href returns the URL by writing all its components. // The query is replaced by a dump of search params. // - // the caller must free the returned string. - // TODO return a disposable string - // https://github.com/lightpanda-io/jsruntime-lib/issues/195 pub fn get_href(self: *URL, state: *SessionState) ![]const u8 { const arena = state.arena; // retrieve the query search from search_params. const cur = self.uri.query; defer self.uri.query = cur; var q = std.ArrayList(u8).init(arena); - defer q.deinit(); try self.search_params.values.encode(q.writer()); self.uri.query = .{ .percent_encoded = q.items }; - return try self.format(arena); + return try self.toString(arena); } // format the url with all its components. - pub fn format(self: *URL, arena: std.mem.Allocator) ![]const u8 { + pub fn toString(self: *URL, arena: std.mem.Allocator) ![]const u8 { var buf = std.ArrayList(u8).init(arena); - defer buf.deinit(); try self.uri.writeToStream(.{ .scheme = true, @@ -113,12 +107,9 @@ pub const URL = struct { .query = uriComponentNullStr(self.uri.query).len > 0, .fragment = uriComponentNullStr(self.uri.fragment).len > 0, }, buf.writer()); - return try buf.toOwnedSlice(); + return buf.items; } - // the caller must free the returned string. - // TODO return a disposable string - // https://github.com/lightpanda-io/jsruntime-lib/issues/195 pub fn get_protocol(self: *URL, state: *SessionState) ![]const u8 { return try std.mem.concat(state.arena, u8, &[_][]const u8{ self.uri.scheme, ":" }); } @@ -131,12 +122,8 @@ pub const URL = struct { return uriComponentNullStr(self.uri.password); } - // the caller must free the returned string. - // TODO return a disposable string - // https://github.com/lightpanda-io/jsruntime-lib/issues/195 pub fn get_host(self: *URL, state: *SessionState) ![]const u8 { var buf = std.ArrayList(u8).init(state.arena); - defer buf.deinit(); try self.uri.writeToStream(.{ .scheme = false, @@ -146,25 +133,20 @@ pub const URL = struct { .query = false, .fragment = false, }, buf.writer()); - return try buf.toOwnedSlice(); + return buf.items; } pub fn get_hostname(self: *URL) []const u8 { return uriComponentNullStr(self.uri.host); } - // the caller must free the returned string. - // TODO return a disposable string - // https://github.com/lightpanda-io/jsruntime-lib/issues/195 pub fn get_port(self: *URL, state: *SessionState) ![]const u8 { const arena = state.arena; if (self.uri.port == null) return try arena.dupe(u8, ""); var buf = std.ArrayList(u8).init(arena); - defer buf.deinit(); - try std.fmt.formatInt(self.uri.port.?, 10, .lower, .{}, buf.writer()); - return try buf.toOwnedSlice(); + return buf.items; } pub fn get_pathname(self: *URL) []const u8 { @@ -172,24 +154,17 @@ pub const URL = struct { return uriComponentStr(self.uri.path); } - // the caller must free the returned string. - // TODO return a disposable string - // https://github.com/lightpanda-io/jsruntime-lib/issues/195 pub fn get_search(self: *URL, state: *SessionState) ![]const u8 { const arena = state.arena; if (self.search_params.get_size() == 0) return try arena.dupe(u8, ""); var buf: std.ArrayListUnmanaged(u8) = .{}; - defer buf.deinit(arena); try buf.append(arena, '?'); try self.search_params.values.encode(buf.writer(arena)); - return buf.toOwnedSlice(arena); + return buf.items; } - // the caller must free the returned string. - // TODO return a disposable string - // https://github.com/lightpanda-io/jsruntime-lib/issues/195 pub fn get_hash(self: *URL, state: *SessionState) ![]const u8 { const arena = state.arena; if (self.uri.fragment == null) return try arena.dupe(u8, ""); @@ -226,7 +201,7 @@ fn uriComponentStr(c: std.Uri.Component) []const u8 { pub const URLSearchParams = struct { values: query.Values, - pub fn constructor(state: *SessionState, qs: ?[]const u8) !URLSearchParams { + pub fn constructor(qs: ?[]const u8, state: *SessionState) !URLSearchParams { return init(state.arena, qs); } diff --git a/src/browser/xhr/xhr.zig b/src/browser/xhr/xhr.zig index 8d2c1225..b0aa59d5 100644 --- a/src/browser/xhr/xhr.zig +++ b/src/browser/xhr/xhr.zig @@ -451,7 +451,7 @@ pub const XMLHttpRequest = struct { } // TODO body can be either a XMLHttpRequestBodyInit or a document - pub fn _send(self: *XMLHttpRequest, session_state: *SessionState, body: ?[]const u8) !void { + pub fn _send(self: *XMLHttpRequest, body: ?[]const u8, session_state: *SessionState) !void { if (self.state != .opened) return DOMError.InvalidState; if (self.send_flag) return DOMError.InvalidState; diff --git a/src/browser/xmlserializer/xmlserializer.zig b/src/browser/xmlserializer/xmlserializer.zig index d4e20d71..ea2a4697 100644 --- a/src/browser/xmlserializer/xmlserializer.zig +++ b/src/browser/xmlserializer/xmlserializer.zig @@ -34,7 +34,7 @@ pub const XMLSerializer = struct { return .{}; } - pub fn _serializeToString(_: *const XMLSerializer, state: *SessionState, root: *parser.Node) ![]const u8 { + pub fn _serializeToString(_: *const XMLSerializer, root: *parser.Node, state: *SessionState) ![]const u8 { var buf = std.ArrayList(u8).init(state.arena); if (try parser.nodeType(root) == .document) { try dump.writeHTML(@as(*parser.Document, @ptrCast(root)), buf.writer()); diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 5ef243f4..797d88c5 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -975,8 +975,23 @@ pub fn Env(comptime S: type, comptime types: anytype) type { // typeTaggedAnyOpaque, we'll also know there that // the type is empty and can create an empty instance. } + + // Do not move this _AFTER_ the postAttach code. + // postAttach is likely to call back into this function + // mutating our identity_map, and making the gop pointers + // invalid. const js_persistent = PersistentObject.init(isolate, js_obj); gop.value_ptr.* = js_persistent; + + if (@hasDecl(ptr.child, "postAttach")) { + const obj_wrap = JsObject{ .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), + else => @compileError(@typeName(ptr.child) ++ ".postAttach must take 2 or 3 parameters"), + } + } + return js_persistent; }, else => @compileError("Expected a struct or pointer, got " ++ @typeName(T) ++ " (constructors must return struct or pointers)"), @@ -1117,6 +1132,41 @@ pub fn Env(comptime S: type, comptime types: anytype) type { } }; + pub const JsObject = struct { + js_obj: v8.Object, + executor: *Executor, + + // If a Zig struct wants the Object parameter, it'll declare a + // function like: + // fn _length(self: *const NodeList, js_obj: Env.Object) usize + // + // When we're trying to call this function, we can't just do + // if (params[i].type.? == Object) + // Because there is _no_ object, there's only an Env.Object, where + // Env is a generic. + // We could probably figure out a way to do this, but simply checking + // for this declaration is _a lot_ easier. + const _JSOBJECT_ID_KLUDGE = true; + + pub fn setIndex(self: JsObject, index: usize, value: anytype) !void { + const key = switch (index) { + inline 0...1000 => |i| std.fmt.comptimePrint("{d}", .{i}), + else => try std.fmt.allocPrint(self.executor.scope_arena.allocator(), "{d}", .{index}), + }; + return self.set(key, value); + } + + pub fn set(self: JsObject, key: []const u8, value: anytype) !void { + const executor = self.executor; + + const js_key = v8.String.initUtf8(executor.isolate, key); + const js_value = try executor.zigValueToJs(value); + if (!self.js_obj.setValue(executor.context, js_key, js_value)) { + return error.FailedToSet; + } + } + }; + pub const TryCatch = struct { inner: v8.TryCatch, executor: *const Executor, @@ -1641,64 +1691,93 @@ fn Caller(comptime E: type) type { // parameters into the array. fn getArgs(self: *const Self, comptime named_function: anytype, comptime offset: usize, info: anytype) !ParamterTypes(@TypeOf(named_function.func)) { const F = @TypeOf(named_function.func); - const zig_function_parameters = @typeInfo(F).@"fn".params; - var args: ParamterTypes(F) = undefined; - if (zig_function_parameters.len == 0) { + + const params = @typeInfo(F).@"fn".params[offset..]; + // Except for the constructor, the first parameter is always `self` + // This isn't something we'll bind from JS, so skip it. + const params_to_map = blk: { + if (params.len == 0) { + return args; + } + + // If the last parameter is the State, set it, and exclude it + // from our params slice, because we don't want to bind it to + // a JS argument + if (comptime isState(params[params.len - 1].type.?)) { + @field(args, std.fmt.comptimePrint("{d}", .{params.len - 1 + offset})) = self.executor.state; + break :blk params[0 .. params.len - 1]; + } + + // If the last parameter is a JsObject, 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(), + .executor = self.executor, + }; + + // AND the 2nd last parameter is state + if (params.len > 1 and comptime isState(params[params.len - 2].type.?)) { + @field(args, std.fmt.comptimePrint("{d}", .{params.len - 2 + offset})) = self.executor.state; + break :blk params[0 .. params.len - 2]; + } + + break :blk params[0 .. params.len - 1]; + } + + // we have neither a State nor a JsObject. All params must be + // bound to a JavaScript value. + break :blk params; + }; + + if (params_to_map.len == 0) { return args; } - const adjusted_offset = blk: { - if (zig_function_parameters.len > offset and comptime isState(zig_function_parameters[offset].type.?)) { - @field(args, std.fmt.comptimePrint("{d}", .{offset})) = self.executor.state; - break :blk offset + 1; - } else { - break :blk offset; - } - }; - const js_parameter_count = info.length(); - const expected_js_parameters = zig_function_parameters.len - adjusted_offset; - + const last_js_parameter = params_to_map.len - 1; var is_variadic = false; - const last_parameter_index = zig_function_parameters.len - 1; + { // This is going to get complicated. If the last Zig paremeter // is a slice AND the corresponding javascript parameter is // NOT an an array, then we'll treat it as a variadic. - const last_parameter_type = zig_function_parameters[last_parameter_index].type.?; + const last_parameter_type = params_to_map[params_to_map.len - 1].type.?; const last_parameter_type_info = @typeInfo(last_parameter_type); if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) { const slice_type = last_parameter_type_info.pointer.child; - const corresponding_js_index = last_parameter_index - adjusted_offset; - const corresponding_js_value = info.getArg(@as(u32, @intCast(corresponding_js_index))); + const corresponding_js_value = info.getArg(@as(u32, @intCast(last_js_parameter))); if (corresponding_js_value.isArray() == false and slice_type != u8) { is_variadic = true; if (js_parameter_count == 0) { - @field(args, tupleFieldName(last_parameter_index)) = &.{}; + @field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{}; } else { - const arr = try self.call_allocator.alloc(last_parameter_type_info.pointer.child, js_parameter_count - expected_js_parameters + 1); - for (arr, corresponding_js_index..) |*a, i| { + const arr = try self.call_allocator.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1); + for (arr, last_js_parameter..) |*a, i| { const js_value = info.getArg(@as(u32, @intCast(i))); a.* = try self.jsValueToZig(named_function, slice_type, js_value); } - @field(args, tupleFieldName(last_parameter_index)) = arr; + @field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr; } } } } - inline for (zig_function_parameters[adjusted_offset..], 0..) |param, i| { - const field_index = comptime i + adjusted_offset; - if (comptime field_index == last_parameter_index) { + inline for (params_to_map, 0..) |param, i| { + const field_index = comptime i + offset; + if (comptime i == params_to_map.len - 1) { if (is_variadic) { break; } } if (comptime isState(param.type.?)) { - @compileError("State must be the 2nd parameter: " ++ named_function.full_name); + @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); } else if (i >= js_parameter_count) { if (@typeInfo(param.type.?) != .optional) { return error.InvalidArgument; @@ -1882,6 +1961,10 @@ fn Caller(comptime E: type) type { const Const_State = if (ti == .pointer) *const ti.pointer.child else State; return T == State or T == Const_State; } + + fn isJsObject(comptime T: type) bool { + return @typeInfo(T) == .@"struct" and @hasDecl(T, "_JSOBJECT_ID_KLUDGE"); + } }; } diff --git a/src/runtime/test_complex_types.zig b/src/runtime/test_complex_types.zig index 05744e4c..9794ad2b 100644 --- a/src/runtime/test_complex_types.zig +++ b/src/runtime/test_complex_types.zig @@ -22,7 +22,7 @@ const Allocator = std.mem.Allocator; const MyList = struct { items: []u8, - pub fn constructor(state: State, elem1: u8, elem2: u8, elem3: u8) MyList { + pub fn constructor(elem1: u8, elem2: u8, elem3: u8, state: State) MyList { var items = state.arena.alloc(u8, 3) catch unreachable; items[0] = elem1; items[1] = elem2; diff --git a/src/testing.zig b/src/testing.zig index b4c121a1..33000bc3 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -480,7 +480,7 @@ pub const JsRunner = struct { return self.executor.exec(src, null) catch |err| { if (try try_catch.err(self.arena)) |msg| { err_msg.* = msg; - std.debug.print("Error runnign script: {s}\n", .{msg}); + std.debug.print("Error running script: {s}\n", .{msg}); } return err; };