runite 0.1.0

An event-loop-per-thread async runtime built on io_uring (Linux), kqueue (macOS), and IOCP (Windows)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
*This document describes the runtime as implemented for 0.1. Forward-looking work is tracked in the project's GitHub issues.*

# Overview

`runite` is a single-threaded async runtime with explicit worker-thread support,
JavaScript-style microtask/macrotask scheduling, and platform-specific I/O drivers. It is meant for
UI front-ends, embedded event loops, and fine-grained reactive systems that need deterministic flush
points between input and rendering. It is deliberately not a general-purpose high-throughput server
runtime: it prefers simple per-thread event loops, local state, and predictable scheduling over
work-stealing, `Send`-future ergonomics, and maximum I/O throughput.

# Threading model

- A runtime thread owns one `ThreadState`, one scheduler, one timer heap, and one driver.
- `spawn_worker` creates a new OS thread, installs a separate runtime state on it, queues the
  initial macrotask, and runs that worker loop (`src/platform/runtime_shared/scheduler.rs`).
- Linux: each runtime thread owns its own `io_uring` ring and `ThreadNotifier`; the
  notifier wakes the ring with `IORING_OP_MSG_RING`
  (`src/platform/linux/driver.rs`).
- macOS `aarch64`: each runtime thread owns a `kqueue` plus a nonblocking wake pipe
  (`src/platform/macos_aarch64/driver.rs`).
- Windows: each runtime thread owns an I/O completion port plus a waitable timer; the
  notifier wakes the port with `PostQueuedCompletionStatus`
  (`src/platform/windows/driver.rs`, `docs/WINDOWS.md`).
- `ThreadHandle::queue_macrotask` is the cross-thread boundary. It accepts only `Send` closures,
  pushes into the target thread's remote macrotask queue, and wakes the target driver
  (`src/platform/runtime_shared/handles.rs`, `src/platform/runtime_shared/state.rs`).

What `!Send` means here:

- Most reactive values are intended to stay on their originating runtime thread.
- `JoinHandle<T>` and `AbortHandle` store `Rc`s, so they are `!Send`
  (`src/platform/runtime_shared/handles.rs`).
- `TimeoutHandle`/`IntervalHandle` are plain `{id, generation}` cancellation tokens and *are*
  sendable; cancelling from a thread other than the creator is a documented no-op (the generation
  check fails), not a panic.
- A task's `Waker`, by `std` contract, **is** `Send + Sync`: it carries an `Arc<TaskWaker>` holding
  only the owning `ThreadHandle` and a numeric task id, and resolves the `!Send` task through the
  owner thread's registry (`src/platform/runtime_shared/future_task.rs`). Leaf futures may therefore
  move `cx.waker()` to other threads freely; the task itself still never migrates.
- Cross-thread work must be expressed as `FnOnce() + Send + 'static` and submitted through
  `ThreadHandle::queue_macrotask`.

Lazy initialization contract:

- Runtime state is reached only through scoped accessors — `with_current_thread(|state| ...)`
  (lazy-initializing) and `with_installed_thread`/`try_with_installed_thread` (non-initializing) —
  in `src/platform/runtime_shared/state.rs`; no `'static` reference to `ThreadState` escapes.
- Public entry points use the lazy accessor, so any of them (including `current_thread_handle()`)
  called on a thread with no installed runtime creates a driver, notifier, shared state, and
  `ThreadState` — on Linux, that includes an `io_uring` ring. Whether auto-initialization should
  be narrowed (e.g. `current_thread_handle` returning `Option`) remains an open design question.

## Scaling across cores

`runite` scales by running more independent event loops, not by making one scheduler shared. Each
runtime thread owns its own scheduler queues, timer heap, and platform I/O driver (`io_uring` ring on
Linux, `kqueue` on macOS, I/O completion port on Windows). There is no work-stealing layer between
runtime threads.

Futures are thread-local and deliberately `!Send`: a task queued on one runtime thread is always
polled, woken, aborted, and joined on that same thread. Tasks never migrate between threads. This is a
trade-off: applications do not get cross-thread future ergonomics or automatic load balancing, but the
hot path stays free of scheduler synchronization and scheduling remains predictable.

To use multiple cores, the application starts one event loop per core with `spawn_worker`. Each worker
installs its own runtime state and runs independently. Cross-thread coordination is explicit and goes
through the `Send`-only boundary: `ThreadHandle::queue_macrotask` accepts
`FnOnce() + Send + 'static`, enqueues a macrotask on the target worker, and wakes that worker's
driver.

For network servers, the idiomatic multi-core shape on Linux and macOS is `SO_REUSEPORT`: start one
worker per core, let each worker bind its own listener to the same address, and run an independent
accept loop on each worker. The kernel then load-balances incoming connections across those per-core
listeners without a shared accept lock or a userspace dispatch thread. Windows has no `SO_REUSEPORT`
(`TcpSocket::set_reuseport` reports `Unsupported` there); a single accept loop that distributes
connections to workers over `ThreadHandle::queue_macrotask` is the portable alternative.

# Task scheduling — micro vs macro tasks

Microtasks:

- Are strictly thread-local; remote threads cannot enqueue them
  (`src/platform/runtime_shared/state.rs`).
- Run to completion between macrotasks.
- Drive future continuations: a wake looks the task up by id in the owner thread's registry and
  `FutureTask::schedule` enqueues one poll microtask, deduplicated while a poll is already queued
  (`src/platform/runtime_shared/future_task.rs`).
- Match JavaScript Promise microtasks.

Macrotasks:

- May be local (`queue_macrotask`) or remote (`ThreadHandle::queue_macrotask`).
- Carry I/O completion callbacks, timer expirations, worker-exit callbacks, and host/event callbacks.
- Remote macrotasks are swapped into the local macrotask queue during `drain_remote_tasks`
  (`src/platform/runtime_shared/scheduler.rs`).
- Expired timers dispatch by pushing timer callbacks into the macrotask queue; worker exits enqueue
  their `on_exit` callback as a macrotask (`src/platform/runtime_shared/scheduler.rs`).

## Backpressure

Local microtask and macrotask queues are intentionally unbounded: they are owned by one runtime
thread, and runaway local production is a scheduler bug rather than a cross-thread backpressure
problem. The cross-thread remote macrotask queue is bounded to 65,536 entries by default, tunable
with `RUNITE_REMOTE_QUEUE_CAPACITY` (parsed as `usize`, minimum 1, capped at a sane maximum). When it
is full, `ThreadHandle::queue_macrotask` returns `Err(QueueError::Full)` immediately. The runtime
does not block the producer and does not silently drop the task; the caller must choose whether to
retry, drop, or fail. The first overflow per runtime thread emits a scheduler warning for
observability without spamming logs during overload.

The capacity limit applies to **user** tasks only. Internal wakes — an I/O/channel completion whose
result is already stored, or a spawned task's waker firing from another thread — route through a
capacity-bypassing path (`ThreadShared::enqueue_internal_wake`), because dropping one would strand
its target forever (a lost wakeup). Their count is naturally bounded: one coalesced pending wake per
in-flight operation or live task, so the bypass cannot grow the queue without matching outstanding
work.

Per-turn ordering in `run()`:

1. Drain driver events (I/O completions, expired timers).
2. Drain remote tasks.
3. Flush completed workers.
4. Run all microtasks until the queue is empty (one checkpoint). The driver is **not** re-polled
   between microtasks: everything the driver produces is a macrotask, so it cannot run
   mid-checkpoint anyway, and polling once per turn instead of once per microtask removes a
   syscall per microtask without changing observable ordering.
5. Run one macrotask.
6. Repeat.

If a single checkpoint crosses 1000 microtasks while a macrotask is actually waiting — queued
locally or remotely, or a timer past its deadline — a starvation warning is emitted from *inside*
the drain loop; a checkpoint that never empties (the pathology the warning exists for) would never
reach an after-the-loop check. A long checkpoint with nothing queued behind it starves no one and
warns nothing. Implemented by `drain_all()`, `drain_microtasks()`, and the single
`pop_macrotask()` per turn (`src/platform/runtime_shared/scheduler.rs`).

Why this shape exists:

- It gives a deterministic flush point between input handling and rendering.
- It lets a fine-grained reactive UI settle all same-turn future continuations before one external
  event callback mutates the world again.
- It prevents cross-thread senders from injecting microtasks into the middle of a local microtask
  flush.

Guideline:

- Schedule continuations of in-thread work as microtasks.
- Schedule everything originating from I/O or another thread as a macrotask.
- Each completion carries an explicit wake class chosen by its creator
  (`WakeClass::Microtask` for in-process resolutions — channel sends, `Notify` — and
  `WakeClass::Macrotask` for I/O completions). A same-thread I/O completion therefore wakes as a
  **local macrotask** (a poll-phase callback, like Node), never mid-checkpoint; same-thread channel
  resolutions join the current microtask checkpoint (the `Promise.resolve` analog); cross-thread
  completions are always macrotasks (`src/op/completion.rs`).

Minimal shape:

```rust
queue_microtask(|| resolve_same_thread_promise());
thread_handle.queue_macrotask(|| handle_remote_or_io_event());
```

# Run lifecycle

`run()`:

- Is the canonical run-to-idle loop.
- Runs until no ready tasks, timers, live child workers, or pending async operations remain
  (`src/platform/runtime_shared/scheduler.rs`).
- Blocks in `driver.wait()` when timers, children, or async operations still exist but no work is
  ready.

`block_on(future)`:

- Drives the same per-turn machinery but exits as soon as the given future resolves, returning its
  output; other spawned tasks stay queued for a later driver call.
- Polls the future in place (stack-pinned) with a `Send + Sync` waker, so the future may borrow
  locals and need not be `Send` or `'static` (`src/platform/runtime_shared/scheduler.rs`).

`run_until_stalled()`:

- Drains all currently ready work.
- Does not block for future driver events.
- Returns when no immediately runnable work remains, and clears `closing`
  (`src/platform/runtime_shared/scheduler.rs`).
- Intended for tests and host-loop integrations that own the outer wait.

`run_ready_tasks()`:

- Drains remote tasks, completed workers, microtasks, and local macrotasks.
- Does not poll the driver and therefore does not re-enter timers or I/O readiness
  (`src/platform/runtime_shared/scheduler.rs`).
- Intended for host callbacks that need to flush application work without re-entering timer or I/O
  callbacks.

All four drivers set an `in_event_loop` flag for their duration; re-entering any of them from inside
a task or callback already running on the loop panics rather than double-driving the same queues.
For `run`/`run_until_stalled`/`run_ready_tasks` that panic is absorbed by the per-task firewall
(the offending task resolves to `JoinError::Panicked`); `block_on` is a direct driver, so it
propagates to the caller.

## Panic isolation

A panic must never tear down the event loop that observes it:

- `FutureTask::poll` wraps the future poll in `catch_unwind`; a panicking task is dropped,
  deregistered, and its joiner resolves to `Err(JoinError::Panicked)`
  (`src/platform/runtime_shared/future_task.rs`).
- Every other scheduled closure (timer/interval callbacks, `queue_*` closures, worker `on_exit`)
  runs through the `run_guarded` firewall; a panic is logged and the loop continues.
- Blocking-pool jobs are firewalled in the worker loop so one panicking job cannot retire a pool
  thread; `spawn_blocking` additionally converts its closure's panic into `JoinError::Panicked`
  for the awaiter.
- A worker thread whose loop somehow unwinds still marks its `WorkerCompletion` finished and
  notifies the parent, so a dead worker can never hang the thread waiting on it.
- The shutdown probe in `run()` holds a scope guard that restores the `closing` flag on any
  non-committed exit, including a panic unwind.

In all cases the panic is still reported through the process panic hook.

## Shutdown commit protocol

`run()` uses a two-phase commit so no remote task can be accepted and then stranded after the event
loop exits:

1. Drain everything currently observable.
2. If ready work exists, keep running.
3. Set `closing = true` with a CAS via `try_begin_shutdown`
   (`src/platform/runtime_shared/state.rs`).
4. Drain again.
5. If any ready work appeared, clear `closing` and continue
   (`src/platform/runtime_shared/scheduler.rs`).
6. If timers, child workers, or async operations remain, clear `closing`, wait on the driver, and
   continue (`src/platform/runtime_shared/scheduler.rs`).
7. Otherwise take the remote-queue lock.
8. While holding that lock, if the remote queue is still empty, set `closed = true`
   (`src/platform/runtime_shared/scheduler.rs`).
9. `ThreadShared::enqueue_macro` checks `closed` under the same lock before accepting a task, so the
   exit path and the remote enqueue path are mutually ordered
   (`src/platform/runtime_shared/scheduler.rs`).
10. If the queue was not empty, clear `closing` and process the newly arrived work.
11. If the thread is a worker, mark its completion finished and notify the parent
    (`src/platform/runtime_shared/scheduler.rs`).
12. Notify the local driver, tear down TLS state, and return
    (`src/platform/runtime_shared/scheduler.rs`).

# Cancellation semantics

Drop is the I/O cancellation primitive today. Aborting a queued future uses the same mechanism:
`JoinHandle::abort` marks the task aborted, drops the task future on its owning runtime thread, and
wakes any joiner. Awaiting a `JoinHandle<T>` yields `Result<T, JoinError>`; an aborted task resolves
to `Err(JoinError::Aborted)` and a task that panicked while polled resolves to
`Err(JoinError::Panicked)` (`src/platform/runtime_shared/future_task.rs`,
`src/platform/runtime_shared/handles.rs`).

Dropping a `CompletionFuture`:

- Sets `interested = false` so later completions do not store a result or queue a wake
  (`src/op/completion.rs`).
- Clears any stored result and waker.
- If the operation is unfinished and has a cancel callback, runs that callback.
- Linux cancel callbacks submit `IORING_OP_ASYNC_CANCEL` through the driver
  (`src/platform/linux/driver.rs`,
  `src/sys/linux/fs.rs`, `src/sys/linux/net.rs`).
- Windows cancel callbacks call `CancelIoEx(handle, overlapped)`; the aborted operation's
  completion packet still arrives and reclaims the operation context
  (`src/sys/windows/overlapped.rs`).
- If no cancel callback exists because submission failed before registration, the future decrements
  the pending operation count directly (`src/op/completion.rs`).

There is no separate cancellation plumbing for in-flight I/O. Dropping an async task cascades normal
Rust `Drop` through the suspended future tree; any in-flight operation future dropped by that cascade
runs the same completion cancel callback and, on Linux, submits `IORING_OP_ASYNC_CANCEL`.

What Drop does not mean:

- It does not guarantee the kernel or OS operation stopped.
- The operation may still complete after the user-visible future is dropped.
- The runtime discards that result because `interested` is false.

Implication for buffer ownership:

- Any buffer the kernel may read from or write into must outlive the I/O even after Drop returns.
- The current implementation satisfies this by moving owned staging buffers into completion/cancel
  guards as described below.

An explicit `CancellationToken` is a candidate post-0.1 addition so cancellation can be expressed
without relying only on Drop; tracked in the project's GitHub issues.

# I/O buffer ownership rules

The public API uses borrowed buffers:

- `File::read(&mut [u8])`, `TcpStream::read(&mut [u8])`, `UdpSocket::recv(&mut [u8])`, and
  `Stdin::read(&mut [u8])` return the byte count written into the caller's buffer.
- `File::write(&[u8])`, `TcpStream::write(&[u8])`, and UDP send APIs return the byte count consumed.
- `File::read_to_end` and `File::read_to_string` are convenience helpers that allocate internally.

For v1, Linux does **not** point io_uring SQEs at arbitrary caller-owned `&mut [u8]` memory. That
would be unsound on Drop: after the future is dropped, the caller's borrow ends, but the kernel may
still write until the original CQE or an acknowledged `IORING_OP_ASYNC_CANCEL`. Instead, the runtime
uses a conservative staging model:

- Read operations allocate an internal boxed byte buffer, submit that stable allocation to io_uring,
  and copy the completed bytes into the caller's slice before returning.
- Write operations copy the caller's slice into an internal owned buffer before submission so the
  kernel can keep reading from it after the user-visible future is dropped.
- On normal completion, the operation callback drops the internal buffer after mapping the CQE.
- On Drop before completion, the cancel callback submits `IORING_OP_ASYNC_CANCEL` and detaches a
  guard into the Linux driver's `pending_cancel_buffers` map, keyed by the original operation token.
  `pending_cancel_tokens` maps the cancel SQE token back to that original token. The driver drops
  guards when either the original operation CQE or the cancel CQE arrives.

On top of the staging model, the concrete I/O types make **reads cancel-safe** by stashing the
in-flight operation on the object rather than in the transient future: `TcpStream`, `UnixStream`,
`File`, `Stdin`, and the child pipes hold their `pending_read`, and a read that completes with more
bytes than the current caller buffer retains the surplus in a `ReadOverflow` buffer served to the
next read — a dropped read future therefore loses nothing. Writes stash `pending_write` the same way
but record the submitted buffer's identity; a re-poll presenting a *different* buffer while a write
is in flight is rejected rather than misreporting that write's byte count against the wrong buffer
(`src/io/mod.rs`, `src/net/mod.rs`).

This is sound for arbitrary borrowed buffers, but it is not zero-copy. A hot-path API that transfers
ownership of a stable allocation to the operation (tokio-uring style) and registered buffers are
tracked in the project's GitHub issues.

# Subprocesses

On Linux, child process exit is represented as fd readiness. `Command::spawn` opens a pidfd for the
child, and `Child::wait` parks on `wait_readable(pidfd)`, which submits `IORING_OP_POLL_ADD` through
the thread's io_uring driver before collecting the status with `waitpid`
(`src/sys/linux/process.rs`, `src/sys/linux/fd.rs`). There is no SIGCHLD handler and no blocking-pool
offload for child exit.

On Windows, `Child::wait` parks the process handle on the OS wait-thread pool with
`RegisterWaitForSingleObject`, whose callback completes a runtime completion; on macOS, the wait
registers `EVFILT_PROC` and polls it (`src/sys/windows/process.rs`, `src/sys/macos/process.rs`).

Pipes attached to child stdin/stdout/stderr use the same platform byte-stream paths as other fds:
Linux goes through the runtime-owned-buffer I/O path plus readiness where needed, macOS uses the
existing kqueue/blocking-pool split, and Windows adopts the overlapped named-pipe parent ends and
drives them through the completion port.

# Driver abstraction

The scheduler, timer heap, `JoinHandle`, `FutureTask`, `ThreadShared`, macrotask queues, and the
task waker live **once** in `src/platform/runtime_shared/` and are platform-agnostic. Platform state
is fully type-erased: `ThreadState` holds the driver as `Box<dyn DriverBackend>` and the
cross-thread notifier as `Box<dyn Notifier>` (`src/platform/runtime_shared/driver_backend.rs`).
Each platform's `runtime.rs` is a thin shim that implements the tiny `Runtime` glue trait (mint a
driver/notifier pair; read the monotonic clock) on a marker type and re-exports the generic public
functions with that type fixed.

The backends behind the trait:

- Linux: one `io_uring` ring per runtime thread, used for timers, cross-thread wake notifications
  (`IORING_OP_MSG_RING`), fs ops, network ops, and fd readiness (`src/platform/linux/driver.rs`).
- macOS `aarch64`: `kqueue` for the wait/wake path, timers, and fd readiness, plus a wake pipe;
  filesystem work is offloaded to the blocking pool (`src/platform/macos_aarch64/driver.rs`,
  `src/sys/macos/fs.rs`).
- Windows: an I/O completion port for the wait/wake path and for dispatching overlapped
  operation completions; the runtime timer is a waitable-timer APC that interrupts the
  alertable port wait, and work with no overlapped form is offloaded to the blocking pool
  (`src/platform/windows/driver.rs`, `src/sys/windows/`, `docs/WINDOWS.md`).
- Driver-specific entry points that the shared scheduler does not know about (e.g. Linux
  `cancel_operation`) are reached by downcasting through `DriverBackend::as_any` in the platform
  shim.

## Feature probing

On Linux x86_64, `Driver::create_driver` initializes an `io_uring` ring and records the
process-wide `IORING_REGISTER_PROBE` result from `src/platform/linux/uring.rs`.
The supported-op bitmap is cached behind a `OnceLock` because kernel opcode support cannot change
under a running process, and probing once per runtime thread would waste syscalls.

If `io_uring_setup(2)` reports that io_uring is unavailable (`ENOSYS`) or blocked (`EPERM`, usually
seccomp), driver creation returns an `io::ErrorKind::Unsupported` with a named error value so callers
can choose a fallback backend. If `IORING_REGISTER_PROBE` itself is unavailable on very old kernels,
the driver logs one warning during cache initialization and uses a permissive bitmap; submission is
then allowed to preserve compatibility with kernels that support io_uring but not probing. When a
probe bitmap is available, `submit_operation` rejects unsupported `IORING_OP_*` values with
`io::ErrorKind::Unsupported` before the SQE reaches the kernel.

# Platform parity matrix

| Capability | Linux io_uring path | macOS path | Windows path |
| --- | --- | --- | --- |
| open | `IORING_OP_OPENAT` | blocking pool (`std::fs::OpenOptions`) | blocking pool (`CreateFileW` + `FILE_FLAG_OVERLAPPED`), then port association |
| read | `IORING_OP_READ` into guarded internal buffer, then copy into caller slice | blocking pool (`read`/`pread`) | overlapped `ReadFile` at explicit offsets through the port |
| write | `IORING_OP_WRITE` from guarded internal buffer | blocking pool (`write`/`pwrite`) | overlapped `WriteFile` at explicit offsets through the port |
| metadata | `IORING_OP_STATX` | blocking pool (`metadata`/`fstat`) | blocking pool (`GetFileInformationByHandle`) |
| sync | `IORING_OP_FSYNC` | blocking pool (`fsync`/`F_FULLFSYNC`) | blocking pool (`FlushFileBuffers`) |
| set_len | `IORING_OP_FTRUNCATE` (6.9+), inline `ftruncate(2)` fallback | blocking pool (`ftruncate`) | blocking pool (`SetFileInformationByHandle`) |
| try_clone | inline `fcntl(F_DUPFD_CLOEXEC)` (never blocks) | blocking pool | inline `DuplicateHandle` (never blocks) |
| read_dir | offloaded streaming producer (`getdents` can block, no io_uring opcode) | blocking pool producer | blocking pool producer |
| close | synchronous `close(2)` via `OwnedFd` `Drop` | synchronous `close(2)` via `OwnedFd` `Drop` | synchronous `CloseHandle`/`closesocket` via owned-handle `Drop` (an awaitable close is tracked in the project's GitHub issues for every platform) |
| network ops | `io_uring` first; non-blocking control ops fall back inline, data-path ops fall back to a non-blocking readiness path (`IORING_OP_POLL_ADD`) on unsupported kernels — never the blocking pool | `kqueue` readiness plus synchronous nonblocking socket calls | overlapped `ConnectEx`/`AcceptEx`/`WSASend`/`WSARecv`/`WSASendTo`/`WSARecvFrom`; control ops inline |
| Unix domain sockets | stream/datagram APIs reuse guarded send/recv paths plus readiness for path-addressed ops | stream/datagram APIs use the same guarded send/recv and readiness path | not provided (tracked in the project's GitHub issues) |
| stdin | Linux tries `IORING_OP_READ`, then per-call blocking fallback | blocking fallback path | blocking pool (console handles cannot overlap) |
| child exit | pidfd readiness via `IORING_OP_POLL_ADD`; no SIGCHLD handler or blocking-pool offload | process wait backend | `RegisterWaitForSingleObject` on the process handle (OS wait-thread pool) |
| wait_readable | `IORING_OP_POLL_ADD` | `kqueue` `EVFILT_READ` one-shot | not provided (readiness has no IOCP analogue; `runite::fd` is Unix-only) |

Notes:

- Linux fs `read_dir` streams from the blocking pool (`getdents` can block on disk and has no
  io_uring opcode); `try_clone` runs the non-blocking `fcntl(F_DUPFD_CLOEXEC)` inline on the
  event-loop thread rather than offloading (`src/sys/linux/fs.rs`).
- Linux network operations use io_uring first. When an opcode is unsupported on the running
  kernel, the *non-blocking* control operations (socket, bind, listen, shutdown, close, dup) run
  inline on the event loop, and the *data-path* operations that can block (connect, accept, send,
  recv, datagram recv) fall back to a **readiness path** — they mark the fd non-blocking and park
  on `IORING_OP_POLL_ADD` readiness (`wait_readable`/`wait_writable`) rather than offloading to
  the blocking pool, mirroring the macOS and Unix-domain-socket model (`src/sys/linux/net.rs`).
  The blocking pool is reserved for genuinely synchronous-only work (DNS resolution via
  `getaddrinfo`, `read_dir`/`getdents`). On a modern kernel the io_uring completion path always
  wins; the readiness fallback is validated by a direct unit test that exercises it explicitly.
- macOS has no io_uring equivalent. Its filesystem backend is entirely blocking-pool-based
  (`src/sys/macos/fs.rs`).
- macOS network behavior is readiness-driven, not completion-driven; performance characteristics
  differ substantially from Linux io_uring (`src/sys/macos/net.rs`).
- Windows is completion-driven like Linux: every overlapped submission produces exactly one
  completion packet, and the packet context owns the staging buffers, which is what makes the
  buffer-ownership-across-Drop rules hold without a separate cancel-guard map
  (`src/sys/windows/overlapped.rs`, `docs/WINDOWS.md`).
- Windows child stdio pipes are the overlapped named-pipe parent ends std creates for
  `Stdio::piped`; the backend adopts them and drives them like any other overlapped handle
  (`src/sys/windows/process.rs`).

# Cross-thread completion path

Current path:

1. A backend creates a `CompletionFuture`/`CompletionHandle` pair for the current thread
   (`src/op/completion.rs`).
2. The owner is captured as a `ThreadHandle`.
3. On completion, `CompletionHandle::finish` stores the result if `interested` is still true and
   calls `CompletionState::queue_wake` (`src/op/completion.rs`).
4. `queue_wake` queues the wake on the owner with `ThreadHandle::queue_internal_wake` — the
   capacity-bypassing enqueue, because a wake whose result is already stored must not be droppable
   by user-queue backpressure (`src/op/completion.rs`).
5. The enqueue locks the target remote queue, pushes the task, and calls
   `ThreadShared::notify` (`src/platform/runtime_shared/state.rs`).
6. On Linux, the notifier submits `IORING_OP_MSG_RING` to the target ring
   (`src/platform/linux/driver.rs`,
   `src/platform/linux/uring.rs`).
7. The target driver receives the wake CQE, records a pending wake, and the scheduler drains remote
   tasks (`src/platform/linux/driver.rs`,
   `src/platform/runtime_shared/scheduler.rs`).

This adds a syscall per wake on Linux. That is acceptable for cross-thread completions, where there
is no cheaper way to wake the owner reliably.

Same-thread completions short-circuit this path: `CompletionState::queue_wake` calls
`ThreadHandle::is_current` (`src/platform/runtime_shared/handles.rs`) — which
`Arc::ptr_eq`s the handle's `ThreadShared` against the current thread's installed state — and on a
match enqueues directly per the completion's wake class: a *local macrotask* for I/O completions, a
microtask for in-process resolutions. `is_current` returns `false` when the caller has no runtime
state installed (e.g. a blocking-pool worker), so non-runtime threads safely fall through to the
remote path. Only true cross-thread completions pay for the remote queue and `MSG_RING`.

# Safety invariants

Load-bearing invariants:

- Runtime TLS uses `thread_local!(static CURRENT_THREAD: ...)` via the scoped
  accessor `with_current_thread` (`src/platform/runtime_shared/state.rs`).
- Accessors are scoped: `with_current_thread(|state| ...)` and
  `try_with_installed_thread`. No `'static` references to `ThreadState` escape
  the runtime crate; downstream code never holds a borrow across an `await`.
- The runtime crate itself is **stable-Rust clean** — no `#![feature(...)]`. `runite` does
  not depend on any nightly-only crate; it builds on stable Rust.
- `TimeoutHandle`/`IntervalHandle` carry a process-wide generation token
  (`NEXT_GENERATION: AtomicU64`) so stale handles from a destroyed
  `ThreadState` can never false-match a freshly-installed one
  (`src/platform/runtime_shared/handles.rs`).
- `IoUring` has `unsafe impl Send` with a precise safety comment
  (`src/platform/linux/uring.rs`): the ring is moved
  into `Driver` during construction and remains pinned to that runtime thread;
  the submitter TLS pointer is per-thread.
- io_uring SQ tail and CQ head publication use real `atomic::fence(Release)`
  and `atomic::fence(Acquire)` around the kernel-visible boundaries, and a failed
  `io_uring_enter` rolls the SQ tail back so a stale SQE can never reference freed buffers
  (`src/platform/linux/uring.rs`).
- The task `Waker` honors `std`'s `Send + Sync` contract: it carries an `Arc` payload (owner
  handle + task id) and never touches the `!Send` task from a foreign thread; wakes resolve
  through the owner thread's task registry (`src/platform/runtime_shared/future_task.rs`).
- Cross-thread notifiers own a **dup** of their target fd (`OwnedFd`), so a notify racing the
  target driver's teardown can only ever reach the original, draining ring/pipe — never a
  recycled fd number (`src/platform/linux/driver.rs`, `src/platform/macos_aarch64/driver.rs`).
- The Linux driver watches the CQ overflow counter and the `FEAT_NODROP`/`MSG_RING` capability
  bits, warning (or erroring) instead of silently losing completions
  (`src/platform/linux/uring.rs`).

# What this runtime is NOT (and the reasons)

- Not a general-purpose high-throughput server runtime.
  - No registered buffers, fixed files, vectored I/O, or splice; these are tracked in the
    project's GitHub issues, not 0.1 goals.
  - The design optimizes for interactive applications and deterministic scheduling first.
- Not a multi-threaded work-stealing runtime.
  - Each runtime thread owns its own queues and driver.
  - Cross-thread work is explicit through `ThreadHandle::queue_macrotask`.
  - This preserves deterministic UI/reactive ordering.
- Not `Send`-future based.
  - Futures queued with `spawn` only need `Future + 'static`, not `Send`.
  - `JoinHandle`/`AbortHandle` are `!Send` by design; a task is joined on its own thread. (Timer
    handles, by contrast, are sendable tokens whose cross-thread cancel is a documented no-op.)
- Nightly-toolchain-free in `runite` itself.
  - The runtime crate compiles on stable Rust with no `#![feature(...)]`.

# Glossary

microtask
: A thread-local continuation queue drained fully before one macrotask runs. Used for same-thread
  future wakeups and Promise-like continuation semantics.

macrotask
: A local or remote event callback queue. Used for I/O completions today, timer callbacks, worker
  exits, host callbacks, and cross-thread work.

driver
: The per-runtime-thread platform event backend. Linux uses `io_uring`; macOS uses `kqueue` plus a
  wake pipe and blocking pools for fs; Windows uses an I/O completion port plus a waitable timer.

ring
: A Linux `io_uring` instance with submission and completion queues. Each Linux runtime thread owns
  its own ring.

notifier
: A cloneable handle that wakes another runtime thread's driver. Linux uses `MSG_RING`; macOS writes
  to a wake pipe; Windows posts a reserved-key packet with `PostQueuedCompletionStatus`.

ThreadHandle
: A cloneable, `Send`-capable handle for queueing a `Send` macrotask onto a specific runtime
  thread.

JoinHandle
: The `!Send` handle returned by `spawn`; awaiting it yields `Result<T, JoinError>`, with
  `Err(JoinError::Aborted)` after `abort` and `Err(JoinError::Panicked)` if the task's poll
  unwound.

completion
: The `CompletionFuture`/`CompletionHandle` pair that bridges backend operation completion to a
  future waker on the owning runtime thread.

CQE
: Completion Queue Entry. Kernel/driver result record for a submitted operation.

SQE
: Submission Queue Entry. Kernel-visible request record for an operation.

MSG_RING
: Linux io_uring operation used to send a wake message to another ring.

parking/wait
: Blocking the runtime thread in its driver (`driver.wait()`) because no work is ready but timers,
  children, or async operations remain.

# Async I/O traits

The runtime exposes crate-local `io::AsyncRead` and `io::AsyncWrite` traits as the byte-stream
polling primitives for borrowed-buffer I/O. `File`, `TcpStream`, and `Stdin` implement `AsyncRead`;
`File` and `TcpStream` implement `AsyncWrite`. `UdpSocket` intentionally does not implement these
traits because datagram sockets preserve message boundaries rather than stream semantics. The
crate-local `io::Stream` trait provides the same poll-based shape for asynchronous item streams;
directory iterators and MPSC receivers implement it, and `AsyncReadExt::lines` adapts any
`AsyncRead` into a `Stream<Item = io::Result<String>>`.

The extension traits in `io` provide concrete future adapters (`Read`, `ReadExact`, `ReadToEnd`,
`Write`, `WriteAll`, `Flush`, `Close`, `Next`, `Collect`, and `ForEach`) and stream adapters (`Map`,
`Filter`, `Take`, and `Skip`) that repeatedly drive the poll methods. Implementations submit
operations with runtime-owned buffers, preserving the phase-7 cancellation rule: after a
borrowed-buffer operation returns `Pending`, the kernel-visible buffer is owned by the runtime and is
kept alive by the existing completion/cancel guard path until the original completion or cancel CQE
arrives.

With the optional `futures-compat` feature, `io::compat::Compat<T>` adapts runtime async I/O types to
`futures_io` traits, and `io::compat::FuturesCompat<T>` adapts `futures_io` types back to the runtime
traits.