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()); }