Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
9 changes: 9 additions & 0 deletions src/bun_alloc/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@ impl StdAllocator {
// no longer leaks on grow.
pub use mimalloc_arena::MimallocArena;
pub type Arena = MimallocArena;

/// The process allocator as a `'static` [`Arena`] ([`MimallocArena::borrowing_default`]):
/// for long-lived `Transpiler<'static>`-style holders whose arena must be
/// genuinely `'static`. Allocations made through it are ordinary global-heap
/// allocations and are only reclaimed by their own destructors.
pub fn default_arena() -> &'static Arena {
static DEFAULT_ARENA: std::sync::OnceLock<MimallocArena> = std::sync::OnceLock::new();
DEFAULT_ARENA.get_or_init(MimallocArena::borrowing_default)
}
mod baby_vec;
pub use baby_vec::BabyVec;
/// Arena-backed `Vec` with `u32` length/capacity.
Expand Down
2 changes: 1 addition & 1 deletion src/bundler/ParseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2591,7 +2591,7 @@ pub mod parse_worker {
// TYPE_ONLY mirrors of `bake.Framework`. Project the fields the parser
// reads into the parser-side mirror and bump-alloc
// so `opts` can borrow it.
opts.framework = topts.framework.map(|f| {
opts.framework = topts.framework.as_deref().map(|f| {
// `Framework` is bump-allocated below, so `Drop` never runs — use arena-owned slices.
let projected = js_parser::options::Framework {
is_built_in_react: f.is_built_in_react,
Expand Down
51 changes: 37 additions & 14 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3229,6 +3229,7 @@ pub mod bv2_impl {
&mut self,
files: &bake_types::EntryPointList,
css_data: &mut ArrayHashMap<Index, CssEntryPointMeta>,
resolve_failures: &mut Vec<EntryPointResolveFailure>,
) -> Result<(), Error> {
self.enqueue_entry_points_common()?;
debug_assert!(self.dev_server.is_some());
Expand Down Expand Up @@ -3299,22 +3300,25 @@ pub mod bv2_impl {
let mut resolved = match unsafe { &mut *transpiler }.resolve_entry_point(abs_path) {
Ok(r) => r,
Err(err) => {
let dev = self.dev_server.expect("unreachable");
dev.handle_parse_task_failure(
// The dev server handles these once `start_from_bake_dev_server`
// returns; handling them needs `&mut self`, which the dispatch
// handle must not alias.
// SAFETY: `transpiler.log` is the `'a`-owned log; sole writer here.
let log = unsafe { &mut *(*transpiler).log };
let mut taken = bun_ast::Log::init();
taken.level = log.level;
taken.clone_line_text = log.clone_line_text;
core::mem::swap(&mut taken, log);
resolve_failures.push(EntryPointResolveFailure {
err,
if flags.client() {
graph: if flags.client() {
bake::Graph::Client
} else {
bake::Graph::Server
},
abs_path,
// SAFETY: `transpiler` points at one of self's transpilers, live for `'a`.
unsafe { (*transpiler).log }.cast_const(),
std::ptr::from_mut(self),
)
.expect("oom");
// SAFETY: `transpiler.log` is the `'a`-owned log; sole writer here.
unsafe { (*(*transpiler).log).reset() };
abs_path: Box::from(&**abs_path),
log: taken,
});
continue;
}
};
Expand Down Expand Up @@ -5466,22 +5470,30 @@ pub mod bv2_impl {
}

/// Dev Server uses this instead to run a subset of the transpiler, and to run it asynchronously.
/// Entry points that failed to resolve are returned for the dev server
/// to record (`DevServer::handle_parse_task_failure`, which needs this
/// `BundleV2` mutably) rather than reported through the dispatch handle.
pub fn start_from_bake_dev_server(
&mut self,
bake_entry_points: &bake_types::EntryPointList,
) -> Result<DevServerInput, Error> {
) -> Result<(DevServerInput, Vec<EntryPointResolveFailure>), Error> {
self.unique_key = generate_unique_key();

/* arena: help_catch_memory_issues — no-op (mimalloc TLH check) */

let mut ctx = DevServerInput {
css_entry_points: ArrayHashMap::new(),
};
self.enqueue_entry_points_dev_server(bake_entry_points, &mut ctx.css_entry_points)?;
let mut resolve_failures = Vec::new();
self.enqueue_entry_points_dev_server(
bake_entry_points,
&mut ctx.css_entry_points,
&mut resolve_failures,
)?;

/* arena: help_catch_memory_issues — no-op (mimalloc TLH check) */

Ok(ctx)
Ok((ctx, resolve_failures))
}

// The body has deep DevServer field access (current_bundle.start_data,
Expand Down Expand Up @@ -7985,6 +7997,17 @@ pub mod bv2_impl {
pub(crate) css_entry_points: ArrayHashMap<Index, CssEntryPointMeta>,
}

/// A dev-server entry point that failed to resolve during
/// `start_from_bake_dev_server`.
pub struct EntryPointResolveFailure {
pub err: Error,
pub graph: bake_types::Graph,
pub abs_path: Box<[u8]>,
/// The resolver's log messages for this failure (taken from the
/// transpiler log, which is left empty).
pub log: bun_ast::Log,
}

/// The lifetime of this structure is tied to the bundler's arena
pub struct DevServerOutput<'a> {
pub chunks: &'a mut [Chunk],
Expand Down
11 changes: 11 additions & 0 deletions src/bundler/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,17 @@ bun_dispatch::link_interface! {
fn register_barrel_export(barrel_path: &[u8], alias: &[u8]);
}
}
impl DevServerHandle {
/// The handle for the dev server `owner` points at. The handle is another
/// back-reference: whoever stores it takes on `owner`'s holder obligation
/// (must be dropped before the pointee).
#[inline]
pub fn from_owner<T: DevServerHandleOwner>(owner: bun_ptr::BackRef<T, bun_ptr::Root>) -> Self {
// SAFETY: `BackRef` invariant — the pointee is live while the handle's
// holder (bound by the same obligation) uses it.
unsafe { Self::of(owner.this_ptr().as_ptr()) }
}
}
// SAFETY: the handle is `{ kind, owner: *mut () }`; the raw pointer is what
// defeats the auto-impl. `owner` is the single per-process `bake::DevServer`
// (established at `unsafe fn new()`), which outlives every bundler worker
Expand Down
4 changes: 2 additions & 2 deletions src/bundler/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1353,7 +1353,7 @@ pub struct BundleOptions<'a> {
// directly — all access goes through crate::dispatch::DevServerVTable.
pub dev_server: *const (),
/// Set when Bake is bundling. Affects module resolution.
pub framework: Option<&'a crate::bake_types::Framework>,
pub framework: Option<std::sync::Arc<crate::bake_types::Framework>>,

pub serve_plugins: Option<Box<[Box<[u8]>]>>,
pub bunfig_path: Box<[u8]>,
Expand Down Expand Up @@ -1543,7 +1543,7 @@ impl<'a> BundleOptions<'a> {
metafile_json_path: self.metafile_json_path.clone(),
metafile_markdown_path: self.metafile_markdown_path.clone(),
dev_server: self.dev_server,
framework: self.framework,
framework: self.framework.clone(),
serve_plugins: self.serve_plugins.clone(),
bunfig_path: self.bunfig_path.clone(),
unwrap_commonjs_packages: self.unwrap_commonjs_packages,
Expand Down
2 changes: 1 addition & 1 deletion src/bundler/transpiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1130,7 +1130,7 @@ fn resolver_bundle_options_subset(
},
external: src.external.clone(),
extra_cjs_extensions: src.extra_cjs_extensions.clone(),
framework: src.framework.map(|f| {
framework: src.framework.as_deref().map(|f| {
// Bundler-local `bake_types::BuiltInModule` and
// `bun_options_types::BuiltInModule` are nominally distinct (the
// former predates the TYPE_ONLY move-down); convert variant-wise.
Expand Down
7 changes: 7 additions & 0 deletions src/collections/zig_hash_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,13 @@ impl<'a, K, V, C> IntoIterator for &'a mut HashMap<K, V, C> {
self.iter_mut()
}
}
impl<K, V, C> IntoIterator for HashMap<K, V, C> {
type Item = (K, V);
type IntoIter = core::iter::Flatten<std::vec::IntoIter<Option<(K, V)>>>;
fn into_iter(self) -> Self::IntoIter {
self.slots.into_iter().flatten()
}
}

// ─── Entry API ─────────────────────────────────────────────────────────────

Expand Down
19 changes: 19 additions & 0 deletions src/http_jsc/headers_jsc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,25 @@ pub fn to_fetch_headers(
)
}

/// [`to_fetch_headers`], owning the result.
pub fn to_fetch_headers_ref(
this: &Headers,
global: &JSGlobalObject,
) -> JsResult<bun_jsc::fetch_headers::HeadersRef> {
use bun_http_types::ETag::HeaderEntryColumns;
if this.entries.len() == 0 {
return Ok(bun_jsc::fetch_headers::HeadersRef::create_empty());
}
bun_jsc::fetch_headers::HeadersRef::create(
global,
this.entries.items_name(),
this.entries.items_value(),
// `from_bytes` scans for non-ASCII and tags UTF-8; `init` would leave
// the buffer Latin-1 and mojibake any UTF-8 header value bytes ≥0x80.
&EncodedSlice::from_bytes(this.buf.as_slice()),
)
}

struct H2TestingAPIs;

impl H2TestingAPIs {
Expand Down
21 changes: 21 additions & 0 deletions src/jsc/FetchHeaders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,27 @@ impl HeadersRef {
Self(FetchHeaders::create_empty())
}

/// [`FetchHeaders::create`] from parallel name/value columns into `buf`,
/// owning the fresh allocation.
#[inline]
pub fn create(
global: &JSGlobalObject,
names: &[StringPointer],
values: &[StringPointer],
buf: &EncodedSlice,
) -> JsResult<Self> {
assert_eq!(names.len(), values.len());
// C++ only reads the columns.
FetchHeaders::create(
global,
names.as_ptr().cast_mut(),
values.as_ptr().cast_mut(),
buf,
names.len() as u32,
)
.map(Self)
}

/// `FetchHeaders.createFromUWS(req)` — fresh C++ allocation, refcount 1.
#[inline]
pub fn create_from_uws(uws_request: *mut c_void) -> Self {
Expand Down
22 changes: 22 additions & 0 deletions src/jsc/VmHandle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,28 @@ impl VmHandle {
})
}

/// Queue an owned `T` on the VM's `kind` loop (dispatched under `T::TAG`,
/// whose arm reclaims the box), or hand it back if the VM is closed.
pub fn post_boxed<T: bun_event_loop::Taskable>(
&self,
kind: LoopKind,
task: Box<T>,
) -> Result<(), Box<T>> {
let raw: *mut T = bun_core::heap::into_raw(task);
let ct = ConcurrentTaskItem::create(bun_event_loop::Task::init(raw));
match self.post(kind, ct) {
Posted::Queued => Ok(()),
Posted::Refused(ct) => {
// SAFETY: refused ⇒ nothing else holds the carrier `create` boxed
// or the payload we boxed into it two lines up.
unsafe {
drop(bun_core::heap::take(ct.as_ptr()));
Err(bun_core::heap::take(raw))
}
}
}
}

/// Queue a C++ `EventLoopTask` on the VM's `kind` loop from another
/// thread (WebCore's `postTaskTo` / `postTaskConcurrently`), or delete it
/// unrun if the VM is closed.
Expand Down
12 changes: 9 additions & 3 deletions src/jsc/bindings/InspectorBunFrontendDevServerAgent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,18 @@ void InspectorBunFrontendDevServerAgent__notifyClientDisconnected(InspectorBunFr
agent->clientDisconnected(devServerId, connectionId);
}

void InspectorBunFrontendDevServerAgent__notifyBundleStart(InspectorBunFrontendDevServerAgent* agent, int devServerId, BunString* triggerFiles, size_t triggerFilesLen)
// `bun_core::ffi::FfiSlice<BunString>` (borrowed).
struct BunStringSlice {
const BunString* ptr;
size_t len;
};

void InspectorBunFrontendDevServerAgent__notifyBundleStart(InspectorBunFrontendDevServerAgent* agent, int devServerId, BunStringSlice triggerFiles)
{
// Create a JSON array for the triggerFiles
Ref<JSON::ArrayOf<String>> files = JSON::ArrayOf<String>::create();
for (size_t i = 0; i < triggerFilesLen; i++) {
files->addItem(triggerFiles[i].transferToWTFString());
for (size_t i = 0; i < triggerFiles.len; i++) {
files->addItem(triggerFiles.ptr[i].toWTFString());
}

agent->bundleStart(devServerId, WTF::move(files));
Expand Down
84 changes: 84 additions & 0 deletions src/ptr/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,90 @@ unsafe impl<T: ?Sized + Sync, P> Send for BackRef<T, P> {}
// sound.
unsafe impl<T: ?Sized + Sync, P> Sync for BackRef<T, P> {}

// ─────────────────────────────────────────────────────────────────────────────
// ThreadBound<T> — a back-reference other threads may carry but not follow.
// ─────────────────────────────────────────────────────────────────────────────

/// A [`BackRef`] to a thread-affine `T` that any thread may hold, copy and
/// pass along, but that only the thread it was created on can dereference:
/// [`get`](Self::get) panics everywhere else.
///
/// This is the typed way to hand a JS-thread object's address to machinery
/// that runs on another thread (a watcher thread's registration, a task it
/// queues back) purely so that it can be handed back to the owning thread.
/// Liveness is the usual `BackRef` holder obligation, stated where the value
/// is constructed; thread affinity is enforced here, which is what makes the
/// `Send`/`Sync` impls below hold for every `T`.
pub struct ThreadBound<T: ?Sized> {
ptr: BackRef<T>,
/// `std`'s id (a process-unique counter, never reused) rather than the OS
/// thread id, so a later thread cannot pass for the owner.
owner: std::thread::ThreadId,
}

impl<T: ?Sized> ThreadBound<T> {
/// Bind `r`'s address to the calling thread.
#[inline]
pub fn new(r: &T) -> Self {
ThreadBound {
ptr: BackRef::new(r),
owner: std::thread::current().id(),
}
}

/// Whether the calling thread is the one this reference was created on.
#[inline]
pub fn is_owner_thread(&self) -> bool {
std::thread::current().id() == self.owner
}

/// Borrow the pointee. Panics when called from any thread other than the
/// one that created this reference.
#[inline]
#[track_caller]
pub fn get(&self) -> &T {
assert!(
self.is_owner_thread(),
"ThreadBound: dereferenced on a thread other than its owner"
);
self.ptr.get()
}

/// The pointee's address, for identity comparisons on any thread.
#[inline]
pub fn addr(&self) -> usize {
self.ptr.as_const_ptr().cast::<()>() as usize
}
}

impl<T: ?Sized> Clone for ThreadBound<T> {
#[inline]
fn clone(&self) -> Self {
ThreadBound {
ptr: self.ptr,
owner: self.owner,
}
}
}

impl<T: ?Sized> core::fmt::Debug for ThreadBound<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ThreadBound")
.field("ptr", &self.ptr)
.field("owner", &self.owner)
.finish()
}
}

// SAFETY: the pointee is reachable only through `get`, which refuses every
// thread but the one the reference was created on (where a `&T` already
// existed), so moving the handle to another thread exposes no `T` state there.
unsafe impl<T: ?Sized> Send for ThreadBound<T> {}
// SAFETY: as above — a shared `&ThreadBound<T>` on a foreign thread yields
// nothing but the address; on the owner thread it yields aliased `&T`s, which
// is ordinary same-thread sharing.
unsafe impl<T: ?Sized> Sync for ThreadBound<T> {}

// ─────────────────────────────────────────────────────────────────────────────
// DetachablePtr<T> — scoped `&mut T` parked behind `&self` for re-entrant reads.
//
Expand Down
10 changes: 9 additions & 1 deletion src/runtime/bake/BakeSourceProvider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,15 @@ extern "C" JSC::EncodedJSValue BakeLoadServerHmrPatch(GlobalObject* global, BunS
return JSC::JSValue::encode(result);
}

extern "C" JSC::EncodedJSValue BakeLoadServerHmrPatchWithSourceMap(GlobalObject* global, BunString source, const char* sourceMapJSONPtr, size_t sourceMapJSONLength) {
// `bun_core::ffi::FfiSlice<u8>`; ownership of the buffer transfers to the provider.
struct BakeSourceMapJSON {
const char* ptr;
size_t len;
};

extern "C" JSC::EncodedJSValue BakeLoadServerHmrPatchWithSourceMap(GlobalObject* global, BunString source, BakeSourceMapJSON sourceMapJSON) {
const char* sourceMapJSONPtr = sourceMapJSON.ptr;
size_t sourceMapJSONLength = sourceMapJSON.len;
JSC::VM&vm = global->vm();
auto scope = DECLARE_THROW_SCOPE(vm);

Expand Down
Loading
Loading