From eb04e4e6404bd5de869ae36d3414d90955e6728e Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Fri, 26 Sep 2025 17:18:30 -0700 Subject: [PATCH] Make `bun.webcore.Blob` smaller and ref-counted (#23015) Reduce the size of `bun.webcore.Blob` from 120 bytes to 96. Also make it ref-counted: in-progress work on improving the bindings generator depends on this, as it means C++ can pass a pointer to the `Blob` to Zig without risking it being destroyed if the GC collects the associated `JSBlob`. Note that this PR depends on #23013. (For internal tracking: fixes STAB-1289, STAB-1290) --- src/StandaloneModuleGraph.zig | 1 - src/ast/Macro.zig | 15 +-- src/bun.js/api/BunObject.zig | 4 - src/bun.js/api/server/FileRoute.zig | 4 +- src/bun.js/api/server/StaticRoute.zig | 6 +- src/bun.js/bindings/blob.cpp | 6 +- src/bun.js/bindings/blob.h | 68 ++++++---- src/bun.js/webcore/Blob.zig | 160 ++++++++++++++--------- src/bun.js/webcore/Body.zig | 10 +- src/bun.js/webcore/ObjectURLRegistry.zig | 1 - src/bun.js/webcore/S3Client.zig | 1 - src/bun.js/webcore/S3File.zig | 4 +- src/bun.js/webcore/blob/read_file.zig | 3 +- 13 files changed, 165 insertions(+), 118 deletions(-) diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index 68b3006688..0da83a40e6 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -199,7 +199,6 @@ pub const StandaloneModuleGraph = struct { store.ref(); const b = bun.webcore.Blob.initWithStore(store, globalObject).new(); - b.allocator = bun.default_allocator; if (bun.http.MimeType.byExtensionNoDefault(bun.strings.trimLeadingChar(std.fs.path.extension(this.name), '.'))) |mime| { store.mime_type = mime; diff --git a/src/ast/Macro.zig b/src/ast/Macro.zig index 623d996554..97863921a4 100644 --- a/src/ast/Macro.zig +++ b/src/ast/Macro.zig @@ -325,7 +325,7 @@ pub const Runner = struct { return _entry.value_ptr.*; } - var blob_: ?jsc.WebCore.Blob = null; + var blob_: ?*const jsc.WebCore.Blob = null; const mime_type: ?MimeType = null; if (value.jsType() == .DOMWrapper) { @@ -334,30 +334,23 @@ pub const Runner = struct { } else if (value.as(jsc.WebCore.Request)) |resp| { return this.run(try resp.getBlobWithoutCallFrame(this.global)); } else if (value.as(jsc.WebCore.Blob)) |resp| { - blob_ = resp.*; - blob_.?.allocator = null; + blob_ = resp; } else if (value.as(bun.api.ResolveMessage) != null or value.as(bun.api.BuildMessage) != null) { _ = this.macro.vm.uncaughtException(this.global, value, false); return error.MacroFailed; } } - if (blob_) |*blob| { - const out_expr = Expr.fromBlob( + if (blob_) |blob| { + return Expr.fromBlob( blob, this.allocator, mime_type, this.log, this.caller.loc, ) catch { - blob.deinit(); return error.MacroFailed; }; - if (out_expr.data == .e_string) { - blob.deinit(); - } - - return out_expr; } return Expr.init(E.String, E.String.empty, this.caller.loc); diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index a5af653677..edf05d293a 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -1337,7 +1337,6 @@ pub fn getEmbeddedFiles(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) bun.J // We call .dupe() on this to ensure that we don't return a blob that might get freed later. const input_blob = file.blob(globalThis); const blob = jsc.WebCore.Blob.new(input_blob.dupeWithContentType(true)); - blob.allocator = bun.default_allocator; blob.name = input_blob.name.dupeRef(); try array.putIndex(globalThis, i, blob.toJS(globalThis)); i += 1; @@ -2048,7 +2047,6 @@ pub fn createBunStdin(globalThis: *jsc.JSGlobalObject) callconv(.C) jsc.JSValue var blob = jsc.WebCore.Blob.new( jsc.WebCore.Blob.initWithStore(store, globalThis), ); - blob.allocator = bun.default_allocator; return blob.toJS(globalThis); } @@ -2059,7 +2057,6 @@ pub fn createBunStderr(globalThis: *jsc.JSGlobalObject) callconv(.C) jsc.JSValue var blob = jsc.WebCore.Blob.new( jsc.WebCore.Blob.initWithStore(store, globalThis), ); - blob.allocator = bun.default_allocator; return blob.toJS(globalThis); } @@ -2070,7 +2067,6 @@ pub fn createBunStdout(globalThis: *jsc.JSGlobalObject) callconv(.C) jsc.JSValue var blob = jsc.WebCore.Blob.new( jsc.WebCore.Blob.initWithStore(store, globalThis), ); - blob.allocator = bun.default_allocator; return blob.toJS(globalThis); } diff --git a/src/bun.js/api/server/FileRoute.zig b/src/bun.js/api/server/FileRoute.zig index 88b3eb03a1..ab4b57bfa8 100644 --- a/src/bun.js/api/server/FileRoute.zig +++ b/src/bun.js/api/server/FileRoute.zig @@ -68,7 +68,7 @@ pub fn fromJS(globalThis: *jsc.JSGlobalObject, argument: jsc.JSValue) bun.JSErro var blob = response.body.value.use(); blob.globalThis = globalThis; - blob.allocator = null; + bun.assertf(!blob.isHeapAllocated(), "expected blob not to be heap-allocated", .{}); response.body.value = .{ .Blob = blob.dupe() }; const headers = bun.handleOom(Headers.from(response.init.headers, bun.default_allocator, .{ .body = &.{ .Blob = blob } })); @@ -87,7 +87,7 @@ pub fn fromJS(globalThis: *jsc.JSGlobalObject, argument: jsc.JSValue) bun.JSErro if (blob.needsToReadFile()) { var b = blob.dupe(); b.globalThis = globalThis; - b.allocator = null; + bun.assertf(!b.isHeapAllocated(), "expected blob not to be heap-allocated", .{}); return bun.new(FileRoute, .{ .ref_count = .init(), .server = null, diff --git a/src/bun.js/api/server/StaticRoute.zig b/src/bun.js/api/server/StaticRoute.zig index 7fbfcd5758..9e931826a8 100644 --- a/src/bun.js/api/server/StaticRoute.zig +++ b/src/bun.js/api/server/StaticRoute.zig @@ -113,7 +113,11 @@ pub fn fromJS(globalThis: *jsc.JSGlobalObject, argument: jsc.JSValue) bun.JSErro } var blob = response.body.value.use(); blob.globalThis = globalThis; - blob.allocator = null; + bun.assertf( + !blob.isHeapAllocated(), + "expected blob not to be heap-allocated", + .{}, + ); response.body.value = .{ .Blob = blob.dupe() }; break :brk .{ .Blob = blob }; diff --git a/src/bun.js/bindings/blob.cpp b/src/bun.js/bindings/blob.cpp index 2e650ae479..315b5d0468 100644 --- a/src/bun.js/bindings/blob.cpp +++ b/src/bun.js/bindings/blob.cpp @@ -2,14 +2,14 @@ #include "ZigGeneratedClasses.h" extern "C" JSC::EncodedJSValue SYSV_ABI Blob__create(JSC::JSGlobalObject* globalObject, void* impl); -extern "C" void* Blob__setAsFile(void* impl, BunString* filename); +extern "C" void Blob__setAsFile(void* impl, BunString* filename); namespace WebCore { JSC::JSValue toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, WebCore::Blob& impl) { BunString filename = Bun::toString(impl.fileName()); - impl.m_impl = Blob__setAsFile(impl.impl(), &filename); + Blob__setAsFile(impl.impl(), &filename); return JSC::JSValue::decode(Blob__create(lexicalGlobalObject, Blob__dupe(impl.impl()))); } @@ -28,7 +28,7 @@ JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlo size_t Blob::memoryCost() const { - return sizeof(Blob) + JSBlob::memoryCost(m_impl); + return sizeof(Blob) + JSBlob::memoryCost(impl()); } } diff --git a/src/bun.js/bindings/blob.h b/src/bun.js/bindings/blob.h index 9e59790070..5b3b923154 100644 --- a/src/bun.js/bindings/blob.h +++ b/src/bun.js/bindings/blob.h @@ -8,44 +8,60 @@ namespace WebCore { extern "C" void* Blob__dupeFromJS(JSC::EncodedJSValue impl); extern "C" void* Blob__dupe(void* impl); -extern "C" void Blob__destroy(void* impl); extern "C" void* Blob__getDataPtr(JSC::EncodedJSValue blob); extern "C" size_t Blob__getSize(JSC::EncodedJSValue blob); extern "C" void* Blob__fromBytes(JSC::JSGlobalObject* globalThis, const void* ptr, size_t len); +extern "C" void* Blob__ref(void* impl); +extern "C" void* Blob__deref(void* impl); +// Opaque type corresponding to `bun.webcore.Blob`. +class BlobImpl; + +struct BlobImplRefDerefTraits { + static ALWAYS_INLINE BlobImpl* refIfNotNull(BlobImpl* ptr) + { + if (ptr) [[likely]] + Blob__ref(ptr); + return ptr; + } + + static ALWAYS_INLINE BlobImpl& ref(BlobImpl& ref) + { + Blob__ref(&ref); + return ref; + } + + static ALWAYS_INLINE void derefIfNotNull(BlobImpl* ptr) + { + if (ptr) [[likely]] + Blob__deref(ptr); + } +}; + +using BlobRef = Ref, BlobImplRefDerefTraits>; +using BlobRefPtr = RefPtr, BlobImplRefDerefTraits>; + +// TODO: Now that `bun.webcore.Blob` is ref-counted, can `RefPtr` be replaced with `Blob`? class Blob : public RefCounted { public: - void* impl() + BlobImpl* impl() const { - return m_impl; + return m_impl.get(); } static RefPtr create(JSC::JSValue impl) { - void* implPtr = Blob__dupeFromJS(JSValue::encode(impl)); - if (!implPtr) - return nullptr; - - return adoptRef(*new Blob(implPtr)); + return createAdopted(Blob__dupeFromJS(JSValue::encode(impl))); } static RefPtr create(std::span bytes, JSC::JSGlobalObject* globalThis) { - return adoptRef(*new Blob(Blob__fromBytes(globalThis, bytes.data(), bytes.size()))); + return createAdopted(Blob__fromBytes(globalThis, bytes.data(), bytes.size())); } static RefPtr create(void* ptr) { - void* implPtr = Blob__dupe(ptr); - if (!implPtr) - return nullptr; - - return adoptRef(*new Blob(implPtr)); - } - - ~Blob() - { - Blob__destroy(m_impl); + return createAdopted(Blob__dupe(ptr)); } String fileName() @@ -57,17 +73,25 @@ public: { m_fileName = fileName; } - void* m_impl; size_t memoryCost() const; private: Blob(void* impl, String fileName = String()) + : m_impl(adoptRef, BlobImplRefDerefTraits>( + static_cast(impl))) + , m_fileName(std::move(fileName)) { - m_impl = impl; - m_fileName = fileName; } + static RefPtr createAdopted(void* ptr) + { + if (!ptr) + return nullptr; + return adoptRef(new Blob(ptr)); + } + + BlobRefPtr m_impl; String m_fileName; }; diff --git a/src/bun.js/webcore/Blob.zig b/src/bun.js/webcore/Blob.zig index 4b8242bb5c..37e6ff6f25 100644 --- a/src/bun.js/webcore/Blob.zig +++ b/src/bun.js/webcore/Blob.zig @@ -13,7 +13,12 @@ pub const read_file = @import("./blob/read_file.zig"); pub const write_file = @import("./blob/write_file.zig"); pub const copy_file = @import("./blob/copy_file.zig"); -pub const new = bun.TrivialNew(@This()); +pub fn new(blob: Blob) *Blob { + const result = bun.new(Blob, blob); + result.#ref_count = .init(1); + return result; +} + pub const js = jsc.Codegen.JSBlob; pub const fromJS = js.fromJS; pub const fromJSDirect = js.fromJSDirect; @@ -22,9 +27,6 @@ reported_estimated_size: usize = 0, size: SizeType = 0, offset: SizeType = 0, -/// When set, the blob will be freed on finalization callbacks -/// If the blob is contained in Response or Request, this must be null -allocator: ?std.mem.Allocator = null, store: ?*Store = null, content_type: string = "", content_type_allocated: bool = false, @@ -32,11 +34,15 @@ content_type_was_set: bool = false, /// JavaScriptCore strings are either latin1 or UTF-16 /// When UTF-16, they're nearly always due to non-ascii characters -is_all_ascii: ?bool = null, +charset: Charset = .unknown, /// Was it created via file constructor? is_jsdom_file: bool = false, +/// Reference count, for use with `bun.ptr.ExternalShared`. If the reference count is 0, that means +/// this blob is *not* heap-allocated, and will not be freed in `deinit`. +#ref_count: bun.ptr.RawRefCount(u32, .single_threaded) = .init(0), + globalThis: *JSGlobalObject = undefined, last_modified: f64 = 0.0, @@ -45,6 +51,8 @@ last_modified: f64 = 0.0, /// https://github.com/oven-sh/bun/issues/10178 name: bun.String = .dead, +pub const Ref = bun.ptr.ExternalShared(Blob); + /// Max int of double precision /// 9 petabytes is probably enough for awhile /// We want to avoid coercing to a BigInt because that's a heap allocation @@ -72,7 +80,7 @@ pub fn getFormDataEncoding(this: *Blob) ?*bun.FormData.AsyncFormData { var content_type_slice: ZigString.Slice = this.getContentType() orelse return null; defer content_type_slice.deinit(); const encoding = bun.FormData.Encoding.get(content_type_slice.slice()) orelse return null; - return bun.handleOom(bun.FormData.AsyncFormData.init(this.allocator orelse bun.default_allocator, encoding)); + return bun.handleOom(bun.FormData.AsyncFormData.init(bun.default_allocator, encoding)); } pub fn hasContentTypeFromUser(this: *const Blob) bool { @@ -113,6 +121,7 @@ pub fn doReadFromS3(this: *Blob, comptime Function: anytype, global: *JSGlobalOb }; return S3BlobDownloadTask.init(global, this, WrappedFn.wrapped); } + pub fn doReadFile(this: *Blob, comptime Function: anytype, global: *JSGlobalObject) JSValue { debug("doReadFile", .{}); @@ -497,7 +506,7 @@ fn _onStructuredCloneDeserialize( if (version == 3) break :versions; } - blob.allocator = allocator; + bun.assertf(blob.isHeapAllocated(), "expected blob to be heap-allocated", .{}); blob.offset = @as(u52, @intCast(offset)); if (content_type.len > 0) { blob.content_type = content_type; @@ -612,7 +621,7 @@ export fn Blob__dupeFromJS(value: jsc.JSValue) ?*Blob { return Blob__dupe(this); } -export fn Blob__setAsFile(this: *Blob, path_str: *bun.String) *Blob { +export fn Blob__setAsFile(this: *Blob, path_str: *bun.String) void { this.is_jsdom_file = true; // This is not 100% correct... @@ -624,19 +633,10 @@ export fn Blob__setAsFile(this: *Blob, path_str: *bun.String) *Blob { } } } - - return this; } -export fn Blob__dupe(ptr: *anyopaque) *Blob { - const this = bun.cast(*Blob, ptr); - const new_ptr = new(this.dupeWithContentType(true)); - new_ptr.allocator = bun.default_allocator; - return new_ptr; -} - -export fn Blob__destroy(this: *Blob) void { - this.finalize(); +export fn Blob__dupe(this: *Blob) *Blob { + return new(this.dupeWithContentType(true)); } export fn Blob__getFileNameString(this: *Blob) callconv(.C) bun.String { @@ -649,7 +649,6 @@ export fn Blob__getFileNameString(this: *Blob) callconv(.C) bun.String { comptime { _ = Blob__dupeFromJS; - _ = Blob__destroy; _ = Blob__dupe; _ = Blob__setAsFile; _ = Blob__getFileNameString; @@ -1076,10 +1075,7 @@ pub fn writeFileWithSourceDestination(ctx: *jsc.JSGlobalObject, source_blob: *Bl // this is an edgecase // it will happen if someone did Bun.write(new Blob([123]), new Blob([456])) // eventually, this could be like Buffer.concat - var clone = source_blob.dupe(); - clone.allocator = bun.default_allocator; - const cloned = Blob.new(clone); - cloned.allocator = bun.default_allocator; + const cloned = Blob.new(source_blob.dupe()); return JSPromise.resolvedPromiseValue(ctx, cloned.toJS(ctx)); } else if (destination_type == .bytes and (source_type == .file or source_type == .s3)) { const blob_value = source_blob.getSliceFrom(ctx, 0, 0, "", false); @@ -1820,7 +1816,6 @@ pub fn JSDOMFile__construct_(globalThis: *jsc.JSGlobalObject, callframe: *jsc.Ca } var blob_ = Blob.new(blob); - blob_.allocator = allocator; blob_.is_jsdom_file = true; return blob_; } @@ -1908,7 +1903,6 @@ pub fn constructBunFile( } var ptr = Blob.new(blob); - ptr.allocator = bun.default_allocator; return ptr.toJS(globalObject); } @@ -2769,7 +2763,7 @@ pub fn getSliceFrom(this: *Blob, globalThis: *jsc.JSGlobalObject, relativeStart: const offset = this.offset +| @as(SizeType, @intCast(relativeStart)); const len = @as(SizeType, @intCast(@max(relativeEnd -| relativeStart, 0))); - // This copies over the is_all_ascii flag + // This copies over the charset field // which is okay because this will only be a <= slice var blob = this.dupe(); blob.offset = offset; @@ -2785,7 +2779,6 @@ pub fn getSliceFrom(this: *Blob, globalThis: *jsc.JSGlobalObject, relativeStart: blob.content_type_was_set = this.content_type_was_set or content_type_was_allocated; var blob_ = Blob.new(blob); - blob_.allocator = bun.default_allocator; return blob_.toJS(globalThis); } @@ -2806,7 +2799,6 @@ pub fn getSlice( if (this.size == 0) { const empty = Blob.initEmpty(globalThis); var ptr = Blob.new(empty); - ptr.allocator = allocator; return ptr.toJS(globalThis); } @@ -3059,15 +3051,12 @@ export fn Blob__getSize(value: jsc.JSValue) callconv(.C) usize { export fn Blob__fromBytes(globalThis: *jsc.JSGlobalObject, ptr: ?[*]const u8, len: usize) callconv(.C) *Blob { if (ptr == null or len == 0) { const blob = new(initEmpty(globalThis)); - blob.allocator = bun.default_allocator; return blob; } const bytes = bun.handleOom(bun.default_allocator.dupe(u8, ptr.?[0..len])); const store = Store.init(bytes, bun.default_allocator); - var blob = initWithStore(store, globalThis); - blob.allocator = bun.default_allocator; - return new(blob); + return new(initWithStore(store, globalThis)); } pub fn getStat(this: *Blob, globalThis: *jsc.JSGlobalObject, callback: *jsc.CallFrame) bun.JSError!jsc.JSValue { @@ -3237,14 +3226,17 @@ pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) b } blob.calculateEstimatedByteSize(); - - var blob_ = Blob.new(blob); - blob_.allocator = allocator; - return blob_; + return Blob.new(blob); } pub fn finalize(this: *Blob) void { - this.deinit(); + bun.assertf( + this.isHeapAllocated(), + "`finalize` may only be called on a heap-allocated Blob", + .{}, + ); + var shared = Blob.Ref.adopt(this); + shared.deinit(); } pub fn initWithAllASCII(bytes: []u8, allocator: std.mem.Allocator, globalThis: *JSGlobalObject, is_all_ascii: bool) Blob { @@ -3257,10 +3249,9 @@ pub fn initWithAllASCII(bytes: []u8, allocator: std.mem.Allocator, globalThis: * return Blob{ .size = @as(SizeType, @truncate(bytes.len)), .store = store, - .allocator = null, .content_type = "", .globalThis = globalThis, - .is_all_ascii = is_all_ascii, + .charset = .fromIsAllASCII(is_all_ascii), }; } @@ -3272,7 +3263,6 @@ pub fn init(bytes: []u8, allocator: std.mem.Allocator, globalThis: *JSGlobalObje Blob.Store.init(bytes, allocator) else null, - .allocator = null, .content_type = "", .globalThis = globalThis, }; @@ -3290,7 +3280,6 @@ pub fn createWithBytesAndAllocator( Blob.Store.init(bytes, allocator) else null, - .allocator = null, .content_type = if (was_string) MimeType.text.value else "", .globalThis = globalThis, }; @@ -3343,7 +3332,6 @@ pub fn initWithStore(store: *Blob.Store, globalThis: *JSGlobalObject) Blob { return Blob{ .size = store.size(), .store = store, - .allocator = null, .content_type = if (store.data == .file) store.data.file.mime_type.value else @@ -3356,7 +3344,6 @@ pub fn initEmpty(globalThis: *JSGlobalObject) Blob { return Blob{ .size = 0, .store = null, - .allocator = null, .content_type = "", .globalThis = globalThis, }; @@ -3383,7 +3370,8 @@ pub fn dupe(this: *const Blob) Blob { pub fn dupeWithContentType(this: *const Blob, include_content_type: bool) Blob { if (this.store != null) this.store.?.ref(); var duped = this.*; - if (duped.content_type_allocated and duped.allocator != null and !include_content_type) { + duped.setNotHeapAllocated(); + if (duped.content_type_allocated and duped.isHeapAllocated() and !include_content_type) { // for now, we just want to avoid a use-after-free here if (jsc.VirtualMachine.get().mimeType(duped.content_type)) |mime| { @@ -3400,18 +3388,16 @@ pub fn dupeWithContentType(this: *const Blob, include_content_type: bool) Blob { if (this.content_type_was_set) { duped.content_type_was_set = duped.content_type.len > 0; } - } else if (duped.content_type_allocated and duped.allocator != null and include_content_type) { + } else if (duped.content_type_allocated and duped.isHeapAllocated() and include_content_type) { duped.content_type = bun.handleOom(bun.default_allocator.dupe(u8, this.content_type)); } duped.name = duped.name.dupeRef(); - - duped.allocator = null; return duped; } pub fn toJS(this: *Blob, globalObject: *jsc.JSGlobalObject) jsc.JSValue { // if (comptime Environment.allow_assert) { - // assert(this.allocator != null); + // assert(this.isHeapAllocated()); // } this.calculateEstimatedByteSize(); @@ -3427,10 +3413,7 @@ pub fn deinit(this: *Blob) void { this.name.deref(); this.name = .dead; - // TODO: remove this field, make it a boolean. - if (this.allocator) |alloc| { - this.allocator = null; - bun.debugAssert(alloc.vtable == bun.default_allocator.vtable); + if (this.isHeapAllocated()) { bun.destroy(this); } } @@ -3445,8 +3428,9 @@ pub fn sharedView(this: *const Blob) []const u8 { } pub const Lifetime = jsc.WebCore.Lifetime; + pub fn setIsASCIIFlag(this: *Blob, is_all_ascii: bool) void { - this.is_all_ascii = is_all_ascii; + this.charset = .fromIsAllASCII(is_all_ascii); // if this Blob represents the entire binary data // which will be pretty common // we can update the store's is_all_ascii flag @@ -3479,7 +3463,7 @@ pub fn toStringWithBytes(this: *Blob, global: *JSGlobalObject, raw_bytes: []cons // null == unknown // false == can't be - const could_be_all_ascii = this.is_all_ascii orelse this.store.?.is_all_ascii; + const could_be_all_ascii = this.isAllASCII() orelse this.store.?.is_all_ascii; if (could_be_all_ascii == null or !could_be_all_ascii.?) { // if toUTF16Alloc returns null, it means there are no non-ASCII characters @@ -3589,7 +3573,7 @@ pub fn toJSONWithBytes(this: *Blob, global: *JSGlobalObject, raw_bytes: []const } // null == unknown // false == can't be - const could_be_all_ascii = this.is_all_ascii orelse this.store.?.is_all_ascii; + const could_be_all_ascii = this.isAllASCII() orelse this.store.?.is_all_ascii; defer if (comptime lifetime == .temporary) bun.default_allocator.free(@constCast(buf)); if (could_be_all_ascii == null or !could_be_all_ascii.?) { @@ -3870,7 +3854,7 @@ fn fromJSWithoutDeferGC( if (top_value.as(Blob)) |blob| { if (comptime move) { var _blob = blob.*; - _blob.allocator = null; + _blob.setNotHeapAllocated(); blob.transfer(); return _blob; } else { @@ -3978,7 +3962,7 @@ fn fromJSWithoutDeferGC( .DOMWrapper => { if (item.as(Blob)) |blob| { - could_have_non_ascii = could_have_non_ascii or !(blob.is_all_ascii orelse false); + could_have_non_ascii = could_have_non_ascii or blob.charset != .all_ascii; joiner.pushStatic(blob.sharedView()); continue; } else if (current.toSliceClone(global)) |sliced| { @@ -3997,7 +3981,7 @@ fn fromJSWithoutDeferGC( .DOMWrapper => { if (current.as(Blob)) |blob| { - could_have_non_ascii = could_have_non_ascii or !(blob.is_all_ascii orelse false); + could_have_non_ascii = could_have_non_ascii or blob.charset != .all_ascii; joiner.pushStatic(blob.sharedView()); } else if (current.toSliceClone(global)) |sliced| { const allocator = sliced.allocator.get(); @@ -4144,7 +4128,6 @@ pub const Any = union(enum) { }, .blob => { const result = Blob.new(this.toBlob(globalThis)); - result.allocator = bun.default_allocator; result.globalThis = globalThis; return result.toJS(globalThis); }, @@ -4355,7 +4338,7 @@ pub const Any = union(enum) { pub fn wasString(self: *const @This()) bool { return switch (self.*) { - .Blob => self.Blob.is_all_ascii orelse false, + .Blob => self.Blob.charset == .all_ascii, .WTFStringImpl => true, // .InlineBlob => self.InlineBlob.was_string, .InternalBlob => self.InternalBlob.was_string, @@ -4761,6 +4744,61 @@ pub fn FileCloser(comptime This: type) type { }; } +/// This takes up less space than a `?bool`. +pub const Charset = enum { + unknown, + all_ascii, + non_ascii, + + pub fn fromIsAllASCII(is_all_ascii: ?bool) Charset { + return if (is_all_ascii orelse return .unknown) + .all_ascii + else + .non_ascii; + } +}; + +pub fn isAllASCII(self: *const Blob) ?bool { + return switch (self.charset) { + .unknown => null, + .all_ascii => true, + .non_ascii => false, + }; +} + +/// Takes ownership of `self` by value. Invalidates `self`. +pub fn takeOwnership(self: *Blob) Blob { + var result = self.*; + self.* = undefined; + result.setNotHeapAllocated(); + return result; +} + +pub fn isHeapAllocated(self: *const Blob) bool { + return self.#ref_count.raw_value != 0; +} + +fn setNotHeapAllocated(self: *Blob) void { + self.#ref_count = .init(0); +} + +pub const external_shared_descriptor = struct { + pub const ref = Blob__ref; + pub const deref = Blob__deref; +}; + +export fn Blob__ref(self: *Blob) void { + bun.assertf(self.isHeapAllocated(), "cannot ref: this Blob is not heap-allocated", .{}); + self.#ref_count.increment(); +} + +export fn Blob__deref(self: *Blob) void { + bun.assertf(self.isHeapAllocated(), "cannot deref: this Blob is not heap-allocated", .{}); + if (self.#ref_count.decrement() == .should_destroy) { + self.deinit(); + } +} + const WriteFilePromise = write_file.WriteFilePromise; const WriteFileWaitFromLockedValueTask = write_file.WriteFileWaitFromLockedValueTask; const NewReadFileHandler = read_file.NewReadFileHandler; diff --git a/src/bun.js/webcore/Body.zig b/src/bun.js/webcore/Body.zig index fc47b6570c..04dbf20f0f 100644 --- a/src/bun.js/webcore/Body.zig +++ b/src/bun.js/webcore/Body.zig @@ -717,7 +717,6 @@ pub const Value = union(Tag) { }, .none, .getBlob => { var blob = Blob.new(new.use()); - blob.allocator = bun.default_allocator; if (headers) |fetch_headers| { if (fetch_headers.fastGet(.ContentType)) |content_type| { var content_slice = content_type.toSlice(bun.default_allocator); @@ -761,7 +760,7 @@ pub const Value = union(Tag) { switch (this.*) { .Blob => { const new_blob = this.Blob; - assert(new_blob.allocator == null); // owned by Body + assert(!new_blob.isHeapAllocated()); // owned by Body this.* = .{ .Used = {} }; return new_blob; }, @@ -1080,7 +1079,7 @@ pub fn extract( body.value = try Value.fromJS(globalThis, value); if (body.value == .Blob) { - assert(body.value.Blob.allocator == null); // owned by Body + assert(!body.value.Blob.isHeapAllocated()); // owned by Body } return body; } @@ -1289,14 +1288,13 @@ pub fn Mixin(comptime Type: type) type { } var blob = Blob.new(value.use()); - blob.allocator = bun.default_allocator; if (blob.content_type.len == 0) { if (this.getFetchHeaders()) |fetch_headers| { if (fetch_headers.fastGet(.ContentType)) |content_type| { - var content_slice = content_type.toSlice(blob.allocator.?); + var content_slice = content_type.toSlice(bun.default_allocator); defer content_slice.deinit(); var allocated = false; - const mimeType = MimeType.init(content_slice.slice(), blob.allocator.?, &allocated); + const mimeType = MimeType.init(content_slice.slice(), bun.default_allocator, &allocated); blob.content_type = mimeType.value; blob.content_type_allocated = allocated; blob.content_type_was_set = true; diff --git a/src/bun.js/webcore/ObjectURLRegistry.zig b/src/bun.js/webcore/ObjectURLRegistry.zig index 82f1dbba5d..c7c99da422 100644 --- a/src/bun.js/webcore/ObjectURLRegistry.zig +++ b/src/bun.js/webcore/ObjectURLRegistry.zig @@ -65,7 +65,6 @@ pub fn resolveAndDupe(this: *ObjectURLRegistry, pathname: []const u8) ?jsc.WebCo pub fn resolveAndDupeToJS(this: *ObjectURLRegistry, pathname: []const u8, globalObject: *jsc.JSGlobalObject) ?jsc.JSValue { var blob = jsc.WebCore.Blob.new(this.resolveAndDupe(pathname) orelse return null); - blob.allocator = bun.default_allocator; return blob.toJS(globalObject); } diff --git a/src/bun.js/webcore/S3Client.zig b/src/bun.js/webcore/S3Client.zig index 5a6a239f5b..414caa1d1a 100644 --- a/src/bun.js/webcore/S3Client.zig +++ b/src/bun.js/webcore/S3Client.zig @@ -136,7 +136,6 @@ pub const S3Client = struct { errdefer path.deinit(); const options = args.nextEat(); var blob = Blob.new(try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class)); - blob.allocator = bun.default_allocator; return blob.toJS(globalThis); } diff --git a/src/bun.js/webcore/S3File.zig b/src/bun.js/webcore/S3File.zig index b8dfbb73a9..945a5c9772 100644 --- a/src/bun.js/webcore/S3File.zig +++ b/src/bun.js/webcore/S3File.zig @@ -343,9 +343,7 @@ fn constructS3FileInternal( path: jsc.Node.PathLike, options: ?jsc.JSValue, ) bun.JSError!*Blob { - var ptr = Blob.new(try constructS3FileInternalStore(globalObject, path, options)); - ptr.allocator = bun.default_allocator; - return ptr; + return Blob.new(try constructS3FileInternalStore(globalObject, path, options)); } pub const S3BlobStatTask = struct { diff --git a/src/bun.js/webcore/blob/read_file.zig b/src/bun.js/webcore/blob/read_file.zig index ece0bcc56d..6dcacad14f 100644 --- a/src/bun.js/webcore/blob/read_file.zig +++ b/src/bun.js/webcore/blob/read_file.zig @@ -10,8 +10,7 @@ pub fn NewReadFileHandler(comptime Function: anytype) type { pub fn run(handler: *@This(), maybe_bytes: ReadFileResultType) void { var promise = handler.promise.swap(); - var blob = handler.context; - blob.allocator = null; + var blob = handler.context.takeOwnership(); const globalThis = handler.globalThis; bun.destroy(handler); switch (maybe_bytes) {