Skip to content
Open
5 changes: 5 additions & 0 deletions src/bun_core/external_shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ impl<T: ExternalSharedDescriptor> ExternalShared<T> {
self.ptr.as_ptr()
}

/// Give up the handle without releasing its ref; the caller now owns that +1.
pub fn into_raw(self) -> *mut T {
core::mem::ManuallyDrop::new(self).ptr.as_ptr()
}

/// # Safety
/// `raw` must be a valid pointer managed by the external refcount.
pub unsafe fn clone_from_raw(raw: *mut T) -> Self {
Expand Down
18 changes: 18 additions & 0 deletions src/bun_core/string/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,17 @@ impl String {
core::ptr::null_mut()
}
}
/// Move the owned `WTF::StringImpl` ref out as a [`WTFString`](crate::WTFString);
/// `None` (and `self` dropped) for the non-WTF tags.
#[inline]
pub fn into_wtf(self) -> Option<crate::WTFString> {
if self.tag != Tag::WTFStringImpl {
return None;
}
// SAFETY: tag checked — a live, non-null `StringImpl` whose +1 we own
// and hand over.
Some(unsafe { crate::WTFString::adopt(self.leak_wtf_impl()) })
}
/// An isolated copy of a WTF-backed impl (+1, `clone()` for other tags),
/// for handing the value to one other thread; not for sharing one impl
/// between VMs.
Expand Down Expand Up @@ -1055,6 +1066,13 @@ impl Drop for String {
self.deref();
}
}
impl From<crate::WTFString> for String {
/// Re-wrap an owned `WTF::StringImpl` ref (no count change).
#[inline]
fn from(wtf: crate::WTFString) -> Self {
Self::adopt_wtf_impl(wtf.into_raw())
}
}
impl Clone for String {
/// +1 on the same `WTF::StringImpl` (bitwise for the non-WTF tags).
#[inline]
Expand Down
32 changes: 32 additions & 0 deletions src/codegen/generate-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2214,6 +2214,38 @@ ${cachedExterns}
let ok = ${symbolName(typeName, "dangerouslySetPtr")}(value, core::ptr::null_mut());
debug_assert!(ok);
}
${
refCounted
? `/// Take the wrapper's \`m_ctx\` back as the reference it held (what
/// its finalizer would otherwise release), leaving the wrapper detached.
/// \`None\` if \`value\` is not a live \`${typeName}\` wrapper.
#[inline] pub fn take_ref(value: JSValue) -> Option<::bun_ptr::RefPtr<${typeName}>> {
let ptr = ${symbolName(typeName, "fromJS")}(value);
if ptr.is_null() {
return None;
}
let ok = ${symbolName(typeName, "dangerouslySetPtr")}(value, core::ptr::null_mut());
debug_assert!(ok);
// SAFETY: \`m_ctx\` carried the wrapper's ref; it was just cleared, so
// that ref is now ours.
Some(unsafe { ::bun_ptr::RefPtr::from_raw(ptr) })
}`
: `/// Take the wrapper's \`m_ctx\` back as the \`Box\` its constructor handed
/// over (what its finalizer would otherwise receive), leaving the wrapper
/// detached. \`None\` if \`value\` is not a live \`${typeName}\` wrapper.
#[inline] pub fn take_ptr(value: JSValue) -> Option<::std::boxed::Box<${typeName}>> {
let ptr = ${symbolName(typeName, "fromJS")}(value);
if ptr.is_null() {
return None;
}
let ok = ${symbolName(typeName, "dangerouslySetPtr")}(value, core::ptr::null_mut());
debug_assert!(ok);
// SAFETY: \`m_ctx\` is the \`heap::into_raw\` allocation the wrapper owned
// (its finalizer reclaims it the same way); it was just cleared, so this
// is the only owner.
Some(unsafe { ::std::boxed::Box::from_raw(ptr) })
}`
}
${gcAccessors}
}`;

Expand Down
185 changes: 175 additions & 10 deletions src/codegen/generate-jssink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1124,9 +1124,45 @@ use bun_jsc::{self, host_fn, CallFrame, JSGlobalObject, JSValue};
#[allow(dead_code, unreachable_pub, unused)]
pub use ${rustPath} as ${name};

/// The live \`m_sinkPtr\` of the \`JS${name}\` wrapper \`value\` encodes, or \`None\`
/// if it is not one or is detached. Frame-scoped like \`JSValue::as_class_this_ptr\`:
/// \`value\` keeps the payload alive while it is on the stack.
#[allow(dead_code, unreachable_pub, unused)]
pub fn ${name}__fromJSThis(value: JSValue) -> Option<bun_ptr::ThisPtr<${name}>> {
// SAFETY: \`from_js\` returns the wrapper's live, non-null payload (\`JSSink<T>\`
// is \`repr(transparent)\` over \`T\`).
${JSSinkT}::from_js(value).map(|p| unsafe { bun_ptr::ThisPtr::new(p.cast::<${name}>()) })
}

`;

const hostFns = ["construct", "write", "end", "flush", "start"] as const;
// `callframe.this()` → the wrapper's `m_sinkPtr` as `&mut JSSink<T>`, or the
// detached / wrong-type error. Unbounded `'a`: the sink lives in its own heap
// allocation behind the wrapper; host fns are single-threaded and synchronous,
// so this is the only `&mut` for the body of the call.
templ += `#[allow(dead_code, unreachable_pub, unused, non_snake_case)]
fn ${name}__getThis<'a>(
global: &JSGlobalObject,
callframe: &CallFrame,
) -> bun_jsc::JsResult<&'a mut ${JSSinkT}> {
let ptr = ${JSSinkT}::this_ptr_from_frame(global, callframe)?;
// SAFETY: \`${name}__fromJS\` returned the wrapper's live, non-null \`m_sinkPtr\`.
Ok(unsafe { &mut *ptr.as_ptr() })
}

`;

symbols.push(`${name}__construct`);
templ += `bun_jsc::jsc_host_abi! {
#[allow(dead_code, unreachable_pub, unused)]
#[unsafe(no_mangle)]
pub unsafe fn ${name}__construct(global: &JSGlobalObject, callframe: &CallFrame) -> JSValue {
host_fn::host_fn_static(global, callframe, ${JSSinkT}::js_construct)
}
}

`;
const hostFns = ["write", "end", "flush", "start"] as const;
for (const fn of hostFns) {
const sym = `${name}__${fn}`;
symbols.push(sym);
Expand All @@ -1135,7 +1171,7 @@ pub use ${rustPath} as ${name};
#[allow(dead_code, unreachable_pub, unused)]
#[unsafe(no_mangle)]
pub unsafe fn ${sym}(global: &JSGlobalObject, callframe: &CallFrame) -> JSValue {
host_fn::host_fn_static(global, callframe, ${JSSinkT}::js_${fn})
host_fn::host_fn_static(global, callframe, |g, c| ${JSSinkT}::js_${fn}(g, c, ${name}__getThis))
}
}

Expand Down Expand Up @@ -1172,8 +1208,15 @@ pub extern "C" fn ${name}__memoryCost(this: &${name}) -> usize {
templ += `#[allow(dead_code, unreachable_pub, unused)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ${name}__finalize(this: *mut ${name}) {
// SAFETY: C++ hands over its live \`m_sinkPtr\` once and never uses it again.
${JSSinkT}::js_finalize(unsafe { bun_ptr::ThisPtr::new(this) })
use crate::webcore::sink::{FinalizeReceiver, JsSinkType};
match <${name} as JsSinkType>::FINALIZE {
// SAFETY: C++ hands over its live \`m_sinkPtr\` once and never uses it again.
FinalizeReceiver::ThisPtr => ${JSSinkT}::js_finalize(unsafe { bun_ptr::ThisPtr::new(this) }),
// SAFETY: as above; the sink's owner keeps it alive across the call.
FinalizeReceiver::Mut => <${name} as JsSinkType>::finalize_mut(unsafe { &mut *this }),
// SAFETY: the wrapper's \`m_sinkPtr\` is the Box \`construct\` leaked for it.
FinalizeReceiver::Box => <${name} as JsSinkType>::finalize_boxed(unsafe { Box::from_raw(this) }),
}
}

`;
Expand All @@ -1184,8 +1227,15 @@ pub unsafe extern "C" fn ${name}__finalize(this: *mut ${name}) {
templ += `#[allow(dead_code, unreachable_pub, unused)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ${name}__controllerFinalize(this: *mut ${name}) {
// SAFETY: as \`__finalize\`: the controller's live \`m_sinkPtr\`, handed over once.
${JSSinkT}::js_controller_finalize(unsafe { bun_ptr::ThisPtr::new(this) })
use crate::webcore::sink::{FinalizeReceiver, JsSinkType};
match <${name} as JsSinkType>::FINALIZE {
// SAFETY: as \`__finalize\`: the controller's live \`m_sinkPtr\`, handed over once.
FinalizeReceiver::ThisPtr => ${JSSinkT}::js_controller_finalize(unsafe { bun_ptr::ThisPtr::new(this) }),
// SAFETY: as \`__finalize\`.
FinalizeReceiver::Mut => <${name} as JsSinkType>::finalize_mut(unsafe { &mut *this }),
// SAFETY: as \`__finalize\`.
FinalizeReceiver::Box => <${name} as JsSinkType>::finalize_boxed(unsafe { Box::from_raw(this) }),
}
}

`;
Expand All @@ -1203,14 +1253,26 @@ pub extern "C" fn ${name}__controllerDetached(this: &mut ${name}, controller: JS
`;

// ZIG_DECL JSC::EncodedJSValue ${name}__close(JSC::JSGlobalObject*, void* sinkPtr, JSC::EncodedJSValue reason)
// C++ caller null-checks `ptr` before calling. `*mut`: a failing close can
// re-enter the sink (see `JsSinkType::close_with_error`).
// C++ caller null-checks `ptr` before calling. `reason` is the empty value
// for a clean close, otherwise the failed source's reason. A failing close
// gets a `ThisPtr` (it can re-enter and free the sink, see
// `JsSinkType::close_with_error`); a clean one the usual `&mut`.
symbols.push(`${name}__close`);
templ += `#[allow(dead_code, unreachable_pub, unused)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ${name}__close(global: &JSGlobalObject, this: *mut ${name}, reason: JSValue) -> JSValue {
// SAFETY: C++ passes its live, null-checked \`m_sinkPtr\`.
unsafe { ${JSSinkT}::js_close(global, this, reason) }
// SAFETY: C++ passes its live, null-checked \`m_sinkPtr\`; this borrow ends
// before the sink can be re-entered.
if let Some(v) = ${JSSinkT}::js_close_pending_error(global, unsafe { &mut *this }) {
return v;
}
if !reason.is_empty() && <${name} as crate::webcore::sink::JsSinkType>::CLOSES_WITH_ERROR {
// SAFETY: as above.
${JSSinkT}::js_close_with_error(global, unsafe { bun_ptr::ThisPtr::new(this) }, reason)
} else {
// SAFETY: as above; a clean \`end\` does not free the sink.
${JSSinkT}::js_close(global, unsafe { &mut *this })
}
}

`;
Expand Down Expand Up @@ -1241,6 +1303,109 @@ pub extern "C" fn ${name}__updateRef(this: &mut ${name}, value: bool) {
`;
}

// ── Bun__NativeTransformSink__writeBytes ──────────────────────────────────
// Route a borrowed byte chunk from a native transform (`JSTransformStream`
// with `m_nativeSinkPtr` attached) into the concrete sink via
// `SinkHandle::write`. `sink_id` is `WebCore::SinkID` (src/jsc/bindings/Sink.h);
// `sink_ptr` is the wrapper's `m_sinkPtr` (`*mut JSSink<T>`, which is
// `repr(transparent)` over `T`).
const sinkIds: Record<string, number> = {
ArrayBufferSink: 0,
FileSink: 2,
HTMLRewriterSink: 3,
HTTPResponseSink: 4,
HTTPSResponseSink: 5,
NetworkSink: 6,
FetchRequestBodySink: 7,
};
// `SinkHandle` variant + `BackRef` constructor per sink.
const sinkHandles: Record<string, [string, string]> = {
ArrayBufferSink: ["ArrayBuffer", "from_raw_mut"],
FileSink: ["FileSink", "from_raw"],
HTMLRewriterSink: ["HTMLRewriter", "from_raw"],
HTTPResponseSink: ["HttpResponse", "from_raw_mut"],
HTTPSResponseSink: ["HttpsResponse", "from_raw_mut"],
NetworkSink: ["S3Upload", "from_raw_mut"],
FetchRequestBodySink: ["FetchRequestBody", "from_raw_mut"],
};
symbols.push("Bun__NativeTransformSink__writeBytes");
templ += `/// Map a C++ \`WebCore::SinkID\` + erased \`m_sinkPtr\` to a \`SinkHandle\`.
///
/// # Safety
/// \`ptr\` must be a live, properly-aligned pointer to the concrete sink type
/// that \`id\` names (the same pointer the per-sink thunks receive), valid for
/// the lifetime of the returned handle.
#[allow(dead_code, unreachable_pub, unused)]
pub unsafe fn sink_handle_from_id(
id: u8,
ptr: ::core::ptr::NonNull<::core::ffi::c_void>,
) -> crate::webcore::SinkHandle {
use crate::webcore::SinkHandle;
let raw = ptr.as_ptr();
// SAFETY: caller contract.
unsafe {
match id {
${classes
.map(
name =>
` ${sinkIds[name]} => SinkHandle::${sinkHandles[name][0]}(bun_ptr::BackRef::${sinkHandles[name][1]}(raw.cast::<${name}>())),`,
)
.join("\n")}
// 1 (TextSink) and any unknown id → no native sink.
_ => SinkHandle::None,
}
}
}

/// See \`crate::webcore::sink::native_transform_sink_write\`.
#[allow(dead_code, unreachable_pub, unused)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn Bun__NativeTransformSink__writeBytes(
sink_id: u8,
sink_ptr: *mut ::core::ffi::c_void,
global: &JSGlobalObject,
ptr: *const u8,
len: usize,
) -> JSValue {
let Some(sink_ptr) = ::core::ptr::NonNull::new(sink_ptr) else {
return JSValue::js_number(0.0);
};
// SAFETY: C++ passes the live \`m_sinkPtr\` of the type \`sink_id\` names, valid
// for the duration of this synchronous call.
let handle = unsafe { sink_handle_from_id(sink_id, sink_ptr) };
if ptr.is_null() {
return JSValue::js_number(0.0);
}
// SAFETY: C++ passes \`len\` live readable bytes at non-null \`ptr\` (a GC-kept
// \`JSArrayBufferView\` or a caller-owned scratch buffer).
let bytes = unsafe { ::bun_core::ffi::slice(ptr, len) };
crate::webcore::sink::native_transform_sink_write(handle, global, bytes)
}

`;

// ── Bun__onSinkDestroyed ──────────────────────────────────────────────────
// The wrapper/controller's \`onDestroy\` tagged pointer (see
// \`crate::webcore::sink::DestructorPtr\`) decoded back to its pointee.
symbols.push("Bun__onSinkDestroyed");
templ += `#[allow(dead_code, unreachable_pub, unused)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn Bun__onSinkDestroyed(
ptr_value: *mut ::core::ffi::c_void,
_sink_ptr: *mut ::core::ffi::c_void,
) {
use crate::webcore::sink::{DestructorPtr, Destructor};
match Destructor::decode(DestructorPtr::from(Some(ptr_value))) {
Destructor::None | Destructor::Detached => {}
// SAFETY: C++ round-trips the \`onDestroy\` value \`destructor_ptr_subprocess\`
// encoded from a live \`Subprocess\` that outlives its stdin sink wrapper.
Destructor::Subprocess(subprocess) => unsafe { &mut *subprocess }.on_stdin_destroyed(),
Destructor::Unknown => ::bun_core::debug_warn!("Unknown sink type"),
}
}

`;

templ += `// sinks: ${classes.length}, exported symbols: ${symbols.length}\n`;
return { src: templ, symbols };
}
Expand Down
9 changes: 9 additions & 0 deletions src/collections/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,15 @@ where
}
}

/// [`get`](Self::get) that only reuses a pooled node (never allocates).
pub fn try_get() -> Option<PoolGuard<'static, T>> {
Some(PoolGuard {
node: Self::get_if_exists()?,
release: Self::release,
_marker: PhantomData,
})
}

/// Return a node to the pool's free list (or free it if the pool is full).
///
/// Takes a raw `*mut Node<T>`, not `&mut Node<T>`: when the pool is already
Expand Down
27 changes: 27 additions & 0 deletions src/event_loop/ConcurrentTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,33 @@ pub trait Taskable {
unsafe fn release_unrun(this: *mut Self);
}

/// [`Taskable`] for a type that is only ever queued as a leaked `Box<Self>`
/// (`heap::into_raw` / `Box::into_raw` at every post site, `Box::from_raw` in
/// its `bun_runtime::dispatch` arm). Released unrun by reclaiming the box and
/// handing it to `|this| $release` (default: drop it).
///
/// ```ignore
/// bun_event_loop::boxed_taskable!(ShellGlobTask, ShellGlobTask, |this| this.task.unref_unrun());
/// ```
#[macro_export]
macro_rules! boxed_taskable {
($ty:ty, $tag:ident) => {
$crate::boxed_taskable!($ty, $tag, |this| ());
};
($ty:ty, $tag:ident, |$this:ident| $release:expr) => {
impl $crate::Taskable for $ty {
const TAG: $crate::TaskTag = $crate::task_tag::$tag;
unsafe fn release_unrun(this: *mut Self) {
// SAFETY: `release_unrun` contract — `this` is the queued
// `Task::ptr` under `TAG`, which for this type is a leaked `Box<Self>`.
#[allow(unused_mut)]
let mut $this: ::std::boxed::Box<Self> = unsafe { ::bun_core::heap::take(this) };
$release;
}
}
};
}

impl TaskTag {
/// The tag's identifier, for diagnostics.
pub fn name(self) -> &'static str {
Expand Down
Loading
Loading