From 072110481f4db9cb76982e7dfb7327f0b317bfa6 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 26 Apr 2025 22:17:29 +0800 Subject: [PATCH] Unify the Zig and JS events using an intrusive node. The approach borrows heavily from Zig's new LinkedList API. The main benefit is that it unifies how event callbacks are done. When the Page.windowClick event was added, the Event structure was changed to a union, supporting a distinct Zig and JS event. This new approach more or less treats everything like a Zig event. A JS event is just a Zig struct that has a Env.Callback which it can invoke in its handle method. The intrusive nature of the EventNode means that what used to be 1 or 2 allocations is now 0 or 1. It also has the benefit of making netsurf completely unaware of Env.Callbacks. --- src/browser/browser.zig | 13 +- src/browser/dom/event_target.zig | 20 ++- src/browser/dom/mutation_observer.zig | 41 ++---- src/browser/events/event.zig | 28 +++- src/browser/netsurf.zig | 203 ++++++++------------------ src/browser/xhr/event_target.zig | 44 +++--- src/runtime/js.zig | 13 +- 7 files changed, 142 insertions(+), 220 deletions(-) diff --git a/src/browser/browser.zig b/src/browser/browser.zig index ca37d30f..5ceb0bbc 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -341,12 +341,15 @@ pub const Page = struct { renderer: FlatRenderer, + window_clicked_event_node: parser.EventNode, + fn init(session: *Session) Page { const arena = session.browser.page_arena.allocator(); return .{ .arena = arena, .session = session, .renderer = FlatRenderer.init(arena), + .window_clicked_event_node = .{ .func = windowClicked }, }; } @@ -481,12 +484,10 @@ pub const Page = struct { self.doc = doc; const document_element = (try parser.documentGetDocumentElement(doc)) orelse return error.DocumentElementError; - try parser.eventTargetAddZigListener( + try parser.eventTargetAddEventListener( parser.toEventTarget(parser.Element, document_element), - arena, "click", - windowClicked, - self, + &self.window_clicked_event_node, false, ); @@ -766,8 +767,8 @@ pub const Page = struct { _ = try parser.elementDispatchEvent(element, @ptrCast(event)); } - fn windowClicked(ctx: *anyopaque, event: *parser.Event) void { - const self: *Page = @alignCast(@ptrCast(ctx)); + fn windowClicked(node: *parser.EventNode, event: *parser.Event) void { + const self: *Page = @fieldParentPtr("window_clicked_event_node", node); self._windowClicked(event) catch |err| { log.err("window click handler: {}", .{err}); }; diff --git a/src/browser/dom/event_target.zig b/src/browser/dom/event_target.zig index 959a35ca..ffee9993 100644 --- a/src/browser/dom/event_target.zig +++ b/src/browser/dom/event_target.zig @@ -46,7 +46,7 @@ pub const EventTarget = struct { pub fn _addEventListener( self: *parser.EventTarget, - eventType: []const u8, + typ: []const u8, cbk: Env.Callback, capture: ?bool, state: *SessionState, @@ -56,7 +56,7 @@ pub const EventTarget = struct { // check if event target has already this listener const lst = try parser.eventTargetHasListener( self, - eventType, + typ, capture orelse false, cbk.id, ); @@ -64,29 +64,28 @@ pub const EventTarget = struct { return; } + const eh = try EventHandler.init(state.arena, try cbk.withThis(self)); + try parser.eventTargetAddEventListener( self, - state.arena, - eventType, - EventHandler, - .{ .cbk = cbk }, + typ, + &eh.node, capture orelse false, ); } pub fn _removeEventListener( self: *parser.EventTarget, - eventType: []const u8, + typ: []const u8, cbk: Env.Callback, capture: ?bool, - state: *SessionState, // TODO: hanle EventListenerOptions // see #https://github.com/lightpanda-io/jsruntime-lib/issues/114 ) !void { // check if event target has already this listener const lst = try parser.eventTargetHasListener( self, - eventType, + typ, capture orelse false, cbk.id, ); @@ -97,8 +96,7 @@ pub const EventTarget = struct { // remove listener try parser.eventTargetRemoveEventListener( self, - state.arena, - eventType, + typ, lst.?, capture orelse false, ); diff --git a/src/browser/dom/mutation_observer.zig b/src/browser/dom/mutation_observer.zig index 8aac8f3b..3a9a7c35 100644 --- a/src/browser/dom/mutation_observer.zig +++ b/src/browser/dom/mutation_observer.zig @@ -59,56 +59,45 @@ pub const MutationObserver = struct { .node = node, .options = options, .mutation_observer = self, + .event_node = .{ .id = self.cbk.id, .func = Observer.handle }, }; - const arena = self.arena; - // register node's events if (options.childList or options.subtree) { - try parser.eventTargetAddZigListener( + try parser.eventTargetAddEventListener( parser.toEventTarget(parser.Node, node), - arena, "DOMNodeInserted", - Observer.handle, - observer, + &observer.event_node, false, ); - try parser.eventTargetAddZigListener( + try parser.eventTargetAddEventListener( parser.toEventTarget(parser.Node, node), - arena, "DOMNodeRemoved", - Observer.handle, - observer, + &observer.event_node, false, ); } if (options.attr()) { - try parser.eventTargetAddZigListener( + try parser.eventTargetAddEventListener( parser.toEventTarget(parser.Node, node), - arena, "DOMAttrModified", - Observer.handle, - observer, + &observer.event_node, false, ); } if (options.cdata()) { - try parser.eventTargetAddZigListener( + try parser.eventTargetAddEventListener( parser.toEventTarget(parser.Node, node), - arena, "DOMCharacterDataModified", - Observer.handle, - observer, + &observer.event_node, false, ); } if (options.subtree) { - try parser.eventTargetAddZigListener( + try parser.eventTargetAddEventListener( parser.toEventTarget(parser.Node, node), - arena, "DOMSubtreeModified", - Observer.handle, - observer, + &observer.event_node, false, ); } @@ -221,6 +210,8 @@ const Observer = struct { // and batch the mutation records. mutation_observer: *MutationObserver, + event_node: parser.EventNode, + fn appliesTo(o: *const Observer, target: *parser.Node) bool { // mutation on any target is always ok. if (o.options.subtree) { @@ -250,9 +241,9 @@ const Observer = struct { return false; } - fn handle(ctx: *anyopaque, event: *parser.Event) void { - // retrieve the observer from the data. - var self: *Observer = @alignCast(@ptrCast(ctx)); + fn handle(en: *parser.EventNode, event: *parser.Event) void { + const self: *Observer = @fieldParentPtr("event_node", en); + var mutation_observer = self.mutation_observer; const node = blk: { diff --git a/src/browser/events/event.zig b/src/browser/events/event.zig index ff89af70..70298cc2 100644 --- a/src/browser/events/event.zig +++ b/src/browser/events/event.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const Allocator = std.mem.Allocator; const parser = @import("../netsurf.zig"); const Callback = @import("../env.zig").Callback; @@ -136,14 +137,35 @@ pub const Event = struct { }; pub const EventHandler = struct { - fn handle(event: ?*parser.Event, data: *const parser.JSEventHandlerData) void { + callback: Callback, + node: parser.EventNode, + + pub fn init(allocator: Allocator, callback: Callback) !*EventHandler { + const eh = try allocator.create(EventHandler); + eh.* = .{ + .callback = callback, + .node = .{ + .id = callback.id, + .func = handle, + }, + }; + return eh; + } + + fn handle(node: *parser.EventNode, event: *parser.Event) void { + const ievent = Event.toInterface(event) catch |err| { + log.err("Event.toInterface: {}", .{err}); + return; + }; + + const self: *EventHandler = @fieldParentPtr("node", node); var result: Callback.Result = undefined; - data.cbk.tryCall(.{if (event) |evt| Event.toInterface(evt) catch unreachable else null}, &result) catch { + self.callback.tryCall(.{ievent}, &result) catch { log.err("event handler error: {s}", .{result.exception}); log.debug("stack:\n{s}", .{result.stack orelse "???"}); }; } -}.handle; +}; const testing = @import("../../testing.zig"); test "Browser.Event" { diff --git a/src/browser/netsurf.zig b/src/browser/netsurf.zig index df5ab147..9629def7 100644 --- a/src/browser/netsurf.zig +++ b/src/browser/netsurf.zig @@ -29,9 +29,6 @@ const c = @cImport({ const mimalloc = @import("mimalloc.zig"); -const Callback = @import("env.zig").Callback; -const SessionState = @import("env.zig").SessionState; - // init initializes netsurf lib. // init starts a mimalloc heap arena for the netsurf session. The caller must // call deinit() to free the arena memory. @@ -587,11 +584,61 @@ pub inline fn toEventTarget(comptime T: type, v: *T) *EventTarget { return @as(*EventTarget, @ptrCast(et_aligned)); } +// The way we implement events is a lot like how Zig implements linked lists. +// A Zig struct contains an `EventNode` field, i.e.: +// node: parser.EventNode, +// +// When eventTargetAddEventListener is called, we pass in `&self.node`. +// This is the pointer that's stored in the netsurf listener and it's the data +// we can get back from the listener. We can call the node's `func` function, +// passing the node itself, and the receiving function will know how to turn +// that node into the our "self", i..e by using @fieldParentPtr. +// https://www.openmymind.net/Zigs-New-LinkedList-API/ +pub const EventNode = struct { + // Event id, used for removing. Internal Zig events won't have an id. + // This is normally set to the callback.id for a JavaScript event. + id: ?usize = null, + + func: *const fn (node: *EventNode, event: *Event) void, + + fn idFromListener(lst: *EventListener) ?usize { + const ctx = eventListenerGetData(lst) orelse return null; + const node: *EventNode = @alignCast(@ptrCast(ctx)); + return node.id; + } +}; + +pub fn eventTargetAddEventListener( + et: *EventTarget, + typ: []const u8, + node: *EventNode, + capture: bool, +) !void { + const event_handler = struct { + fn handle(event_: ?*Event, ptr_: ?*anyopaque) callconv(.C) void { + const ptr = ptr_ orelse return; + const event = event_ orelse return; + + const node_: *EventNode = @alignCast(@ptrCast(ptr)); + node_.func(node_, event); + } + }.handle; + + var listener: ?*EventListener = undefined; + const errLst = c.dom_event_listener_create(event_handler, node, &listener); + try DOMErr(errLst); + defer c.dom_event_listener_unref(listener); + + const s = try strFromData(typ); + const err = eventTargetVtable(et).add_event_listener.?(et, s, listener, capture); + try DOMErr(err); +} + pub fn eventTargetHasListener( et: *EventTarget, typ: []const u8, capture: bool, - cbk_id: usize, + id: usize, ) !?*EventListener { const str = try strFromData(typ); @@ -616,12 +663,9 @@ pub fn eventTargetHasListener( // and capture property, // let's check if the callback handler is the same defer c.dom_event_listener_unref(listener); - if (EventHandlerData.fromListener(listener)) |ehd| { - switch (ehd.*) { - .js => |js| if (cbk_id == js.data.cbk.id) { - return lst; - }, - .zig => {}, + if (EventNode.idFromListener(listener)) |node_id| { + if (node_id == id) { + return lst; } } } @@ -638,144 +682,18 @@ pub fn eventTargetHasListener( return null; } -// The *anyopque that get stored in the libdom listener, which we'll retrieve -// when then event is dispatched so that we can execute the JS or Zig callback. -const EventHandlerData = union(enum) { - js: JS, - zig: Zig, - - const JS = struct { - data: JSEventHandlerData, - func: JSEventHandlerFunc, - }; - - const Zig = struct { - ctx: *anyopaque, - func: ZigEventHandlerFunc, - }; - - // retrieve a EventHandlerDataInternal from a listener. - fn fromListener(lst: *EventListener) ?*EventHandlerData { - const ctx = eventListenerGetData(lst) orelse return null; - const ehd: *EventHandlerData = @alignCast(@ptrCast(ctx)); - return ehd; - } - - pub fn deinit(self: *EventHandlerData, alloc: std.mem.Allocator) void { - switch (self.*) { - .js => |*js| { - const js_data = &js.data; - if (js_data.deinitFunc) |df| { - df(js_data.ctx, alloc); - } - }, - .zig => {}, - } - alloc.destroy(self); - } - - pub fn handle(self: *EventHandlerData, event: ?*Event) void { - switch (self.*) { - .js => |*js| js.func(event, &js.data), - .zig => |zig| zig.func(zig.ctx, event.?), - } - } -}; - -pub const JSEventHandlerData = struct { - cbk: Callback, - ctx: ?*anyopaque = null, - // deinitFunc implements the data deinitialization. - deinitFunc: ?DeinitFunc = null, - - pub const DeinitFunc = *const fn (data: ?*anyopaque, alloc: std.mem.Allocator) void; -}; - -const JSEventHandlerFunc = *const fn (event: ?*Event, data: *JSEventHandlerData) void; -const ZigEventHandlerFunc = *const fn (ctx: *anyopaque, event: *Event) void; - -pub fn eventTargetAddEventListener( - et: *EventTarget, - alloc: std.mem.Allocator, - typ: []const u8, - func: JSEventHandlerFunc, - data: JSEventHandlerData, - capture: bool, -) !void { - // this allocation will be removed either on - // eventTargetRemoveEventListener or eventTargetRemoveAllEventListeners - const ehd = try alloc.create(EventHandlerData); - errdefer alloc.destroy(ehd); - ehd.* = .{ .js = .{ .data = data, .func = func } }; - errdefer ehd.deinit(alloc); - - // When a function is used as an event handler, its this parameter is bound - // to the DOM element on which the listener is placed. - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#this_in_dom_event_handlers - try ehd.js.data.cbk.setThis(et); - - return addEventTargetListener(et, typ, ehd, capture); -} - -pub fn eventTargetAddZigListener( - et: *EventTarget, - alloc: std.mem.Allocator, - typ: []const u8, - func: ZigEventHandlerFunc, - ctx: *anyopaque, - capture: bool, -) !void { - const ehd = try alloc.create(EventHandlerData); - errdefer alloc.destroy(ehd); - ehd.* = .{ .zig = .{ .ctx = ctx, .func = func } }; - return addEventTargetListener(et, typ, ehd, capture); -} - -fn addEventTargetListener(et: *EventTarget, typ: []const u8, data: *anyopaque, capture: bool) !void { - // event_handler implements the function exposed in C and called by libdom. - // It retrieves the EventHandler and calls the appropriate (JS or Zig) - // handler function with the corresponding data. - const event_handler = struct { - fn handle(event: ?*Event, ptr_: ?*anyopaque) callconv(.C) void { - const ptr = ptr_ orelse return; - @as(*EventHandlerData, @alignCast(@ptrCast(ptr))).handle(event); - // NOTE: we can not call func.deinit here - // b/c the handler can be called several times - // either on this dispatch event or in anoter one - } - }.handle; - - var listener: ?*EventListener = undefined; - const errLst = c.dom_event_listener_create(event_handler, data, &listener); - try DOMErr(errLst); - defer c.dom_event_listener_unref(listener); - - const s = try strFromData(typ); - const err = eventTargetVtable(et).add_event_listener.?(et, s, listener, capture); - try DOMErr(err); -} - pub fn eventTargetRemoveEventListener( et: *EventTarget, - alloc: std.mem.Allocator, typ: []const u8, lst: *EventListener, capture: bool, ) !void { - // free data allocation made on eventTargetAddEventListener - if (EventHandlerData.fromListener(lst)) |ehd| { - ehd.deinit(alloc); - } - const s = try strFromData(typ); const err = eventTargetVtable(et).remove_event_listener.?(et, s, lst, capture); try DOMErr(err); } -pub fn eventTargetRemoveAllEventListeners( - et: *EventTarget, - alloc: std.mem.Allocator, -) !void { +pub fn eventTargetRemoveAllEventListeners(et: *EventTarget) !void { var next: ?*EventListenerEntry = undefined; var lst: ?*EventListener = undefined; @@ -792,15 +710,8 @@ pub fn eventTargetRemoveAllEventListeners( try DOMErr(errIter); if (lst) |listener| { - defer c.dom_event_listener_unref(listener); - - if (EventHandlerData.fromListener(listener)) |ehd| { - if (ehd.* == .zig) { - // we don't remove Zig listeners - continue; - } - - ehd.deinit(alloc); + if (EventNode.idFromListener(listener) != null) { + defer c.dom_event_listener_unref(listener); const err = eventTargetVtable(et).remove_event_listener.?( et, null, diff --git a/src/browser/xhr/event_target.zig b/src/browser/xhr/event_target.zig index 02feb929..1f11bdbc 100644 --- a/src/browser/xhr/event_target.zig +++ b/src/browser/xhr/event_target.zig @@ -48,25 +48,25 @@ pub const XMLHttpRequestEventTarget = struct { typ: []const u8, cbk: Callback, ) !void { + const target = @as(*parser.EventTarget, @ptrCast(self)); + const eh = try EventHandler.init(alloc, try cbk.withThis(target)); try parser.eventTargetAddEventListener( - @as(*parser.EventTarget, @ptrCast(self)), - alloc, + target, typ, - EventHandler, - .{ .cbk = cbk }, + &eh.node, false, ); } - fn unregister(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void { + fn unregister(self: *XMLHttpRequestEventTarget, typ: []const u8, cbk_id: usize) !void { const et = @as(*parser.EventTarget, @ptrCast(self)); // check if event target has already this listener - const lst = try parser.eventTargetHasListener(et, typ, false, cbk.id); + const lst = try parser.eventTargetHasListener(et, typ, false, cbk_id); if (lst == null) { return; } // remove listener - try parser.eventTargetRemoveEventListener(et, alloc, typ, lst.?, false); + try parser.eventTargetRemoveEventListener(et, typ, lst.?, false); } pub fn get_onloadstart(self: *XMLHttpRequestEventTarget) ?Callback { @@ -89,39 +89,33 @@ pub const XMLHttpRequestEventTarget = struct { } pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { - const arena = state.arena; - if (self.onloadstart_cbk) |cbk| try self.unregister(arena, "loadstart", cbk); - try self.register(arena, "loadstart", handler); + if (self.onloadstart_cbk) |cbk| try self.unregister("loadstart", cbk.id); + try self.register(state.arena, "loadstart", handler); self.onloadstart_cbk = handler; } pub fn set_onprogress(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { - const arena = state.arena; - if (self.onprogress_cbk) |cbk| try self.unregister(arena, "progress", cbk); - try self.register(arena, "progress", handler); + if (self.onprogress_cbk) |cbk| try self.unregister("progress", cbk.id); + try self.register(state.arena, "progress", handler); self.onprogress_cbk = handler; } pub fn set_onabort(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { - const arena = state.arena; - if (self.onabort_cbk) |cbk| try self.unregister(arena, "abort", cbk); - try self.register(arena, "abort", handler); + if (self.onabort_cbk) |cbk| try self.unregister("abort", cbk.id); + try self.register(state.arena, "abort", handler); self.onabort_cbk = handler; } pub fn set_onload(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { - const arena = state.arena; - if (self.onload_cbk) |cbk| try self.unregister(arena, "load", cbk); - try self.register(arena, "load", handler); + if (self.onload_cbk) |cbk| try self.unregister("load", cbk.id); + try self.register(state.arena, "load", handler); self.onload_cbk = handler; } pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { - const arena = state.arena; - if (self.ontimeout_cbk) |cbk| try self.unregister(arena, "timeout", cbk); - try self.register(arena, "timeout", handler); + if (self.ontimeout_cbk) |cbk| try self.unregister("timeout", cbk.id); + try self.register(state.arena, "timeout", handler); self.ontimeout_cbk = handler; } pub fn set_onloadend(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { - const arena = state.arena; - if (self.onloadend_cbk) |cbk| try self.unregister(arena, "loadend", cbk); - try self.register(arena, "loadend", handler); + if (self.onloadend_cbk) |cbk| try self.unregister("loadend", cbk.id); + try self.register(state.arena, "loadend", handler); self.onloadend_cbk = handler; } diff --git a/src/runtime/js.zig b/src/runtime/js.zig index b861aa91..4a20ef35 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -1200,7 +1200,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 @@ -1215,8 +1215,13 @@ pub fn Env(comptime S: type, comptime types: anytype) type { exception: []const u8, }; - pub fn setThis(self: *Callback, value: anytype) !void { - self._this = try self.executor.valueToExistingObject(value); + pub fn withThis(self: *const Callback, value: anytype) !Callback { + return .{ + .id = self.id, + .func = self.func, + .executor = self.executor, + .this = try self.executor.valueToExistingObject(value), + }; } pub fn call(self: *const Callback, args: anytype) !void { @@ -1264,7 +1269,7 @@ 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(); + return self.this orelse self.executor.context.getGlobal(); } // debug/helper to print the source of the JS callback