Skip to content
Open
7 changes: 7 additions & 0 deletions src/bun_core/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,13 @@ pub mod vec {
self.0.as_mut_ptr().cast::<u8>()
}

/// The whole buffer as uninitialized bytes, for producers typed over `MaybeUninit`.
#[inline(always)]
pub fn as_uninit_mut(&mut self) -> &mut [core::mem::MaybeUninit<u8>] {
// SAFETY: `MaybeUninit<[u8; N]>` and `[MaybeUninit<u8>; N]` have identical layout and no validity invariant.
unsafe { core::slice::from_raw_parts_mut(self.0.as_mut_ptr().cast(), N) }
}

/// # Safety
/// Write-only view, same contract as [`spare_bytes_mut`]: only a producer may store into it, and only the prefix it reports may be read back.
#[inline(always)]
Expand Down
8 changes: 8 additions & 0 deletions src/codegen/generate-host-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ function ptrify(ty: string): { cTy: string; deref: (n: string) => string; extraL
`{\n // SAFETY: C++ caller passes \`${n}_len\` live elements at \`${n}\` (or 0).\n unsafe { ::bun_core::ffi::slice(${n}, ${n}_len) }\n }`,
};
}
// `Option<&CStr>` — C passes a nullable `const char*`.
if (/^Option\s*<\s*&\s*(?:(?:core|std)::ffi::)?CStr\s*>$/.test(ty)) {
return {
cTy: `*const c_char`,
deref: n =>
`{\n // SAFETY: C++ caller passes null or a NUL-terminated string live for the call.\n if ${n}.is_null() { None } else { Some(unsafe { ::core::ffi::CStr::from_ptr(${n}) }) }\n }`,
};
}
// Other slice shapes (`&mut [T]`, `&'a [T]`) and `&str` are NOT FFI-safe; reject.
if (/^&[^\[]*\[/.test(ty) || /^&\s*str\b/.test(ty)) {
throw new Error(`slice/str param \`${ty}\` is not FFI-safe; use \`&[T]\` (const) or (ptr, len)`);
Expand Down
40 changes: 40 additions & 0 deletions src/event_loop/AnyTaskWithExtraContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,46 @@ impl AnyTaskWithExtraContext {
}
}

/// Heap-allocates a task that owns `value`; when it runs it calls
/// `callback(value, extra)` and frees itself. The mini loop that receives
/// the returned pointer owns the allocation until then.
pub fn from_value<T: 'static>(
value: T,
callback: fn(T, *mut c_void),
) -> NonNull<AnyTaskWithExtraContext> {
#[repr(C)]
struct Wrapper<T> {
any_task: AnyTaskWithExtraContext,
value: T,
callback: fn(T, *mut c_void),
}

fn function<T>(this: *mut (), extra: *mut ()) {
// SAFETY: `this` is the `ctx` set below: the heap `Wrapper<T>`, whose
// first (`repr(C)`) field is the task the loop just dequeued.
let that: Box<Wrapper<T>> = unsafe { bun_core::heap::take(this.cast::<Wrapper<T>>()) };
let Wrapper {
value, callback, ..
} = *that;
callback(value, extra.cast::<c_void>());
}

let task = bun_core::heap::into_raw(Box::new(Wrapper::<T> {
any_task: AnyTaskWithExtraContext {
callback: function::<T>,
ctx: None,
next: bun_threading::Link::new(),
},
value,
callback,
}));
// SAFETY: `task` was just produced by `into_raw`; valid and exclusive.
unsafe {
(*task).any_task.ctx = NonNull::new(task.cast::<()>());
NonNull::new_unchecked(core::ptr::addr_of_mut!((*task).any_task))
}
}

/// Initializes `self` in place to call `callback(of, extra)`.
// The unit context means the callee is effectively `fn(*T)` only; mapped
// to `*mut ()` to keep the two-arg stored ABI uniform.
Expand Down
28 changes: 28 additions & 0 deletions src/event_loop/ConcurrentTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,34 @@ impl Task {
}
}

/// `impl Taskable` for a payload that is queued as an owned `Box` (via
/// [`Task::from_boxed`]) and whose dispatch arm reclaims that box: an unrun
/// task is released by dropping it. Forms:
///
/// ```ignore
/// boxed_taskable!(MyTask => task_tag::MyTask);
/// boxed_taskable!([const B: bool] MyTask<B> => if B { task_tag::A } else { task_tag::C });
/// boxed_taskable!([R, A] MyReq<R, A> where [R: X, A: Y] => task_tag::Z);
/// ```
#[macro_export]
macro_rules! boxed_taskable {
([$($gen:tt)*] $ty:ty where [$($bounds:tt)*] => $tag:expr) => {
impl<$($gen)*> $crate::Taskable for $ty where $($bounds)* {
const TAG: $crate::TaskTag = $tag;
unsafe fn release_unrun(this: *mut Self) {
// SAFETY: fn contract — `this` is the box queued under `TAG`.
drop(unsafe { ::bun_core::heap::take(this) });
}
}
};
([$($gen:tt)*] $ty:ty => $tag:expr) => {
$crate::boxed_taskable!([$($gen)*] $ty where [] => $tag);
};
($ty:ty => $tag:expr) => {
$crate::boxed_taskable!([] $ty where [] => $tag);
};
}

// Taskable impls for the low-tier task wrappers defined in this crate.
impl Taskable for crate::ManagedTask::ManagedTask {
const TAG: TaskTag = task_tag::ManagedTask;
Expand Down
2 changes: 2 additions & 0 deletions src/io/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,8 @@ pub use pipe_read_scratch::{PipeReadScratch, PipeReadScratchGuard};
#[cfg(windows)]
#[path = "source.rs"]
pub mod source;
#[cfg(windows)]
pub mod uv_fs;
#[path = "write.rs"]
pub mod write;

Expand Down
163 changes: 163 additions & 0 deletions src/io/uv_fs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
//! Typed one-shot libuv fs requests: a heap object that embeds its
//! `uv_fs_t`, is owned by libuv while the request is in flight, and comes
//! back as a `Box` when it completes.

use core::ffi::c_int;

use bun_core::ZStr;
use bun_sys::windows::libuv as uv;

/// A heap object with an embedded [`uv::OwnedFsReq`].
///
/// The submitters below take it as a `Box`, hand the allocation to libuv for
/// the duration of the request (nothing else may touch it meanwhile), and give
/// it back to [`on_complete`](Self::on_complete) on the loop thread once libuv
/// has filled in `req().result` (and, per op, `req()`'s result accessors).
pub trait UvFsRequest: Sized + 'static {
fn req(&mut self) -> &mut uv::OwnedFsReq;
fn on_complete(this: Box<Self>);
}

/// I/O parameters for [`read`]/[`write`], read off the boxed owner once it is
/// at its final address: `(request, fd, buffers, position)` in one split
/// borrow (`position` is `-1` for the current one). libuv copies the
/// descriptor array before returning; the memory the descriptors cover must
/// stay valid and otherwise untouched until completion, which the owner
/// guarantees by holding (and, for JS-backed buffers, pinning) it — the
/// `PlatformIoVec` convention used by every vectored-I/O entry point in
/// `bun_sys`.
pub trait UvFsIo: UvFsRequest {
fn io_parts(&mut self) -> (&mut uv::OwnedFsReq, uv::uv_file, IoBufs<'_>, i64);
}

pub enum IoBufs<'a> {
One(uv::uv_buf_t),
Many(&'a [uv::uv_buf_t]),
}

impl IoBufs<'_> {
#[inline]
fn as_slice(&self) -> &[uv::uv_buf_t] {
match self {
IoBufs::One(b) => core::slice::from_ref(b),
IoBufs::Many(s) => s,
}
}
}

extern "C" fn on_uv_fs_done<T: UvFsRequest>(req: *mut uv::fs_t) {
// SAFETY: `req.data` is the `Box<T>` released in `start`; libuv is done
// with the request, so ownership returns here exactly once.
let owner: Box<T> = unsafe { bun_core::heap::take((*req).data.cast::<T>()) };
T::on_complete(owner);
}

/// Release `owner` to libuv and run `submit` on its embedded request. A
/// submission error comes back synchronously as `Err((owner, rc))` with
/// `req().result` also holding `rc` — libuv will not call back in that case.
fn start<T: UvFsRequest>(
owner: Box<T>,
submit: impl FnOnce(*mut uv::Loop, *mut uv::fs_t, uv::uv_fs_cb) -> uv::ReturnCode,
) -> Result<(), (Box<T>, uv::ReturnCode)> {
let raw: *mut T = bun_core::heap::into_raw(owner);
// SAFETY: `raw` is the live allocation released above; exclusive here.
let req: *mut uv::fs_t = unsafe {
let req: &mut uv::fs_t = (*raw).req();
req.data = raw.cast();
req
};
let rc = submit(uv::Loop::get(), req, Some(on_uv_fs_done::<T>));
if rc.is_err() {
// SAFETY: libuv rejected the request and will not call back; reclaim.
let mut owner = unsafe { bun_core::heap::take(raw) };
owner.req().result = rc.into();
return Err((owner, rc));
}
Ok(())
}

/// `uv_fs_open`; libuv copies `path` before returning.
pub fn open<T: UvFsRequest>(
owner: Box<T>,
path: &ZStr,
flags: c_int,
mode: c_int,
) -> Result<(), (Box<T>, uv::ReturnCode)> {
start(owner, |l, req, cb| {
// SAFETY: `req` is the owner's embedded request; `path` is NUL-terminated.
unsafe { uv::uv_fs_open(l, req, path.as_ptr(), flags, mode, cb) }
})
}

/// `uv_fs_close`.
pub fn close<T: UvFsRequest>(
owner: Box<T>,
fd: uv::uv_file,
) -> Result<(), (Box<T>, uv::ReturnCode)> {
// SAFETY: `req` is the owner's embedded request.
start(owner, |l, req, cb| unsafe {
uv::uv_fs_close(l, req, fd, cb)
})
}

/// `uv_fs_statfs`; libuv copies `path` before returning. Read the result with
/// `req().statfs_result()`.
pub fn statfs<T: UvFsRequest>(owner: Box<T>, path: &ZStr) -> Result<(), (Box<T>, uv::ReturnCode)> {
start(owner, |l, req, cb| {
// SAFETY: `req` is the owner's embedded request; `path` is NUL-terminated.
unsafe { uv::uv_fs_statfs(l, req, path.as_ptr(), cb) }
})
}

fn start_io<T: UvFsIo>(
owner: Box<T>,
op: unsafe extern "C" fn(
*mut uv::Loop,
*mut uv::fs_t,
uv::uv_file,
*const uv::uv_buf_t,
core::ffi::c_uint,
i64,
uv::uv_fs_cb,
) -> uv::ReturnCode,
) -> Result<(), (Box<T>, uv::ReturnCode)> {
let raw: *mut T = bun_core::heap::into_raw(owner);
// SAFETY: `raw` is the live allocation released above; exclusive here (one
// `&mut` reborrow, split by `io_parts`). The descriptor array is copied by
// libuv during the call; what it describes is kept valid by `*raw` until
// completion (see `UvFsIo`).
let rc = unsafe {
let owner: &mut T = &mut *raw;
let (req, fd, bufs, position) = owner.io_parts();
req.data = raw.cast();
let req: *mut uv::fs_t = &mut **req;
let slice = bufs.as_slice();
let (ptr, len) = (slice.as_ptr(), slice.len());
op(
uv::Loop::get(),
req,
fd,
ptr,
core::ffi::c_uint::try_from(len).expect("int cast"),
position,
Some(on_uv_fs_done::<T>),
)
};
if rc.is_err() {
// SAFETY: libuv rejected the request and will not call back; reclaim.
let mut owner = unsafe { bun_core::heap::take(raw) };
owner.req().result = rc.into();
return Err((owner, rc));
}
Ok(())
}

/// `uv_fs_read` into the owner's buffers.
pub fn read<T: UvFsIo>(owner: Box<T>) -> Result<(), (Box<T>, uv::ReturnCode)> {
start_io(owner, uv::uv_fs_read)
}

/// `uv_fs_write` from the owner's buffers.
pub fn write<T: UvFsIo>(owner: Box<T>) -> Result<(), (Box<T>, uv::ReturnCode)> {
start_io(owner, uv::uv_fs_write)
}
48 changes: 35 additions & 13 deletions src/jsc/array_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ impl ArrayBuffer {
return Self::create::<{ JSType::Uint8Array }>(ctx, b"");
}

// TODO: others
return create_typed_array_copy(ctx, self.typed_array_type.to_typed_array_type(), b"");
}

if self.typed_array_type == JSType::ArrayBuffer {
Expand Down Expand Up @@ -849,18 +849,7 @@ impl BinaryType {
| BinaryType::Float64Array
| BinaryType::BigInt64Array
| BinaryType::BigUint64Array => {
crate::host_fn::from_js_host_call(global, || {
// SAFETY: `global` is a live opaque ZST handle; `bytes` is a
// valid slice whose pointer/len are only read (copied) by C++.
unsafe {
Bun__createTypedArrayForCopy(
global,
self.to_typed_array_type(),
bytes.as_ptr().cast(),
bytes.len(),
)
}
})
create_typed_array_copy(global, self.to_typed_array_type(), bytes)
}
}
}
Expand Down Expand Up @@ -960,6 +949,24 @@ impl MarkedArrayBuffer {
}
}

/// Take ownership of heap bytes (freed by `destroy`/`Drop`, or handed to
/// JSC by `to_node_buffer`/`to_js`). An empty slice yields [`Self::EMPTY`].
pub fn from_owned_bytes(bytes: Box<[u8]>, typed_array_type: JSType) -> MarkedArrayBuffer {
if bytes.is_empty() {
return MarkedArrayBuffer {
buffer: ArrayBuffer {
typed_array_type,
..ArrayBuffer::EMPTY
},
owns_buffer: false,
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
MarkedArrayBuffer {
buffer: ArrayBuffer::from_owned_bytes(bytes, typed_array_type),
owns_buffer: true,
}
}

pub const EMPTY: MarkedArrayBuffer = MarkedArrayBuffer {
owns_buffer: false,
buffer: ArrayBuffer::EMPTY,
Expand Down Expand Up @@ -1040,6 +1047,21 @@ pub(crate) unsafe fn make_array_buffer_with_bytes_no_copy(
})
}

/// A new typed array of `array_type` over a fresh JSC-owned copy of `bytes`
/// (a real 1-byte backing store when empty, so the result is never detached).
pub(crate) fn create_typed_array_copy(
global: &JSGlobalObject,
array_type: TypedArrayType,
bytes: &[u8],
) -> JsResult<JSValue> {
crate::host_fn::from_js_host_call(global, || {
// SAFETY: `bytes` is a live slice; C++ only reads `len` bytes from it.
unsafe {
Bun__createTypedArrayForCopy(global, array_type, bytes.as_ptr().cast(), bytes.len())
}
})
}

/// Wrap caller-provided bytes in a JS typed array of `array_type` without
/// copying. JSC adopts `ptr..ptr+len` as the backing store of the returned
/// object and calls `deallocator(ptr, deallocator_context)` on the JS thread
Expand Down
Loading
Loading