const enable_debug = bun.Environment.isDebug; const enable_single_threaded_checks = enable_debug; pub const RefCountOptions = struct { /// Defaults to the type basename. debug_name: ?[]const u8 = null, }; /// Add managed reference counting to a struct type. This implements a `ref()` /// and `deref()` method to add to the struct itself. This mixin doesn't handle /// memory management, but is very easy to integrate with bun.new + bun.destroy. /// /// Avoid reference counting when an object only has one owner. /// /// const Thing = struct { /// const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinit, .{}); /// // expose `ref` and `deref` as public methods /// pub const ref = RefCount.ref; /// pub const deref = RefCount.deref; /// /// ref_count: RefCount, /// other_field: u32, /// /// pub fn init(field: u32) *T { /// return bun.new(T, .{ .ref_count = .init(), .other_field = field }); /// } /// /// // Destructor should be private so it is only called once ref_count is 0 /// // When this is empty, `bun.destroy` can be passed directly to `RefCount` /// fn deinit(thing: *T) void { /// std.debug.print("deinit {d}\n", .{thing.other_field}); /// bun.destroy(thing); /// } /// }; /// /// When `RefCount` is implemented, it can be used with `RefPtr(T)` to track where /// a reference leak may be happening. /// /// const my_ptr = RefPtr(Thing).adoptRef(.init(123)); /// assert(my_ptr.data.other_field == 123); /// /// const second_pointer = my_ptr.dupeRef(); /// assert(second_pointer.data == my_ptr.data); /// /// my_ptr.deref(); // pointer stays valid /// /// // my_ptr.data // INVALID ACCESS /// assert(second_pointer.data.other_field == 123); /// /// second_pointer.deref(); // pointer is destroyed /// /// Code that migrates to using `RefPtr` would put the .adoptRef call in it's /// initializer. To combine `bun.new` and `adoptRef`, you can use `RefPtr(T).new`. /// /// pub fn init(field: u32) RefPtr(T) { /// return .new(.{ /// .ref_count = .init(), /// .other_field = field, /// }); /// } /// /// Code can incrementally migrate to `RefPtr`, using `.initRef` to increment the count /// /// const ref_ptr = RefPtr(T).initRef(existing_raw_pointer); /// pub fn RefCount(T: type, field_name: []const u8, destructor_untyped: anytype, options: RefCountOptions) type { return struct { const destructor: fn (*T) void = destructor_untyped; active_counts: u32, thread: if (enable_single_threaded_checks) ?bun.DebugThreadLock else void, debug: if (enable_debug) DebugData(false) else void, const debug_name = options.debug_name orelse bun.meta.typeBaseName(@typeName(T)); pub const scope = bun.Output.Scoped(debug_name, true); pub fn init() @This() { return .initExactRefs(1); } /// Caller will have to call `unref()` exactly `count` times to destroy. pub fn initExactRefs(count: u32) @This() { assert(count > 0); return .{ .thread = if (enable_single_threaded_checks) if (@inComptime()) null else .initLocked(), .active_counts = count, .debug = if (enable_debug) .empty else undefined, }; } // trait implementation pub fn ref(self: *T) void { const counter = getCounter(self); if (enable_debug) { counter.debug.assertValid(); } if (comptime bun.Environment.enable_logs) { scope.log("0x{x} ref {d} -> {d}", .{ @intFromPtr(self), counter.active_counts, counter.active_counts + 1, }); } counter.assertNonThreadSafeCountIsSingleThreaded(); counter.active_counts += 1; } pub fn deref(self: *T) void { const counter = getCounter(self); if (enable_debug) { counter.debug.assertValid(); } if (comptime bun.Environment.enable_logs) { scope.log("0x{x} deref {d} -> {d}", .{ @intFromPtr(self), counter.active_counts, counter.active_counts - 1, }); } counter.assertNonThreadSafeCountIsSingleThreaded(); counter.active_counts -= 1; if (counter.active_counts == 0) { if (enable_debug) { counter.debug.deinit(std.mem.asBytes(self), @returnAddress()); } destructor(self); } } pub fn dupeRef(self: anytype) RefPtr(@TypeOf(self)) { _ = @as(*const T, self); // ensure ptr child is T return .initRef(self); } // utility functions pub fn hasOneRef(count: *const @This()) bool { count.assertNonThreadSafeCountIsSingleThreaded(); return count.active_counts == 1; } pub fn dumpActiveRefs(count: *@This()) void { if (enable_debug) { const ptr: *T = @fieldParentPtr(field_name, count); count.debug.dump(@typeName(T), ptr, count.active_counts); } } /// The active_counts value is 0 after the destructor is called. pub fn assertNoRefs(count: *const @This()) void { if (enable_debug) { bun.assert(count.active_counts == 0); } } fn assertNonThreadSafeCountIsSingleThreaded(count: *@This()) void { if (enable_single_threaded_checks) { const thread = if (count.thread) |*ptr| ptr else { count.thread = .initLocked(); return; }; thread.assertLocked(); // this counter is not thread-safe } } fn getCounter(self: *T) *@This() { return &@field(self, field_name); } }; } /// Add thread-safe reference counting to a struct type. This implements a `ref()` /// and `deref()` method to add to the struct itself. This mixin doesn't handle /// memory management, but is very easy to integrate with bun.new + bun.destroy. /// /// See `RefCount`'s comment defined above for examples & best practices. /// /// Avoid reference counting when an object only has one owner. /// Avoid thread-safe reference counting when only one thread allocates and frees. pub fn ThreadSafeRefCount(T: type, field_name: []const u8, destructor: fn (*T) void, options: RefCountOptions) type { return struct { active_counts: std.atomic.Value(u32), debug: if (enable_debug) DebugData(true) else void, const debug_name = options.debug_name orelse bun.meta.typeBaseName(@typeName(T)); pub const scope = bun.Output.Scoped(debug_name, true); pub fn init() @This() { return .initExactRefs(1); } /// Caller will have to call `unref()` exactly `count` times to destroy. pub fn initExactRefs(count: u32) @This() { assert(count > 0); return .{ .active_counts = .init(count), .debug = if (enable_debug) .empty, }; } // trait implementation pub fn ref(self: *T) void { const counter = getCounter(self); if (enable_debug) counter.debug.assertValid(); const new_count = counter.active_counts.fetchAdd(1, .seq_cst); if (comptime bun.Environment.enable_logs) { scope.log("0x{x} ref {d} -> {d}", .{ @intFromPtr(self), new_count - 1, new_count, }); } bun.debugAssert(new_count > 0); } pub fn deref(self: *T) void { const counter = getCounter(self); if (enable_debug) counter.debug.assertValid(); const new_count = counter.active_counts.fetchSub(1, .seq_cst); if (comptime bun.Environment.enable_logs) { scope.log("0x{x} deref {d} -> {d}", .{ @intFromPtr(self), new_count + 1, new_count, }); } bun.debugAssert(new_count > 0); if (new_count == 1) { if (enable_debug) { counter.debug.deinit(std.mem.asBytes(self), @returnAddress()); } destructor(self); } } pub fn dupeRef(self: anytype) RefPtr(@TypeOf(self)) { if (enable_debug) getCounter(self).debug.assertValid(); _ = @as(*const T, self); // ensure ptr child is T return .initRef(self); } // utility functions pub fn hasOneRef(counter: *const @This()) bool { if (enable_debug) counter.debug.assertValid(); return counter.active_counts.load(.seq_cst) == 1; } pub fn dumpActiveRefs(count: *@This()) void { if (enable_debug) { const ptr: *T = @fieldParentPtr(field_name, count); count.debug.dump(@typeName(T), ptr, count.active_counts.load(.seq_cst)); } } /// The active_counts value is 0 after the destructor is called. pub fn assertNoRefs(count: *const @This()) void { if (enable_debug) { bun.assert(count.active_counts.load(.seq_cst) == 0); } } fn getCounter(self: *T) *@This() { return &@field(self, field_name); } }; } /// A pointer to an object implementing `RefCount` or `ThreadSafeRefCount` /// The benefit of this over `T*` is that instances of `RefPtr` are tracked. /// /// By using this, you gain the following memory debugging tools: /// /// - `T.ref_count.dump()` to dump all active references. /// - AllocationScope integration via `.newTracked()` and `.trackAll()` /// /// See `RefCount`'s comment defined above for examples & best practices. pub fn RefPtr(T: type) type { return struct { data: *T, debug: DebugId, comptime { const RefCountMixin = @FieldType(T, "ref_count"); bun.assert(@field(T, "ref") == @field(RefCountMixin, "ref")); bun.assert(@field(T, "deref") == @field(RefCountMixin, "deref")); } const DebugId = if (enable_debug) TrackedRef.Id else void; /// Increment the reference count, and return a structure boxing the pointer. pub fn initRef(raw_ptr: *T) @This() { raw_ptr.ref(); return uncheckedAndUnsafeInit(raw_ptr, @returnAddress()); } /// Decrement the reference count, and destroy the object if the count is 0. pub fn deref(self: *const @This()) void { if (enable_debug) { self.data.ref_count.debug.release(self.debug, @returnAddress()); } self.data.deref(); if (bun.Environment.isDebug) { // make UAF fail faster (ideally integrate this with ASAN) // this @constCast is "okay" because it makes no sense to store // an object with a heap pointer in the read only segment @constCast(self).data = undefined; } } pub fn dupeRef(ref: @This()) @This() { return .initRef(ref.data); } // Allocate a new object, returning a RefPtr to it. pub fn new(init_data: T) @This() { return .adoptRef(bun.new(T, init_data)); } /// Initialize a newly allocated pointer, returning a RefPtr to it. /// Care must be taken when using non-default allocators. pub fn adoptRef(raw_ptr: *T) @This() { if (enable_debug) { bun.assert(raw_ptr.ref_count.hasOneRef()); raw_ptr.ref_count.debug.assertValid(); } return uncheckedAndUnsafeInit(raw_ptr, @returnAddress()); } /// This will assert that ALL references are cleaned up by the time the allocation scope ends. pub fn newTracked(scope: *AllocationScope, init_data: T) @This() { const ptr: @This() = .new(init_data); ptr.trackImpl(scope, @returnAddress()); return ptr; } /// This will assert that ALL references are cleaned up by the time the allocation scope ends. pub fn trackAll(ref: @This(), scope: *AllocationScope) void { ref.trackImpl(scope, @returnAddress()); } fn trackImpl(ref: @This(), scope: *AllocationScope, ret_addr: usize) void { const debug = &ref.data.ref_count.debug; debug.allocation_scope = &scope; scope.trackExternalAllocation( std.mem.asBytes(ref.data), ret_addr, .{ .ref_count = debug }, ); } fn uncheckedAndUnsafeInit(raw_ptr: *T, ret_addr: ?usize) @This() { return .{ .data = raw_ptr, .debug = if (enable_debug) raw_ptr.ref_count.debug.acquire( &raw_ptr.ref_count.active_counts, ret_addr orelse @returnAddress(), ), }; } }; } const TrackedRef = struct { acquired_at: bun.crash_handler.StoredTrace, /// Not an index, just a unique identifier for the debug data pub const Id = bun.GenericIndex(u32, TrackedRef); }; const TrackedDeref = struct { acquired_at: bun.crash_handler.StoredTrace, released_at: bun.crash_handler.StoredTrace, }; /// Provides Ref tracking. This is not generic over the pointer T to reduce analysis complexity. pub fn DebugData(thread_safe: bool) type { return struct { const Debug = @This(); const Count = if (thread_safe) std.atomic.Value(u32) else u32; magic: enum(u128) { valid = 0x2f84e51d } align(@alignOf(u32)), lock: if (thread_safe) std.debug.SafetyLock else bun.Mutex, next_id: u32, map: std.AutoHashMapUnmanaged(TrackedRef.Id, TrackedRef), frees: std.AutoArrayHashMapUnmanaged(TrackedRef.Id, TrackedDeref), // Allocation Scope integration allocation_scope: ?*AllocationScope, count_pointer: ?*Count, pub const empty: @This() = .{ .magic = .valid, .lock = .{}, .next_id = 0, .map = .empty, .frees = .empty, .allocation_scope = null, .count_pointer = null, }; fn assertValid(debug: *const @This()) void { bun.assert(debug.magic == .valid); } fn dump(debug: *@This(), type_name: ?[]const u8, ptr: *anyopaque, ref_count: u32) void { debug.lock.lock(); defer debug.lock.unlock(); genericDump(type_name, ptr, ref_count, &debug.map); } fn nextId(debug: *@This()) TrackedRef.Id { if (thread_safe) { return .init(@atomicRmw(u32, &debug.next_id, .Add, 1, .seq_cst)); } else { defer debug.next_id += 1; return .init(debug.next_id); } } fn acquire(debug: *@This(), count_pointer: *Count, return_address: usize) TrackedRef.Id { debug.lock.lock(); defer debug.lock.unlock(); debug.count_pointer = count_pointer; const id = nextId(debug); debug.map.put(bun.default_allocator, id, .{ .acquired_at = .capture(return_address), }) catch bun.outOfMemory(); return id; } fn release(debug: *@This(), id: TrackedRef.Id, return_address: usize) void { debug.lock.lock(); defer debug.lock.unlock(); const entry = debug.map.fetchRemove(id) orelse { return; }; debug.frees.put(bun.default_allocator, id, .{ .acquired_at = entry.value.acquired_at, .released_at = .capture(return_address), }) catch bun.outOfMemory(); } fn deinit(debug: *@This(), data: []const u8, ret_addr: usize) void { assertValid(debug); debug.magic = undefined; debug.lock.lock(); debug.map.clearAndFree(bun.default_allocator); debug.frees.clearAndFree(bun.default_allocator); if (debug.allocation_scope) |scope| { _ = scope.trackExternalFree(data, ret_addr); } } // Trait function for AllocationScope pub fn onAllocationLeak(debug: *@This(), data: []u8) void { debug.lock.lock(); defer debug.lock.unlock(); const count = debug.count_pointer.?; debug.dump(null, data.ptr, if (thread_safe) count.load(.seq_cst) else count.*); } }; } fn genericDump( type_name: ?[]const u8, ptr: *anyopaque, total_ref_count: usize, map: *std.AutoHashMapUnmanaged(TrackedRef.Id, TrackedRef), ) void { const tracked_refs = map.count(); const untracked_refs = total_ref_count - tracked_refs; bun.Output.prettyError("{s}{s}{x} has ", .{ type_name orelse "", if (type_name != null) "@" else "", @intFromPtr(ptr), }); if (tracked_refs > 0) { bun.Output.prettyError("{d} tracked{s}", .{ tracked_refs, if (untracked_refs > 0) ", " else "" }); } if (untracked_refs > 0) { bun.Output.prettyError("{d} untracked refs\n", .{untracked_refs}); } else { bun.Output.prettyError("refs\n", .{}); } var i: usize = 0; var it = map.iterator(); while (it.next()) |entry| { bun.Output.prettyError("RefPtr acquired at:\n", .{}); bun.crash_handler.dumpStackTrace(entry.value_ptr.acquired_at.trace(), AllocationScope.trace_limits); i += 1; if (i >= 3) { bun.Output.prettyError(" {d} omitted ...\n", .{map.count() - i}); break; } } } pub fn maybeAssertNoRefs(T: type, ptr: *const T) void { if (!@hasField(T, "ref_count")) return; const Rc = @FieldType(T, "ref_count"); switch (@typeInfo(Rc)) { .@"struct" => if (@hasDecl(Rc, "assertNoRefs")) ptr.ref_count.assertNoRefs(), else => {}, } } const std = @import("std"); const bun = @import("bun"); const assert = bun.assert; const AllocationScope = bun.AllocationScope;