From 01ecb296e5a52666eca5876ff61185d3b6b658bc Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 28 Mar 2026 14:23:03 +0800 Subject: [PATCH] Rework finalizers This commit involves a number of changes to finalizers, all aimed towards better consistency and reliability. A big part of this has to do with v8::Inspector's ability to move objects across IsolatedWorlds. There has been a few previous efforts on this, the most significant being https://github.com/lightpanda-io/browser/pull/1901. To recap, a Zig instance can map to 0-N v8::Objects. Where N is the total number of IsolatedWorlds. Generally, IsolatedWorlds between origins are...isolated...but the v8::Inspector isn't bound by this. So a Zig instance cannot be tied to a Context/Identity/IsolatedWorld...it has to live until all references, possibly from different IsolatedWorlds, are released (or the page is reset). Finalizers could previously be managed via reference counting or explicitly toggling the instance as weak/strong. Now, only reference counting is supported. weak/strong can essentially be seen as an acquireRef (rc += 1) and releaseRef (rc -= 1). Explicit setting did make some things easier, like not having to worry so much about double-releasing (e.g. XHR abort being called multiple times), but it was only used in a few places AND it simply doesn't work with objects shared between IsolatedWorlds. It is never a boolean now, as 3 different IsolatedWorlds can each hold a reference. Temps and Globals are tracked on the Session. Previously, they were tracked on the Identity, but that makes no sense. If a Zig instance can outlive an Identity, then any of its Temp references can too. This hasn't been a problem because we've only seen MutationObserver and IntersectionObserver be used cross-origin, but the right CDP script can make this crash with a use-after-free (e.g. `MessageEvent.data` is released when the Identity is done, but `MessageEvent` is still referenced by a different IsolateWorld). Rather than deinit with a `comptime shutdown: bool`, there is now an explicit `releaseRef` and `deinit`. Bridge registration has been streamlined. Previously, types had to register their finalizer AND acquireRef/releaseRef/deinit had to be declared on the entire prototype chain, even if these methods just delegated to their proto. Finalizers are now automatically enabled if a type has a `acquireRef` function. If a type has an `acquireRef`, then it must have a `releaseRef` and a `deinit`. So if there's custom cleanup to do in `deinit`, then you also have to define `acquireRef` and `releaseRef` which will just delegate to the _proto. Furthermore these finalizer methods can be defined anywhere on the chain. Previously: ```zig const KeywboardEvent = struct { _proto: *Event, ... pub fn deinit(self: *KeyboardEvent, session: *Session) void { self._proto.deinit(session); } pub fn releaseRef(self: *KeyboardEvent, session: *Session) void { self._proto.releaseRef(session); } } ``` ```zig const KeyboardEvent = struct { _proto: *Event, ... // no deinit, releaseRef, acquireref } ``` Since the `KeyboardEvent` doesn't participate in finalization directly, it doesn't have to define anything. The bridge will detect the most specific place they are defined and call them there. --- src/browser/EventManager.zig | 4 +- src/browser/Factory.zig | 5 +- src/browser/Page.zig | 4 +- src/browser/Session.zig | 74 +++++--- src/browser/js/Context.zig | 71 +------- src/browser/js/Function.zig | 2 +- src/browser/js/Identity.zig | 33 +--- src/browser/js/Local.zig | 169 ++++++++++++++---- src/browser/js/Promise.zig | 2 +- src/browser/js/Value.zig | 2 +- src/browser/js/bridge.zig | 50 ++---- src/browser/webapi/AbstractRange.zig | 28 ++- src/browser/webapi/Blob.zig | 18 +- src/browser/webapi/Document.zig | 2 +- src/browser/webapi/Event.zig | 29 ++- src/browser/webapi/EventTarget.zig | 2 +- src/browser/webapi/File.zig | 8 +- src/browser/webapi/FileReader.zig | 15 +- src/browser/webapi/IntersectionObserver.zig | 70 +++++--- src/browser/webapi/MutationObserver.zig | 46 +++-- src/browser/webapi/Permissions.zig | 14 +- src/browser/webapi/Range.zig | 6 - src/browser/webapi/Selection.zig | 17 +- src/browser/webapi/URL.zig | 6 +- src/browser/webapi/animation/Animation.zig | 18 +- src/browser/webapi/collections/NodeList.zig | 27 +-- src/browser/webapi/collections/iterator.zig | 15 +- src/browser/webapi/css/FontFace.zig | 14 +- src/browser/webapi/css/FontFaceSet.zig | 14 +- src/browser/webapi/encoding/TextDecoder.zig | 14 +- src/browser/webapi/event/CompositionEvent.zig | 6 - src/browser/webapi/event/CustomEvent.zig | 14 +- src/browser/webapi/event/ErrorEvent.zig | 12 +- src/browser/webapi/event/FocusEvent.zig | 6 - src/browser/webapi/event/FormDataEvent.zig | 6 - src/browser/webapi/event/InputEvent.zig | 6 - src/browser/webapi/event/KeyboardEvent.zig | 6 - src/browser/webapi/event/MessageEvent.zig | 14 +- src/browser/webapi/event/MouseEvent.zig | 6 - .../NavigationCurrentEntryChangeEvent.zig | 6 - .../webapi/event/PageTransitionEvent.zig | 6 - src/browser/webapi/event/PointerEvent.zig | 6 - src/browser/webapi/event/PopStateEvent.zig | 6 - src/browser/webapi/event/ProgressEvent.zig | 6 - .../webapi/event/PromiseRejectionEvent.zig | 14 +- src/browser/webapi/event/SubmitEvent.zig | 6 - src/browser/webapi/event/TextEvent.zig | 6 - src/browser/webapi/event/UIEvent.zig | 6 - src/browser/webapi/event/WheelEvent.zig | 6 - src/browser/webapi/net/Fetch.zig | 8 +- src/browser/webapi/net/Response.zig | 20 ++- src/browser/webapi/net/XMLHttpRequest.zig | 38 ++-- src/lightpanda.zig | 31 ++++ 53 files changed, 541 insertions(+), 479 deletions(-) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index a5f8c475..49169f1d 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -205,7 +205,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) Dispat pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, comptime opts: DispatchOpts) DispatchError!void { event.acquireRef(); - defer event.deinit(false, self.page._session); + defer _ = event.releaseRef(self.page._session); if (comptime IS_DEBUG) { log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles }); @@ -240,7 +240,7 @@ pub fn dispatchDirect(self: *EventManager, target: *EventTarget, event: *Event, defer window._current_event = prev_event; event.acquireRef(); - defer event.deinit(false, page._session); + defer _ = event.releaseRef(page._session); if (comptime IS_DEBUG) { log.debug(.event, "dispatchDirect", .{ .type = event._type_string, .context = opts.context }); diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 0d8b87cd..14770cea 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -239,7 +239,7 @@ fn eventInit(arena: Allocator, typ: String, value: anytype) !Event { const time_stamp = (raw_timestamp / 2) * 2; return .{ - ._rc = 0, + ._rc = .{}, ._arena = arena, ._type = unionInit(Event.Type, value), ._type_string = typ, @@ -255,6 +255,7 @@ pub fn blob(_: *const Factory, arena: Allocator, child: anytype) !*@TypeOf(child const blob_ptr = chain.get(0); blob_ptr.* = .{ + ._rc = .{}, ._arena = arena, ._type = unionInit(Blob.Type, chain.get(1)), ._slice = "", @@ -271,7 +272,7 @@ pub fn abstractRange(_: *const Factory, arena: Allocator, child: anytype, page: const doc = page.document.asNode(); const abstract_range = chain.get(0); abstract_range.* = AbstractRange{ - ._rc = 0, + ._rc = .{}, ._arena = arena, ._page_id = page.id, ._type = unionInit(AbstractRange.Type, chain.get(1)), diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 36be0708..f32218cd 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -3395,7 +3395,7 @@ pub fn handleClick(self: *Page, target: *Node) !void { pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void { const event = keyboard_event.asEvent(); const element = self.window._document._active_element orelse { - keyboard_event.deinit(false, self._session); + _ = event.releaseRef(self._session); return; }; @@ -3491,7 +3491,7 @@ pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form // so submit_event is still valid when we check _prevent_default submit_event.acquireRef(); - defer submit_event.deinit(false, self._session); + defer _ = submit_event.releaseRef(self._session); try self._event_manager.dispatch(form_element.asEventTarget(), submit_event); // If the submit event was prevented, don't submit the form diff --git a/src/browser/Session.zig b/src/browser/Session.zig index 5801038b..c4e6d1ca 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -71,6 +71,18 @@ origins: std.StringHashMapUnmanaged(*js.Origin) = .empty, // ensuring object identity works across same-origin frames. identity: js.Identity = .{}, +// Shared finalizer callbacks across all Identities. Keyed by Zig instance ptr. +// This ensures objects are only freed when ALL v8 wrappers are gone. +finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty, + +// Tracked global v8 objects that need to be released on cleanup. +// Lives at Session level so objects can outlive individual Identities. +globals: std.ArrayList(v8.Global) = .empty, + +// Temporary v8 globals that can be released early. Key is global.data_ptr. +// Lives at Session level so objects holding Temps can outlive individual Identities. +temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, + // Shared resources for all pages in this session. // These live for the duration of the page tree (root + frames). arena_pool: *ArenaPool, @@ -224,6 +236,30 @@ pub fn releaseOrigin(self: *Session, origin: *js.Origin) void { /// Reset page_arena and factory for a clean slate. /// Called when root page is removed. fn resetPageResources(self: *Session) void { + // Force cleanup all remaining finalized objects + { + var it = self.finalizer_callbacks.valueIterator(); + while (it.next()) |fc| { + fc.*.deinit(self); + } + self.finalizer_callbacks = .empty; + } + + { + for (self.globals.items) |*global| { + v8.v8__Global__Reset(global); + } + self.globals = .empty; + } + + { + var it = self.temps.valueIterator(); + while (it.next()) |global| { + v8.v8__Global__Reset(global); + } + self.temps = .empty; + } + self.identity.deinit(); self.identity = .{}; @@ -457,35 +493,25 @@ pub fn nextPageId(self: *Session) u32 { return id; } -// A type that has a finalizer can have its finalizer called one of two ways. -// The first is from V8 via the WeakCallback we give to weakRef. But that isn't -// guaranteed to fire, so we track this in finalizer_callbacks and call them on -// page reset. +// Every finalizable instance of Zig gets 1 FinalizerCallback registered in the +// session. This is to ensure that, if v8 doesn't finalize the value, we can +// release on page reset. pub const FinalizerCallback = struct { arena: Allocator, session: *Session, - ptr: *anyopaque, - global: v8.Global, - identity: *js.Identity, - zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void, + resolved_ptr_id: usize, + finalizer_ptr_id: usize, + _deinit: *const fn (ptr_id: usize, session: *Session) void, - pub fn deinit(self: *FinalizerCallback) void { - self.zig_finalizer(self.ptr, self.session); - self.session.releaseArena(self.arena); - } - - /// Release this item from the identity tracking maps (called after finalizer runs from V8) - pub fn releaseIdentity(self: *FinalizerCallback) void { - const session = self.session; - const id = @intFromPtr(self.ptr); - - if (self.identity.identity_map.fetchRemove(id)) |kv| { - var global = kv.value; - v8.v8__Global__Reset(&global); - } - - _ = self.identity.finalizer_callbacks.remove(id); + // For every FinalizerCallback we'll have 1+ FinalizerCallback.Identity: one + // for every identity that gets the instance. In most cases, that'l be 1. + pub const Identity = struct { + identity: *js.Identity, + fc: *Session.FinalizerCallback, + }; + fn deinit(self: *FinalizerCallback, session: *Session) void { + self._deinit(self.finalizer_ptr_id, session); session.releaseArena(self.arena); } }; diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 4b4fcead..beec0625 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -21,6 +21,7 @@ const lp = @import("lightpanda"); const log = @import("../../log.zig"); const js = @import("js.zig"); +const bridge = @import("bridge.zig"); const Env = @import("Env.zig"); const Origin = @import("Origin.zig"); const Scheduler = @import("Scheduler.zig"); @@ -213,48 +214,11 @@ pub fn setOrigin(self: *Context, key: ?[]const u8) !void { } pub fn trackGlobal(self: *Context, global: v8.Global) !void { - return self.identity.globals.append(self.identity_arena, global); + return self.session.globals.append(self.session.page_arena, global); } pub fn trackTemp(self: *Context, global: v8.Global) !void { - return self.identity.temps.put(self.identity_arena, global.data_ptr, global); -} - -pub fn weakRef(self: *Context, obj: anytype) void { - const resolved = js.Local.resolveValue(obj); - const fc = self.identity.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse { - if (comptime IS_DEBUG) { - // should not be possible - std.debug.assert(false); - } - return; - }; - v8.v8__Global__SetWeakFinalizer(&fc.global, fc, resolved.finalizer_from_v8, v8.kParameter); -} - -pub fn safeWeakRef(self: *Context, obj: anytype) void { - const resolved = js.Local.resolveValue(obj); - const fc = self.identity.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse { - if (comptime IS_DEBUG) { - // should not be possible - std.debug.assert(false); - } - return; - }; - v8.v8__Global__ClearWeak(&fc.global); - v8.v8__Global__SetWeakFinalizer(&fc.global, fc, resolved.finalizer_from_v8, v8.kParameter); -} - -pub fn strongRef(self: *Context, obj: anytype) void { - const resolved = js.Local.resolveValue(obj); - const fc = self.identity.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse { - if (comptime IS_DEBUG) { - // should not be possible - std.debug.assert(false); - } - return; - }; - v8.v8__Global__ClearWeak(&fc.global); + return self.session.temps.put(self.session.page_arena, global.data_ptr, global); } pub const IdentityResult = struct { @@ -270,35 +234,6 @@ pub fn addIdentity(self: *Context, ptr: usize) !IdentityResult { }; } -pub fn releaseTemp(self: *Context, global: v8.Global) void { - if (self.identity.temps.fetchRemove(global.data_ptr)) |kv| { - var g = kv.value; - v8.v8__Global__Reset(&g); - } -} - -pub fn createFinalizerCallback( - self: *Context, - global: v8.Global, - ptr: *anyopaque, - zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void, -) !*Session.FinalizerCallback { - const session = self.session; - const arena = try session.getArena(.{ .debug = "FinalizerCallback" }); - errdefer session.releaseArena(arena); - const fc = try arena.create(Session.FinalizerCallback); - fc.* = .{ - .arena = arena, - .session = session, - .ptr = ptr, - .global = global, - .zig_finalizer = zig_finalizer, - // Store identity pointer for cleanup when V8 GCs the object - .identity = self.identity, - }; - return fc; -} - // Any operation on the context have to be made from a local. pub fn localScope(self: *Context, ls: *js.Local.Scope) void { const isolate = self.isolate; diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig index 14cc8d14..16664cbe 100644 --- a/src/browser/js/Function.zig +++ b/src/browser/js/Function.zig @@ -213,7 +213,7 @@ fn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Gl return .{ .handle = global, .temps = {} }; } try ctx.trackTemp(global); - return .{ .handle = global, .temps = &ctx.identity.temps }; + return .{ .handle = global, .temps = &ctx.session.temps }; } pub fn tempWithThis(self: *const Function, value: anytype) !Temp { diff --git a/src/browser/js/Identity.zig b/src/browser/js/Identity.zig index 2e2b35f5..e101003e 100644 --- a/src/browser/js/Identity.zig +++ b/src/browser/js/Identity.zig @@ -38,38 +38,9 @@ const Identity = @This(); // Maps Zig instance pointers to their v8::Global(Object) wrappers. identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, -// Tracked global v8 objects that need to be released on cleanup. -globals: std.ArrayList(v8.Global) = .empty, - -// Temporary v8 globals that can be released early. Key is global.data_ptr. -temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, - -// Finalizer callbacks for weak references. Key is @intFromPtr of the Zig instance. -finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *Session.FinalizerCallback) = .empty, - pub fn deinit(self: *Identity) void { - { - var it = self.finalizer_callbacks.valueIterator(); - while (it.next()) |finalizer| { - finalizer.*.deinit(); - } - } - - { - var it = self.identity_map.valueIterator(); - while (it.next()) |global| { - v8.v8__Global__Reset(global); - } - } - - for (self.globals.items) |*global| { + var it = self.identity_map.valueIterator(); + while (it.next()) |global| { v8.v8__Global__Reset(global); } - - { - var it = self.temps.valueIterator(); - while (it.next()) |global| { - v8.v8__Global__Reset(global); - } - } } diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig index 6dda5f38..9543d078 100644 --- a/src/browser/js/Local.zig +++ b/src/browser/js/Local.zig @@ -17,10 +17,11 @@ // along with this program. If not, see . const std = @import("std"); -const Session = @import("../Session.zig"); const log = @import("../../log.zig"); const string = @import("../../string.zig"); +const Session = @import("../Session.zig"); + const js = @import("js.zig"); const bridge = @import("bridge.zig"); const Caller = @import("Caller.zig"); @@ -213,7 +214,8 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object, .pointer => |ptr| { const resolved = resolveValue(value); - const gop = try ctx.addIdentity(@intFromPtr(resolved.ptr)); + const resolved_ptr_id = @intFromPtr(resolved.ptr); + const gop = try ctx.addIdentity(resolved_ptr_id); if (gop.found_existing) { // we've seen this instance before, return the same object return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self); @@ -262,31 +264,27 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object, // dont' use js_obj.persist(), because we don't want to track this in // context.global_objects, we want to track it in context.identity_map. v8.v8__Global__New(isolate.handle, js_obj.handle, gop.value_ptr); - if (@hasDecl(JsApi.Meta, "finalizer")) { - // It would be great if resolved knew the resolved type, but I - // can't figure out how to make that work, since it depends on - // the [runtime] `value`. - // We need the resolved finalizer, which we have in resolved. - // - // The above if statement would be more clear as: - // if (resolved.finalizer_from_v8) |finalizer| { - // But that's a runtime check. - // Instead, we check if the base has finalizer. The assumption - // here is that if a resolve type has a finalizer, then the base - // should have a finalizer too. - const fc = try ctx.createFinalizerCallback(gop.value_ptr.*, resolved.ptr, resolved.finalizer_from_zig.?); - { - errdefer fc.deinit(); - try ctx.identity.finalizer_callbacks.put(ctx.identity_arena, @intFromPtr(resolved.ptr), fc); - } + if (resolved.finalizer) |finalizer| { + const finalizer_ptr_id = finalizer.ptr_id; + finalizer.acquireRef(finalizer_ptr_id); - conditionallyReference(value); - if (@hasDecl(JsApi.Meta, "weak")) { - if (comptime IS_DEBUG) { - std.debug.assert(JsApi.Meta.weak == true); - } - v8.v8__Global__SetWeakFinalizer(gop.value_ptr, fc, resolved.finalizer_from_v8, v8.kParameter); + const session = ctx.session; + const finalizer_gop = try session.finalizer_callbacks.getOrPut(session.page_arena, finalizer_ptr_id); + if (finalizer_gop.found_existing == false) { + // This is the first context (and very likely only one) to + // see this Zig instance. We need to create the FinalizerCallback + // so that we can cleanup on page reset if v8 doesn't finalize. + errdefer _ = session.finalizer_callbacks.remove(finalizer_ptr_id); + finalizer_gop.value_ptr.* = try self.createFinalizerCallback(resolved_ptr_id, finalizer_ptr_id, finalizer.deinit); } + const fc = finalizer_gop.value_ptr.*; + const identity_finalizer = try fc.arena.create(Session.FinalizerCallback.Identity); + identity_finalizer.* = .{ + .fc = fc, + .identity = ctx.identity, + }; + + v8.v8__Global__SetWeakFinalizer(gop.value_ptr, identity_finalizer, finalizer.release, v8.kParameter); } return js_obj; }, @@ -1121,12 +1119,19 @@ fn jsUnsignedIntToZig(comptime T: type, max: comptime_int, maybe: u32) !T { // This function recursively walks the _type union field (if there is one) to // get the most specific class_id possible. const Resolved = struct { - weak: bool, ptr: *anyopaque, class_id: u16, prototype_chain: []const @import("TaggedOpaque.zig").PrototypeChainEntry, - finalizer_from_v8: ?*const fn (handle: ?*const v8.WeakCallbackInfo) callconv(.c) void = null, - finalizer_from_zig: ?*const fn (ptr: *anyopaque, session: *Session) void = null, + finalizer: ?Finalizer, + + const Finalizer = struct { + // Resolved.ptr is the most specific value in a chain (e.g. IFrame, not EventTarget, Node, ...) + // Finalizer.ptr_id is the most specific value in a chain that defines an acquireRef + ptr_id: usize, + deinit: *const fn (ptr_id: usize, session: *Session) void, + acquireRef: *const fn (ptr_id: usize) void, + release: *const fn (handle: ?*const v8.WeakCallbackInfo) callconv(.c) void, + }; }; pub fn resolveValue(value: anytype) Resolved { const T = bridge.Struct(@TypeOf(value)); @@ -1153,27 +1158,85 @@ pub fn resolveValue(value: anytype) Resolved { unreachable; } -fn resolveT(comptime T: type, value: *anyopaque) Resolved { +fn resolveT(comptime T: type, value: *T) Resolved { const Meta = T.JsApi.Meta; return .{ .ptr = value, .class_id = Meta.class_id, .prototype_chain = &Meta.prototype_chain, - .weak = if (@hasDecl(Meta, "weak")) Meta.weak else false, - .finalizer_from_v8 = if (@hasDecl(Meta, "finalizer")) Meta.finalizer.from_v8 else null, - .finalizer_from_zig = if (@hasDecl(Meta, "finalizer")) Meta.finalizer.from_zig else null, + .finalizer = blk: { + const FT = (comptime findFinalizerType(T)) orelse break :blk null; + const getFinalizerPtr = comptime finalizerPtrGetter(T, FT); + const finalizer_ptr = getFinalizerPtr(value); + + const Wrap = struct { + fn deinit(ptr_id: usize, session: *Session) void { + FT.deinit(@ptrFromInt(ptr_id), session); + } + + fn acquireRef(ptr_id: usize) void { + FT.acquireRef(@ptrFromInt(ptr_id)); + } + + fn release(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void { + const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?; + const identity_finalizer: *Session.FinalizerCallback.Identity = @ptrCast(@alignCast(ptr)); + + const fc = identity_finalizer.fc; + if (identity_finalizer.identity.identity_map.fetchRemove(fc.resolved_ptr_id)) |kv| { + var global = kv.value; + v8.v8__Global__Reset(&global); + } + + FT.releaseRef(@ptrFromInt(fc.finalizer_ptr_id), fc.session); + } + }; + break :blk .{ + .ptr_id = @intFromPtr(finalizer_ptr), + .deinit = Wrap.deinit, + .acquireRef = Wrap.acquireRef, + .release = Wrap.release, + }; + }, }; } -fn conditionallyReference(value: anytype) void { - const T = bridge.Struct(@TypeOf(value)); - if (@hasDecl(T, "acquireRef")) { - value.acquireRef(); - return; +// Start at the "resolved" type (the most specific) and work our way up the +// prototype chain looking for the type that defines acquireRef +fn findFinalizerType(comptime T: type) ?type { + const S = bridge.Struct(T); + if (@hasDecl(S, "acquireRef")) { + return S; } - if (@hasField(T, "_proto")) { - conditionallyReference(value._proto); + if (@hasField(S, "_proto")) { + const ProtoPtr = std.meta.fieldInfo(S, ._proto).type; + const ProtoChild = @typeInfo(ProtoPtr).pointer.child; + return findFinalizerType(ProtoChild); } + return null; +} + +// Generate a function that follows the _proto pointer chain to get to the finalizer type +fn finalizerPtrGetter(comptime T: type, comptime FT: type) *const fn (*T) *FT { + const S = bridge.Struct(T); + if (S == FT) { + return struct { + fn get(v: *T) *FT { + return v; + } + }.get; + } + if (@hasField(S, "_proto")) { + const ProtoPtr = std.meta.fieldInfo(S, ._proto).type; + const ProtoChild = @typeInfo(ProtoPtr).pointer.child; + const childGetter = comptime finalizerPtrGetter(ProtoChild, FT); + return struct { + fn get(v: *T) *FT { + return childGetter(v._proto); + } + }.get; + } + @compileError("Cannot find path from " ++ @typeName(T) ++ " to " ++ @typeName(FT)); } pub fn stackTrace(self: *const Local) !?[]const u8 { @@ -1381,6 +1444,34 @@ pub fn debugContextId(self: *const Local) i32 { return v8.v8__Context__DebugContextId(self.handle); } +fn createFinalizerCallback( + self: *const Local, + + // Key in identity map + // The most specific value (KeyboardEvent, not Event) + resolved_ptr_id: usize, + + // The most specific value where finalizers are defined + // What actually gets acquired / released / deinit + finalizer_ptr_id: usize, + deinit: *const fn (ptr_id: usize, session: *Session) void, +) !*Session.FinalizerCallback { + const session = self.ctx.session; + + const arena = try session.getArena(.{ .debug = "FinalizerCallback" }); + errdefer session.releaseArena(arena); + + const fc = try arena.create(Session.FinalizerCallback); + fc.* = .{ + .arena = arena, + .session = session, + ._deinit = deinit, + .resolved_ptr_id = resolved_ptr_id, + .finalizer_ptr_id = finalizer_ptr_id, + }; + return fc; +} + // Encapsulates a Local and a HandleScope. When we're going from V8->Zig // we easily get both a Local and a HandleScope via Caller.init. // But when we're going from Zig -> V8, things are more complicated. diff --git a/src/browser/js/Promise.zig b/src/browser/js/Promise.zig index 4d8d1f11..8418c408 100644 --- a/src/browser/js/Promise.zig +++ b/src/browser/js/Promise.zig @@ -67,7 +67,7 @@ fn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Glo return .{ .handle = global, .temps = {} }; } try ctx.trackTemp(global); - return .{ .handle = global, .temps = &ctx.identity.temps }; + return .{ .handle = global, .temps = &ctx.session.temps }; } pub const Temp = G(.temp); diff --git a/src/browser/js/Value.zig b/src/browser/js/Value.zig index 02d2a404..af5dd031 100644 --- a/src/browser/js/Value.zig +++ b/src/browser/js/Value.zig @@ -303,7 +303,7 @@ fn _persist(self: *const Value, comptime is_global: bool) !(if (is_global) Globa return .{ .handle = global, .temps = {} }; } try ctx.trackTemp(global); - return .{ .handle = global, .temps = &ctx.identity.temps }; + return .{ .handle = global, .temps = &ctx.session.temps }; } pub fn toZig(self: Value, comptime T: type) !T { diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index a6fe16cb..c4217201 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -101,36 +101,21 @@ pub fn Builder(comptime T: type) type { } return entries; } - - pub fn finalizer(comptime func: *const fn (self: *T, shutdown: bool, session: *Session) void) Finalizer { - return .{ - .from_zig = struct { - fn wrap(ptr: *anyopaque, session: *Session) void { - func(@ptrCast(@alignCast(ptr)), true, session); - } - }.wrap, - - .from_v8 = struct { - fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void { - const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?; - const fc: *Session.FinalizerCallback = @ptrCast(@alignCast(ptr)); - - const value_ptr = fc.ptr; - if (fc.identity.finalizer_callbacks.contains(@intFromPtr(value_ptr))) { - func(@ptrCast(@alignCast(value_ptr)), false, fc.session); - fc.releaseIdentity(); - } else { - // A bit weird, but v8 _requires_ that we release it - // If we don't. We'll 100% crash. - v8.v8__Global__Reset(&fc.global); - } - } - }.wrap, - }; - } }; } +fn releaseRef(comptime T: type, ptr_id: usize, session: *Session) void { + if (@hasDecl(T, "releaseRef")) { + T.releaseRef(@ptrFromInt(ptr_id), session); + return; + } + if (@hasField(T, "_proto")) { + releaseRef(Struct(std.meta.fieldInfo(T, ._proto).type), ptr_id, session); + return; + } + @compileError(@typeName(T) ++ " marked with finalizer without an acquireRef in its prototype chain"); +} + pub const Constructor = struct { func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void, @@ -411,17 +396,6 @@ pub const Property = struct { } }; -const Finalizer = struct { - // The finalizer wrapper when called from Zig. This is only called on - // Origin.deinit - from_zig: *const fn (ctx: *anyopaque, session: *Session) void, - - // The finalizer wrapper when called from V8. This may never be called - // (hence why we fallback to calling in Origin.deinit). If it is called, - // it is only ever called after we SetWeak on the Global. - from_v8: *const fn (?*const v8.WeakCallbackInfo) callconv(.c) void, -}; - pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 { const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; var caller: Caller = undefined; diff --git a/src/browser/webapi/AbstractRange.zig b/src/browser/webapi/AbstractRange.zig index d80fc115..a775ee22 100644 --- a/src/browser/webapi/AbstractRange.zig +++ b/src/browser/webapi/AbstractRange.zig @@ -17,6 +17,8 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); + const js = @import("../js/js.zig"); const Session = @import("../Session.zig"); @@ -31,7 +33,7 @@ const AbstractRange = @This(); pub const _prototype_root = true; -_rc: u8, +_rc: lp.RC(u8) = .{}, _type: Type, _page_id: u32, _arena: Allocator, @@ -44,24 +46,18 @@ _start_container: *Node, _range_link: std.DoublyLinkedList.Node = .{}, pub fn acquireRef(self: *AbstractRange) void { - self._rc += 1; + self._rc.acquire(); } -pub fn deinit(self: *AbstractRange, shutdown: bool, session: *Session) void { - _ = shutdown; - const rc = self._rc; - if (comptime IS_DEBUG) { - std.debug.assert(rc != 0); +pub fn deinit(self: *AbstractRange, session: *Session) void { + if (session.findPageById(self._page_id)) |page| { + page._live_ranges.remove(&self._range_link); } + session.releaseArena(self._arena); +} - if (rc == 1) { - if (session.findPageById(self._page_id)) |page| { - page._live_ranges.remove(&self._range_link); - } - session.releaseArena(self._arena); - return; - } - self._rc = rc - 1; +pub fn releaseRef(self: *AbstractRange, session: *Session) void { + self._rc.release(self, session); } pub const Type = union(enum) { @@ -338,8 +334,6 @@ pub const JsApi = struct { pub const name = "AbstractRange"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(AbstractRange.deinit); }; pub const startContainer = bridge.accessor(AbstractRange.getStartContainer, null, .{}); diff --git a/src/browser/webapi/Blob.zig b/src/browser/webapi/Blob.zig index 5b8bf81c..bf0c1118 100644 --- a/src/browser/webapi/Blob.zig +++ b/src/browser/webapi/Blob.zig @@ -17,7 +17,7 @@ // along with this program. If not, see . const std = @import("std"); -const Writer = std.Io.Writer; +const lp = @import("lightpanda"); const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); @@ -25,6 +25,7 @@ const Session = @import("../Session.zig"); const Mime = @import("../Mime.zig"); +const Writer = std.Io.Writer; const Allocator = std.mem.Allocator; /// https://w3c.github.io/FileAPI/#blob-section @@ -34,6 +35,7 @@ const Blob = @This(); pub const _prototype_root = true; _type: Type, +_rc: lp.RC(u32), _arena: Allocator, @@ -120,6 +122,7 @@ pub fn initWithMimeValidation( const self = try arena.create(Blob); self.* = .{ + ._rc = .{}, ._arena = arena, ._type = .generic, ._slice = data, @@ -128,11 +131,18 @@ pub fn initWithMimeValidation( return self; } -pub fn deinit(self: *Blob, shutdown: bool, session: *Session) void { - _ = shutdown; +pub fn deinit(self: *Blob, session: *Session) void { session.releaseArena(self._arena); } +pub fn releaseRef(self: *Blob, session: *Session) void { + self._rc.release(self, session); +} + +pub fn acquireRef(self: *Blob) void { + self._rc.acquire(); +} + const largest_vector = @max(std.simd.suggestVectorLength(u8) orelse 1, 8); /// Array of possible vector sizes for the current arch in decrementing order. /// We may move this to some file for SIMD helpers in the future. @@ -325,8 +335,6 @@ pub const JsApi = struct { pub const name = "Blob"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(Blob.deinit); }; pub const constructor = bridge.constructor(Blob.init, .{}); diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index d61be42c..1891ae96 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -61,7 +61,7 @@ _fonts: ?*FontFaceSet = null, _write_insertion_point: ?*Node = null, _script_created_parser: ?Parser.Streaming = null, _adopted_style_sheets: ?js.Object.Global = null, -_selection: Selection = .init, +_selection: Selection = .{._rc = .init(1)}, // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#throw-on-dynamic-markup-insertion-counter // Incremented during custom element reactions when parsing. When > 0, diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 54a53d7e..b48bc059 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -17,6 +17,8 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); + const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); @@ -55,7 +57,7 @@ _is_trusted: bool = false, // - 0: no reference, always a transient state going to either 1 or about to be deinit'd // - 1: either zig or v8 have a reference // - 2: both zig and v8 have a reference -_rc: u8 = 0, +_rc: lp.RC(u8) = .{}, pub const EventPhase = enum(u8) { none = 0, @@ -139,25 +141,16 @@ pub fn initEvent( } pub fn acquireRef(self: *Event) void { - self._rc += 1; + self._rc.acquire(); } -pub fn deinit(self: *Event, shutdown: bool, session: *Session) void { - if (shutdown) { - session.releaseArena(self._arena); - return; - } +/// Force cleanup on Session shutdown. +pub fn deinit(self: *Event, session: *Session) void { + session.releaseArena(self._arena); +} - const rc = self._rc; - if (comptime IS_DEBUG) { - std.debug.assert(rc != 0); - } - - if (rc == 1) { - session.releaseArena(self._arena); - } else { - self._rc = rc - 1; - } +pub fn releaseRef(self: *Event, session: *Session) void { + self._rc.release(self, session); } pub fn as(self: *Event, comptime T: type) *T { @@ -440,8 +433,6 @@ pub const JsApi = struct { pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(Event.deinit); pub const enumerable = false; }; diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index cfdf0872..704efeb3 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -60,7 +60,7 @@ pub fn dispatchEvent(self: *EventTarget, event: *Event, page: *Page) !bool { event._is_trusted = false; event.acquireRef(); - defer event.deinit(false, page._session); + defer _ = event.releaseRef(page._session); try page._event_manager.dispatch(self, event); return !event._cancelable or !event._prevent_default; } diff --git a/src/browser/webapi/File.zig b/src/browser/webapi/File.zig index f41e44bb..fb27359a 100644 --- a/src/browser/webapi/File.zig +++ b/src/browser/webapi/File.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); @@ -26,7 +27,6 @@ const Blob = @import("Blob.zig"); const File = @This(); -/// `File` inherits `Blob`. _proto: *Blob, // TODO: Implement File API. @@ -36,10 +36,6 @@ pub fn init(page: *Page) !*File { return page._factory.blob(arena, File{ ._proto = undefined }); } -pub fn deinit(self: *File, shutdown: bool, session: *Session) void { - self._proto.deinit(shutdown, session); -} - pub const JsApi = struct { pub const bridge = js.Bridge(File); @@ -47,8 +43,6 @@ pub const JsApi = struct { pub const name = "File"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(File.deinit); }; pub const constructor = bridge.constructor(File.init, .{}); diff --git a/src/browser/webapi/FileReader.zig b/src/browser/webapi/FileReader.zig index da7ec71c..109fdc7b 100644 --- a/src/browser/webapi/FileReader.zig +++ b/src/browser/webapi/FileReader.zig @@ -17,6 +17,8 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); + const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); @@ -31,6 +33,7 @@ const Allocator = std.mem.Allocator; /// https://developer.mozilla.org/en-US/docs/Web/API/FileReader const FileReader = @This(); +_rc: lp.RC(u8) = .{}, _page: *Page, _proto: *EventTarget, _arena: Allocator, @@ -70,7 +73,7 @@ pub fn init(page: *Page) !*FileReader { }); } -pub fn deinit(self: *FileReader, _: bool, session: *Session) void { +pub fn deinit(self: *FileReader, session: *Session) void { if (self._on_abort) |func| func.release(); if (self._on_error) |func| func.release(); if (self._on_load) |func| func.release(); @@ -81,6 +84,14 @@ pub fn deinit(self: *FileReader, _: bool, session: *Session) void { session.releaseArena(self._arena); } +pub fn releaseRef(self: *FileReader, session: *Session) void { + self._rc.release(self, session); +} + +pub fn acquireRef(self: *FileReader) void { + self._rc.acquire(); +} + fn asEventTarget(self: *FileReader) *EventTarget { return self._proto; } @@ -309,8 +320,6 @@ pub const JsApi = struct { pub const name = "FileReader"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(FileReader.deinit); }; pub const constructor = bridge.constructor(FileReader.init, .{}); diff --git a/src/browser/webapi/IntersectionObserver.zig b/src/browser/webapi/IntersectionObserver.zig index 68cf086b..5d6c6e4d 100644 --- a/src/browser/webapi/IntersectionObserver.zig +++ b/src/browser/webapi/IntersectionObserver.zig @@ -16,6 +16,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); + const js = @import("../js/js.zig"); const log = @import("../../log.zig"); @@ -39,6 +41,7 @@ pub fn registerTypes() []const type { const IntersectionObserver = @This(); +_rc: lp.RC(u8) = .{}, _arena: Allocator, _callback: js.Function.Temp, _observing: std.ArrayList(*Element) = .{}, @@ -108,15 +111,22 @@ pub fn init(callback: js.Function.Temp, options: ?ObserverInit, page: *Page) !*I return self; } -pub fn deinit(self: *IntersectionObserver, shutdown: bool, session: *Session) void { +pub fn deinit(self: *IntersectionObserver, session: *Session) void { self._callback.release(); - if ((comptime IS_DEBUG) and !shutdown) { - std.debug.assert(self._observing.items.len == 0); + for (self._pending_entries.items) |entry| { + entry.deinitIfUnused(session); } - session.releaseArena(self._arena); } +pub fn releaseRef(self: *IntersectionObserver, session: *Session) void { + self._rc.release(self, session); +} + +pub fn acquireRef(self: *IntersectionObserver) void { + self._rc.acquire(); +} + pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void { // Check if already observing this target for (self._observing.items) |elem| { @@ -127,7 +137,7 @@ pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void // Register with page if this is our first observation if (self._observing.items.len == 0) { - page.js.strongRef(self); + self._rc._refs += 1; try page.registerIntersectionObserver(self); } @@ -144,17 +154,19 @@ pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void } pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) void { + const original_length = self._observing.items.len; for (self._observing.items, 0..) |elem, i| { if (elem == target) { _ = self._observing.swapRemove(i); _ = self._previous_states.remove(target); - // Remove any pending entries for this target + // Remove any pending entries for this target. + // Entries will be cleaned up by V8 GC via the finalizer. var j: usize = 0; while (j < self._pending_entries.items.len) { if (self._pending_entries.items[j]._target == target) { const entry = self._pending_entries.swapRemove(j); - entry.deinit(false, page._session); + entry.deinitIfUnused(page._session); } else { j += 1; } @@ -163,21 +175,26 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) voi } } - if (self._observing.items.len == 0) { - page.js.safeWeakRef(self); + if (original_length > 0 and self._observing.items.len == 0) { + self._rc._refs -= 1; } } pub fn disconnect(self: *IntersectionObserver, page: *Page) void { - page.unregisterIntersectionObserver(self); - self._observing.clearRetainingCapacity(); - self._previous_states.clearRetainingCapacity(); - for (self._pending_entries.items) |entry| { - entry.deinit(false, page._session); + entry.deinitIfUnused(page._session); } self._pending_entries.clearRetainingCapacity(); - page.js.safeWeakRef(self); + self._previous_states.clearRetainingCapacity(); + + const observing_count = self._observing.items.len; + self._observing.clearRetainingCapacity(); + + if (observing_count > 0) { + _ = self.releaseRef(page._session); + } + + page.unregisterIntersectionObserver(self); } pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry { @@ -268,7 +285,6 @@ fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page) ._bounding_client_rect = try page._factory.create(data.bounding_client_rect), ._intersection_ratio = data.intersection_ratio, }; - try self._pending_entries.append(self._arena, entry); } @@ -310,6 +326,7 @@ pub fn deliverEntries(self: *IntersectionObserver, page: *Page) !void { } pub const IntersectionObserverEntry = struct { + _rc: lp.RC(u8) = .{}, _arena: Allocator, _time: f64, _target: *Element, @@ -319,10 +336,25 @@ pub const IntersectionObserverEntry = struct { _intersection_ratio: f64, _is_intersecting: bool, - pub fn deinit(self: *IntersectionObserverEntry, _: bool, session: *Session) void { + pub fn deinit(self: *IntersectionObserverEntry, session: *Session) void { session.releaseArena(self._arena); } + fn deinitIfUnused(self: *IntersectionObserverEntry, session: *Session) void { + if (self._rc._refs == 0) { + // hasn't been handed to JS yet. + self.deinit(session); + } + } + + pub fn releaseRef(self: *IntersectionObserverEntry, session: *Session) void { + self._rc.release(self, session); + } + + pub fn acquireRef(self: *IntersectionObserverEntry) void { + self._rc.acquire(); + } + pub fn getTarget(self: *const IntersectionObserverEntry) *Element { return self._target; } @@ -358,8 +390,6 @@ pub const IntersectionObserverEntry = struct { pub const name = "IntersectionObserverEntry"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(IntersectionObserverEntry.deinit); }; pub const target = bridge.accessor(IntersectionObserverEntry.getTarget, null, .{}); @@ -379,8 +409,6 @@ pub const JsApi = struct { pub const name = "IntersectionObserver"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(IntersectionObserver.deinit); }; pub const constructor = bridge.constructor(init, .{}); diff --git a/src/browser/webapi/MutationObserver.zig b/src/browser/webapi/MutationObserver.zig index b8608381..fa2d7f29 100644 --- a/src/browser/webapi/MutationObserver.zig +++ b/src/browser/webapi/MutationObserver.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); const String = @import("../../string.zig").String; const js = @import("../js/js.zig"); @@ -39,6 +40,7 @@ pub fn registerTypes() []const type { const MutationObserver = @This(); +_rc: lp.RC(u8) = .{}, _arena: Allocator, _callback: js.Function.Temp, _observing: std.ArrayList(Observing) = .{}, @@ -85,15 +87,20 @@ pub fn init(callback: js.Function.Temp, page: *Page) !*MutationObserver { return self; } -pub fn deinit(self: *MutationObserver, shutdown: bool, session: *Session) void { +/// Force cleanup on Session shutdown. +pub fn deinit(self: *MutationObserver, session: *Session) void { self._callback.release(); - if ((comptime IS_DEBUG) and !shutdown) { - std.debug.assert(self._observing.items.len == 0); - } - session.releaseArena(self._arena); } +pub fn releaseRef(self: *MutationObserver, session: *Session) void { + self._rc.release(self, session); +} + +pub fn acquireRef(self: *MutationObserver) void { + self._rc.acquire(); +} + pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void { const arena = self._arena; @@ -158,7 +165,7 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, // Register with page if this is our first observation if (self._observing.items.len == 0) { - page.js.strongRef(self); + self._rc._refs += 1; try page.registerMutationObserver(self); } @@ -169,13 +176,17 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, } pub fn disconnect(self: *MutationObserver, page: *Page) void { - page.unregisterMutationObserver(self); - self._observing.clearRetainingCapacity(); for (self._pending_records.items) |record| { - record.deinit(false, page._session); + _ = record.releaseRef(page._session); } self._pending_records.clearRetainingCapacity(); - page.js.safeWeakRef(self); + const observing_count = self._observing.items.len; + self._observing.clearRetainingCapacity(); + + if (observing_count > 0) { + _ = self.releaseRef(page._session); + } + page.unregisterMutationObserver(self); } pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord { @@ -348,6 +359,7 @@ pub fn deliverRecords(self: *MutationObserver, page: *Page) !void { } pub const MutationRecord = struct { + _rc: lp.RC(u8) = .{}, _type: Type, _target: *Node, _arena: Allocator, @@ -364,10 +376,18 @@ pub const MutationRecord = struct { characterData, }; - pub fn deinit(self: *MutationRecord, _: bool, session: *Session) void { + pub fn deinit(self: *MutationRecord, session: *Session) void { session.releaseArena(self._arena); } + pub fn releaseRef(self: *MutationRecord, session: *Session) void { + self._rc.release(self, session); + } + + pub fn acquireRef(self: *MutationRecord) void { + self._rc.acquire(); + } + pub fn getType(self: *const MutationRecord) []const u8 { return switch (self._type) { .attributes => "attributes", @@ -418,8 +438,6 @@ pub const MutationRecord = struct { pub const name = "MutationRecord"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(MutationRecord.deinit); }; pub const @"type" = bridge.accessor(MutationRecord.getType, null, .{}); @@ -441,8 +459,6 @@ pub const JsApi = struct { pub const name = "MutationObserver"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(MutationObserver.deinit); }; pub const constructor = bridge.constructor(MutationObserver.init, .{}); diff --git a/src/browser/webapi/Permissions.zig b/src/browser/webapi/Permissions.zig index ee197d3f..8a06b4f4 100644 --- a/src/browser/webapi/Permissions.zig +++ b/src/browser/webapi/Permissions.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); const Session = @import("../Session.zig"); @@ -50,14 +51,23 @@ pub fn query(_: *const Permissions, qd: QueryDescriptor, page: *Page) !js.Promis } const PermissionStatus = struct { + _rc: lp.RC(u8) = .{}, _arena: Allocator, _name: []const u8, _state: []const u8, - pub fn deinit(self: *PermissionStatus, _: bool, session: *Session) void { + pub fn deinit(self: *PermissionStatus, session: *Session) void { session.releaseArena(self._arena); } + pub fn releaseRef(self: *PermissionStatus, session: *Session) void { + self._rc.release(self, session); + } + + pub fn acquireRef(self: *PermissionStatus) void { + self._rc.acquire(); + } + fn getName(self: *const PermissionStatus) []const u8 { return self._name; } @@ -72,8 +82,6 @@ const PermissionStatus = struct { pub const name = "PermissionStatus"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(PermissionStatus.deinit); }; pub const name = bridge.accessor(getName, null, .{}); pub const state = bridge.accessor(getState, null, .{}); diff --git a/src/browser/webapi/Range.zig b/src/browser/webapi/Range.zig index 8d4bcd5c..720fc5ff 100644 --- a/src/browser/webapi/Range.zig +++ b/src/browser/webapi/Range.zig @@ -38,10 +38,6 @@ pub fn init(page: *Page) !*Range { return page._factory.abstractRange(arena, Range{ ._proto = undefined }, page); } -pub fn deinit(self: *Range, shutdown: bool, session: *Session) void { - self._proto.deinit(shutdown, session); -} - pub fn asAbstractRange(self: *Range) *AbstractRange { return self._proto; } @@ -697,8 +693,6 @@ pub const JsApi = struct { pub const name = "Range"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(Range.deinit); }; // Constants for compareBoundaryPoints diff --git a/src/browser/webapi/Selection.zig b/src/browser/webapi/Selection.zig index c6b84e89..9aaecc49 100644 --- a/src/browser/webapi/Selection.zig +++ b/src/browser/webapi/Selection.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); @@ -32,18 +33,27 @@ const Selection = @This(); pub const SelectionDirection = enum { backward, forward, none }; +_rc: lp.RC(u8) = .{}, _range: ?*Range = null, _direction: SelectionDirection = .none, pub const init: Selection = .{}; -pub fn deinit(self: *Selection, shutdown: bool, session: *Session) void { +pub fn deinit(self: *Selection, session: *Session) void { if (self._range) |r| { - r.deinit(shutdown, session); + r.asAbstractRange().deinit(session); self._range = null; } } +pub fn releaseRef(self: *Selection, session: *Session) void { + self._rc.release(self, session); +} + +pub fn acquireRef(self: *Selection) void { + self._rc.acquire(); +} + fn dispatchSelectionChangeEvent(page: *Page) !void { const event = try Event.init("selectionchange", .{}, page); try page._event_manager.dispatch(page.document.asEventTarget(), event); @@ -693,7 +703,7 @@ pub fn toString(self: *const Selection, page: *Page) ![]const u8 { fn setRange(self: *Selection, new_range: ?*Range, page: *Page) void { if (self._range) |existing| { - existing.deinit(false, page._session); + _ = existing.asAbstractRange().releaseRef(page._session); } if (new_range) |nr| { nr.asAbstractRange().acquireRef(); @@ -708,7 +718,6 @@ pub const JsApi = struct { pub const name = "Selection"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const finalizer = bridge.finalizer(Selection.deinit); }; pub const anchorNode = bridge.accessor(Selection.getAnchorNode, null, .{}); diff --git a/src/browser/webapi/URL.zig b/src/browser/webapi/URL.zig index cb8e5d11..8856c83d 100644 --- a/src/browser/webapi/URL.zig +++ b/src/browser/webapi/URL.zig @@ -256,8 +256,7 @@ pub fn createObjectURL(blob: *Blob, page: *Page) ![]const u8 { .{ page.origin orelse "null", uuid_buf }, ); try page._blob_urls.put(page.arena, blob_url, blob); - // prevent GC from cleaning up the blob while it's in the registry - page.js.strongRef(blob); + blob.acquireRef(); return blob_url; } @@ -267,9 +266,8 @@ pub fn revokeObjectURL(url: []const u8, page: *Page) void { return; } - // Remove from registry and release strong ref (no-op if not found) if (page._blob_urls.fetchRemove(url)) |entry| { - page.js.weakRef(entry.value); + entry.value.releaseRef(page._session); } } diff --git a/src/browser/webapi/animation/Animation.zig b/src/browser/webapi/animation/Animation.zig index 8d445733..818cef58 100644 --- a/src/browser/webapi/animation/Animation.zig +++ b/src/browser/webapi/animation/Animation.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); const log = @import("../../../log.zig"); const js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); @@ -33,6 +34,7 @@ const PlayState = enum { finished, }; +_rc: lp.RC(u32) = .{}, _page: *Page, _arena: Allocator, @@ -62,10 +64,18 @@ pub fn init(page: *Page) !*Animation { return self; } -pub fn deinit(self: *Animation, _: bool, session: *Session) void { +pub fn deinit(self: *Animation, session: *Session) void { session.releaseArena(self._arena); } +pub fn releaseRef(self: *Animation, session: *Session) void { + self._rc.release(self, session); +} + +pub fn acquireRef(self: *Animation) void { + self._rc.acquire(); +} + pub fn play(self: *Animation, page: *Page) !void { if (self._playState == .running) { return; @@ -75,7 +85,7 @@ pub fn play(self: *Animation, page: *Page) !void { self._playState = .running; // Schedule the transition from .running => .finished in 10ms. - page.js.strongRef(self); + self.acquireRef(); try page.js.scheduler.add( self, Animation.update, @@ -201,7 +211,7 @@ fn update(ctx: *anyopaque) !?u32 { } // No future change scheduled, set the object weak for garbage collection. - self._page.js.weakRef(self); + self.releaseRef(self._page._session); return null; } @@ -220,8 +230,6 @@ pub const JsApi = struct { pub const name = "Animation"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(Animation.deinit); }; pub const play = bridge.function(Animation.play, .{}); diff --git a/src/browser/webapi/collections/NodeList.zig b/src/browser/webapi/collections/NodeList.zig index a61cc598..97ec0e39 100644 --- a/src/browser/webapi/collections/NodeList.zig +++ b/src/browser/webapi/collections/NodeList.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); const log = @import("../../../log.zig"); const js = @import("../../js/js.zig"); @@ -37,15 +38,9 @@ _data: union(enum) { radio_node_list: *RadioNodeList, name: NodeLive(.name), }, -_rc: usize = 0, - -pub fn deinit(self: *NodeList, _: bool, session: *Session) void { - const rc = self._rc; - if (rc > 1) { - self._rc = rc - 1; - return; - } +_rc: lp.RC(u32) = .{}, +pub fn deinit(self: *NodeList, session: *Session) void { switch (self._data) { .selector_list => |list| list.deinit(session), .child_nodes => |cn| cn.deinit(session), @@ -53,8 +48,12 @@ pub fn deinit(self: *NodeList, _: bool, session: *Session) void { } } +pub fn releaseRef(self: *NodeList, session: *Session) void { + self._rc.release(self, session); +} + pub fn acquireRef(self: *NodeList) void { - self._rc += 1; + self._rc.acquire(); } pub fn length(self: *NodeList, page: *Page) !u32 { @@ -119,8 +118,12 @@ const Iterator = struct { const Entry = struct { u32, *Node }; - pub fn deinit(self: *Iterator, shutdown: bool, session: *Session) void { - self.list.deinit(shutdown, session); + pub fn deinit(self: *Iterator, session: *Session) void { + self.list.deinit(session); + } + + pub fn releaseRef(self: *Iterator, session: *Session) void { + self.list.releaseRef(session); } pub fn acquireRef(self: *Iterator) void { @@ -143,8 +146,6 @@ pub const JsApi = struct { pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; pub const enumerable = false; - pub const weak = true; - pub const finalizer = bridge.finalizer(NodeList.deinit); }; pub const length = bridge.accessor(NodeList.length, null, .{}); diff --git a/src/browser/webapi/collections/iterator.zig b/src/browser/webapi/collections/iterator.zig index 9fe3354d..6443c6ac 100644 --- a/src/browser/webapi/collections/iterator.zig +++ b/src/browser/webapi/collections/iterator.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); const js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); const Session = @import("../../Session.zig"); @@ -40,9 +41,15 @@ pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type { return page._factory.create(Self{ .inner = inner }); } - pub fn deinit(self: *Self, shutdown: bool, session: *Session) void { - if (@hasDecl(Inner, "deinit")) { - self.inner.deinit(shutdown, session); + pub fn deinit(self: *Self, session: *Session) void { + _ = self; + _ = session; + } + + pub fn releaseRef(self: *Self, session: *Session) void { + // Release the reference to the inner type that we acquired + if (@hasDecl(Inner, "releaseRef")) { + self.inner.releaseRef(session); } } @@ -73,8 +80,6 @@ pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type { pub const Meta = struct { pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(Self.deinit); }; pub const next = bridge.function(Self.next, .{ .null_as_undefined = true }); diff --git a/src/browser/webapi/css/FontFace.zig b/src/browser/webapi/css/FontFace.zig index f3c4059d..075d9135 100644 --- a/src/browser/webapi/css/FontFace.zig +++ b/src/browser/webapi/css/FontFace.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); const js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); const Session = @import("../../Session.zig"); @@ -25,6 +26,7 @@ const Allocator = std.mem.Allocator; const FontFace = @This(); +_rc: lp.RC(u8) = .{}, _arena: Allocator, _family: []const u8, @@ -42,10 +44,18 @@ pub fn init(family: []const u8, source: []const u8, page: *Page) !*FontFace { return self; } -pub fn deinit(self: *FontFace, _: bool, session: *Session) void { +pub fn deinit(self: *FontFace, session: *Session) void { session.releaseArena(self._arena); } +pub fn releaseRef(self: *FontFace, session: *Session) void { + self._rc.release(self, session); +} + +pub fn acquireRef(self: *FontFace) void { + self._rc.acquire(); +} + pub fn getFamily(self: *const FontFace) []const u8 { return self._family; } @@ -67,8 +77,6 @@ pub const JsApi = struct { pub const name = "FontFace"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(FontFace.deinit); }; pub const constructor = bridge.constructor(FontFace.init, .{}); diff --git a/src/browser/webapi/css/FontFaceSet.zig b/src/browser/webapi/css/FontFaceSet.zig index 36c89570..b20017ca 100644 --- a/src/browser/webapi/css/FontFaceSet.zig +++ b/src/browser/webapi/css/FontFaceSet.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); const js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); const Session = @import("../../Session.zig"); @@ -28,6 +29,7 @@ const Allocator = std.mem.Allocator; const FontFaceSet = @This(); +_rc: lp.RC(u8) = .{}, _proto: *EventTarget, _arena: Allocator, @@ -41,10 +43,18 @@ pub fn init(page: *Page) !*FontFaceSet { }); } -pub fn deinit(self: *FontFaceSet, _: bool, session: *Session) void { +pub fn deinit(self: *FontFaceSet, session: *Session) void { session.releaseArena(self._arena); } +pub fn releaseRef(self: *FontFaceSet, session: *Session) void { + self._rc.release(self, session); +} + +pub fn acquireRef(self: *FontFaceSet) void { + self._rc.acquire(); +} + pub fn asEventTarget(self: *FontFaceSet) *EventTarget { return self._proto; } @@ -95,8 +105,6 @@ pub const JsApi = struct { pub const name = "FontFaceSet"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(FontFaceSet.deinit); }; pub const size = bridge.property(0, .{ .template = false, .readonly = true }); diff --git a/src/browser/webapi/encoding/TextDecoder.zig b/src/browser/webapi/encoding/TextDecoder.zig index 736a4008..c117df09 100644 --- a/src/browser/webapi/encoding/TextDecoder.zig +++ b/src/browser/webapi/encoding/TextDecoder.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); const js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); @@ -25,6 +26,7 @@ const Allocator = std.mem.Allocator; const TextDecoder = @This(); +_rc: lp.RC(u8) = .{}, _fatal: bool, _arena: Allocator, _ignore_bom: bool, @@ -60,10 +62,18 @@ pub fn init(label_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*TextDecoder { return self; } -pub fn deinit(self: *TextDecoder, _: bool, session: *Session) void { +pub fn deinit(self: *TextDecoder, session: *Session) void { session.releaseArena(self._arena); } +pub fn releaseRef(self: *TextDecoder, session: *Session) void { + self._rc.release(self, session); +} + +pub fn acquireRef(self: *TextDecoder) void { + self._rc.acquire(); +} + pub fn getIgnoreBOM(self: *const TextDecoder) bool { return self._ignore_bom; } @@ -109,8 +119,6 @@ pub const JsApi = struct { pub const name = "TextDecoder"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(TextDecoder.deinit); }; pub const constructor = bridge.constructor(TextDecoder.init, .{}); diff --git a/src/browser/webapi/event/CompositionEvent.zig b/src/browser/webapi/event/CompositionEvent.zig index 6de4e702..7f3fd1d2 100644 --- a/src/browser/webapi/event/CompositionEvent.zig +++ b/src/browser/webapi/event/CompositionEvent.zig @@ -53,10 +53,6 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*CompositionEvent { return event; } -pub fn deinit(self: *CompositionEvent, shutdown: bool, session: *Session) void { - self._proto.deinit(shutdown, session); -} - pub fn asEvent(self: *CompositionEvent) *Event { return self._proto; } @@ -72,8 +68,6 @@ pub const JsApi = struct { pub const name = "CompositionEvent"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(CompositionEvent.deinit); }; pub const constructor = bridge.constructor(CompositionEvent.init, .{}); diff --git a/src/browser/webapi/event/CustomEvent.zig b/src/browser/webapi/event/CustomEvent.zig index eaba2e7d..9013bb4a 100644 --- a/src/browser/webapi/event/CustomEvent.zig +++ b/src/browser/webapi/event/CustomEvent.zig @@ -73,11 +73,19 @@ pub fn initCustomEvent( self._detail = detail_; } -pub fn deinit(self: *CustomEvent, shutdown: bool, session: *Session) void { +pub fn deinit(self: *CustomEvent, session: *Session) void { if (self._detail) |d| { d.release(); } - self._proto.deinit(shutdown, session); + self._proto.deinit(session); +} + +pub fn acquireRef(self: *CustomEvent) void { + self._proto.acquireRef(); +} + +pub fn releaseRef(self: *CustomEvent, session: *Session) void { + self._proto._rc.release(self, session); } pub fn asEvent(self: *CustomEvent) *Event { @@ -95,8 +103,6 @@ pub const JsApi = struct { pub const name = "CustomEvent"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(CustomEvent.deinit); pub const enumerable = false; }; diff --git a/src/browser/webapi/event/ErrorEvent.zig b/src/browser/webapi/event/ErrorEvent.zig index 2d3b857f..aef63a0e 100644 --- a/src/browser/webapi/event/ErrorEvent.zig +++ b/src/browser/webapi/event/ErrorEvent.zig @@ -80,11 +80,19 @@ fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool return event; } -pub fn deinit(self: *ErrorEvent, shutdown: bool, session: *Session) void { +pub fn deinit(self: *ErrorEvent, session: *Session) void { if (self._error) |e| { e.release(); } - self._proto.deinit(shutdown, session); + self._proto.deinit(session); +} + +pub fn acquireRef(self: *ErrorEvent) void { + self._proto.acquireRef(); +} + +pub fn releaseRef(self: *ErrorEvent, session: *Session) void { + self._proto._rc.release(self, session); } pub fn asEvent(self: *ErrorEvent) *Event { diff --git a/src/browser/webapi/event/FocusEvent.zig b/src/browser/webapi/event/FocusEvent.zig index f6823c23..776605db 100644 --- a/src/browser/webapi/event/FocusEvent.zig +++ b/src/browser/webapi/event/FocusEvent.zig @@ -70,10 +70,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool return event; } -pub fn deinit(self: *FocusEvent, shutdown: bool, session: *Session) void { - self._proto.deinit(shutdown, session); -} - pub fn asEvent(self: *FocusEvent) *Event { return self._proto.asEvent(); } @@ -89,8 +85,6 @@ pub const JsApi = struct { pub const name = "FocusEvent"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(FocusEvent.deinit); }; pub const constructor = bridge.constructor(FocusEvent.init, .{}); diff --git a/src/browser/webapi/event/FormDataEvent.zig b/src/browser/webapi/event/FormDataEvent.zig index d50a0fc1..93eadfa3 100644 --- a/src/browser/webapi/event/FormDataEvent.zig +++ b/src/browser/webapi/event/FormDataEvent.zig @@ -66,10 +66,6 @@ fn initWithTrusted(arena: Allocator, typ: String, maybe_options: ?Options, trust return event; } -pub fn deinit(self: *FormDataEvent, shutdown: bool, session: *Session) void { - self._proto.deinit(shutdown, session); -} - pub fn asEvent(self: *FormDataEvent) *Event { return self._proto; } @@ -85,8 +81,6 @@ pub const JsApi = struct { pub const name = "FormDataEvent"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(FormDataEvent.deinit); }; pub const constructor = bridge.constructor(FormDataEvent.init, .{}); diff --git a/src/browser/webapi/event/InputEvent.zig b/src/browser/webapi/event/InputEvent.zig index d81d51d6..3b01b900 100644 --- a/src/browser/webapi/event/InputEvent.zig +++ b/src/browser/webapi/event/InputEvent.zig @@ -83,10 +83,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool return event; } -pub fn deinit(self: *InputEvent, shutdown: bool, session: *Session) void { - self._proto.deinit(shutdown, session); -} - pub fn asEvent(self: *InputEvent) *Event { return self._proto.asEvent(); } @@ -110,8 +106,6 @@ pub const JsApi = struct { pub const name = "InputEvent"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(InputEvent.deinit); }; pub const constructor = bridge.constructor(InputEvent.init, .{}); diff --git a/src/browser/webapi/event/KeyboardEvent.zig b/src/browser/webapi/event/KeyboardEvent.zig index d79d1e78..ddc7548d 100644 --- a/src/browser/webapi/event/KeyboardEvent.zig +++ b/src/browser/webapi/event/KeyboardEvent.zig @@ -229,10 +229,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool return event; } -pub fn deinit(self: *KeyboardEvent, shutdown: bool, session: *Session) void { - self._proto.deinit(shutdown, session); -} - pub fn asEvent(self: *KeyboardEvent) *Event { return self._proto.asEvent(); } @@ -296,8 +292,6 @@ pub const JsApi = struct { pub const name = "KeyboardEvent"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(KeyboardEvent.deinit); }; pub const constructor = bridge.constructor(KeyboardEvent.init, .{}); diff --git a/src/browser/webapi/event/MessageEvent.zig b/src/browser/webapi/event/MessageEvent.zig index 32ced1d8..03530400 100644 --- a/src/browser/webapi/event/MessageEvent.zig +++ b/src/browser/webapi/event/MessageEvent.zig @@ -73,11 +73,19 @@ fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool return event; } -pub fn deinit(self: *MessageEvent, shutdown: bool, session: *Session) void { +pub fn deinit(self: *MessageEvent, session: *Session) void { if (self._data) |d| { d.release(); } - self._proto.deinit(shutdown, session); + self._proto.deinit(session); +} + +pub fn acquireRef(self: *MessageEvent) void { + self._proto.acquireRef(); +} + +pub fn releaseRef(self: *MessageEvent, session: *Session) void { + self._proto._rc.release(self, session); } pub fn asEvent(self: *MessageEvent) *Event { @@ -103,8 +111,6 @@ pub const JsApi = struct { pub const name = "MessageEvent"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(MessageEvent.deinit); }; pub const constructor = bridge.constructor(MessageEvent.init, .{}); diff --git a/src/browser/webapi/event/MouseEvent.zig b/src/browser/webapi/event/MouseEvent.zig index e13dc1b3..6d57142d 100644 --- a/src/browser/webapi/event/MouseEvent.zig +++ b/src/browser/webapi/event/MouseEvent.zig @@ -121,10 +121,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool return event; } -pub fn deinit(self: *MouseEvent, shutdown: bool, session: *Session) void { - self._proto.deinit(shutdown, session); -} - pub fn asEvent(self: *MouseEvent) *Event { return self._proto.asEvent(); } @@ -203,8 +199,6 @@ pub const JsApi = struct { pub const name = "MouseEvent"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(MouseEvent.deinit); }; pub const constructor = bridge.constructor(MouseEvent.init, .{}); diff --git a/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig b/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig index 98cba330..816fa1c8 100644 --- a/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig +++ b/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig @@ -83,10 +83,6 @@ fn initWithTrusted( return event; } -pub fn deinit(self: *NavigationCurrentEntryChangeEvent, shutdown: bool, session: *Session) void { - self._proto.deinit(shutdown, session); -} - pub fn asEvent(self: *NavigationCurrentEntryChangeEvent) *Event { return self._proto; } @@ -106,8 +102,6 @@ pub const JsApi = struct { pub const name = "NavigationCurrentEntryChangeEvent"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(NavigationCurrentEntryChangeEvent.deinit); }; pub const constructor = bridge.constructor(NavigationCurrentEntryChangeEvent.init, .{}); diff --git a/src/browser/webapi/event/PageTransitionEvent.zig b/src/browser/webapi/event/PageTransitionEvent.zig index bf747c9a..e11be386 100644 --- a/src/browser/webapi/event/PageTransitionEvent.zig +++ b/src/browser/webapi/event/PageTransitionEvent.zig @@ -66,10 +66,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool return event; } -pub fn deinit(self: *PageTransitionEvent, shutdown: bool, session: *Session) void { - self._proto.deinit(shutdown, session); -} - pub fn asEvent(self: *PageTransitionEvent) *Event { return self._proto; } @@ -85,8 +81,6 @@ pub const JsApi = struct { pub const name = "PageTransitionEvent"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(PageTransitionEvent.deinit); }; pub const constructor = bridge.constructor(PageTransitionEvent.init, .{}); diff --git a/src/browser/webapi/event/PointerEvent.zig b/src/browser/webapi/event/PointerEvent.zig index c5178faf..c5440d45 100644 --- a/src/browser/webapi/event/PointerEvent.zig +++ b/src/browser/webapi/event/PointerEvent.zig @@ -128,10 +128,6 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PointerEvent { return event; } -pub fn deinit(self: *PointerEvent, shutdown: bool, session: *Session) void { - self._proto.deinit(shutdown, session); -} - pub fn asEvent(self: *PointerEvent) *Event { return self._proto.asEvent(); } @@ -191,8 +187,6 @@ pub const JsApi = struct { pub const name = "PointerEvent"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(PointerEvent.deinit); }; pub const constructor = bridge.constructor(PointerEvent.init, .{}); diff --git a/src/browser/webapi/event/PopStateEvent.zig b/src/browser/webapi/event/PopStateEvent.zig index f26c17b6..cd430cf8 100644 --- a/src/browser/webapi/event/PopStateEvent.zig +++ b/src/browser/webapi/event/PopStateEvent.zig @@ -67,10 +67,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool return event; } -pub fn deinit(self: *PopStateEvent, shutdown: bool, session: *Session) void { - self._proto.deinit(shutdown, session); -} - pub fn asEvent(self: *PopStateEvent) *Event { return self._proto; } @@ -92,8 +88,6 @@ pub const JsApi = struct { pub const name = "PopStateEvent"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(PopStateEvent.deinit); }; pub const constructor = bridge.constructor(PopStateEvent.init, .{}); diff --git a/src/browser/webapi/event/ProgressEvent.zig b/src/browser/webapi/event/ProgressEvent.zig index b257f12b..6498da48 100644 --- a/src/browser/webapi/event/ProgressEvent.zig +++ b/src/browser/webapi/event/ProgressEvent.zig @@ -68,10 +68,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool return event; } -pub fn deinit(self: *ProgressEvent, shutdown: bool, session: *Session) void { - self._proto.deinit(shutdown, session); -} - pub fn asEvent(self: *ProgressEvent) *Event { return self._proto; } @@ -96,8 +92,6 @@ pub const JsApi = struct { pub const name = "ProgressEvent"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(ProgressEvent.deinit); }; pub const constructor = bridge.constructor(ProgressEvent.init, .{}); diff --git a/src/browser/webapi/event/PromiseRejectionEvent.zig b/src/browser/webapi/event/PromiseRejectionEvent.zig index 5be6d792..cc014b39 100644 --- a/src/browser/webapi/event/PromiseRejectionEvent.zig +++ b/src/browser/webapi/event/PromiseRejectionEvent.zig @@ -56,14 +56,22 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*PromiseRejectionEve return event; } -pub fn deinit(self: *PromiseRejectionEvent, shutdown: bool, session: *Session) void { +pub fn deinit(self: *PromiseRejectionEvent, session: *Session) void { if (self._reason) |r| { r.release(); } if (self._promise) |p| { p.release(); } - self._proto.deinit(shutdown, session); + self._proto.deinit(session); +} + +pub fn acquireRef(self: *PromiseRejectionEvent) void { + self._proto.acquireRef(); +} + +pub fn releaseRef(self: *PromiseRejectionEvent, session: *Session) void { + self._proto._rc.release(self, session); } pub fn asEvent(self: *PromiseRejectionEvent) *Event { @@ -85,8 +93,6 @@ pub const JsApi = struct { pub const name = "PromiseRejectionEvent"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(PromiseRejectionEvent.deinit); }; pub const constructor = bridge.constructor(PromiseRejectionEvent.init, .{}); diff --git a/src/browser/webapi/event/SubmitEvent.zig b/src/browser/webapi/event/SubmitEvent.zig index fbb1af06..f48365dc 100644 --- a/src/browser/webapi/event/SubmitEvent.zig +++ b/src/browser/webapi/event/SubmitEvent.zig @@ -67,10 +67,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool return event; } -pub fn deinit(self: *SubmitEvent, shutdown: bool, session: *Session) void { - self._proto.deinit(shutdown, session); -} - pub fn asEvent(self: *SubmitEvent) *Event { return self._proto; } @@ -86,8 +82,6 @@ pub const JsApi = struct { pub const name = "SubmitEvent"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(SubmitEvent.deinit); }; pub const constructor = bridge.constructor(SubmitEvent.init, .{}); diff --git a/src/browser/webapi/event/TextEvent.zig b/src/browser/webapi/event/TextEvent.zig index fd5e32fb..3ddb2636 100644 --- a/src/browser/webapi/event/TextEvent.zig +++ b/src/browser/webapi/event/TextEvent.zig @@ -59,10 +59,6 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*TextEvent { return event; } -pub fn deinit(self: *TextEvent, shutdown: bool, session: *Session) void { - self._proto.deinit(shutdown, session); -} - pub fn asEvent(self: *TextEvent) *Event { return self._proto.asEvent(); } @@ -101,8 +97,6 @@ pub const JsApi = struct { pub const name = "TextEvent"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(TextEvent.deinit); }; // No constructor - TextEvent is created via document.createEvent('TextEvent') diff --git a/src/browser/webapi/event/UIEvent.zig b/src/browser/webapi/event/UIEvent.zig index bbd9a60f..6874d6d5 100644 --- a/src/browser/webapi/event/UIEvent.zig +++ b/src/browser/webapi/event/UIEvent.zig @@ -71,10 +71,6 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*UIEvent { return event; } -pub fn deinit(self: *UIEvent, shutdown: bool, session: *Session) void { - self._proto.deinit(shutdown, session); -} - pub fn as(self: *UIEvent, comptime T: type) *T { return self.is(T).?; } @@ -122,8 +118,6 @@ pub const JsApi = struct { pub const name = "UIEvent"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(UIEvent.deinit); }; pub const constructor = bridge.constructor(UIEvent.init, .{}); diff --git a/src/browser/webapi/event/WheelEvent.zig b/src/browser/webapi/event/WheelEvent.zig index 831c4e02..4711ac25 100644 --- a/src/browser/webapi/event/WheelEvent.zig +++ b/src/browser/webapi/event/WheelEvent.zig @@ -87,10 +87,6 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*WheelEvent { return event; } -pub fn deinit(self: *WheelEvent, shutdown: bool, session: *Session) void { - self._proto.deinit(shutdown, session); -} - pub fn asEvent(self: *WheelEvent) *Event { return self._proto.asEvent(); } @@ -118,8 +114,6 @@ pub const JsApi = struct { pub const name = "WheelEvent"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(WheelEvent.deinit); }; pub const constructor = bridge.constructor(WheelEvent.init, .{}); diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index de1af4ec..af60f3b7 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -62,7 +62,7 @@ pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise { } const response = try Response.init(null, .{ .status = 0 }, page); - errdefer response.deinit(true, page._session); + errdefer response.deinit(page._session); const fetch = try response._arena.create(Fetch); fetch.* = .{ @@ -225,7 +225,7 @@ fn httpDoneCallback(ctx: *anyopaque) !void { return ls.toLocal(self._resolver).resolve("fetch done", js_val); } -fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void { +fn httpErrorCallback(ctx: *anyopaque, _: anyerror) void { const self: *Fetch = @ptrCast(@alignCast(ctx)); var response = self._response; @@ -234,7 +234,7 @@ fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void { // clear this. (defer since `self is in the response's arena). defer if (self._owns_response) { - response.deinit(err == error.Abort, self._page._session); + response.deinit(self._page._session); self._owns_response = false; }; @@ -256,7 +256,7 @@ fn httpShutdownCallback(ctx: *anyopaque) void { if (self._owns_response) { var response = self._response; response._transfer = null; - response.deinit(true, self._page._session); + response.deinit(self._page._session); // Do not access `self` after this point: the Fetch struct was // allocated from response._arena which has been released. } diff --git a/src/browser/webapi/net/Response.zig b/src/browser/webapi/net/Response.zig index 7020ac05..b9df006e 100644 --- a/src/browser/webapi/net/Response.zig +++ b/src/browser/webapi/net/Response.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); const js = @import("../../js/js.zig"); const HttpClient = @import("../../HttpClient.zig"); @@ -38,6 +39,7 @@ pub const Type = enum { opaqueredirect, }; +_rc: lp.RC(u8) = .{}, _status: u16, _arena: Allocator, _headers: *Headers, @@ -78,18 +80,22 @@ pub fn init(body_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*Response { return self; } -pub fn deinit(self: *Response, shutdown: bool, session: *Session) void { +pub fn deinit(self: *Response, session: *Session) void { if (self._transfer) |transfer| { - if (shutdown) { - transfer.terminate(); - } else { - transfer.abort(error.Abort); - } + transfer.abort(error.Abort); self._transfer = null; } session.releaseArena(self._arena); } +pub fn releaseRef(self: *Response, session: *Session) void { + self._rc.release(self, session); +} + +pub fn acquireRef(self: *Response) void { + self._rc.acquire(); +} + pub fn getStatus(self: *const Response) u16 { return self._status; } @@ -197,8 +203,6 @@ pub const JsApi = struct { pub const name = "Response"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(Response.deinit); }; pub const constructor = bridge.constructor(Response.init, .{}); diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index e8780394..a601ce13 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); const js = @import("../../js/js.zig"); const log = @import("../../../log.zig"); @@ -38,6 +39,7 @@ const Allocator = std.mem.Allocator; const IS_DEBUG = @import("builtin").mode == .Debug; const XMLHttpRequest = @This(); +_rc: lp.RC(u8) = .{}, _page: *Page, _proto: *XMLHttpRequestEventTarget, _arena: Allocator, @@ -87,21 +89,18 @@ const ResponseType = enum { pub fn init(page: *Page) !*XMLHttpRequest { const arena = try page.getArena(.{ .debug = "XMLHttpRequest" }); errdefer page.releaseArena(arena); - return page._factory.xhrEventTarget(arena, XMLHttpRequest{ + const xhr = try page._factory.xhrEventTarget(arena, XMLHttpRequest{ ._page = page, ._arena = arena, ._proto = undefined, ._request_headers = try Headers.init(null, page), }); + return xhr; } -pub fn deinit(self: *XMLHttpRequest, shutdown: bool, session: *Session) void { +pub fn deinit(self: *XMLHttpRequest, session: *Session) void { if (self._transfer) |transfer| { - if (shutdown) { - transfer.terminate(); - } else { - transfer.abort(error.Abort); - } + transfer.abort(error.Abort); self._transfer = null; } @@ -137,6 +136,14 @@ pub fn deinit(self: *XMLHttpRequest, shutdown: bool, session: *Session) void { session.releaseArena(self._arena); } +pub fn releaseRef(self: *XMLHttpRequest, session: *Session) void { + self._rc.release(self, session); +} + +pub fn acquireRef(self: *XMLHttpRequest) void { + self._rc.acquire(); +} + fn asEventTarget(self: *XMLHttpRequest) *EventTarget { return self._proto._proto; } @@ -244,8 +251,6 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void { .error_callback = httpErrorCallback, .shutdown_callback = httpShutdownCallback, }); - - page.js.strongRef(self); } fn handleBlobUrl(self: *XMLHttpRequest, page: *Page) !void { @@ -387,6 +392,7 @@ fn httpStartCallback(transfer: *HttpClient.Transfer) !void { log.debug(.http, "request start", .{ .method = self._method, .url = self._url, .source = "xhr" }); } self._transfer = transfer; + self.acquireRef(); } fn httpHeaderCallback(transfer: *HttpClient.Transfer, header: net_http.Header) !void { @@ -485,15 +491,17 @@ fn httpDoneCallback(ctx: *anyopaque) !void { .loaded = loaded, }, page); - page.js.weakRef(self); + self.releaseRef(page._session); } fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void { const self: *XMLHttpRequest = @ptrCast(@alignCast(ctx)); // http client will close it after an error, it isn't safe to keep around - self._transfer = null; self.handleError(err); - self._page.js.weakRef(self); + if (self._transfer != null) { + self._transfer = null; + self.releaseRef(self._page._session); + } } fn httpShutdownCallback(ctx: *anyopaque) void { @@ -504,10 +512,10 @@ fn httpShutdownCallback(ctx: *anyopaque) void { pub fn abort(self: *XMLHttpRequest) void { self.handleError(error.Abort); if (self._transfer) |transfer| { - transfer.abort(error.Abort); self._transfer = null; + transfer.abort(error.Abort); + self.releaseRef(self._page._session); } - self._page.js.weakRef(self); } fn handleError(self: *XMLHttpRequest, err: anyerror) void { @@ -581,8 +589,6 @@ pub const JsApi = struct { pub const name = "XMLHttpRequest"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; - pub const weak = true; - pub const finalizer = bridge.finalizer(XMLHttpRequest.deinit); }; pub const constructor = bridge.constructor(XMLHttpRequest.init, .{}); diff --git a/src/lightpanda.zig b/src/lightpanda.zig index de9c0835..9d163de4 100644 --- a/src/lightpanda.zig +++ b/src/lightpanda.zig @@ -209,6 +209,37 @@ noinline fn assertionFailure(comptime ctx: []const u8, args: anytype) noreturn { @import("crash_handler.zig").crash(ctx, args, @returnAddress()); } +// Reference counting helper +pub fn RC(comptime T: type) type { + return struct { + _refs: T = 0, + + pub fn init(refs: T) @This() { + return .{._refs = refs}; + } + + pub fn acquire(self: *@This()) void { + self._refs += 1; + } + + pub fn release(self: *@This(), value: anytype, session: *Session) void { + if (comptime IS_DEBUG) { + std.debug.assert(self._refs > 0); + } + + const refs = self._refs - 1; + self._refs = refs; + if (refs > 0) { + return; + } + value.deinit(session); + if (session.finalizer_callbacks.fetchRemove(@intFromPtr(value))) |kv| { + session.releaseArena(kv.value.arena); + } + } + }; +} + test { std.testing.refAllDecls(@This()); }