Skip to main content

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