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
64 changes: 29 additions & 35 deletions src/bundler/transpiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -858,8 +858,17 @@ pub enum AlreadyBundled {
None,
SourceCode,
SourceCodeCjs,
Bytecode(Box<[u8]>),
BytecodeCjs(Box<[u8]>),
Bytecode(BytecodeFiles),
BytecodeCjs(BytecodeFiles),
}

/// The files behind a `// @bun @bytecode` module. JSC keeps both for the
/// module's lifetime and decodes the bytecode lazily, so they are mapped.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub struct BytecodeFiles {
/// The `<module>.jsc` sidecar, never empty.
pub bytecode: bun_sys::MappedFile,
/// The module text; `None` when it could not be mapped.
pub source: Option<bun_sys::MappedFile>,
}

impl AlreadyBundled {
Expand All @@ -869,10 +878,12 @@ impl AlreadyBundled {
AlreadyBundled::SourceCodeCjs | AlreadyBundled::BytecodeCjs(_)
)
}
pub fn into_bytecode(self) -> Box<[u8]> {
pub fn into_bytecode(self) -> Option<BytecodeFiles> {
match self {
AlreadyBundled::Bytecode(bytes) | AlreadyBundled::BytecodeCjs(bytes) => bytes,
_ => Box::default(),
AlreadyBundled::Bytecode(files) | AlreadyBundled::BytecodeCjs(files) => Some(files),
AlreadyBundled::None | AlreadyBundled::SourceCode | AlreadyBundled::SourceCodeCjs => {
None
}
}
}
}
Expand Down Expand Up @@ -1730,11 +1741,6 @@ impl<'a> Transpiler<'a> {
js_ast::AlreadyBundled::BunCjs => AlreadyBundled::SourceCodeCjs,
js_ast::AlreadyBundled::BytecodeCjs
| js_ast::AlreadyBundled::Bytecode => 'brk: {
// When the parser
// saw `// @bun @bytecode`, attempt to load the
// sidecar `<path>.jsc` cached bytecode. Only
// fall back to re-parsing source on read
// failure / empty file.
let is_cjs =
matches!(already_bundled, js_ast::AlreadyBundled::BytecodeCjs);
let default_value = if is_cjs {
Expand All @@ -1745,39 +1751,27 @@ impl<'a> Transpiler<'a> {
if this_parse.virtual_source.is_none()
&& this_parse.allow_bytecode_cache
{
// No shared const for the bytecode extension
// in `bun_core` yet, so inline the literal.
const BYTECODE_EXT: &[u8] = b".jsc";
let mut path_buf2 = bun_paths::PathBuffer::uninit();
let mut sidecar_path = bun_paths::PathBuffer::uninit();
let n = path.text.len();
let total = n + BYTECODE_EXT.len();
// `ZStr::from_buf` needs `buf[total] == 0`
// in-bounds; fall back to re-parsing the
// source instead of panicking on an
// over-long path.
if total >= path_buf2.len() {
if total > sidecar_path.len() {
break 'brk default_value;
}
path_buf2[..n].copy_from_slice(path.text);
path_buf2[n..][..BYTECODE_EXT.len()]
.copy_from_slice(BYTECODE_EXT);
path_buf2[total] = 0;
let zpath = bun_core::ZStr::from_buf(&path_buf2[..], total);
// `bun.sys.File.toSourceAt(...)` is
// `read_from` + wrap-in-`bun_ast::Source`.
// We only need `.contents`, so call
// `read_from` directly.
sidecar_path[..n].copy_from_slice(path.text);
sidecar_path[n..total].copy_from_slice(BYTECODE_EXT);
let dir = dirname_fd.unwrap_valid().unwrap_or_else(FD::cwd);
match bun_sys::File::read_from(dir, zpath) {
Ok(contents) if !contents.is_empty() => {
match bun_sys::MappedFile::open(dir, &sidecar_path[..total]) {
Ok(bytecode) if !bytecode.is_empty() => {
let files = BytecodeFiles {
bytecode,
source: bun_sys::MappedFile::map(dir, path.text)
.ok(),
};
break 'brk if is_cjs {
AlreadyBundled::BytecodeCjs(
contents.into_boxed_slice(),
)
AlreadyBundled::BytecodeCjs(files)
} else {
AlreadyBundled::Bytecode(
contents.into_boxed_slice(),
)
AlreadyBundled::Bytecode(files)
};
}
_ => {}
Expand Down
2 changes: 1 addition & 1 deletion src/jsc/Errorable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ impl<T> Errorable<T> {
}
}

bun_core::assert_ffi_layout!(Errorable<crate::ResolvedSource>, 144, 8);
bun_core::assert_ffi_layout!(Errorable<crate::ResolvedSource>, 152, 8);
bun_core::assert_ffi_layout!(Errorable<bun_core::String>, 32, 8);

impl<T> Drop for Errorable<T> {
Expand Down
111 changes: 88 additions & 23 deletions src/jsc/ResolvedSource.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use bun_bundler::analyze_transpiled_module::ModuleInfoDeserialized;
use core::ffi::c_void;

use bun_bundler::analyze_transpiled_module::ModuleInfoDeserialized;
use bun_bundler::transpiler::AlreadyBundled;
use bun_core::String as BunString;
use bun_sys::MappedFile;

use crate::JSValue;
// The tag type lives in `crate::resolved_source_tag` so it can be kept in lock-step with
Expand Down Expand Up @@ -44,15 +47,71 @@ pub struct ResolvedSource {
pub bytecode_origin_path: BunString,
}

/// `ResolvedSource.bytecode_cache`: C++ sees `{ uint8_t* ptr; size_t len; bool owned; }`.
/// When `owned`, `ptr` is a `heap::into_raw(Box<[u8]>)` freed on drop (or by
/// the C++ consumer once it `std::exchange`s the pointer out); otherwise it is
/// borrowed from the standalone module graph or the compile cache.
impl ResolvedSource {
/// The fields of a `// @bun` module (the caller adds the URL and tag), or
/// `None` for an ordinary one. `source_text` is copied unless the module
/// text was mapped along with its bytecode.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub fn from_already_bundled(
already_bundled: AlreadyBundled,
source_text: &[u8],
) -> Option<Self> {
if matches!(already_bundled, AlreadyBundled::None) {
return None;
}
let is_commonjs_module = already_bundled.is_common_js();
let (source_code, bytecode_cache) = match already_bundled.into_bytecode() {
Some(files) => (
files
.source
.and_then(mapped_source_text)
.unwrap_or_else(|| BunString::clone_latin1(source_text)),
Bytecode::mapped(files.bytecode),
),
None => (BunString::clone_latin1(source_text), Bytecode::default()),
};
Some(Self {
source_code,
is_commonjs_module,
already_bundled: true,
bytecode_cache,
..Default::default()
})
}
}

/// A WTF string over the mapped text; `None` when it exceeds the WTF length limit.
fn mapped_source_text(text: MappedFile) -> Option<BunString> {
if text.as_slice().len() > BunString::max_length() {
return None;
}
let file = bun_core::heap::into_raw(Box::new(text));
// SAFETY: `file` is freed only by the string created below, after this
// borrow ends.
let bytes = unsafe { &*file }.as_slice();
Some(BunString::create_external(
bytes,
true,
file,
destroy_mapped_source_text,
))
}

/// `WTF::ExternalStringImpl` free function for [`mapped_source_text`].
extern "C" fn destroy_mapped_source_text(file: *mut MappedFile, _bytes: *mut c_void, _len: usize) {
// SAFETY: `file` is the `into_raw` pointer `mapped_source_text` registered;
// WTF calls this exactly once.
unsafe { bun_core::heap::destroy(file) };
}

/// `ResolvedSource.bytecode_cache`: C++ sees `{ uint8_t* ptr; size_t len; void* file; bool persistent; }`.
/// `file` is the `Box<MappedFile>` owning `ptr[..len]` (a `.jsc` sidecar), or
/// null when the bytes are borrowed from the standalone module graph or the
/// compile cache.
Comment thread
robobun marked this conversation as resolved.
Resolved
#[repr(C)]
pub struct Bytecode {
ptr: *mut u8,
len: usize,
owned: bool,
file: *mut c_void,
/// The bytes outlive every VM (executable section, retired compile-cache blob), so JSC may alias them instead of copying.
persistent: bool,
}
Expand All @@ -62,7 +121,7 @@ impl Default for Bytecode {
Self {
ptr: core::ptr::null_mut(),
len: 0,
owned: false,
file: core::ptr::null_mut(),
persistent: false,
}
}
Expand All @@ -76,7 +135,7 @@ impl Bytecode {
Self {
ptr: bytes.as_ptr().cast_mut(),
len: bytes.len(),
owned: false,
file: core::ptr::null_mut(),
persistent: false,
}
}
Expand All @@ -88,35 +147,41 @@ impl Bytecode {
..Self::borrowed(bytes)
}
}
pub fn owned(bytes: Box<[u8]>) -> Self {
if bytes.is_empty() {
pub fn mapped(file: MappedFile) -> Self {
if file.is_empty() {
return Self::default();
}
let len = bytes.len();
let file = bun_core::heap::into_raw(Box::new(file));
// SAFETY: `file` is destroyed only through the returned value, after
// this borrow ends.
let bytes = unsafe { &*file }.as_slice();
Self {
ptr: bun_core::heap::into_raw(bytes).cast::<u8>(),
len,
owned: true,
ptr: bytes.as_ptr().cast_mut(),
len: bytes.len(),
file: file.cast::<c_void>(),
persistent: false,
}
}
}

impl Drop for Bytecode {
fn drop(&mut self) {
if self.owned && !self.ptr.is_null() {
ResolvedSource__freeBytecode(self.ptr);
if !self.file.is_null() {
// SAFETY: this `Bytecode` is the only owner of `file`.
unsafe { ResolvedSource__destroyBytecodeFile(self.file) };
}
}
}

/// `~ErrorableResolvedSource` / `JSC::CachedBytecode` destructor for an owned
/// `Bytecode` (see above).
/// `~ErrorableResolvedSource` / `JSC::CachedBytecode` destructor for a
/// [`Bytecode::mapped`] file.
///
/// # Safety
/// `file` is a `Bytecode::file` that nothing else will destroy or read through.
Comment thread
robobun marked this conversation as resolved.
Resolved
#[unsafe(no_mangle)]
extern "C" fn ResolvedSource__freeBytecode(bytecode: *mut u8) {
// SAFETY: only called with the `heap::into_raw(Box<[u8]>)` base pointer;
// mimalloc recovers the size from the heap.
unsafe { bun_alloc::default_alloc::free(bytecode.cast()) };
unsafe extern "C" fn ResolvedSource__destroyBytecodeFile(file: *mut c_void) {
// SAFETY: per the contract above.
unsafe { bun_core::heap::destroy(file.cast::<MappedFile>()) };
}

bun_core::assert_ffi_layout!(ResolvedSource, 136, 8);
bun_core::assert_ffi_layout!(ResolvedSource, 144, 8);
17 changes: 6 additions & 11 deletions src/jsc/RuntimeTranspilerStore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use bun_ast::{ASTMemoryAllocator, ExportsKind};
use bun_ast::{ImportRecord, ImportRecordFlags};
use bun_bundler::analyze_transpiled_module;
use bun_bundler::options::ModuleType;
use bun_bundler::transpiler::{self as transpiler, AlreadyBundled, ParseOptions, Transpiler};
use bun_bundler::transpiler::{self as transpiler, ParseOptions, Transpiler};
use bun_collections::HiveArrayFallback;
use bun_core::{MutableString, String, strings};
use bun_event_loop::{TaskTag, Taskable, task_tag};
Expand Down Expand Up @@ -976,18 +976,13 @@ impl TranspilerJob {
return;
}

if !matches!(parse_result.already_bundled, AlreadyBundled::None) {
let already_bundled = core::mem::take(&mut parse_result.already_bundled);
let is_commonjs_module = already_bundled.is_common_js();
let bytecode_cache =
crate::resolved_source::Bytecode::owned(already_bundled.into_bytecode());
if let Some(already_bundled) = ResolvedSource::from_already_bundled(
core::mem::take(&mut parse_result.already_bundled),
&parse_result.source.contents,
) {
self.resolved_source = ResolvedSource {
source_code: String::clone_latin1(&parse_result.source.contents),
already_bundled: true,
bytecode_cache,
is_commonjs_module,
tag: this_tag,
..Default::default()
..already_bundled
};
self.resolved_source.source_code.ensure_hash();
return;
Expand Down
15 changes: 9 additions & 6 deletions src/jsc/bindings/ZigSourceProvider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,15 @@ Ref<SourceProvider> SourceProvider::create(
const auto getProvider = [&]() -> Ref<SourceProvider> {
auto origin = getSourceOrigin();
if (resolvedSource.bytecode_cache != nullptr) {
const auto destructorOwned = [](const void* ptr) {
ResolvedSource__freeBytecode(static_cast<uint8_t*>(const_cast<void*>(ptr)));
};
// Borrowed from the standalone module graph / compile cache.
const auto destructorNoOp = [](const void*) {};
Ref<JSC::CachedBytecode> bytecode = JSC::CachedBytecode::create(std::span<uint8_t>(std::exchange(resolvedSource.bytecode_cache, nullptr), resolvedSource.bytecode_cache_size), resolvedSource.bytecode_cache_owned ? destructorOwned : destructorNoOp, {});
std::span<uint8_t> bytes(std::exchange(resolvedSource.bytecode_cache, nullptr), resolvedSource.bytecode_cache_size);
// No file means the bytes are borrowed from the standalone module graph / compile cache.
JSC::CachePayload::Destructor destructor;
if (void* file = std::exchange(resolvedSource.bytecode_cache_file, nullptr)) {
destructor = [file](const void*) {
ResolvedSource__destroyBytecodeFile(file);
};
}
Ref<JSC::CachedBytecode> bytecode = JSC::CachedBytecode::create(bytes, WTF::move(destructor), {});
if (resolvedSource.bytecode_cache_persistent)
bytecode->setPayloadIsPersistent();
auto provider = adoptRef(*new SourceProvider(
Expand Down
16 changes: 8 additions & 8 deletions src/jsc/bindings/headers-handwritten.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,11 @@ typedef struct ResolvedSource {
uint32_t tag;
bool already_bundled;
// -- Bytecode cache fields --
// Owned (`ResolvedSource__freeBytecode`) iff `bytecode_cache_owned`; otherwise
// borrowed from the standalone module graph / compile cache.
// Owned by bytecode_cache_file (`ResolvedSource__destroyBytecodeFile`) when it is
// non-null; otherwise borrowed from the standalone module graph / compile cache.
Comment thread
robobun marked this conversation as resolved.
Resolved
uint8_t* bytecode_cache;
size_t bytecode_cache_size;
bool bytecode_cache_owned;
void* bytecode_cache_file;
// The bytes outlive every VM (executable section / retired compile-cache blob): JSC may alias them.
bool bytecode_cache_persistent;
// Owned; Zig::SourceProvider takes it (nulling the field).
Expand All @@ -132,14 +132,14 @@ typedef struct ResolvedSource {
// Converted to file:// URL. If empty, origin is derived from source_url.
BunString bytecode_origin_path;
} ResolvedSource;
static_assert(sizeof(ResolvedSource) == 136, "ResolvedSource layout is mirrored in src/jsc/ResolvedSource.rs");
static_assert(sizeof(ResolvedSource) == 144, "ResolvedSource layout is mirrored in src/jsc/ResolvedSource.rs");
inline constexpr uint32_t ResolvedSourceTagPackageJSONTypeModule = 1;
typedef union ErrorableResolvedSourceResult {
ResolvedSource value;
JSC::EncodedJSValue err;
} ErrorableResolvedSourceResult;
extern "C" void zig__ModuleInfoDeserialized__deinit(bun_ModuleInfoDeserialized* info);
extern "C" void ResolvedSource__freeBytecode(uint8_t* bytecode);
extern "C" void ResolvedSource__destroyBytecodeFile(void* file);
struct ErrorableResolvedSource {
WTF_MAKE_NONCOPYABLE(ErrorableResolvedSource);

Expand All @@ -155,13 +155,13 @@ struct ErrorableResolvedSource {
result.value.source_code.deref();
result.value.source_url.deref();
result.value.bytecode_origin_path.deref();
if (result.value.bytecode_cache_owned && result.value.bytecode_cache)
ResolvedSource__freeBytecode(result.value.bytecode_cache);
if (result.value.bytecode_cache_file)
ResolvedSource__destroyBytecodeFile(result.value.bytecode_cache_file);
if (result.value.module_info)
zig__ModuleInfoDeserialized__deinit(result.value.module_info);
}
};
static_assert(sizeof(ErrorableResolvedSource) == 144 && alignof(ErrorableResolvedSource) == 8, "ErrorableResolvedSource layout is mirrored in src/jsc/Errorable.rs");
static_assert(sizeof(ErrorableResolvedSource) == 152 && alignof(ErrorableResolvedSource) == 8, "ErrorableResolvedSource layout is mirrored in src/jsc/Errorable.rs");

typedef struct SystemError {
int errno_;
Expand Down
Loading
Loading