Merge pull request #577 from lightpanda-io/unified_intrusive_events

Unify the Zig and JS events using an intrusive node.
This commit is contained in:
Karl Seguin
2025-05-01 09:50:19 +08:00
committed by GitHub
7 changed files with 142 additions and 220 deletions

View File

@@ -229,6 +229,8 @@ pub const Page = struct {
renderer: FlatRenderer, renderer: FlatRenderer,
window_clicked_event_node: parser.EventNode,
scope: *Env.Scope, scope: *Env.Scope,
// current_script is the script currently evaluated by the page. // current_script is the script currently evaluated by the page.
@@ -245,6 +247,7 @@ pub const Page = struct {
.url = URL.empty, .url = URL.empty,
.session = session, .session = session,
.renderer = FlatRenderer.init(arena), .renderer = FlatRenderer.init(arena),
.window_clicked_event_node = .{ .func = windowClicked },
.state = .{ .state = .{
.arena = arena, .arena = arena,
.document = null, .document = null,
@@ -400,12 +403,10 @@ pub const Page = struct {
self.doc = doc; self.doc = doc;
const document_element = (try parser.documentGetDocumentElement(doc)) orelse return error.DocumentElementError; const document_element = (try parser.documentGetDocumentElement(doc)) orelse return error.DocumentElementError;
try parser.eventTargetAddZigListener( try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Element, document_element), parser.toEventTarget(parser.Element, document_element),
arena,
"click", "click",
windowClicked, &self.window_clicked_event_node,
self,
false, false,
); );
@@ -675,8 +676,8 @@ pub const Page = struct {
_ = try parser.elementDispatchEvent(element, @ptrCast(event)); _ = try parser.elementDispatchEvent(element, @ptrCast(event));
} }
fn windowClicked(ctx: *anyopaque, event: *parser.Event) void { fn windowClicked(node: *parser.EventNode, event: *parser.Event) void {
const self: *Page = @alignCast(@ptrCast(ctx)); const self: *Page = @fieldParentPtr("window_clicked_event_node", node);
self._windowClicked(event) catch |err| { self._windowClicked(event) catch |err| {
log.err("window click handler: {}", .{err}); log.err("window click handler: {}", .{err});
}; };

View File

@@ -46,7 +46,7 @@ pub const EventTarget = struct {
pub fn _addEventListener( pub fn _addEventListener(
self: *parser.EventTarget, self: *parser.EventTarget,
eventType: []const u8, typ: []const u8,
cbk: Env.Callback, cbk: Env.Callback,
capture: ?bool, capture: ?bool,
state: *SessionState, state: *SessionState,
@@ -56,7 +56,7 @@ pub const EventTarget = struct {
// check if event target has already this listener // check if event target has already this listener
const lst = try parser.eventTargetHasListener( const lst = try parser.eventTargetHasListener(
self, self,
eventType, typ,
capture orelse false, capture orelse false,
cbk.id, cbk.id,
); );
@@ -64,29 +64,28 @@ pub const EventTarget = struct {
return; return;
} }
const eh = try EventHandler.init(state.arena, try cbk.withThis(self));
try parser.eventTargetAddEventListener( try parser.eventTargetAddEventListener(
self, self,
state.arena, typ,
eventType, &eh.node,
EventHandler,
.{ .cbk = cbk },
capture orelse false, capture orelse false,
); );
} }
pub fn _removeEventListener( pub fn _removeEventListener(
self: *parser.EventTarget, self: *parser.EventTarget,
eventType: []const u8, typ: []const u8,
cbk: Env.Callback, cbk: Env.Callback,
capture: ?bool, capture: ?bool,
state: *SessionState,
// TODO: hanle EventListenerOptions // TODO: hanle EventListenerOptions
// see #https://github.com/lightpanda-io/jsruntime-lib/issues/114 // see #https://github.com/lightpanda-io/jsruntime-lib/issues/114
) !void { ) !void {
// check if event target has already this listener // check if event target has already this listener
const lst = try parser.eventTargetHasListener( const lst = try parser.eventTargetHasListener(
self, self,
eventType, typ,
capture orelse false, capture orelse false,
cbk.id, cbk.id,
); );
@@ -97,8 +96,7 @@ pub const EventTarget = struct {
// remove listener // remove listener
try parser.eventTargetRemoveEventListener( try parser.eventTargetRemoveEventListener(
self, self,
state.arena, typ,
eventType,
lst.?, lst.?,
capture orelse false, capture orelse false,
); );

View File

@@ -59,56 +59,45 @@ pub const MutationObserver = struct {
.node = node, .node = node,
.options = options, .options = options,
.mutation_observer = self, .mutation_observer = self,
.event_node = .{ .id = self.cbk.id, .func = Observer.handle },
}; };
const arena = self.arena;
// register node's events // register node's events
if (options.childList or options.subtree) { if (options.childList or options.subtree) {
try parser.eventTargetAddZigListener( try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
arena,
"DOMNodeInserted", "DOMNodeInserted",
Observer.handle, &observer.event_node,
observer,
false, false,
); );
try parser.eventTargetAddZigListener( try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
arena,
"DOMNodeRemoved", "DOMNodeRemoved",
Observer.handle, &observer.event_node,
observer,
false, false,
); );
} }
if (options.attr()) { if (options.attr()) {
try parser.eventTargetAddZigListener( try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
arena,
"DOMAttrModified", "DOMAttrModified",
Observer.handle, &observer.event_node,
observer,
false, false,
); );
} }
if (options.cdata()) { if (options.cdata()) {
try parser.eventTargetAddZigListener( try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
arena,
"DOMCharacterDataModified", "DOMCharacterDataModified",
Observer.handle, &observer.event_node,
observer,
false, false,
); );
} }
if (options.subtree) { if (options.subtree) {
try parser.eventTargetAddZigListener( try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
arena,
"DOMSubtreeModified", "DOMSubtreeModified",
Observer.handle, &observer.event_node,
observer,
false, false,
); );
} }
@@ -221,6 +210,8 @@ const Observer = struct {
// and batch the mutation records. // and batch the mutation records.
mutation_observer: *MutationObserver, mutation_observer: *MutationObserver,
event_node: parser.EventNode,
fn appliesTo(o: *const Observer, target: *parser.Node) bool { fn appliesTo(o: *const Observer, target: *parser.Node) bool {
// mutation on any target is always ok. // mutation on any target is always ok.
if (o.options.subtree) { if (o.options.subtree) {
@@ -250,9 +241,9 @@ const Observer = struct {
return false; return false;
} }
fn handle(ctx: *anyopaque, event: *parser.Event) void { fn handle(en: *parser.EventNode, event: *parser.Event) void {
// retrieve the observer from the data. const self: *Observer = @fieldParentPtr("event_node", en);
var self: *Observer = @alignCast(@ptrCast(ctx));
var mutation_observer = self.mutation_observer; var mutation_observer = self.mutation_observer;
const node = blk: { const node = blk: {

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator;
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Callback = @import("../env.zig").Callback; const Callback = @import("../env.zig").Callback;
@@ -136,14 +137,35 @@ pub const Event = struct {
}; };
pub const EventHandler = 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; 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.err("event handler error: {s}", .{result.exception});
log.debug("stack:\n{s}", .{result.stack orelse "???"}); log.debug("stack:\n{s}", .{result.stack orelse "???"});
}; };
} }
}.handle; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser.Event" { test "Browser.Event" {

View File

@@ -29,9 +29,6 @@ const c = @cImport({
const mimalloc = @import("mimalloc.zig"); const mimalloc = @import("mimalloc.zig");
const Callback = @import("env.zig").Callback;
const SessionState = @import("env.zig").SessionState;
// init initializes netsurf lib. // init initializes netsurf lib.
// init starts a mimalloc heap arena for the netsurf session. The caller must // init starts a mimalloc heap arena for the netsurf session. The caller must
// call deinit() to free the arena memory. // 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)); 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( pub fn eventTargetHasListener(
et: *EventTarget, et: *EventTarget,
typ: []const u8, typ: []const u8,
capture: bool, capture: bool,
cbk_id: usize, id: usize,
) !?*EventListener { ) !?*EventListener {
const str = try strFromData(typ); const str = try strFromData(typ);
@@ -616,12 +663,9 @@ pub fn eventTargetHasListener(
// and capture property, // and capture property,
// let's check if the callback handler is the same // let's check if the callback handler is the same
defer c.dom_event_listener_unref(listener); defer c.dom_event_listener_unref(listener);
if (EventHandlerData.fromListener(listener)) |ehd| { if (EventNode.idFromListener(listener)) |node_id| {
switch (ehd.*) { if (node_id == id) {
.js => |js| if (cbk_id == js.data.cbk.id) { return lst;
return lst;
},
.zig => {},
} }
} }
} }
@@ -638,144 +682,18 @@ pub fn eventTargetHasListener(
return null; 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( pub fn eventTargetRemoveEventListener(
et: *EventTarget, et: *EventTarget,
alloc: std.mem.Allocator,
typ: []const u8, typ: []const u8,
lst: *EventListener, lst: *EventListener,
capture: bool, capture: bool,
) !void { ) !void {
// free data allocation made on eventTargetAddEventListener
if (EventHandlerData.fromListener(lst)) |ehd| {
ehd.deinit(alloc);
}
const s = try strFromData(typ); const s = try strFromData(typ);
const err = eventTargetVtable(et).remove_event_listener.?(et, s, lst, capture); const err = eventTargetVtable(et).remove_event_listener.?(et, s, lst, capture);
try DOMErr(err); try DOMErr(err);
} }
pub fn eventTargetRemoveAllEventListeners( pub fn eventTargetRemoveAllEventListeners(et: *EventTarget) !void {
et: *EventTarget,
alloc: std.mem.Allocator,
) !void {
var next: ?*EventListenerEntry = undefined; var next: ?*EventListenerEntry = undefined;
var lst: ?*EventListener = undefined; var lst: ?*EventListener = undefined;
@@ -792,15 +710,8 @@ pub fn eventTargetRemoveAllEventListeners(
try DOMErr(errIter); try DOMErr(errIter);
if (lst) |listener| { if (lst) |listener| {
defer c.dom_event_listener_unref(listener); if (EventNode.idFromListener(listener) != null) {
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);
const err = eventTargetVtable(et).remove_event_listener.?( const err = eventTargetVtable(et).remove_event_listener.?(
et, et,
null, null,

View File

@@ -48,25 +48,25 @@ pub const XMLHttpRequestEventTarget = struct {
typ: []const u8, typ: []const u8,
cbk: Callback, cbk: Callback,
) !void { ) !void {
const target = @as(*parser.EventTarget, @ptrCast(self));
const eh = try EventHandler.init(alloc, try cbk.withThis(target));
try parser.eventTargetAddEventListener( try parser.eventTargetAddEventListener(
@as(*parser.EventTarget, @ptrCast(self)), target,
alloc,
typ, typ,
EventHandler, &eh.node,
.{ .cbk = cbk },
false, 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)); const et = @as(*parser.EventTarget, @ptrCast(self));
// check if event target has already this listener // 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) { if (lst == null) {
return; return;
} }
// remove listener // remove listener
try parser.eventTargetRemoveEventListener(et, alloc, typ, lst.?, false); try parser.eventTargetRemoveEventListener(et, typ, lst.?, false);
} }
pub fn get_onloadstart(self: *XMLHttpRequestEventTarget) ?Callback { 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 { pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
const arena = state.arena; if (self.onloadstart_cbk) |cbk| try self.unregister("loadstart", cbk.id);
if (self.onloadstart_cbk) |cbk| try self.unregister(arena, "loadstart", cbk); try self.register(state.arena, "loadstart", handler);
try self.register(arena, "loadstart", handler);
self.onloadstart_cbk = handler; self.onloadstart_cbk = handler;
} }
pub fn set_onprogress(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { pub fn set_onprogress(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
const arena = state.arena; if (self.onprogress_cbk) |cbk| try self.unregister("progress", cbk.id);
if (self.onprogress_cbk) |cbk| try self.unregister(arena, "progress", cbk); try self.register(state.arena, "progress", handler);
try self.register(arena, "progress", handler);
self.onprogress_cbk = handler; self.onprogress_cbk = handler;
} }
pub fn set_onabort(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { pub fn set_onabort(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
const arena = state.arena; if (self.onabort_cbk) |cbk| try self.unregister("abort", cbk.id);
if (self.onabort_cbk) |cbk| try self.unregister(arena, "abort", cbk); try self.register(state.arena, "abort", handler);
try self.register(arena, "abort", handler);
self.onabort_cbk = handler; self.onabort_cbk = handler;
} }
pub fn set_onload(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { pub fn set_onload(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
const arena = state.arena; if (self.onload_cbk) |cbk| try self.unregister("load", cbk.id);
if (self.onload_cbk) |cbk| try self.unregister(arena, "load", cbk); try self.register(state.arena, "load", handler);
try self.register(arena, "load", handler);
self.onload_cbk = handler; self.onload_cbk = handler;
} }
pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
const arena = state.arena; if (self.ontimeout_cbk) |cbk| try self.unregister("timeout", cbk.id);
if (self.ontimeout_cbk) |cbk| try self.unregister(arena, "timeout", cbk); try self.register(state.arena, "timeout", handler);
try self.register(arena, "timeout", handler);
self.ontimeout_cbk = handler; self.ontimeout_cbk = handler;
} }
pub fn set_onloadend(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { pub fn set_onloadend(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
const arena = state.arena; if (self.onloadend_cbk) |cbk| try self.unregister("loadend", cbk.id);
if (self.onloadend_cbk) |cbk| try self.unregister(arena, "loadend", cbk); try self.register(state.arena, "loadend", handler);
try self.register(arena, "loadend", handler);
self.onloadend_cbk = handler; self.onloadend_cbk = handler;
} }

View File

@@ -766,7 +766,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
pub const Callback = struct { pub const Callback = struct {
id: usize, id: usize,
scope: *Scope, scope: *Scope,
_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
@@ -781,8 +781,13 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
exception: []const u8, exception: []const u8,
}; };
pub fn setThis(self: *Callback, value: anytype) !void { pub fn withThis(self: *const Callback, value: anytype) !Callback {
self._this = try self.scope.valueToExistingObject(value); return .{
.id = self.id,
.func = self.func,
.scope = self.scope,
.this = try self.scope.valueToExistingObject(value),
};
} }
pub fn call(self: *const Callback, args: anytype) !void { pub fn call(self: *const Callback, args: anytype) !void {
@@ -830,7 +835,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
} }
fn getThis(self: *const Callback) v8.Object { fn getThis(self: *const Callback) v8.Object {
return self._this orelse self.scope.context.getGlobal(); return self.this orelse self.scope.context.getGlobal();
} }
// debug/helper to print the source of the JS callback // debug/helper to print the source of the JS callback