Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
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
40 changes: 4 additions & 36 deletions src/jsc/CallFrame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use core::ffi::{c_uint, c_void};

use crate::virtual_machine::VirtualMachine;
use crate::{JSGlobalObject, JSValue};
use bun_collections::IntegerBitSet;
#[cfg(debug_assertions)]
use bun_core::ZStr;

Expand Down Expand Up @@ -229,6 +228,10 @@ pub struct CallerSrcLoc {
/// This is an advanced iterator struct which is used by various APIs. In
/// Node.fs, `will_be_async` is set to true which allows string/path APIs to
/// know if they have to do threadsafe clones.
///
/// It never roots anything: while the host call runs, the arguments are kept
/// alive by the caller's frame; whatever must outlive the call takes its own
/// hold (`to_thread_safe`).
pub struct ArgumentsSlice<'a> {
/// Backing storage for the remaining-args view. Both [`Self::init`] and
/// [`Self::init_async`] borrow — `all: &'a [JSValue]` already ties this
Expand All @@ -241,7 +244,6 @@ pub struct ArgumentsSlice<'a> {
remaining_start: usize,
pub vm: &'a VirtualMachine,
pub all: &'a [JSValue],
pub(crate) protected: IntegerBitSet<32>,
pub will_be_async: bool,
}

Expand All @@ -252,40 +254,12 @@ impl<'a> ArgumentsSlice<'a> {
&self.remaining_buf[self.remaining_start..]
}

pub(crate) fn unprotect(&mut self) {
let mut iter = self.protected.iterator::<true, true>();
while let Some(i) = iter.next() {
self.all[i].unprotect();
}
self.protected = IntegerBitSet::<32>::init_empty();
}

pub fn protect_eat(&mut self) {
if self.remaining().is_empty() {
return;
}
// `remaining_buf.len() == all.len()` for both init variants, so
// `all.len() - remaining().len()` reduces to `remaining_start`.
let index = self.all.len() - self.remaining().len();
self.protected.set(index);
self.all[index].protect();
self.eat();
}

pub fn protect_eat_next(&mut self) -> Option<JSValue> {
if self.remaining().is_empty() {
return None;
}
self.next_eat()
}

pub fn init(vm: &'a VirtualMachine, slice: &'a [JSValue]) -> ArgumentsSlice<'a> {
ArgumentsSlice {
remaining_buf: Cow::Borrowed(slice),
remaining_start: 0,
vm,
all: slice,
protected: IntegerBitSet::<32>::init_empty(),
will_be_async: false,
}
}
Expand Down Expand Up @@ -314,12 +288,6 @@ impl<'a> ArgumentsSlice<'a> {
}
}

impl<'a> Drop for ArgumentsSlice<'a> {
fn drop(&mut self) {
self.unprotect();
}
}

// `CallFrame`/`VM`/`JSGlobalObject` are opaque `UnsafeCell`-backed ZST handles;
// `&T` is ABI-identical to non-null `*const T`. Out-params are exclusive `&mut`
// to plain `#[repr(C)]` PODs. `describeFrame` returns a raw C string that the
Expand Down
7 changes: 7 additions & 0 deletions src/jsc/VmHandle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,13 @@ impl VmHandle {
self.0.state() == State::Open
}

/// Is the calling thread this VM's thread? Any thread (a pointer compare
/// against the thread-local VM; nothing is dereferenced).
#[inline]
pub fn is_current_thread(&self) -> bool {
VirtualMachine::get_or_null() == Some(self.0.hot.vm)
}
Comment thread
dylan-conway marked this conversation as resolved.
Outdated

pub(crate) fn tickets_outstanding(&self) -> u32 {
self.0.tickets.load(Ordering::SeqCst)
}
Expand Down
117 changes: 117 additions & 0 deletions src/jsc/array_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,123 @@ unsafe impl bun_ptr::ExternalSharedDescriptor for JSCArrayBuffer {
}
}

unsafe extern "C" {
safe fn JSC__JSValue__retainPinnedArrayBuffer(
value: JSValue,
out_ptr: &mut *const u8,
out_len: &mut usize,
) -> *mut JSCArrayBuffer;
safe fn JSC__ArrayBuffer__releasePinned(self_: &JSCArrayBuffer);
}

/// The byte range of a JS `ArrayBuffer` or view, kept alive and in place by
/// one ref + one pin held directly on its `JSC::ArrayBuffer` — the refcounted
/// owner of the storage, not a GC cell. The JS object may be collected while
/// this lives, and releasing touches no `JSCell`, so it may drop inside a GC
/// finalizer.
///
/// That refcount is not atomic and the owning VM's collector also touches it,
/// so the release must happen on that VM's thread. `slice()` is fine from
/// anywhere; a `Drop` anywhere else (another VM's thread letting go of a
/// shared `Blob` store, a pool thread) is posted back to the owning VM.
pub struct PinnedArrayBuffer {
owner: ptr::NonNull<JSCArrayBuffer>,
ptr: *const u8,
len: usize,
vm: crate::VmHandle,
}

impl PinnedArrayBuffer {
/// JS thread. `None` if `value` is not a buffer/view or is detached. A
/// view that has no `ArrayBuffer` yet gets one (see
/// `retainPinnedArrayBuffer`).
pub fn retain(value: JSValue) -> Option<Self> {
let mut ptr: *const u8 = ptr::null();
let mut len = 0usize;
let owner = ptr::NonNull::new(JSC__JSValue__retainPinnedArrayBuffer(
value, &mut ptr, &mut len,
))?;
Some(Self {
owner,
ptr,
len,
vm: crate::virtual_machine::VirtualMachine::get().handle(),
})
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

#[inline]
pub fn slice(&self) -> &[u8] {
if self.len == 0 {
return &[];
}
// SAFETY: `ptr[..len]` lies inside the storage `owner` keeps allocated
// (ref) and undetachable (pin) until `Drop`.
unsafe { core::slice::from_raw_parts(self.ptr, self.len) }
}
}

impl Drop for PinnedArrayBuffer {
fn drop(&mut self) {
if self.vm.is_current_thread() {
JSC__ArrayBuffer__releasePinned(JSCArrayBuffer::opaque_ref(self.owner.as_ptr()));
} else {
release_on_owning_thread(self.owner, &self.vm);
}
}
}

/// Hand a ref taken on `vm`'s thread back to that thread to release. If the
/// VM has already closed, its heap went with it and the `ArrayBuffer` (whose
/// wrapper `Weak` lived in that heap) must not be touched again: it is left
/// unreleased.
#[cold]
fn release_on_owning_thread(owner: ptr::NonNull<JSCArrayBuffer>, vm: &crate::VmHandle) {
use bun_event_loop::ConcurrentTask::ConcurrentTask;
use bun_event_loop::ManagedTask::ManagedTask;

struct Release {
owner: ptr::NonNull<JSCArrayBuffer>,
vm: crate::VmHandle,
}
impl Drop for Release {
// Runs on the owning thread (task run, or freed unrun while that VM
// drains) — or, if the post was refused, right here: then do nothing.
fn drop(&mut self) {
if self.vm.is_current_thread() {
JSC__ArrayBuffer__releasePinned(JSCArrayBuffer::opaque_ref(self.owner.as_ptr()));
}
}
}
fn run(this: *mut Release) -> bun_event_loop::JsResult<()> {
// SAFETY: `this` is the box handed to `new_owned` below; `run` is its
// only consumer on this path (`ManagedTask::run` does not free `ctx`).
drop(unsafe { bun_core::heap::take(this) });
Ok(())
}

let ctx = bun_core::heap::into_raw(Box::new(Release {
owner,
vm: vm.clone(),
}));
let task = ConcurrentTask::create(ManagedTask::new_owned(ctx, run));
match vm.post(crate::LoopKind::Regular, task) {
crate::Posted::Queued => {
bun_core::scoped_log!(
ArrayBuffer,
"pinned ArrayBuffer released off its VM's thread: posted back"
);
}
crate::Posted::Refused(task) => {
bun_core::scoped_log!(
ArrayBuffer,
"pinned ArrayBuffer outlived its VM: left unreleased"
);
// SAFETY: refused ⇒ not queued anywhere; ours to free.
unsafe { ConcurrentTask::release_refused(task) };
}
}
}

impl JSCArrayBuffer {
pub fn as_array_buffer(&mut self) -> ArrayBuffer {
let mut out = core::mem::MaybeUninit::<ArrayBuffer>::uninit();
Expand Down
43 changes: 43 additions & 0 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3548,6 +3548,49 @@ CPP_DECL void JSC__JSValue__unpinArrayBuffer(JSC::EncodedJSValue v)
buf->unpin();
}

// Hold the backing store itself rather than the JS object: one ref + one pin on
// the `JSC::ArrayBuffer` (refcounted, not a GC cell). The wrapper may then be
// collected while the bytes stay allocated and in place, and releasing touches
// no JSCell — so a native holder with no root to lean on, destroyed from a GC
// finalizer (a Blob store) or at an async op's completion, can release inline.
// This VM's thread only: the refcount is not atomic (the Rust holder,
// `PinnedArrayBuffer`, posts an off-thread release back here).
//
// Unlike `pinStorage`, a bufferless view is given its ArrayBuffer here
// (OversizeTypedArray: adopted in place, no byte copy) because there is no
// caller root keeping the view alive. `out_ptr`/`out_len` are the view's byte
// range, read after that so they point into the storage the ArrayBuffer owns.
CPP_DECL JSC::ArrayBuffer* JSC__JSValue__retainPinnedArrayBuffer(JSC::EncodedJSValue v, const uint8_t** out_ptr, size_t* out_len)
{
auto value = JSC::JSValue::decode(v);
JSC::ArrayBuffer* buf = nullptr;
if (auto* jb = dynamicDowncast<JSC::JSArrayBuffer>(value)) {
buf = jb->impl();
if (!buf || buf->isDetached())
return nullptr;
*out_ptr = static_cast<const uint8_t*>(buf->data());
*out_len = buf->byteLength();
} else if (auto* view = dynamicDowncast<JSC::JSArrayBufferView>(value); view && !view->isDetached()) {
buf = view->possiblySharedBuffer();
if (!buf)
return nullptr;
*out_ptr = static_cast<const uint8_t*>(view->vector());
*out_len = view->byteLength();
} else {
return nullptr;
}
buf->ref();
if (!buf->isShared())
buf->pin();
return buf;
}
CPP_DECL void JSC__ArrayBuffer__releasePinned(JSC::ArrayBuffer* buf)
{
if (!buf->isShared())
buf->unpin();
buf->deref();
}

// Borrow `v`'s byte storage for off-thread reading. Splits out only the
// `FastTypedArray` case from `pinArrayBuffer`, because that's the one mode
// where `possiblySharedBuffer()` actually COPIES data
Expand Down
2 changes: 1 addition & 1 deletion src/jsc/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ pub use self::js_value::{
// and is wired into `event_loop::tick` directly at link time. No fn-pointer
// hook is re-exported from the crate root.
pub use self::array_buffer::{
ArrayBuffer, BinaryType, JSCArrayBuffer, MarkedArrayBuffer, TypedArrayType,
ArrayBuffer, BinaryType, JSCArrayBuffer, MarkedArrayBuffer, PinnedArrayBuffer, TypedArrayType,
};
pub use self::console_object as ConsoleObject;
pub use self::console_object::Formatter;
Expand Down
Loading
Loading