From 1ed87f4e833ca071fbc71044a5c8a3b8c8e16b46 Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Thu, 13 Mar 2025 16:42:06 -0700 Subject: [PATCH] fix: deadlock in `Cow` debug checks (#18173) Co-authored-by: DonIsaac <22823424+DonIsaac@users.noreply.github.com> --- src/bake/DevServer.zig | 14 +- src/bun.js/base.zig | 2 +- src/bun.js/event_loop.zig | 2 +- src/bun.js/javascript.zig | 2 +- src/bun.js/node/fs_events.zig | 1 - src/bun.zig | 313 ++++++------------------------- src/bundler/bundle_v2.zig | 2 +- src/http.zig | 2 +- src/install/install.zig | 1 - src/output.zig | 8 +- src/ptr.zig | 9 + src/ptr/Cow.zig | 81 ++++++++ src/ptr/CowSlice.zig | 267 ++++++++++++++++++++++++++ src/{ => ptr}/ref_count.zig | 2 +- src/{ => ptr}/tagged_pointer.zig | 0 src/shell/braces.zig | 1 - src/shell/interpreter.zig | 6 +- src/shell/shell.zig | 2 +- 18 files changed, 434 insertions(+), 281 deletions(-) create mode 100644 src/ptr.zig create mode 100644 src/ptr/Cow.zig create mode 100644 src/ptr/CowSlice.zig rename src/{ => ptr}/ref_count.zig (99%) rename src/{ => ptr}/tagged_pointer.zig (100%) diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index 93063164c2..afcea6413b 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -2083,7 +2083,7 @@ pub fn finalizeBundle( .gts = undefined, }; - const quoted_source_contents = bv2.linker.graph.files.items(.quoted_source_contents); + const quoted_source_contents: []const []const u8 = bv2.linker.graph.files.items(.quoted_source_contents); // Pass 1, update the graph's nodes, resolving every bundler source // index into its `IncrementalGraph(...).FileIndex` for ( @@ -2112,7 +2112,7 @@ pub fn finalizeBundle( .{ .js = .{ .code = compile_result.code(), .source_map = source_map, - .quoted_contents = .initOwned(quoted_contents, dev.allocator), + .quoted_contents = .initOwned(@constCast(quoted_contents), dev.allocator), } }, graph == .ssr, ), @@ -2195,7 +2195,7 @@ pub fn finalizeBundle( .{ .js = .{ .code = generated_js, .source_map = .empty, - .quoted_contents = comptime .initNeverFree(""), + .quoted_contents = .empty, } }, false, ); @@ -3244,15 +3244,15 @@ pub fn IncrementalGraph(side: bake.Side) type { return self.vlq_ptr[0..self.vlq_len]; } - pub fn quotedContentsCowString(self: @This()) bun.CowString { - return bun.CowString.initUnchecked(self.quoted_contents_ptr[0..self.quoted_contents_flags.len], self.quoted_contents_flags.is_owned); + pub fn quotedContentsCowString(self: @This()) bun.ptr.CowString { + return bun.ptr.CowString.initUnchecked(self.quoted_contents_ptr[0..self.quoted_contents_flags.len], self.quoted_contents_flags.is_owned); } pub fn quotedContents(self: @This()) []const u8 { return self.quoted_contents_ptr[0..self.quoted_contents_flags.len]; } - pub fn fromNonEmptySourceMap(source_map: SourceMap.Chunk, quoted_contents: bun.CowString) !PackedMap { + pub fn fromNonEmptySourceMap(source_map: SourceMap.Chunk, quoted_contents: bun.ptr.CowString) !PackedMap { assert(source_map.buffer.list.items.len > 0); return .{ .vlq_ptr = source_map.buffer.list.items.ptr, @@ -3372,7 +3372,7 @@ pub fn IncrementalGraph(side: bake.Side) type { js: struct { code: []const u8, source_map: SourceMap.Chunk, - quoted_contents: bun.CowString, + quoted_contents: bun.ptr.CowString, }, css: u64, }, diff --git a/src/bun.js/base.zig b/src/bun.js/base.zig index aa409fcc3b..452cf15c2e 100644 --- a/src/bun.js/base.zig +++ b/src/bun.js/base.zig @@ -14,7 +14,7 @@ const Test = @import("./test/jest.zig"); const Router = @import("./api/filesystem_router.zig"); const IdentityContext = @import("../identity_context.zig").IdentityContext; const uws = bun.uws; -const TaggedPointerTypes = @import("../tagged_pointer.zig"); +const TaggedPointerTypes = @import("../ptr.zig"); const TaggedPointerUnion = TaggedPointerTypes.TaggedPointerUnion; const JSError = bun.JSError; diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index 6a3e9e81ae..f54a763841 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -7,7 +7,7 @@ const bun = @import("root").bun; const Environment = bun.Environment; const Fetch = JSC.WebCore.Fetch; const Bun = JSC.API.Bun; -const TaggedPointerUnion = @import("../tagged_pointer.zig").TaggedPointerUnion; +const TaggedPointerUnion = @import("../ptr.zig").TaggedPointerUnion; const typeBaseName = @import("../meta.zig").typeBaseName; const AsyncGlobWalkTask = JSC.API.Glob.WalkTask.AsyncGlobWalkTask; const CopyFilePromiseTask = bun.JSC.WebCore.Blob.Store.CopyFilePromiseTask; diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 397a0b774d..f78a46ef1e 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -79,7 +79,7 @@ const Watcher = bun.Watcher; const ModuleLoader = JSC.ModuleLoader; const FetchFlags = JSC.FetchFlags; -const TaggedPointerUnion = @import("../tagged_pointer.zig").TaggedPointerUnion; +const TaggedPointerUnion = @import("../ptr.zig").TaggedPointerUnion; const Task = JSC.Task; pub const Buffer = MarkedArrayBuffer; diff --git a/src/bun.js/node/fs_events.zig b/src/bun.js/node/fs_events.zig index 483da52d0a..2853dc528c 100644 --- a/src/bun.js/node/fs_events.zig +++ b/src/bun.js/node/fs_events.zig @@ -5,7 +5,6 @@ const Mutex = bun.Mutex; const sync = @import("../../sync.zig"); const Semaphore = sync.Semaphore; const UnboundedQueue = @import("../unbounded_queue.zig").UnboundedQueue; -const TaggedPointerUnion = @import("../../tagged_pointer.zig").TaggedPointerUnion; const string = bun.string; const PathWatcher = @import("./path_watcher.zig").PathWatcher; diff --git a/src/bun.zig b/src/bun.zig index b00c2433d3..d532dc8051 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -329,7 +329,6 @@ pub const StringTypes = @import("string_types.zig"); pub const stringZ = StringTypes.stringZ; pub const string = StringTypes.string; pub const CodePoint = StringTypes.CodePoint; -pub const RefCount = @import("./ref_count.zig").RefCount; pub const MAX_PATH_BYTES: usize = if (Environment.isWasm) 1024 else std.fs.max_path_bytes; pub const PathBuffer = [MAX_PATH_BYTES]u8; @@ -410,22 +409,22 @@ fn Span(comptime T: type) type { } } -pub fn span(ptr: anytype) Span(@TypeOf(ptr)) { - if (@typeInfo(@TypeOf(ptr)) == .optional) { - if (ptr) |non_null| { +pub fn span(pointer: anytype) Span(@TypeOf(pointer)) { + if (@typeInfo(@TypeOf(pointer)) == .optional) { + if (pointer) |non_null| { return span(non_null); } else { return null; } } - const Result = Span(@TypeOf(ptr)); - const l = len(ptr); + const Result = Span(@TypeOf(pointer)); + const l = len(pointer); const ptr_info = @typeInfo(Result).pointer; if (ptr_info.sentinel_ptr) |s_ptr| { const s = @as(*align(1) const ptr_info.child, @ptrCast(s_ptr)).*; - return ptr[0..l :s]; + return pointer[0..l :s]; } else { - return ptr[0..l]; + return pointer[0..l]; } } @@ -734,11 +733,8 @@ pub const http = @import("./http.zig"); pub const Analytics = @import("./analytics/analytics_thread.zig"); -const tagged_pointer = @import("./tagged_pointer.zig"); -pub const TagTypeEnumWithTypeMap = tagged_pointer.TagTypeEnumWithTypeMap; -pub const TaggedPointer = tagged_pointer.TaggedPointer; -pub const TaggedPointerUnion = tagged_pointer.TaggedPointerUnion; -pub const TypeMap = tagged_pointer.TypeMap; +pub const TaggedPointer = ptr.TaggedPointer; +pub const TaggedPointerUnion = ptr.TaggedPointerUnion; pub fn onceUnsafe(comptime function: anytype, comptime ReturnType: type) ReturnType { const Result = struct { @@ -942,8 +938,8 @@ pub fn getenvZ(key: [:0]const u8) ?[]const u8 { return getenvZAnyCase(key); } - const ptr = std.c.getenv(key.ptr) orelse return null; - return sliceTo(ptr, 0); + const pointer = std.c.getenv(key.ptr) orelse return null; + return sliceTo(pointer, 0); } pub fn getenvTruthy(key: [:0]const u8) bool { @@ -1474,18 +1470,18 @@ pub fn getFdPathW(fd_: anytype, buf: *WPathBuffer) ![]u16 { @panic("TODO unsupported platform for getFdPathW"); } -fn lenSliceTo(ptr: anytype, comptime end: std.meta.Elem(@TypeOf(ptr))) usize { - switch (@typeInfo(@TypeOf(ptr))) { +fn lenSliceTo(pointer: anytype, comptime end: std.meta.Elem(@TypeOf(pointer))) usize { + switch (@typeInfo(@TypeOf(pointer))) { .pointer => |ptr_info| switch (ptr_info.size) { .one => switch (@typeInfo(ptr_info.child)) { .array => |array_info| { if (array_info.sentinel_ptr) |sentinel_ptr| { const sentinel = @as(*align(1) const array_info.child, @ptrCast(sentinel_ptr)).*; if (sentinel == end) { - return std.mem.indexOfSentinel(array_info.child, end, ptr); + return std.mem.indexOfSentinel(array_info.child, end, pointer); } } - return std.mem.indexOfScalar(array_info.child, ptr, end) orelse array_info.len; + return std.mem.indexOfScalar(array_info.child, pointer, end) orelse array_info.len; }, else => {}, }, @@ -1495,26 +1491,26 @@ fn lenSliceTo(ptr: anytype, comptime end: std.meta.Elem(@TypeOf(ptr))) usize { // but iterating past the sentinel would be a bug so we need // to check for both. var i: usize = 0; - while (ptr[i] != end and ptr[i] != sentinel) i += 1; + while (pointer[i] != end and pointer[i] != sentinel) i += 1; return i; }, .c => { - assert(ptr != null); - return std.mem.indexOfSentinel(ptr_info.child, end, ptr); + assert(pointer != null); + return std.mem.indexOfSentinel(ptr_info.child, end, pointer); }, .slice => { if (ptr_info.sentinel_ptr) |sentinel_ptr| { const sentinel = @as(*align(1) const ptr_info.child, @ptrCast(sentinel_ptr)).*; if (sentinel == end) { - return std.mem.indexOfSentinel(ptr_info.child, sentinel, ptr); + return std.mem.indexOfSentinel(ptr_info.child, sentinel, pointer); } } - return std.mem.indexOfScalar(ptr_info.child, ptr, end) orelse ptr.len; + return std.mem.indexOfScalar(ptr_info.child, pointer, end) orelse pointer.len; }, }, else => {}, } - @compileError("invalid type given to std.mem.sliceTo: " ++ @typeName(@TypeOf(ptr))); + @compileError("invalid type given to std.mem.sliceTo: " ++ @typeName(@TypeOf(pointer))); } /// Helper for the return type of sliceTo() @@ -1578,19 +1574,19 @@ fn SliceTo(comptime T: type, comptime end: std.meta.Elem(T)) type { /// resulting slice is also sentinel terminated. /// Pointer properties such as mutability and alignment are preserved. /// C pointers are assumed to be non-null. -pub fn sliceTo(ptr: anytype, comptime end: std.meta.Elem(@TypeOf(ptr))) SliceTo(@TypeOf(ptr), end) { - if (@typeInfo(@TypeOf(ptr)) == .optional) { - const non_null = ptr orelse return null; +pub fn sliceTo(pointer: anytype, comptime end: std.meta.Elem(@TypeOf(pointer))) SliceTo(@TypeOf(pointer), end) { + if (@typeInfo(@TypeOf(pointer)) == .optional) { + const non_null = pointer orelse return null; return sliceTo(non_null, end); } - const Result = SliceTo(@TypeOf(ptr), end); - const length = lenSliceTo(ptr, end); + const Result = SliceTo(@TypeOf(pointer), end); + const length = lenSliceTo(pointer, end); const ptr_info = @typeInfo(Result).pointer; if (ptr_info.sentinel_ptr) |s_ptr| { const s = @as(*align(1) const ptr_info.child, @ptrCast(s_ptr)).*; - return ptr[0..length :s]; + return pointer[0..length :s]; } else { - return ptr[0..length]; + return pointer[0..length]; } } @@ -1984,7 +1980,6 @@ pub const bundle_v2 = @import("./bundler/bundle_v2.zig"); pub const BundleV2 = bundle_v2.BundleV2; pub const ParseTask = bundle_v2.ParseTask; -pub const Lock = @compileError("Use bun.Mutex instead"); pub const Mutex = @import("./Mutex.zig"); pub const UnboundedQueue = @import("./bun.js/unbounded_queue.zig").UnboundedQueue; @@ -2498,20 +2493,20 @@ pub const win32 = struct { } var size: usize = 0; - if (kernelenv) |ptr| { + if (kernelenv) |pointer| { // check that env is non-empty - if (ptr[0] != 0 or ptr[1] != 0) { + if (pointer[0] != 0 or pointer[1] != 0) { // array is terminated by two nulls - while (ptr[size] != 0 or ptr[size + 1] != 0) size += 1; + while (pointer[size] != 0 or pointer[size + 1] != 0) size += 1; size += 1; } } - // now ptr[size] is the first null + // now pointer[size] is the first null const envbuf = try allocator.alloc(u16, size + watcherChildEnv.len + 4); defer allocator.free(envbuf); - if (kernelenv) |ptr| { - @memcpy(envbuf[0..size], ptr); + if (kernelenv) |pointer| { + @memcpy(envbuf[0..size], pointer); } @memcpy(envbuf[size .. size + watcherChildEnv.len], watcherChildEnv); envbuf[size + watcherChildEnv.len] = '='; @@ -3044,9 +3039,9 @@ pub fn todoPanic(src: std.builtin.SourceLocation, comptime format: string, args: /// Wrapper around allocator.create(T) that safely initializes the pointer. Prefer this over /// `std.mem.Allocator.create`, but prefer using `bun.new` over `create(default_allocator, T, t)` pub fn create(allocator: std.mem.Allocator, comptime T: type, t: T) *T { - const ptr = allocator.create(T) catch outOfMemory(); - ptr.* = t; - return ptr; + const pointer = allocator.create(T) catch outOfMemory(); + pointer.* = t; + return pointer; } pub const heap_breakdown = @import("./heap_breakdown.zig"); @@ -3064,12 +3059,12 @@ pub const heap_breakdown = @import("./heap_breakdown.zig"); /// On macOS, you can use `Bun.unsafe.mimallocDump()` /// to dump the heap. pub inline fn new(comptime T: type, init: T) *T { - const ptr = if (heap_breakdown.enabled) + const pointer = if (heap_breakdown.enabled) heap_breakdown.getZoneT(T).create(T, init) - else ptr: { - const ptr = default_allocator.create(T) catch outOfMemory(); - ptr.* = init; - break :ptr ptr; + else pointer: { + const pointer = default_allocator.create(T) catch outOfMemory(); + pointer.* = init; + break :pointer pointer; }; // TODO:: @@ -3078,23 +3073,23 @@ pub inline fn new(comptime T: type, init: T) *T { // logAlloc("new({s}) = {*}", .{ meta.typeName(T), ptr }); // } - return ptr; + return pointer; } /// Free a globally-allocated a value from `bun.new()`. Using this with /// pointers allocated from other means may cause crashes. -pub inline fn destroy(ptr: anytype) void { - const T = std.meta.Child(@TypeOf(ptr)); +pub inline fn destroy(pointer: anytype) void { + const T = std.meta.Child(@TypeOf(pointer)); if (Environment.allow_assert) { const logAlloc = Output.scoped(.alloc, @hasDecl(T, "logAllocations")); - logAlloc("destroy({s}) = {*}", .{ meta.typeName(T), ptr }); + logAlloc("destroy({s}) = {*}", .{ meta.typeName(T), pointer }); } if (comptime heap_breakdown.enabled) { - heap_breakdown.getZoneT(T).destroy(T, ptr); + heap_breakdown.getZoneT(T).destroy(T, pointer); } else { - default_allocator.destroy(ptr); + default_allocator.destroy(pointer); } } @@ -3116,8 +3111,8 @@ pub fn New(comptime T: type) type { }; } -pub const NewRefCounted = @import("ref_count.zig").NewRefCounted; -pub const NewThreadSafeRefCounted = @import("ref_count.zig").NewThreadSafeRefCounted; +pub const NewRefCounted = ptr.NewRefCounted; +pub const NewThreadSafeRefCounted = ptr.NewThreadSafeRefCounted; pub fn exitThread() noreturn { const exiter = struct { @@ -3421,7 +3416,7 @@ noinline fn assertionFailureWithMsg(comptime msg: []const u8, args: anytype) nor @compileError(std.fmt.comptimePrint("assertion failure: " ++ msg, args)); } else { @branchHint(.cold); - Output.panic(assertion_failure_msg ++ ": " ++ msg, .args); + Output.panic(assertion_failure_msg ++ ": " ++ msg, args); } } @@ -3495,7 +3490,8 @@ pub fn assertf(ok: bool, comptime format: []const u8, args: anytype) callconv(ca } if (!ok) { - if (comptime Environment.isDebug) unreachable; + // crash handler has runtime-only code. + if (@inComptime()) @compileError(std.fmt.comptimePrint(format, args)); assertionFailureWithMsg(format, args); } } @@ -4151,80 +4147,6 @@ pub inline fn itemOrNull(comptime T: type, slice: []const T, index: usize) ?T { pub const Maybe = bun.JSC.Node.Maybe; -/// Type which could be borrowed or owned -/// The name is from the Rust std's `Cow` type -/// Can't think of a better name -pub fn Cow(comptime T: type, comptime VTable: type) type { - const Handler = struct { - fn copy(this: *const T, allocator: std.mem.Allocator) T { - if (!@hasDecl(VTable, "copy")) @compileError(@typeName(VTable) ++ " needs `copy()` function"); - return VTable.copy(this, allocator); - } - - fn deinit(this: *T, allocator: std.mem.Allocator) void { - if (!@hasDecl(VTable, "deinit")) @compileError(@typeName(VTable) ++ " needs `deinit()` function"); - return VTable.deinit(this, allocator); - } - }; - - return union(enum) { - borrowed: *const T, - owned: T, - - pub fn borrow(val: *const T) @This() { - return .{ - .borrowed = val, - }; - } - - pub fn own(val: T) @This() { - return .{ - .owned = val, - }; - } - - pub fn replace(this: *@This(), allocator: std.mem.Allocator, newval: T) void { - if (this.* == .owned) { - this.deinit(allocator); - } - this.* = .{ .owned = newval }; - } - - /// Get the underlying value. - pub inline fn inner(this: *const @This()) *const T { - return switch (this.*) { - .borrowed => this.borrowed, - .owned => &this.owned, - }; - } - - pub inline fn innerMut(this: *@This()) ?*T { - return switch (this.*) { - .borrowed => null, - .owned => &this.owned, - }; - } - - pub fn toOwned(this: *@This(), allocator: std.mem.Allocator) *T { - switch (this.*) { - .borrowed => { - this.* = .{ - .owned = Handler.copy(this.borrowed, allocator), - }; - }, - .owned => {}, - } - return &this.owned; - } - - pub fn deinit(this: *@This(), allocator: std.mem.Allocator) void { - if (this.* == .owned) { - Handler.deinit(&this.owned, allocator); - } - } - }; -} - /// To handle stack overflows: /// 1. StackCheck.init() /// 2. .isSafeToRecurse() @@ -4304,134 +4226,7 @@ pub const WPathBufferPool = if (Environment.isWindows) PathBufferPoolT(bun.WPath pub const OSPathBufferPool = if (Environment.isWindows) WPathBufferPool else PathBufferPool; pub const S3 = @import("./s3/client.zig"); - -pub const CowString = CowSlice(u8); - -/// "Copy on write" slice. There are many instances when it is desired to re-use -/// a slice, but doing so would make it unknown if that slice should be freed. -/// This structure, in release builds, is the same size as `[]const T`, but -/// stores one bit for if deinitialziation should free the underlying memory. -/// -/// const str = CowSlice(u8).initOwned(try alloc.dupe(u8, "hello!"), alloc); -/// const borrow = str.borrow(); -/// assert(borrow.slice().ptr == str.slice().ptr) -/// borrow.deinit(alloc); // knows it is borrowed, no free -/// str.deinit(alloc); // calls free -/// -/// In a debug build, there are aggressive assertions to ensure unintentional -/// frees do not happen. But in a release build, the developer is expected to -/// keep slice owners alive beyond the lifetimes of the borrowed instances. -/// -/// CowSlice does not support slices longer than 2^(@bitSizeOf(usize)-1). -pub fn CowSlice(T: type) type { - const cow_str_assertions = Environment.isDebug; - const DebugData = if (cow_str_assertions) struct { - mutex: std.Thread.Mutex, - allocator: Allocator, - borrows: usize, - }; - return struct { - ptr: [*]const T, - flags: packed struct(usize) { - len: @Type(.{ .int = .{ - .bits = @bitSizeOf(usize) - 1, - .signedness = .unsigned, - } }), - is_owned: bool, - }, - debug: if (cow_str_assertions) ?*DebugData else void, - - /// `data` is transferred into the returned string, and must be freed with - /// `.deinit()` when the string and its borrows are done being used. - pub fn initOwned(data: []const T, allocator: Allocator) @This() { - if (AllocationScope.downcast(allocator)) |scope| - scope.assertOwned(data); - - return .{ - .ptr = data.ptr, - .flags = .{ - .is_owned = true, - .len = @intCast(data.len), - }, - .debug = if (cow_str_assertions) - bun.new(DebugData, .{ - .mutex = .{}, - .allocator = allocator, - .borrows = 0, - }), - }; - } - - pub fn initDupe(data: []const T, allocator: Allocator) !@This() { - return initOwned(try allocator.dupe(T, data), allocator); - } - - /// `.deinit` will not free memory from this slice. - pub fn initNeverFree(data: []const T) @This() { - return .{ - .ptr = data.ptr, - .flags = .{ - .is_owned = false, - .len = @intCast(data.len), - }, - .debug = if (cow_str_assertions) null, - }; - } - - pub fn slice(str: @This()) []const T { - return str.ptr[0..str.flags.len]; - } - - /// Returns a new string. The borrowed string should be deinitialized - /// so that debug assertions that perform. - pub fn borrow(str: @This()) @This() { - if (cow_str_assertions) if (str.debug) |debug| { - debug.mutex.lock(); - defer debug.mutex.unlock(); - debug.borrows += 1; - }; - return .{ - .ptr = str.ptr, - .flags = .{ - .is_owned = false, - .len = str.flags.len, - }, - .debug = str.debug, - }; - } - - pub fn deinit(str: @This(), allocator: Allocator) void { - if (cow_str_assertions) if (str.debug) |debug| { - debug.mutex.lock(); - bun.assert( - debug.allocator.ptr == allocator.ptr and - debug.allocator.vtable == allocator.vtable, - ); - if (str.flags.is_owned) { - bun.assert(debug.borrows == 0); // active borrows become invalid data - } else { - debug.borrows -= 1; // double deinit of a borrowed string - } - bun.destroy(debug); - }; - if (str.flags.is_owned) { - allocator.free(str.slice()); - } - } - - /// Does not include debug safety checks. - pub fn initUnchecked(data: []const T, is_owned: bool) @This() { - return .{ - .ptr = data.ptr, - .flags = .{ - .is_owned = is_owned, - .len = @intCast(data.len), - }, - .debug = if (cow_str_assertions) null, - }; - } - }; -} +pub const ptr = @import("./ptr.zig"); const Allocator = std.mem.Allocator; diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 5c81129c12..ee7951011c 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -17345,7 +17345,7 @@ pub const Chunk = struct { source_index: Index, }, - const Layers = bun.Cow(bun.BabyList(bun.css.LayerName), struct { + const Layers = bun.ptr.Cow(bun.BabyList(bun.css.LayerName), struct { const Self = bun.BabyList(bun.css.LayerName); pub fn copy(self: *const Self, allocator: std.mem.Allocator) Self { return self.deepClone2(allocator); diff --git a/src/http.zig b/src/http.zig index 457635bee1..f76db2f8ee 100644 --- a/src/http.zig +++ b/src/http.zig @@ -45,7 +45,7 @@ var default_arena: Arena = undefined; pub var http_thread: HTTPThread = undefined; const HiveArray = @import("./hive_array.zig").HiveArray; const Batch = bun.ThreadPool.Batch; -const TaggedPointerUnion = @import("./tagged_pointer.zig").TaggedPointerUnion; +const TaggedPointerUnion = @import("./ptr.zig").TaggedPointerUnion; const DeadSocket = opaque {}; var dead_socket = @as(*DeadSocket, @ptrFromInt(1)); //TODO: this needs to be freed when Worker Threads are implemented diff --git a/src/install/install.zig b/src/install/install.zig index 3a7f569c18..1f34a62eaf 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -2493,7 +2493,6 @@ pub fn NewPackageInstall(comptime kind: PkgInstallKind) type { pub const Resolution = @import("./resolution.zig").Resolution; const Progress = bun.Progress; -const TaggedPointer = @import("../tagged_pointer.zig"); const DependencyInstallContext = struct { tree_id: Lockfile.Tree.Id = 0, diff --git a/src/output.zig b/src/output.zig index e3349670c7..71efbc2e80 100644 --- a/src/output.zig +++ b/src/output.zig @@ -1173,10 +1173,14 @@ pub const ScopedDebugWriter = struct { pub threadlocal var disable_inside_log: isize = 0; }; pub fn disableScopedDebugWriter() void { - ScopedDebugWriter.disable_inside_log += 1; + if (!@inComptime()) { + ScopedDebugWriter.disable_inside_log += 1; + } } pub fn enableScopedDebugWriter() void { - ScopedDebugWriter.disable_inside_log -= 1; + if (!@inComptime()) { + ScopedDebugWriter.disable_inside_log -= 1; + } } extern "c" fn getpid() c_int; diff --git a/src/ptr.zig b/src/ptr.zig new file mode 100644 index 0000000000..9115815701 --- /dev/null +++ b/src/ptr.zig @@ -0,0 +1,9 @@ +//! The `ptr` module contains smart pointer types that are used throughout Bun. +pub const Cow = @import("ptr/Cow.zig").Cow; +pub const CowSlice = @import("ptr/CowSlice.zig").CowSlice; +pub const CowSliceZ = @import("ptr/CowSlice.zig").CowSliceZ; +pub const CowString = CowSlice(u8); +pub const NewRefCounted = @import("ptr/ref_count.zig").NewRefCounted; +pub const NewThreadSafeRefCounted = @import("ptr/ref_count.zig").NewThreadSafeRefCounted; +pub const TaggedPointer = @import("ptr/tagged_pointer.zig").TaggedPointer; +pub const TaggedPointerUnion = @import("ptr/tagged_pointer.zig").TaggedPointerUnion; diff --git a/src/ptr/Cow.zig b/src/ptr/Cow.zig new file mode 100644 index 0000000000..e0441e7e87 --- /dev/null +++ b/src/ptr/Cow.zig @@ -0,0 +1,81 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +/// Type which could be borrowed or owned +/// The name is from the Rust std's `Cow` type +/// Can't think of a better name +pub fn Cow(comptime T: type, comptime VTable: type) type { + const info = @typeInfo(T); + if (info == .pointer and info.pointer.size == .slice) { + @compileError("Cow should not be used with slice types. Use CowSlice or CowSliceZ instead."); + } + + const Handler = struct { + fn copy(this: *const T, allocator: Allocator) T { + if (!@hasDecl(VTable, "copy")) @compileError(@typeName(VTable) ++ " needs `copy()` function"); + return VTable.copy(this, allocator); + } + + fn deinit(this: *T, allocator: Allocator) void { + if (!@hasDecl(VTable, "deinit")) @compileError(@typeName(VTable) ++ " needs `deinit()` function"); + return VTable.deinit(this, allocator); + } + }; + + return union(enum) { + borrowed: *const T, + owned: T, + + pub fn borrow(val: *const T) @This() { + return .{ + .borrowed = val, + }; + } + + pub fn own(val: T) @This() { + return .{ + .owned = val, + }; + } + + pub fn replace(this: *@This(), allocator: Allocator, newval: T) void { + if (this.* == .owned) { + this.deinit(allocator); + } + this.* = .{ .owned = newval }; + } + + /// Get the underlying value. + pub inline fn inner(this: *const @This()) *const T { + return switch (this.*) { + .borrowed => this.borrowed, + .owned => &this.owned, + }; + } + + pub inline fn innerMut(this: *@This()) ?*T { + return switch (this.*) { + .borrowed => null, + .owned => &this.owned, + }; + } + + pub fn toOwned(this: *@This(), allocator: Allocator) *T { + switch (this.*) { + .borrowed => { + this.* = .{ + .owned = Handler.copy(this.borrowed, allocator), + }; + }, + .owned => {}, + } + return &this.owned; + } + + pub fn deinit(this: *@This(), allocator: Allocator) void { + if (this.* == .owned) { + Handler.deinit(&this.owned, allocator); + } + } + }; +} diff --git a/src/ptr/CowSlice.zig b/src/ptr/CowSlice.zig new file mode 100644 index 0000000000..81c61c46c4 --- /dev/null +++ b/src/ptr/CowSlice.zig @@ -0,0 +1,267 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Allocator = std.mem.Allocator; +const Environment = bun.Environment; +const AllocationScope = bun.AllocationScope; + +/// "Copy on write" slice. There are many instances when it is desired to re-use +/// a slice, but doing so would make it unknown if that slice should be freed. +/// This structure, in release builds, is the same size as `[]const T`, but +/// stores one bit for if deinitialziation should free the underlying memory. +/// +/// const str = CowSlice(u8).initOwned(try alloc.dupe(u8, "hello!"), alloc); +/// const borrow = str.borrow(); +/// assert(borrow.slice().ptr == str.slice().ptr) +/// borrow.deinit(alloc); // knows it is borrowed, no free +/// str.deinit(alloc); // calls free +/// +/// In a debug build, there are aggressive assertions to ensure unintentional +/// frees do not happen. But in a release build, the developer is expected to +/// keep slice owners alive beyond the lifetimes of the borrowed instances. +/// +/// CowSlice does not support slices longer than `2^(@bitSizeOf(usize)-1)`. +pub fn CowSlice(T: type) type { + return CowSliceZ(T, null); +} + +/// "Copy on write" slice. There are many instances when it is desired to re-use +/// a slice, but doing so would make it unknown if that slice should be freed. +/// This structure, in release builds, is the same size as `[]const T`, but +/// stores one bit for if deinitialziation should free the underlying memory. +/// +/// const str = CowSlice(u8).initOwned(try alloc.dupe(u8, "hello!"), alloc); +/// const borrow = str.borrow(); +/// assert(borrow.slice().ptr == str.slice().ptr) +/// borrow.deinit(alloc); // knows it is borrowed, no free +/// str.deinit(alloc); // calls free +/// +/// In a debug build, there are aggressive assertions to ensure unintentional +/// frees do not happen. But in a release build, the developer is expected to +/// keep slice owners alive beyond the lifetimes of the borrowed instances. +/// +/// CowSlice does not support slices longer than `2^(@bitSizeOf(usize)-1)`. +pub fn CowSliceZ(T: type, comptime sentinel: ?T) type { + return struct { + /// Pointer to the underlying data. Do not access this directly. + /// + /// NOTE: `ptr` is const if data is borrowed. + ptr: [*]T, + flags: packed struct(usize) { + len: @Type(.{ .int = .{ + .bits = @bitSizeOf(usize) - 1, + .signedness = .unsigned, + } }), + is_owned: bool, + }, + debug: if (cow_str_assertions) ?*DebugData else void, + + const Self = @This(); + pub const Slice = if (sentinel) |z| [:z]const T else []const T; + pub const SliceMut = if (sentinel) |z| [:z]T else []T; + + pub const empty: Self = initStatic(""); + + /// Create a new Cow that owns its allocation. + /// + /// `data` is transferred into the returned string, and must be freed with + /// `.deinit()` when the string and its borrows are done being used. + pub fn initOwned(data: []T, allocator: Allocator) Self { + if (AllocationScope.downcast(allocator)) |scope| + scope.assertOwned(data); + + return .{ + .ptr = data.ptr, + .flags = .{ .is_owned = true, .len = @intCast(data.len) }, + .debug = if (comptime cow_str_assertions) + bun.new(DebugData, .{ + .allocator = allocator, + }), + }; + } + + /// Create a new Cow that copies `data` into a new allocation. + pub fn initDupe(data: Slice, allocator: Allocator) !Self { + const bytes: Slice = if (comptime sentinel) |_| + try allocator.dupeZ(T, data) + else + try allocator.dupe(T, data); + + return initOwned(bytes, allocator); + } + + /// Create a Cow that wraps a static string. + /// + /// Calling `.deinit()` is safe to call, but will will have no effect. + pub fn initStatic(comptime data: Slice) Self { + return .{ + // SAFETY: const semantics are enforced by is_owned flag + .ptr = @constCast(data.ptr), + .flags = .{ + .is_owned = false, + .len = @intCast(data.len), + }, + .debug = if (cow_str_assertions) null, + }; + } + + /// Returns `true` if this string owns its data. + pub inline fn isOwned(str: Self) bool { + return str.flags.is_owned; + } + + /// Borrow this Cow's slice. + pub fn slice(str: Self) Slice { + return str.ptr[0..str.flags.len]; + } + + /// Mutably borrow this `Cow`'s slice. + /// + /// Borrowed `Cow`s will be automatically converted to owned, incurring + /// an allocation. + pub fn sliceMut(str: *Self, allocator: Allocator) Allocator.Error!SliceMut { + if (!str.isOwned()) { + str.intoOwned(allocator); + } + return str.ptr[0..str.flags.len]; + } + + /// Mutably borrow this `Cow`'s slice, assuming it already owns its data. + /// Calling this on a borrowed `Cow` invokes safety-checked Illegal Behavior. + pub fn sliceMutUnsafe(str: *Self) SliceMut { + bun.assert(str.isOwned(), "CowSlice.sliceMutUnsafe cannot be called on Cows that borrow their data.", .{}); + return str.ptr[0..str.flags.len]; + } + + /// Returns a new string that borrows this string's data. + /// + /// The borrowed string should be deinitialized so that debug assertions + /// that perform. + pub fn borrow(str: Self) Self { + if (comptime cow_str_assertions) if (str.debug) |debug| { + debug.mutex.lock(); + defer debug.mutex.unlock(); + debug.borrows += 1; + }; + return .{ + .ptr = str.ptr, + .flags = .{ .is_owned = false, .len = str.flags.len }, + .debug = str.debug, + }; + } + + /// Make this Cow `owned` by duplicating its borrowed data. Does nothing + /// if the Cow is already owned. + pub fn toOwned(self: *Self, allocator: Allocator) Allocator.Error!void { + if (!self.isOwned()) { + self.intoOwned(allocator); + } + } + + /// Make this Cow `owned` by duplicating its borrowed data. Panics if + /// the Cow is already owned. + fn intoOwned(str: *Self, allocator: Allocator) callconv(bun.callconv_inline) Allocator.Error!void { + bun.assert(!str.isOwned()); + + const bytes = try if (comptime sentinel) |_| allocator.dupeZ(T, str.slice()) else allocator.dupe(T, str.slice()); + str.ptr = bytes.ptr; + str.flags.is_owned = true; + + if (comptime cow_str_assertions) { + if (str.debug) |debug| { + debug.mutex.lock(); + defer debug.mutext.unlock(); + bun.assert(debug.borrows > 0); + debug.borrows -= 1; + str.debug = null; + } + str.debug = bun.new(DebugData, .{ .allocator = allocator }); + } + } + + pub fn format(str: Self, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { + return std.fmt.formatType(str.slice(), fmt, options, writer, 1); + } + + /// Free this `Cow`'s allocation if it is owned. + /// + /// In debug builds, deinitializing borrowed strings performs debug + /// checks. In release builds it is a no-op. + pub fn deinit(str: Self, allocator: Allocator) void { + if (comptime cow_str_assertions) if (str.debug) |debug| { + debug.mutex.lock(); + defer debug.mutex.unlock(); + bun.assertf( + debug.allocator.ptr == allocator.ptr and debug.allocator.vtable == allocator.vtable, + "CowSlice.deinit called with a different allocator than the one used to create it", + .{}, + ); + if (str.isOwned()) { + // active borrows become invalid data + bun.assertf( + debug.borrows == 0, + "Cannot deinit() a CowSlice with active borrows. Current borrow count: {d}", + .{debug.borrows}, + ); + bun.destroy(debug); + } else { + debug.borrows -= 1; // double deinit of a borrowed string + } + }; + if (str.flags.is_owned) { + allocator.free(str.slice()); + } + } + + /// Does not include debug safety checks. + pub fn initUnchecked(data: Slice, is_owned: bool) Self { + return .{ + .ptr = @constCast(data.ptr), + .flags = .{ + .is_owned = is_owned, + .len = @intCast(data.len), + }, + .debug = if (cow_str_assertions) null, + }; + } + }; +} + +const cow_str_assertions = Environment.isDebug; +const DebugData = if (cow_str_assertions) struct { + mutex: std.Thread.Mutex = .{}, + allocator: Allocator, + /// number of active borrows + borrows: usize = 0, +}; + +comptime { + const cow_size = @sizeOf(CowSlice(u8)) - if (cow_str_assertions) @sizeOf(?*DebugData) else 0; + bun.assertf( + cow_size == @sizeOf([]const u8), + "CowSlice should be the same size as a native slice, but it was {d} bytes instead of {d}", + .{ cow_size, @sizeOf([]const u8) }, + ); +} + +test CowSlice { + const expect = std.testing.expect; + const expectEqualStrings = std.testing.expectEqualStrings; + const allocator = std.testing.allocator; + + var str = CowSlice(u8).initStatic("hello"); + try expect(!str.isOwned()); + try expectEqualStrings(str.slice(), "hello"); + + var borrow = str.borrow(); + try expect(!borrow.isOwned()); + try expectEqualStrings(borrow.slice(), "hello"); + + str.toOwned(allocator); + try expect(str.isOwned()); + try expectEqualStrings(str.slice(), "hello"); + + str.deinit(allocator); + + // borrow is uneffected by str being deinitialized + try expectEqualStrings(borrow.slice(), "hello"); +} diff --git a/src/ref_count.zig b/src/ptr/ref_count.zig similarity index 99% rename from src/ref_count.zig rename to src/ptr/ref_count.zig index cefc54b112..063951843a 100644 --- a/src/ref_count.zig +++ b/src/ptr/ref_count.zig @@ -1,6 +1,6 @@ const std = @import("std"); const bun = @import("root").bun; -const Output = @import("./output.zig"); +const Output = bun.Output; const strings = bun.strings; const meta = bun.meta; diff --git a/src/tagged_pointer.zig b/src/ptr/tagged_pointer.zig similarity index 100% rename from src/tagged_pointer.zig rename to src/ptr/tagged_pointer.zig diff --git a/src/shell/braces.zig b/src/shell/braces.zig index 07ba07a384..522567e80c 100644 --- a/src/shell/braces.zig +++ b/src/shell/braces.zig @@ -5,7 +5,6 @@ const builtin = @import("builtin"); const Arena = std.heap.ArenaAllocator; const Allocator = std.mem.Allocator; const SmolStr = @import("../string.zig").SmolStr; -const TaggedPointerUnion = @import("../tagged_pointer.zig").TaggedPointerUnion; /// Using u16 because anymore tokens than that results in an unreasonably high /// amount of brace expansion (like around 32k variants to expand) diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 8fb1d68904..b09ab764e5 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -34,8 +34,8 @@ const Syscall = @import("../sys.zig"); const Glob = @import("../glob.zig"); const ResolvePath = @import("../resolver/resolve_path.zig"); const DirIterator = @import("../bun.js/node/dir_iterator.zig"); -const TaggedPointerUnion = @import("../tagged_pointer.zig").TaggedPointerUnion; -const TaggedPointer = @import("../tagged_pointer.zig").TaggedPointer; +const TaggedPointerUnion = @import("../ptr.zig").TaggedPointerUnion; +const TaggedPointer = @import("../ptr.zig").TaggedPointer; pub const WorkPoolTask = @import("../work_pool.zig").Task; pub const WorkPool = @import("../work_pool.zig").WorkPool; const windows = bun.windows; @@ -462,7 +462,7 @@ pub const RefCountedStr = struct { /// A) or B) won't even mutate the environment anyway. /// /// A way to reduce copying is to only do it when the env is mutated: copy-on-write. -pub const CowEnvMap = bun.Cow(EnvMap, struct { +pub const CowEnvMap = bun.ptr.Cow(EnvMap, struct { pub fn copy(val: *const EnvMap) EnvMap { return val.clone(); } diff --git a/src/shell/shell.zig b/src/shell/shell.zig index 69c36714b2..5994339752 100644 --- a/src/shell/shell.zig +++ b/src/shell/shell.zig @@ -16,7 +16,7 @@ const ResolvePath = @import("../resolver/resolve_path.zig"); const DirIterator = @import("../bun.js/node/dir_iterator.zig"); const CodepointIterator = @import("../string_immutable.zig").PackedCodepointIterator; const isAllAscii = @import("../string_immutable.zig").isAllASCII; -const TaggedPointerUnion = @import("../tagged_pointer.zig").TaggedPointerUnion; +const TaggedPointerUnion = @import("../ptr.zig").TaggedPointerUnion; pub const interpret = @import("./interpreter.zig"); pub const subproc = @import("./subproc.zig");