CLAUDE.md@public/files/bun/src · git:20260915.977fced · 2026-09-15 · sha256 c29e62f6116c9c83
CLAUDE.md@public/files/bun/src git:20260915.977fcedA
Immutable. This exact content is served forever at /api/v1/blob/c29e62f6116c9c83.
## Rust
`src/` is a Cargo workspace (rooted at the repo's top-level `Cargo.toml`, ~200
member crates). The runtime is built as `libbun_runtime.a` via `cargo build -p
bun_runtime` (driven by `scripts/build/rust.ts`). Key crates:
- `bun_core` (`src/bun_core/`) — strings, formatting, logging, env vars, allocator/heap helpers, the foundation everything else uses
- `bun_sys` (`src/sys/`) — cross-platform syscall wrappers (`File`, `Fd`, `Dir`, `Error`)
- `bun_paths` (`src/paths/`) — path joining/normalization, the path-buffer pool
- `bun_jsc` (`src/jsc/`) — JSC value types, `Strong`/`Weak`, FFI imports
- `bun_runtime` (`src/runtime/`) — JS-visible APIs (server, fetch, node compat, crypto)
- `bun_js_parser`, `bun_js_printer`, `bun_resolver`, `bun_bundler`, `bun_install`, `bun_collections`, `bun_threading`, `bun_alloc` — the rest of the pipeline
- `bun_runtime::bin_entry` (`src/runtime/bin_entry/`) — the process entry point (`main`) and the
C-ABI symbols that must be direct link inputs; `bun_runtime` itself is the
`staticlib` that `cargo build` produces for the C++ link.
Conventions:
- `cargo check -p <crate>` for fast iteration; `bun bd` builds and links everything.
- Don't `.unwrap()` a fallible path that user input or the OS can hit at runtime — return the error. `.unwrap()` is for invariants you can prove.
- The C ABI / syscall boundary uses `bun_sys::Maybe<T>` (= `Result<T, bun_sys::Error>`); ordinary Rust code uses `Result<T, E>` with `?`.
- Each crate defines its own `Error` enum (a `thiserror::Error` at `<crate>/error.rs`, re-exported as `crate::Error` + `crate::Result`). Errno codes nest via `Sys(#[from] bun_errno::SystemErrno)`; OOM via `Alloc(#[from] bun_alloc::AllocError)`. `bun_sys::Error` is the rich syscall error (errno + syscall tag + path); `From<bun_sys::Error> for bun_errno::SystemErrno` exists for `?`-chaining.
- NEVER add comments to deleted code blocks.
- Do not add comments that reference context from the transcript.
- Avoid adding comments where not necessary.
## Prefer `bun_core` / `bun_sys` over `std`
The `std` equivalents either lose OS error info, allocate where we have pools,
or don't match the cross-platform behavior the runtime needs.
| Instead of | Use |
| --------------------------------------- | ------------------------------------------------------------------------------------ |
| `std::fs::File` | `bun_sys::File` (owns the fd; closes on `Drop`) |
| `std::fs::read` / `write` | `bun_sys::File::read_from` / `File::create` + `write_all` |
| `std::path::Path::join` | `bun_paths::resolve_path::join` / `join_string_buf` |
| `std::path::Path::parent`/`file_name` | `bun_paths::dirname` / `bun_paths::basename` |
| `std::env::var` | `bun_core::env_var::*::get()` (typed + cached) |
| `String::from_utf8` for JS-visible strs | `bun_core::String::clone_utf8` / `borrow_utf8` |
| `&str` operations on byte slices | `bun_core::strings::*` (SIMD-backed `&[u8]` ops) |
| `eprintln!` for debug logging | `bun_core::declare_scope!` + `scoped_log!` |
| `std::process::Command` | `bun_core::util::spawn_sync_inherit` (CLI helpers) or `bun_spawn_sys` (full control) |
| `Box::new` + raw ptr round-trip | `bun_core::heap::{into_raw, take, destroy}` |
## `bun_sys` — System Calls (`src/sys/`)
Syscall wrappers preserve errno via `Maybe<T> = Result<T, bun_sys::Error>`.
```rust
use bun_sys::{File, Fd, O};
let file = File::openat(Fd::cwd(), b"path/to/file", O::RDONLY, 0)?;
let mut buf = vec![0u8; 4096];
let n = file.read_all(&mut buf)?; // loops until EOF or full
// `file` closes on Drop.
```
Key types and functions:
- `Fd` (`bun_core::Fd`, re-exported) — cross-platform file descriptor. `Fd::cwd()`, `Fd::stdin()/stdout()/stderr()`, `fd.close()`.
- `File::open(path: &ZStr, flags, mode)` / `File::openat(dir: Fd, path: &[u8], flags, mode)` / `File::make_open(...)` (creates parent dirs) / `File::create(dir, path, truncate)`
- `file.read(buf)` / `read_all(buf)` / `read_to_end()` / `read_to_end_small()` / `write(buf)` / `write_all(buf)`
- `bun_sys::open`, `read`, `write`, `pread`, `pwrite`, `stat`, `fstat`, `lstat`, `mkdir`, `unlink`, `rename`, `symlink`, `chmod` — free fns over `Fd`
- Open flags: `bun_sys::O::RDONLY`, `O::WRONLY | O::CREAT | O::TRUNC`, etc.
`bun_sys::Error` carries `errno`, `syscall: Tag`, `path: Box<[u8]>`. Convert
to a JS exception via `bun_sys_jsc::ErrorJsc::to_js`:
```rust
use bun_sys_jsc::ErrorJsc;
match File::openat(Fd::cwd(), path, O::RDONLY, 0) {
Ok(f) => f,
Err(err) => return Ok(err.to_js(global)?),
}
// Internally: err.to_system_error().to_error_instance(global)
```
## Strings (`bun_core::String` and `bun_core::strings`)
`bun_core::String` is the FFI-compatible 6-variant tagged union shared with C++
(`BunString` in `BunString.cpp`). It bridges Rust and JSC and can hold a
`WTFStringImpl` (Latin-1 or UTF-16). **Latin-1 is NOT UTF-8** — bytes 128–255
are single chars in Latin-1 but invalid UTF-8 — so converting either direction
requires a real encoder, not a cast.
`String` owns one ref when WTF-backed: `Drop` derefs, `Clone` refs, it is
not `Copy`. Borrow with `&String` (or `StringView<'_>` when a by-value borrow
is needed). In an `extern "C"` signature a by-value `String` means ownership
crosses the boundary (C++ `Bun::toStringRef` return / `transferToWTFString()`
consumer); `&String` ⇔ `const BunString*`.
```rust
use bun_core::{EncodedSlice, String, Utf8Bytes}; // the only import path for all three
let s = String::clone_utf8(utf8_bytes); // copies into a WTFStringImpl
let s = String::borrow_utf8(utf8_bytes); // no copy; caller keeps slice alive
let s = String::static_("literal"); // 'static ASCII slice, never freed
let s = String::from_bytes(bytes); // borrow arbitrary bytes; tags UTF-8 if non-ASCII
s.eq_ascii(b"lit") / s.starts_with_ascii(b"lit") // encoding-aware ASCII compare without transcoding
let utf8: Utf8Bytes<'_> = s.to_utf8(); // borrows `s` (ASCII/UTF-8) or transcodes; for locals
let utf8: Utf8Bytes<'static> = s.into_utf8(); // moves `s`'s ref in / copies; for storing in fields
let utf8: Utf8Bytes<'static> = s.clone().into_utf8(); // from `&String`: shares the WTF ref when 8-bit ASCII, else transcodes
let utf8: Utf8Bytes<'static> = x.to_utf8().into_owned(); // from a borrowed view: always an independent copy
let owned: Vec<u8> = s.to_owned_slice();
```
Rule: a `Utf8Bytes<'static>` field/element must come from an owning producer
(`into_utf8()`, `value.to_utf8(global)?`, `x.to_utf8().into_owned()`,
`Utf8Bytes::Owned(..)`) — never from `to_utf8()` on a `&String`/`StringView`
reached through a `&'static` accessor. Prefer `s.clone().into_utf8()` when
you hold a `&String` (no copy for ASCII); use `.into_owned()` only when the
source is a bare `&[u8]`/`EncodedSlice` view.
`Utf8Bytes<'a>` is `Borrowed(&'a [u8]) | Owned(Vec<u8>) | Shared(String)`
(`Shared` holds an 8-bit all-ASCII WTF-backed `String` and reads its buffer);
it derefs to `[u8]`; `is_owned()` ⇔ the bytes were transcoded/copied.
`Utf8WithString` (`String::into_utf8_with_string[_thread_isolated]()`) keeps the
UTF-8 bytes _and_ the source `String` so the value can go back to JS without
re-encoding; `Utf8WithString::js_only(string)` wraps an output-only string.
`PathLike<'a>` / `StringOrBuffer<'a>` arms: `String`/`ThreadIsolatedString`
(`Utf8WithString` from a JS string), `Utf8(Utf8Bytes<'a>)` (transcoded JS
string, or Rust-side bytes: `PathLike::borrowed(bytes)` lends `&'a [u8]` to a
synchronous call, `PathLike::owned(vec)` when the value must own them),
`Buffer` (`PathLike`: a `PinnedArrayBuffer`, GC-rooted too when parsed for an
async call; `StringOrBuffer`: borrowed for a sync call) and
`StringOrBuffer::PinnedBuffer` (pinned and GC-rooted, parsed for an async
call). Values parsed from JS for an async call, stored, or sent to another thread (the
`from_js_async` parsers, which return `ThreadIsolated<T>`;
`PathLike::thread_isolated_copy` for a `Blob` store) is `'static`.
`EncodedSlice<'a>` is the `{ptr, len}` + encoding-bits (Latin-1/UTF-8/UTF-16)
borrowed view handed to C++. Constructors name the encoding of the bytes:
`utf8(bytes)` for Rust text (`&str`, `format!` output, anything known
UTF-8); `from_bytes(bytes)` for arbitrary bytes (OS paths, env values, user
buffers — scans and tags UTF-8 if non-ASCII); `latin1(bytes)` only for
ASCII literals / `&'static` ASCII tables, bytes already validated as ASCII,
or bytes that really are Latin-1; `utf16(units)`.
`String::to_encoded_slice()` borrows any `String` as one;
`EncodedSlice::to_utf8() -> Utf8Bytes<'a>`; `bun_jsc::EncodedSliceJsc` adds
`to_js`, `to_{,type_,range_,syntax_}error_instance`, `to_json_object`, and
`to_external_value` / `external` (hand a globally-allocated buffer to JSC).
Bytes → JS string: `bun_string_jsc::create_utf8_for_js(global, bytes)?`
(copies; ASCII stays 8-bit). An owned `Vec<u8>` that JS should adopt:
`bun_string_jsc::owned_utf8_into_js(global, vec)?`; an owned `Vec<u16>`:
`bun_string_jsc::owned_utf16_into_js(global, vec)?` (or `owned_latin1_into_js` for a
known-Latin-1/ASCII `Vec<u8>`); all three hand the allocation to JSC in one call. An ASCII literal or
`&'static` ASCII: `String::static_("lit").to_js(global)?`. → `Error` (each
with `type_error`/`range_error`/`syntax_error` siblings, one C++ entry):
`global.create_error_instance(format_args!(..))` (argument-free ASCII
literal → atomized; formatted → copied once), `string.to_error_instance(global)`
(WTF-backed shares the impl, static atomizes, borrowed `EncodedSlice`
copies), `EncodedSlice::utf8(bytes).to_error_instance(global)` for raw UTF-8
bytes (copied). The infallible
`EncodedSlice::…(bytes).to_js(global)` is only for callbacks that cannot
return `JsResult`, or for bytes already validated as ASCII where a rescan
is unwanted (`EncodedSlice::latin1(bytes).to_js(global)`).
JSValue → string: `value.to_bun_string(global)?` (owned `String`),
`value.to_utf8(global)?` (owned UTF-8 `Utf8Bytes<'static>`), or
`value.to_js_string_view(global)?` (borrowed `JSStringView` guard; derefs to
`&String` and keeps the `JSString` cell alive while it is in scope; its
`to_utf8()` is tied to the guard).
To/from JS values, use the `bun_jsc::StringJsc` extension trait:
```rust
use bun_jsc::StringJsc;
let js: JSValue = s.to_js(global)?; // JS takes its own ref; `s` still usable
let js: JSValue = s.into_js(global)?; // hands `s`'s ref to the JSString
let s = bun_core::String::from_js(value, global)?;
```
`bun_core::strings` is the SIMD-backed `&[u8]` toolkit (Google Highway kernels
with runtime CPU dispatch). Byte and substring search **must** go through it —
`str::find`/`contains`/`split*`, `slice::windows`, `memchr::*` and
`bstr::ByteSlice::find*` are denied in `clippy.toml`, and the byte-literal forms
of `<[u8]>::contains`, `iter().position/rposition/any(|b| b == b'x')` and
`.split(|b| ..)` are rejected by `test/internal/source-lints/byte-search.test.ts`:
```rust
use bun_core::strings;
strings::index_of_char_usize(s, b'x') // Option<usize> (not .iter().position())
strings::index_of_any(s, b"\r\n") // Option<usize> first byte in set
strings::last_index_of_char(s, b'x') // Option<usize> (not .iter().rposition())
strings::contains_char(s, b'x') // bool (not .contains(&b'x'))
strings::count_char(s, b'\n') // usize
strings::index_of(haystack, needle) // Option<usize> substring (memmem)
strings::contains(haystack, needle) // bool
strings::split(s, b",") / split_any(s, b" \t") / tokenize(s, b" ") / split_once_char(s, b'=')
strings::eql(a, b) // bool (== / starts_with / ends_with are memcmp and fine as-is)
strings::has_prefix_comptime(s, b"x") // 'static comparand
strings::first_non_ascii(s) // Option<u32>
strings::to_utf16_alloc(...) // encoding conversions
```
## Paths (`bun_paths`)
Path helpers operate on `&[u8]` and are platform-parameterized via the
`Platform` const-generic (`Posix`, `Windows`, `Loose`, `Nt`; `platform::Auto`
picks the host). Never use `std::path` for runtime path logic.
```rust
use bun_paths::{dirname, basename};
use bun_paths::resolve_path::{self, platform};
let dir = dirname(path); // Option<&[u8]>
let name = basename(path); // &[u8]
let joined = resolve_path::join::<platform::Auto>(&[a, b]); // &'static [u8] (threadlocal buf)
let joined = resolve_path::join_string_buf::<platform::Auto>(&mut buf, &[a, b]); // caller buf
let rel = resolve_path::relative(from, to);
```
Use the path-buffer pool to avoid 64 KB stack allocations on Windows
(`PathBuffer` is `[u8; PATH_MAX_BYTES]`, ~64 KB on Windows):
```rust
use bun_paths::path_buffer_pool;
let mut buf = path_buffer_pool::get(); // PoolGuard<PathBuffer>, returns to pool on Drop
let joined = resolve_path::join_string_buf::<platform::Auto>(&mut *buf, &[a, b]);
```
`bun_paths::os_path_buffer_pool` selects the wide (`u16`) variant on Windows
and the narrow (`u8`) variant on POSIX.
## URL Parsing (`bun_url::whatwg`)
WHATWG-compliant, backed by WebKit's URL parser. `Parsed` owns the C++
`WTF::URL` (freed on `Drop`) and derefs to `URL` for the getters; parsing
returns `None` for invalid input. `bun_jsc::url` re-exports both; the
JS-value entry points (`URL::from_js` → `Option<Parsed>`, `URL::href_from_js`)
come from the `bun_jsc::URLJsc` trait.
```rust
use bun_url::whatwg::Parsed;
let url: Parsed = Parsed::from_utf8(href)?; // or Parsed::from_string(&bun_string)?
url.protocol() // bun_core::String
url.pathname() // bun_core::String
url.host() // bun_core::String — the hostname WITHOUT the port (opposite of JS `host`!)
url.hostname() // bun_core::String — the host WITH the port (opposite of JS `hostname`!)
url.port() // u32 (u32::MAX = unset; otherwise u16 range)
```
`bun_url::href_from_string`, `file_url_from_string`, `path_from_file_url`,
`join` do whole-string conversions.
## MIME Types (`bun_http_types::MimeType`)
```rust
use bun_http_types::{MimeType, mime_type};
let mime = mime_type::by_extension(b"html"); // MimeType
let mime = mime_type::by_extension_no_default(b"xyz"); // Option<MimeType>
mime.category // Category::Javascript | Css | Html | Json | Image | Text | Wasm | ...
```
Common constants: `JAVASCRIPT`, `JSON`, `HTML`, `CSS`, `TEXT`, `WASM`, `ICO`, `OTHER`.
## Memory & Allocators
The `#[global_allocator]` is mimalloc (or `std::alloc::System` under
`cfg(bun_asan)`), so plain `Box`/`Vec`/`String` already use it. When pairing
with C/C++ that may free the bytes, route through `bun_alloc::default_alloc`
rather than `mi_*` directly — under ASAN the global allocator is libc's, so a
`mi_free`/`mi_usable_size` on `Box`-owned memory is an allocator mismatch.
OOM handling: do not let a runtime OOM unwind into FFI. Use
`bun_core::handle_oom` (or the `.unwrap_or_oom()` extension) to convert
`Result<T, AllocError>` into a controlled crash:
```rust
use bun_core::{handle_oom, UnwrapOrOom};
let buf = handle_oom(allocator.alloc(size));
let v = vec.try_reserve(n).unwrap_or_oom();
```
Heap round-trips that need to cross FFI use `bun_core::heap`:
```rust
use bun_core::heap;
let raw: *mut T = heap::into_raw(Box::new(value)); // hand ownership to C
let boxed: Box<T> = unsafe { heap::take(raw) }; // reclaim ownership
unsafe { heap::destroy(raw) }; // reclaim + drop in one step
```
**Arena gotcha:** values allocated in `bun_alloc::MimallocArena` (the AST
allocator and similar) do **not** run `Drop` when the arena resets — the
backing pages are bulk-freed. If a type owns a heap allocation, refcount, or
fd, free it explicitly before the arena resets. Don't rely on `Drop` for
correctness in arena-backed code.
## Environment Variables (`bun_core::env_var`)
Typed, cached accessors. Each known env var is a module with a `get()`
returning the right type (`Option<...>` if no default).
```rust
use bun_core::env_var;
env_var::HOME::get() // Option<&[u8]>
env_var::CI::get() // bool (has default)
env_var::BUN_CONFIG_DNS_TIME_TO_LIVE_SECONDS::get() // u64 (has default)
```
## Logging (`bun_core::output`)
Scoped debug logging. Declare a scope once per module; gate with
`BUN_DEBUG_<SCOPE>=1` at runtime; the body dead-strips in release builds.
```rust
bun_core::declare_scope!(my_feature, hidden); // hidden: opt-in via BUN_DEBUG_my_feature=1
// or `visible` to log by default in debug builds
bun_core::scoped_log!(my_feature, "processing {} items", count);
```
User-facing colored output (auto-detects TTY, strips ANSI when piped):
```rust
bun_core::pretty!("<green>success<r>: {}\n", msg);
bun_core::prettyln!("done");
bun_core::pretty_errorln!("<red>error<r>: {}", msg);
```
## Spawning Subprocesses
For simple inherit-stdio CLI helpers:
```rust
use bun_core::util::spawn_sync_inherit;
let status = spawn_sync_inherit(&[b"git", b"status"])?;
```
For full control (pipes, custom env, posix_spawn flags) use `bun_spawn_sys`
(`src/spawn_sys/`). The runtime `Bun.spawn` implementation lives in
`src/runtime/api/bun/{spawn.rs, process.rs, subprocess.rs}` — look there for
the JS-facing path.
## JSC Interop & FFI Safety
These are the patterns that trip people up. Get them wrong and you get
crashes that only reproduce under load or in CI.
### Pointer provenance at FFI boundaries
If a callback may free `self` (close, error, GC finalize), do **not**
materialize `&self`/`&mut self` at the boundary — a `&self`-derived raw
pointer carries `SharedReadOnly` provenance, and `Box::from_raw`/dealloc
through it is UB. Pass and dispatch off `*mut Self` until the body proves
ownership. `src/io/PipeWriter.rs`'s `impl_streaming_writer_parent!` macro
encodes the three modes:
- `borrow = mut` — body forms `&mut *this`; safe when nothing re-enters
- `borrow = shared` — body forms `&*this`; safe when re-entrant code only needs `&Self`
- `borrow = ptr` — body calls `Self::method(this, ..)` with `this: *mut Self`; required when the callback may free `self`
### `Strong` / `Weak` JS handles
`bun_jsc::Strong` keeps a JS value alive; it is `!Send`/`!Sync` and must be
created and dropped on the JS thread.
```rust
use bun_jsc::Strong;
let strong = Strong::create(value, global);
let v: JSValue = strong.get();
// drop(strong) releases the GC handle
```
`bun_jsc::Weak<T>` is the GC-cleared variant. For raw values without a `Strong`
wrapper, `JSValue::protect()` / `unprotect()` and `ensure_still_alive()` are
available, but `Strong` is preferred — it can't be forgotten or unbalanced.
### Refcount transfer on `to_js()` / `create()`
A `to_js()` / `create()` that returns a wrapped pointer **transfers** the
caller's `+1` to the JS wrapper. Do not `ref()` again before the return; the
finalizer derefs once. The leak-or-UAF symptoms of getting this wrong are
distinctive: an extra `ref()` leaks until process exit; a missing `ref()` on a
non-transferring path UAFs at GC.
### Cross-thread string hazards
`StringImpl` refcounts are atomic; two things are per-thread: using a string as
a property key (`Identifier::fromString`) atomizes a non-atom impl _in place_
into the current thread's atom table, and the last `deref()` of an atom removes
it from the _current_ thread's table (`RELEASE_ASSERT(wasRemoved)`). The lazily
computed hash/flags word is also unsynchronized. Rules:
- Handing a value to one other thread (work pool, HTTP thread):
`String::thread_isolated_copy()`, `ThreadIsolated<T>`, or own bytes
(`Box<[u8]>`, `clone_utf8` on arrival).
- Letting several VMs reach one impl (process-global registry, one
`SerializedScriptValue` with many receivers): `String::make_thread_shareable()`
(C++ `Bun::makeThreadShareable` / `threadShareableCopy` /
`toCrossThreadShareable`) once — pre-hashed, never atomized in place, so each
receiver's atom table takes its own copy — then hand out plain `clone()`s.
Static strings already qualify.
Worked examples: `ObjectURLRegistry`, `StandaloneModuleGraph::File`, the
structured-clone object fast paths.
## Common Patterns
```rust
// Read a file, return JS error on failure
let contents = match bun_sys::File::openat(Fd::cwd(), path, O::RDONLY, 0)
.and_then(|f| f.read_to_end())
{
Ok(bytes) => bytes,
Err(err) => return Ok(err.to_js(global)?),
};
// Heap-allocated FFI handle with explicit lifecycle
let raw = bun_core::heap::into_raw(Box::new(MyHandle::new()));
register_with_c(raw);
// ... later, in the matching teardown callback:
unsafe { bun_core::heap::destroy(raw) };
// Hashing
bun_wyhash::hash(bytes) // u64
bun_wyhash::hash_with_seed(seed, bytes)
```