bun_event_loop/MiniEventLoop.rs
1//! MiniEventLoop: A lightweight event loop for non-JavaScript contexts
2//!
3//! This is a simplified version of jsc.EventLoop that provides event loop functionality
4//! without requiring a JavaScript runtime. It enables code reuse between JavaScript-enabled
5//! contexts (like `bun run`) and JavaScript-free contexts (like `bun build`, `bun install`,
6//! and the Bun Shell).
7//!
8//! Key characteristics:
9//! - Wraps the uSockets event loop, same as jsc.EventLoop
10//! - Supports concurrent task execution via thread pools
11//! - Provides file polling capabilities for watching filesystem changes
12//! - Manages stdout/stderr streams without JavaScript bindings
13//! - Handles environment variable loading and management
14//!
15//! Use cases:
16//! - Build processes that need async I/O without JavaScript execution
17//! - Package installation with concurrent network requests
18//! - Shell command execution with proper I/O handling
19//! - Any Bun subsystem that needs event-driven architecture without JS overhead
20
21use core::cell::Cell;
22use core::ffi::c_void;
23use core::ptr::NonNull;
24
25use bun_collections::linear_fifo::{DynamicBuffer, LinearFifo};
26use bun_core::Output;
27use bun_dotenv::{self as dotenv, Loader as DotEnvLoader};
28use bun_io::file_poll::Store as FilePollStore;
29use bun_sys::{self as sys, Fd, Mode};
30use bun_threading::UnboundedQueue;
31use bun_uws::Loop as UwsLoop;
32
33use crate::AnyTaskWithExtraContext::{AnyTaskWithExtraContext, New};
34// MOVE-IN: EventLoopHandle relocated from bun_jsc — see AnyEventLoop.rs.
35use crate::EventLoopHandle;
36
37/// The platform's native event loop type. Zig: `jsc.PlatformEventLoop`.
38#[cfg(not(windows))]
39pub type PlatformEventLoop = UwsLoop;
40#[cfg(windows)]
41pub type PlatformEventLoop = bun_sys::windows::libuv::Loop;
42
43// ─── Upward link-time externs (LAYERING) ────────────────────────────────────
44// Zig has no crate split here — `MiniEventLoop` reached `Blob.Store` /
45// `VirtualMachine.get()` directly. The bodies live in `bun_runtime` (which
46// owns `webcore::Blob` / `jsc::VirtualMachine`) as `#[no_mangle]` Rust-ABI
47// fns; the linker resolves them. No `AtomicPtr`, no init-order hazard.
48unsafe extern "Rust" {
49 /// Constructs a `webcore::blob::Store` for stdout/stderr/stdin (Zig
50 /// rare_data.zig:551 inline `Blob.Store` ctor). Return value is an erased
51 /// `*mut blob::Store` with intrusive refcount = 2; this crate only
52 /// stores/forwards it. Defined in `bun_runtime::webcore::blob`.
53 /// No caller-side preconditions (by-value args, allocates fresh).
54 pub safe fn __bun_stdio_blob_store_new(fd: Fd, is_atty: bool, mode: Mode) -> *mut ();
55 /// Returns the thread's `*mut jsc::VirtualMachine` (Zig:
56 /// `jsc.VirtualMachine.get()`). Backs `JsKind::get_vm()`. Defined in
57 /// `bun_runtime::jsc_hooks`. No caller-side preconditions (reads a
58 /// thread-local; wrong-thread is a logic error, not UB).
59 safe fn __bun_js_vm_get() -> *mut ();
60}
61// ────────────────────────────────────────────────────────────────────────────
62
63pub const PIPE_READ_BUFFER_SIZE: usize = 256 * 1024;
64pub type PipeReadBuffer = [u8; PIPE_READ_BUFFER_SIZE];
65
66/// Intrusive MPSC queue over `AnyTaskWithExtraContext` linked via its `.next` field.
67pub type ConcurrentTaskQueue = UnboundedQueue<AnyTaskWithExtraContext>;
68
69// SAFETY: `next` is the sole intrusive link for `UnboundedQueue<AnyTaskWithExtraContext>`.
70unsafe impl bun_threading::Linked for AnyTaskWithExtraContext {
71 #[inline]
72 unsafe fn link(item: *mut Self) -> *const bun_threading::Link<Self> {
73 // SAFETY: `item` is valid and properly aligned per `UnboundedQueue` contract.
74 unsafe { core::ptr::addr_of!((*item).next) }
75 }
76}
77
78/// FIFO of raw task pointers (tasks are intrusive nodes; the queue does not own them).
79/// Zig: `bun.LinearFifo(*AnyTaskWithExtraContext, .Dynamic)`.
80type Queue = LinearFifo<*mut AnyTaskWithExtraContext, DynamicBuffer<*mut AnyTaskWithExtraContext>>;
81
82pub type Task = AnyTaskWithExtraContext;
83
84pub struct MiniEventLoop<'a> {
85 pub tasks: Queue,
86 pub concurrent_tasks: ConcurrentTaskQueue,
87 // PORT NOTE: Zig `*uws.Loop` — raw pointer because the loop is C-owned
88 // (created by `uws_get_loop`/`us_create_loop`) and outlives this struct.
89 pub loop_: *mut UwsLoop,
90 // PORT NOTE: `std.mem.Allocator param` field dropped — non-AST crate uses global mimalloc.
91 pub file_polls_: Option<Box<FilePollStore>>,
92 /// Zig: `env: ?*bun.DotEnv.Loader` — mutable; callers (shell spawn,
93 /// `createNullDelimitedEnvMap`) write through it. Stored as `NonNull`
94 /// (BACKREF) so [`EventLoopHandle::env`] can hand out a `*mut` with
95 /// mutable provenance. `'a` is preserved via PhantomData below.
96 pub env: Option<NonNull<DotEnvLoader<'a>>>,
97 // PORT NOTE: Zig field is `[]const u8` with mixed provenance (literal "", borrowed `cwd`
98 // param, or `allocator.dupe`). Never freed in `deinit`. Use Box<[u8]> and dupe on assign.
99 pub top_level_dir: Box<[u8]>,
100 // TODO(port): lifetime — opaque ctx assigned externally, only read/cleared here.
101 pub after_event_loop_callback_ctx: Option<NonNull<c_void>>,
102 pub after_event_loop_callback: Option<unsafe extern "C" fn(*mut c_void)>,
103 pub pipe_read_buffer: Option<Box<PipeReadBuffer>>,
104 // SAFETY: erased `*mut webcore::blob::Store` (tier-6). Constructed via
105 // `__bun_stdio_blob_store_new`; intrusive-refcounted on the runtime side.
106 // TODO(port): Blob.Store uses intrusive ref_count (constructed with ref_count=2);
107 // LIFETIMES.tsv classifies as Arc but IntrusiveArc<BlobStore> may be required for FFI compat.
108 pub stdout_store: Option<NonNull<()>>,
109 pub stderr_store: Option<NonNull<()>>,
110}
111
112thread_local! {
113 pub static GLOBAL_INITIALIZED: Cell<bool> = const { Cell::new(false) };
114 // PORT NOTE: Zig `threadlocal var global: *MiniEventLoop = undefined;` — raw pointer
115 // because the global is heap-allocated once (heap::alloc) and lives for the
116 // thread's lifetime (a true thread-lifetime singleton; never freed in Zig either).
117 pub static GLOBAL: Cell<*mut MiniEventLoop<'static>> = const { Cell::new(core::ptr::null_mut()) };
118}
119
120/// Returns the thread-local `*mut MiniEventLoop` (Zig: `*MiniEventLoop`).
121///
122/// PORT NOTE (aliasing): Zig's `*T` aliases freely; returning `&'static mut`
123/// here would let two calls (or `init_global` + `MiniKind::get_vm`) hold
124/// overlapping `&mut` to the same allocation — UB. Return the raw pointer;
125/// callers reborrow `&mut` for the scope they need.
126pub fn init_global(
127 env: Option<&'static mut DotEnvLoader<'static>>,
128 cwd: Option<&[u8]>,
129) -> *mut MiniEventLoop<'static> {
130 if GLOBAL_INITIALIZED.with(|g| g.get()) {
131 // Already initialized: hand back the stored raw pointer. No `&mut` is
132 // materialized here (see fn doc — avoids aliased `&'static mut` UB).
133 return GLOBAL.with(|g| g.get());
134 }
135 let loop_ = MiniEventLoop::init();
136 // PORT NOTE: §Forbidden bans `Box::leak` for `&'static`; this is a
137 // thread-lifetime singleton, so use `heap::alloc` (intrusive ownership)
138 // and store the raw pointer in the thread-local — same as Zig
139 // `bun.default_allocator.create` + `threadlocal var global: *MiniEventLoop`.
140 let global_ptr: *mut MiniEventLoop<'static> = bun_core::heap::into_raw(Box::new(loop_));
141 // SAFETY: `global_ptr` was just allocated via `heap::alloc`; this thread
142 // holds the only reference for the duration of first-init. The `GLOBAL`
143 // thread-local is NOT yet published (set below, after this `&mut` is dropped),
144 // so neither `MiniKind::get_vm()` nor a re-entrant `init_global()` can observe
145 // the pointer while this exclusive borrow is live. The `&mut` is scoped to
146 // this function body — NOT `'static` — and ends before we publish/return the
147 // raw ptr.
148 let global = unsafe { &mut *global_ptr };
149
150 // PORT NOTE: `InternalLoopData::set_parent_event_loop` (typed) lives in a
151 // higher tier; the sys-level API is `set_parent_raw(tag, ptr)`. Tag 1 = JS,
152 // tag 2 = mini (matches Zig `EventLoopHandle` discriminant + 1).
153 {
154 let (tag, ptr) = EventLoopHandle::init_mini(global_ptr).into_tag_ptr();
155 // SAFETY: see `loop_ptr()` invariant.
156 unsafe {
157 (*global.loop_ptr())
158 .internal_loop_data
159 .set_parent_raw(tag, ptr)
160 };
161 }
162
163 // PORT NOTE: Zig `bun.DotEnv.instance` is a `?*Loader` global. The Rust
164 // port stores it as `AtomicPtr<Loader<'static>>`.
165 global.env = env.map(NonNull::from).or_else(|| {
166 NonNull::new(
167 dotenv::INSTANCE
168 .load(core::sync::atomic::Ordering::Acquire)
169 .cast::<DotEnvLoader<'static>>(),
170 )
171 });
172 if global.env.is_none() {
173 // Thread-lifetime singletons (matches Zig `bun.default_allocator.create`).
174 let map: *mut dotenv::Map = bun_core::heap::into_raw(Box::new(dotenv::Map::init()));
175 // SAFETY: `map` lives for the thread (singleton); never freed (Zig parity).
176 let loader =
177 bun_core::heap::into_raw_nn(Box::new(DotEnvLoader::init(unsafe { &mut *map })));
178 global.env = Some(loader);
179 }
180
181 // Set top_level_dir from provided cwd or get current working directory
182 if let Some(dir) = cwd {
183 // PORT NOTE: Zig borrowed `dir`; we dupe to keep Box<[u8]> ownership uniform.
184 global.top_level_dir = Box::<[u8]>::from(dir);
185 } else if global.top_level_dir.is_empty() {
186 let mut buf = bun_paths::PathBuffer::uninit();
187 match sys::getcwd(&mut buf[..]) {
188 Ok(len) => {
189 global.top_level_dir = Box::<[u8]>::from(&buf[..len]);
190 }
191 Err(_) => {
192 global.top_level_dir = Box::default();
193 }
194 }
195 }
196
197 // Publish the thread-local pointer only AFTER the scoped `&mut *global_ptr`
198 // above is no longer used — `MiniKind::get_vm()` reads `GLOBAL` without
199 // checking `GLOBAL_INITIALIZED`, so publishing earlier would let a callee
200 // re-derive a `&mut` aliasing `global` (UB). Nothing between the `&mut`
201 // borrow and here reads `GLOBAL` (`EventLoopHandle::init_mini`/`into_tag_ptr`
202 // only copy the pointer value).
203 GLOBAL.with(|g| g.set(global_ptr));
204 GLOBAL_INITIALIZED.with(|g| g.set(true));
205 global_ptr
206}
207
208impl<'a> MiniEventLoop<'a> {
209 /// Raw `*mut uws::Loop` (Zig: `this.loop`).
210 ///
211 /// This is the sole accessor for the `loop_` field. A `&mut UwsLoop`-
212 /// returning accessor is intentionally **not** provided: `UwsLoop::tick()`
213 /// fires FilePoll callbacks which re-enter this struct via the
214 /// `EventLoopCtx` vtable (`platform_event_loop`) and via
215 /// `EventLoopHandle::Mini` (e.g. `enqueue_task_concurrent` → `wakeup()`),
216 /// so a held `&mut UwsLoop` across `.tick()` would alias. The loop is also
217 /// a C-owned handle whose internals are mutated by uSockets itself. All
218 /// access goes through the raw pointer instead.
219 ///
220 /// SAFETY (invariant): `self.loop_` is the live C-owned uws loop set in
221 /// [`init`](Self::init) via `UwsLoop::get()`; never null, outlives `self`.
222 #[inline]
223 pub fn loop_ptr(&self) -> *mut UwsLoop {
224 self.loop_
225 }
226
227 /// Raw pointer to the `DotEnv::Loader` backref (Zig: `this.env.?`).
228 ///
229 /// Returns `None` until [`init_global`] populates it. Neither a `&`- nor
230 /// a `&mut`-returning accessor is provided: the loader may be shared via
231 /// the process-global `dotenv::INSTANCE` (and `Transpiler::env`), and
232 /// other safe paths (`GlobalMini::create_null_delimited_env_map`,
233 /// `EventLoopHandle::create_null_delimited_env_map`, `interpreter.rs`)
234 /// materialize `&mut DotEnvLoader` from the same allocation via raw deref.
235 /// Handing out a long-lived `&DotEnvLoader` here would let safe code hold
236 /// it across one of those `&mut` paths → aliased `&`/`&mut` UB. Callers
237 /// deref the returned `NonNull` for a tightly-scoped borrow under their
238 /// own SAFETY contract instead (mirrors [`loop_ptr`](Self::loop_ptr)).
239 ///
240 /// SAFETY (invariant): when `Some`, points to a thread-/process-lifetime
241 /// loader set in `init_global` that outlives `self` (never freed — Zig
242 /// parity).
243 #[inline]
244 pub fn env_ptr(&self) -> Option<NonNull<DotEnvLoader<'a>>> {
245 self.env
246 }
247
248 #[inline]
249 pub fn get_vm_impl(&mut self) -> &mut MiniEventLoop<'a> {
250 self
251 }
252
253 pub fn throw_error(&mut self, err: &sys::Error) {
254 bun_core::pretty_errorln!("{}", err);
255 Output::flush();
256 }
257
258 pub fn pipe_read_buffer(&mut self) -> &mut [u8] {
259 // `boxed_zeroed` avoids the 256 KiB stack temporary `Box::new([0u8; N])`
260 // would create in debug builds.
261 &mut self
262 .pipe_read_buffer
263 .get_or_insert_with(bun_core::boxed_zeroed::<PipeReadBuffer>)[..]
264 }
265
266 pub fn on_after_event_loop(&mut self) {
267 if let Some(cb) = self.after_event_loop_callback {
268 let ctx = self.after_event_loop_callback_ctx;
269 self.after_event_loop_callback = None;
270 self.after_event_loop_callback_ctx = None;
271 // SAFETY: `cb` is a C-ABI callback registered by the owner of `ctx`; the owner
272 // guarantees `ctx` is valid until the callback fires (Zig invariant).
273 unsafe { cb(ctx.map_or(core::ptr::null_mut(), |p| p.as_ptr())) };
274 }
275 }
276
277 pub fn file_polls(&mut self) -> &mut FilePollStore {
278 if self.file_polls_.is_none() {
279 self.file_polls_ = Some(Box::new(FilePollStore::init()));
280 }
281 self.file_polls_.as_mut().unwrap()
282 }
283
284 /// Raw-pointer variant of [`file_polls`] for re-entrant callers.
285 ///
286 /// The `mini_ctx` vtable shim (`file_polls`) is reached
287 /// via `EventLoopCtx` from inside FilePoll callbacks fired by
288 /// `UwsLoop::tick()`, which is itself invoked from
289 /// `tick`/`tick_once`/`tick_without_idle` while those methods hold
290 /// `&mut self`. Re-deriving a second `&mut MiniEventLoop` from the stored
291 /// `owner` raw ptr there is aliased-`&mut` UB under Stacked Borrows. This
292 /// accessor lazy-inits the store via `addr_of_mut!` on the field only,
293 /// never materializing a `&mut Self`.
294 ///
295 /// # Safety
296 /// `this` must point to a live `MiniEventLoop`. Caller must not hold a
297 /// live `&mut` to `file_polls_` itself across this call. (Not eligible for
298 /// `unsafe-fn-narrow`: every unsafe op below derefs the caller-supplied
299 /// `this`; the body cannot discharge that precondition.)
300 pub unsafe fn file_polls_raw(this: *mut Self) -> *mut FilePollStore {
301 // SAFETY: caller guarantees `this` points to a live `MiniEventLoop` (see fn `# Safety`);
302 // `addr_of_mut!` projects to `file_polls_` without forming `&mut Self`.
303 unsafe {
304 let slot = core::ptr::addr_of_mut!((*this).file_polls_);
305 if (*slot).is_none() {
306 slot.write(Some(Box::new(FilePollStore::init())));
307 }
308 // SAFETY: ensured `Some` just above; `Box` deref yields a stable
309 // heap address independent of `*this`.
310 match &mut *slot {
311 Some(b) => &raw mut **b,
312 None => core::hint::unreachable_unchecked(),
313 }
314 }
315 }
316
317 pub fn init() -> MiniEventLoop<'a> {
318 MiniEventLoop {
319 tasks: Queue::init(),
320 concurrent_tasks: ConcurrentTaskQueue::default(),
321 loop_: UwsLoop::get(),
322 file_polls_: None,
323 env: None,
324 top_level_dir: Box::default(),
325 after_event_loop_callback_ctx: None,
326 after_event_loop_callback: None,
327 pipe_read_buffer: None,
328 stdout_store: None,
329 stderr_store: None,
330 }
331 }
332
333 pub fn tick_concurrent_with_count(&mut self) -> usize {
334 let concurrent = self.concurrent_tasks.pop_batch();
335 let count = concurrent.count;
336 if count == 0 {
337 return 0;
338 }
339
340 let mut iter = concurrent.iterator();
341 let start_count = self.tasks.readable_length();
342 // Zig resets `self.tasks.head = 0` when `start_count == 0` so
343 // `writableSlice(0)` spans the whole buffer. That reset is
344 // load-bearing: `ensure_unused_capacity` early-returns without
345 // realigning when capacity is already sufficient, and
346 // `writable_slice(0)` only yields the first contiguous segment
347 // `buf[head+count..]` — so an empty fifo with `head > 0` would yield a
348 // short slice and the loop would `break` early, silently dropping
349 // tasks already popped from `concurrent`. Use `writable_with_size`,
350 // which realigns when the contiguous slice is too short, so the
351 // returned slice is always `>= count` long.
352 //
353 // PORT NOTE: reshaped for borrowck — Zig held `writable` (&mut into self.tasks) while
354 // bumping `self.tasks.count` per-iteration (overlapping &mut). Fill the writable slice
355 // first, track items written in a local, then commit via `update()` after the borrow ends.
356 let mut written: usize = 0;
357 {
358 let mut writable = self.tasks.writable_with_size(count).expect("unreachable");
359 loop {
360 let task = iter.next();
361 if task.is_null() {
362 break;
363 }
364 writable[0] = task;
365 writable = &mut writable[1..];
366 written += 1;
367 if writable.is_empty() {
368 break;
369 }
370 }
371 }
372 self.tasks.update(written);
373
374 self.tasks.readable_length() - start_count
375 }
376
377 #[inline]
378 pub fn tick_once(&mut self, context: *mut c_void) {
379 if self.tick_concurrent_with_count() == 0 && self.tasks.readable_length() == 0 {
380 // SAFETY: see `loop_ptr()` invariant.
381 unsafe {
382 (*self.loop_ptr()).inc();
383 (*self.loop_ptr()).tick();
384 (*self.loop_ptr()).dec();
385 }
386 // PORT NOTE: Zig `defer this.onAfterEventLoop()` was block-scoped to this `if`.
387 self.on_after_event_loop();
388 }
389
390 while let Some(task) = self.tasks.read_item() {
391 // SAFETY: tasks are pushed by enqueue_task* and remain valid until run() consumes them.
392 unsafe { (*task).run(context) };
393 }
394 }
395
396 pub fn tick_without_idle(&mut self, context: *mut c_void) {
397 loop {
398 let _ = self.tick_concurrent_with_count();
399 while let Some(task) = self.tasks.read_item() {
400 // SAFETY: see tick_once.
401 unsafe { (*task).run(context) };
402 }
403
404 // SAFETY: see `loop_ptr()` invariant.
405 unsafe { (*self.loop_ptr()).tick_without_idle() };
406
407 if self.tasks.readable_length() == 0 && self.tick_concurrent_with_count() == 0 {
408 break;
409 }
410 }
411 // PORT NOTE: Zig `defer this.onAfterEventLoop()` at fn scope; no early returns above.
412 self.on_after_event_loop();
413 }
414
415 pub fn tick<F>(&mut self, context: *mut c_void, is_done: F)
416 where
417 F: Fn(*mut c_void) -> bool,
418 {
419 // PERF(port): Zig `comptime isDone: *const fn` monomorphized per callsite; generic `F`
420 // here also monomorphizes — should match. `tick_once` is `#[inline]` so codegen is
421 // identical to the previously hand-inlined body.
422 while !is_done(context) {
423 self.tick_once(context);
424 }
425 }
426
427 /// Zig: `enqueueTask(comptime Context, ctx, comptime Callback, comptime field)`.
428 ///
429 /// `comptime field: std.meta.FieldEnum(Context)` + `@field(ctx, name)` is replaced
430 /// per PORTING.md (§reflection) with a caller-supplied `field_offset =
431 /// core::mem::offset_of!(C, <field>)` into the embedded `AnyTaskWithExtraContext`.
432 ///
433 /// PORT NOTE: the Zig body is dead code — it calls `Task.New(Context, Callback)`
434 /// (wrong arity; `New` takes 3 type args) and `this.enqueueJSCTask(...)` (no such
435 /// decl). Zig's lazy analysis lets this compile because no caller exists. The
436 /// faithful port writes to `self.tasks` (the local non-concurrent FIFO), which is
437 /// the only plausible target for a `*JSCTask` push on a MiniEventLoop.
438 ///
439 /// # Safety
440 /// `field_offset == offset_of!(C, <field>)` where `<field>: AnyTaskWithExtraContext`,
441 /// and `ctx` is non-null and live for the task's duration.
442 pub unsafe fn enqueue_task<C>(
443 &mut self,
444 ctx: *mut C,
445 callback: fn(*mut C, *mut ()),
446 field_offset: usize,
447 ) {
448 // SAFETY: caller contract — see fn `# Safety`.
449 let task = unsafe { ctx.byte_add(field_offset).cast::<AnyTaskWithExtraContext>() };
450 // Zig: `@field(ctx, name) = TaskType.init(ctx);`
451 // SAFETY: `task` points at a properly aligned `AnyTaskWithExtraContext` field of `*ctx`.
452 unsafe { task.write(New::<C, ()>::init(ctx, callback)) };
453 // Zig: `this.enqueueJSCTask(&@field(ctx, name))` — see PORT NOTE above.
454 self.tasks.write_item(task).expect("unreachable");
455 }
456
457 /// `task` must outlive the queued work item; ownership of the intrusive
458 /// node stays with the caller until the callback runs.
459 pub fn enqueue_task_concurrent(&mut self, task: NonNull<AnyTaskWithExtraContext>) {
460 self.concurrent_tasks.push(task);
461 // SAFETY: see `loop_ptr()` invariant.
462 unsafe { (*self.loop_ptr()).wakeup() };
463 }
464
465 /// Zig: `enqueueTaskConcurrentWithExtraCtx(comptime Context, comptime ParentContext,
466 /// ctx, comptime Callback, comptime field)`.
467 ///
468 /// `comptime field: std.meta.FieldEnum(Context)` + `@field(ctx, name)` is replaced
469 /// per PORTING.md (§reflection) with a caller-supplied `field_offset =
470 /// core::mem::offset_of!(C, <field>)` into the embedded `AnyTaskWithExtraContext`.
471 ///
472 /// # Safety
473 /// `field_offset == offset_of!(C, <field>)` where `<field>: AnyTaskWithExtraContext`,
474 /// and `ctx` is non-null and outlives the queued task (intrusive node; ownership stays
475 /// with caller).
476 pub unsafe fn enqueue_task_concurrent_with_extra_ctx<C, P>(
477 &mut self,
478 ctx: *mut C,
479 callback: fn(*mut C, *mut P),
480 field_offset: usize,
481 ) {
482 // Zig: jsc.markBinding(@src()) — debug-only source marker; no Rust equivalent needed.
483 // SAFETY: caller contract — see fn `# Safety`.
484 let task = unsafe { ctx.byte_add(field_offset).cast::<AnyTaskWithExtraContext>() };
485 // Zig: `@field(ctx, name) = TaskType.init(ctx);`
486 // SAFETY: `task` points at a properly aligned `AnyTaskWithExtraContext` field of `*ctx`.
487 unsafe { task.write(New::<C, P>::init(ctx, callback)) };
488
489 // SAFETY: `task` was just initialized above and is non-null (derived from `ctx`).
490 self.concurrent_tasks
491 .push(unsafe { NonNull::new_unchecked(task) });
492
493 // SAFETY: see `loop_ptr()` invariant.
494 unsafe { (*self.loop_ptr()).wakeup() };
495 }
496
497 /// Lazy-init helper shared by [`stderr`]/[`stdout`]: `fstat → __bun_stdio_blob_store_new → cache`.
498 /// Zig builds Blob.Store with intrusive `ref_count = 2` and
499 /// `.data = .file{ pathlike = .{ .fd }, is_atty, mode }`.
500 #[inline]
501 fn lazy_stdio_store(slot: &mut Option<NonNull<()>>, fd: Fd, is_atty: bool) -> *mut () {
502 if slot.is_none() {
503 let mut mode: Mode = 0;
504 if let Ok(stat) = sys::fstat(fd) {
505 mode = stat.st_mode as Mode;
506 }
507 let store = __bun_stdio_blob_store_new(fd, is_atty, mode);
508 *slot = NonNull::new(store);
509 }
510 slot.unwrap().as_ptr()
511 }
512
513 /// Returns an erased `*mut webcore::blob::Store`. Callers in tier-6 cast back.
514 pub fn stderr(&mut self) -> *mut () {
515 // NB: spec (MiniEventLoop.zig:243) deliberately uses `FD.fromUV(2)` here, not
516 // `Fd::stderr()` — Windows uv-fd vs native-handle distinction. Do not "tidy".
517 Self::lazy_stdio_store(
518 &mut self.stderr_store,
519 Fd::from_uv(2),
520 Output::stderr_descriptor_type() == Output::OutputStreamDescriptor::Terminal,
521 )
522 }
523
524 /// Returns an erased `*mut webcore::blob::Store`. Callers in tier-6 cast back.
525 pub fn stdout(&mut self) -> *mut () {
526 Self::lazy_stdio_store(
527 &mut self.stdout_store,
528 Fd::stdout(),
529 Output::stdout_descriptor_type() == Output::OutputStreamDescriptor::Terminal,
530 )
531 }
532}
533
534// ───────────── EventLoopCtx adapter (bun_io cycle-break) ─────────────────
535// `bun_io::file_poll::Store::put` and friends take an erased `EventLoopCtx`
536// instead of naming `MiniEventLoop`/`VirtualMachine` directly. This crate owns
537// `MiniEventLoop`, so the Mini-side vtable lives here. The Js-side vtable lives
538// in `bun_runtime` (it must name `jsc::VirtualMachine`).
539
540bun_io::link_impl_EventLoopCtx! {
541 Mini for MiniEventLoop<'static> => |this| {
542 platform_event_loop_ptr() => (*this).loop_ptr(),
543 // `file_polls_raw` to avoid aliased `&mut MiniEventLoop` while `tick*`
544 // holds `&mut self` across the re-entrant `UwsLoop::tick()` that
545 // reaches this body.
546 file_polls_ptr() => MiniEventLoop::file_polls_raw(this),
547 // Mini has no pending_unref_counter; the upstream deliberately panics.
548 increment_pending_unref_counter() => panic!("FIXME TODO"),
549 // `KeepAlive::{,un}refConcurrently` is JS-VM-only (statically rejected
550 // on Mini upstream); preserve that invariant rather than racily
551 // mutating uws counters off-thread.
552 ref_concurrently() => unreachable!("KeepAlive::refConcurrently is JS-VM-only"),
553 unref_concurrently() => unreachable!("KeepAlive::unrefConcurrently is JS-VM-only"),
554 after_event_loop_callback() => (*this).after_event_loop_callback,
555 set_after_event_loop_callback(cb, ctx) => {
556 (*this).after_event_loop_callback = cb;
557 (*this).after_event_loop_callback_ctx = ctx;
558 },
559 pipe_read_buffer() => core::ptr::from_mut::<[u8]>((*this).pipe_read_buffer()),
560 }
561}
562
563impl<'a> MiniEventLoop<'a> {
564 /// `this` is the per-thread `MiniEventLoop` singleton; the returned ctx
565 /// must not outlive it.
566 #[inline]
567 pub fn as_event_loop_ctx(this: &mut MiniEventLoop<'a>) -> bun_io::EventLoopCtx {
568 // SAFETY: `this` is a live `&mut`, so the pointer handed to `new` is
569 // non-null and exclusively borrowed for the call's duration.
570 unsafe { bun_io::EventLoopCtx::new(bun_io::EventLoopCtxKind::Mini, this) }
571 }
572}
573
574impl<'a> Drop for MiniEventLoop<'a> {
575 fn drop(&mut self) {
576 // PORT NOTE: `tasks.deinit()` is implicit via Queue's Drop.
577 debug_assert!(self.concurrent_tasks.is_empty());
578 }
579}
580
581// ───────────────────────────── MiniVM ─────────────────────────────
582
583pub struct MiniVM<'a> {
584 // PORT NOTE: LIFETIMES.tsv classifies this BORROW_PARAM `&'a`, but `file_polls()`
585 // mutates the loop (lazy-inits the store). Hold `&'a mut` instead of
586 // casting `&T`→`&mut T` (UB, and forbidden by PORTING.md "no raw pointers to silence
587 // borrowck"). Zig's `*MiniEventLoop` was always mutable.
588 pub mini: &'a mut MiniEventLoop<'a>,
589}
590
591impl<'a> MiniVM<'a> {
592 pub fn init(inner: &'a mut MiniEventLoop<'a>) -> MiniVM<'a> {
593 MiniVM { mini: inner }
594 }
595
596 #[inline]
597 pub fn loop_(&self) -> &MiniEventLoop<'a> {
598 &*self.mini
599 }
600
601 #[inline]
602 pub fn platform_event_loop(&self) -> *mut PlatformEventLoop {
603 bun_io::uws_to_native(self.mini.loop_ptr())
604 }
605
606 #[inline]
607 pub fn increment_pending_unref_counter(&self) {
608 // Zig spec body: `_ = this; @panic("FIXME TODO");` — MiniEventLoop has no
609 // pending_unref_counter (only `jsc.VirtualMachine` does). This is the REAL
610 // ported body, not a stub.
611 let _ = self;
612 panic!("FIXME TODO");
613 }
614
615 #[inline]
616 pub fn file_polls(&mut self) -> &mut FilePollStore {
617 self.mini.file_polls()
618 }
619}
620
621// ───────────────────────────── EventLoopKind ─────────────────────────────
622
623#[derive(Copy, Clone, Eq, PartialEq, Debug)]
624pub enum EventLoopKind {
625 Js,
626 Mini,
627}
628
629// TODO(port): Zig `Type()` / `refType()` return `type` at comptime. Rust cannot return a type
630// from a runtime enum value. Model as a trait with associated types instead:
631pub trait EventLoopKindT {
632 type Loop;
633 type Ref;
634 fn get_vm() -> Self::Ref;
635}
636
637pub struct JsKind;
638pub struct MiniKind;
639
640impl EventLoopKindT for JsKind {
641 // SAFETY: erased `jsc::EventLoop` / `jsc::VirtualMachine` (tier-6).
642 type Loop = *mut ();
643 type Ref = *mut ();
644 fn get_vm() -> Self::Ref {
645 __bun_js_vm_get()
646 }
647}
648
649impl EventLoopKindT for MiniKind {
650 type Loop = MiniEventLoop<'static>;
651 // PORT NOTE (aliasing): Zig `refType() = *MiniEventLoop` is a freely-aliasing
652 // pointer. Returning `&'static mut` would let two `get_vm()` calls (or
653 // `get_vm()` + `init_global()`) hold overlapping `&mut` — UB. Return the raw
654 // pointer (matches `JsKind::Ref = *mut ()`); callers reborrow scoped `&mut`.
655 type Ref = *mut MiniEventLoop<'static>;
656 fn get_vm() -> Self::Ref {
657 // Caller must have called `init_global()` first (Zig invariant: `global`
658 // is set). No `&mut` materialized here — raw-ptr-only access.
659 GLOBAL.with(|g| g.get())
660 }
661}
662
663// ───────────────────────────── AbstractVM ─────────────────────────────
664
665/// Zig `AbstractVM(inner: anytype)` dispatches on `@TypeOf(inner)` to wrap either a
666/// `*VirtualMachine` or a `*MiniEventLoop`. Rust models this as a trait implemented for both.
667pub trait AbstractVM<'a> {
668 type Wrapped;
669 fn abstract_vm(self) -> Self::Wrapped;
670}
671
672// PORT NOTE (b0): `impl AbstractVM for &VirtualMachine` cannot live here
673// without naming the tier-6 `VirtualMachine` type. The impl moves to
674// `bun_runtime` (move-in pass), which constructs `JsVM { vm, vtable }`.
675
676impl<'a> AbstractVM<'a> for &'a mut MiniEventLoop<'a> {
677 type Wrapped = MiniVM<'a>;
678 fn abstract_vm(self) -> MiniVM<'a> {
679 MiniVM::init(self)
680 }
681}
682
683// ported from: src/event_loop/MiniEventLoop.zig