Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 69 additions & 6 deletions src/jsc/webcore_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,19 +190,36 @@ const _: () = {
safe fn __bun_blob_from_build_artifact(value: JSValue) -> Option<*mut Blob>;
}

// `JSS3File` (src/jsc/bindings/JSS3File.cpp) is the `JSBlob` subclass whose
// prototype carries `presign` / `stat` / `bucket`. Like `Blob__create` it
// adopts `blob` as `m_ctx`; the `JSBlob` finalizer releases it.
crate::jsc_abi_extern! {
fn BUN__createJSS3FileUnsafely(
global: &JSGlobalObject,
blob: *mut core::ffi::c_void,
) -> JSValue;
}

impl JsClass for Blob {
fn from_js(value: JSValue) -> Option<*mut Self> {
JSBlob::from_js(value).or_else(|| __bun_blob_from_build_artifact(value))
}
fn from_js_direct(value: JSValue) -> Option<*mut Self> {
JSBlob::from_js_direct(value)
}
/// The Rust-side way to wrap a `Blob` (the JS constructors and the
/// `Blob__dupe` / `Blob__fromBytes*` exports instead return the
/// [`Blob::new`] pointer for C++ to wrap): heap-promotes `self` and gives
/// the allocation to the wrapper, which owns it from then on
/// ([`Blob::finalize`]). S3-backed blobs get the `JSS3File` subclass.
fn to_js(self, global: &JSGlobalObject) -> JSValue {
// Heap-promote and hand
// ownership to the codegen wrapper. The S3File fast-path (different
// JS wrapper) is layered on by `bun_runtime`'s `BlobExt::to_js` for
// S3-backed blobs; lower-tier callers never construct S3 blobs.
let is_s3 = self.is_s3();
let ptr = Blob::new(self);
if is_s3 {
// SAFETY: `ptr` is the allocation `Blob::new` just returned and
// nothing else holds it; the wrapper becomes its sole owner.
return unsafe { BUN__createJSS3FileUnsafely(global, ptr.cast()) };
}
Comment thread
claude[bot] marked this conversation as resolved.
JSBlob::to_js(ptr, global)
}
fn get_constructor(global: &JSGlobalObject) -> JSValue {
Expand All @@ -212,10 +229,15 @@ const _: () = {
};

impl Blob {
/// Heap-promote and mark as
/// heap-allocated so `deinit` knows to free the heap box.
/// Heap-promote and mark as heap-allocated so `deinit` knows to free the
/// heap box. Every JS wrapper, whether created from Rust ([`JsClass::to_js`])
/// or from C++ (the constructors, `Blob__dupe`, `Blob__fromBytes*`), adopts
/// a pointer minted here and reports `reported_estimated_size` to the GC
/// from then on, so this is where the size is computed: only the store,
/// content type and name set before this call are counted.
#[inline]
pub fn new(mut blob: Blob) -> *mut Blob {
blob.calculate_estimated_byte_size();
blob.ref_count = bun_ptr::RawRefCount::init(1);
bun_core::heap::into_raw(Box::new(blob))
Comment thread
claude[bot] marked this conversation as resolved.
}
Expand Down Expand Up @@ -451,6 +473,47 @@ impl Blob {
}
}

/// Compute `reported_estimated_size`: the in-memory footprint (not the size
/// on disk) that [`Self::estimated_size`] reports. [`Blob::new`] calls this
/// for every heap blob; a type that embeds a `Blob` by value and reports it
/// as part of its own size calls it directly. The GC reads the result from
/// its marking threads once the blob is reachable, which is why it is a
/// value cached up front rather than a walk of the store at report time.
pub fn calculate_estimated_byte_size(&self) {
let mut size = core::mem::size_of::<Blob>();

if let Some(store) = self.store() {
size += core::mem::size_of::<Store>();
match &store.data {
store::Data::Bytes(bytes) => {
size += bytes.stored_name.len();
size += if self.size.get() != MAX_SIZE {
self.size.get() as usize
} else {
bytes.len() as usize
};
}
store::Data::File(file) => size += file.pathlike.estimated_size(),
store::Data::S3(s3) => size += s3.estimated_size(),
}
}

let content_type = self.content_type.get();
if content_type.is_owned() {
size += content_type.as_slice().len();
}
self.reported_estimated_size
.set(size + self.name.get().byte_slice().len());
}

/// Body of the generated `Blob__estimatedSize` thunk (`estimatedSize: true`
/// in response.classes.ts): what `Blob__create` reports when it allocates
/// the wrapper and what `JSBlob::visitChildren` re-reports on every GC.
#[inline]
pub fn estimated_size(&self) -> usize {
self.reported_estimated_size.get()
}

/// Tear down owned resources; if
/// heap-allocated, also frees the heap box.
pub fn deinit(&mut self) {
Expand Down
16 changes: 4 additions & 12 deletions src/runtime/api/Archive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use bun_glob as glob;
use bun_jsc::{
self as jsc, CallFrame, JSGlobalObject, JSMap, JSPromise, JSPromiseStrong, JSValue, JsResult,
};
use bun_jsc::{StringJsc as _, SysErrorJsc as _};
use bun_jsc::{JsClass as _, StringJsc as _, SysErrorJsc as _};
use bun_libarchive as libarchive;
use bun_sys::{self, Fd, FdDirExt as _, FdExt as _, Mode};

Expand Down Expand Up @@ -865,10 +865,7 @@ impl TaskContext for BlobContext {
// self.result already replaced with Uncompressed above — ownership transferred
Ok(PromiseResult::Resolve(match self.output_type {
BlobOutputType::Blob => {
let blob_ptr =
Blob::new(Blob::create_with_bytes_and_allocator(data, global, false));
// SAFETY: blob_ptr is the heap allocation just produced by Blob::new.
unsafe { (*blob_ptr).to_js(global) }
Blob::create_with_bytes_and_allocator(data, global, false).to_js(global)
}
BlobOutputType::Bytes => {
// Ownership transfers to JSC's `MarkedArrayBuffer_deallocator`.
Expand All @@ -881,9 +878,7 @@ impl TaskContext for BlobContext {
// The clone bumps the refcount; ownership of
// the new ref transfers into the Blob via init_with_store.
let store = self.store.clone();
let blob_ptr = Blob::new(Blob::init_with_store(store, global));
// SAFETY: blob_ptr is the heap allocation just produced by Blob::new.
PromiseResult::Resolve(unsafe { (*blob_ptr).to_js(global) })
PromiseResult::Resolve(Blob::init_with_store(store, global).to_js(global))
}
BlobOutputType::Bytes => {
// On allocation failure, reject the promise instead of aborting.
Expand Down Expand Up @@ -1173,10 +1168,7 @@ impl TaskContext for FilesContext {

for entry in entries.iter_mut() {
let data = core::mem::take(&mut entry.data); // Ownership transferred
let blob_ptr =
Blob::new(Blob::create_with_bytes_and_allocator(data, global, false));
// SAFETY: blob_ptr is the heap allocation just produced by Blob::new.
let blob = unsafe { &mut *blob_ptr };
let blob = Blob::create_with_bytes_and_allocator(data, global, false);
blob.is_jsdom_file.set(true);
blob.name.set(bun_core::String::clone_utf8(&entry.path));
blob.last_modified.set((entry.mtime * 1000) as f64);
Expand Down
18 changes: 8 additions & 10 deletions src/runtime/api/BunObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1971,7 +1971,8 @@ fn get_is_standalone_executable(global_this: &JSGlobalObject, _: &JSObject) -> J
}

fn get_embedded_files(global_this: &JSGlobalObject, _: &JSObject) -> JsResult<JSValue> {
use crate::webcore::blob::{Blob, BlobExt as _};
use crate::webcore::blob::Blob;
use bun_jsc::JsClass as _;
use bun_standalone_graph::{File as GraphFile, Graph as StandaloneModuleGraph};
// SAFETY: bun_vm() returns the live thread-local VM for a Bun-owned global.
let vm = global_this.bun_vm();
Expand Down Expand Up @@ -2022,11 +2023,9 @@ fn get_embedded_files(global_this: &JSGlobalObject, _: &JSObject) -> JsResult<JS
// as the blob name, preserving any subdirectory from the asset template.
let input_blob: &mut Blob = file.file_blob(global_this);
// We call .dupe() on this to ensure that we don't return a blob that might get freed later.
let blob = Blob::new(input_blob.dupe_with_content_type(true));
// SAFETY: `Blob::new` returned a fresh heap allocation.
unsafe { (*blob).name.set(input_blob.name.get().dupe_ref()) };
// SAFETY: `blob` is heap-allocated and lives until JS owns it via to_js.
array.put_index(global_this, i as u32, unsafe { (*blob).to_js(global_this) })?;
let blob = input_blob.dupe_with_content_type(true);
blob.name.set(input_blob.name.get().dupe_ref());
array.put_index(global_this, i as u32, blob.to_js(global_this))?;
}

Ok(array)
Expand Down Expand Up @@ -2990,7 +2989,8 @@ mod stdio_stores {
use super::*;
use crate::node::types::PathOrFileDescriptor;
use crate::webcore::blob::store::{Data, File as FileStore};
use crate::webcore::blob::{Blob, BlobExt as _, Store, StoreRef};
use crate::webcore::blob::{Blob, Store, StoreRef};
use bun_jsc::JsClass as _;

thread_local! {
static STDIN: core::cell::RefCell<Option<StoreRef>> = const { core::cell::RefCell::new(None) };
Expand Down Expand Up @@ -3036,9 +3036,7 @@ mod stdio_stores {
// store.ref() — extra +1 for the new Blob.
s.as_ref().unwrap().clone()
});
let blob = Blob::new(Blob::init_with_store(store, global_this));
// SAFETY: `Blob::new` heap-allocates; the JS wrapper takes ownership.
unsafe { (&*blob).to_js(global_this) }
Blob::init_with_store(store, global_this).to_js(global_this)
}

pub(super) fn stdin(global_this: &JSGlobalObject) -> JSValue {
Expand Down
6 changes: 1 addition & 5 deletions src/runtime/image/Image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1854,11 +1854,7 @@ impl PipelineTask {
format.mime().as_bytes(),
));
blob.content_type_was_set.set(true);
// UFCS to pick the consuming `JsClass::to_js(self, _)`
// (heap-promotes via `Blob::new`) over the inherent
// `Blob::to_js(&mut self, _)` that expects an
// already-heap-allocated receiver.
promise.resolve(global, <Blob as bun_jsc::JsClass>::to_js(blob, global))?;
promise.resolve(global, blob.to_js(global))?;
}
tag @ (Deliver::Base64 | Deliver::DataUrl) => {
// This arm copies the bytes out — re-arm `Encoded::drop` so
Expand Down
Loading