xtc_aio(3)

---

xtc_aio(3)

async file I/O for fibers

XTC_AIO(3) Library Functions Manual XTC_AIO(3)

xtc_aio_pread, xtc_aio_pwrite, xtc_aio_preadv, xtc_aio_pwritev, xtc_aio_fsync, xtc_aio_fdatasyncasync file I/O for fibers

#include <xtc.h>
#include <xtc_aio.h>

int
xtc_aio_pread(int fd, void *buf, uint32_t len, int64_t off);

int
xtc_aio_pwrite(int fd, const void *buf, uint32_t len, int64_t off);

int
xtc_aio_preadv(int fd, const struct iovec *iov, int iovcnt, int64_t off);

int
xtc_aio_pwritev(int fd, const struct iovec *iov, int iovcnt, int64_t off);

int
xtc_aio_fsync(int fd);

int
xtc_aio_fdatasync(int fd);

These perform a file read, write, or data-sync that suspends the calling fiber and resumes it on completion, keeping the fiber's loop live for other work while the I/O is outstanding.

They are a . Write storage code once -- a buffer manager paging the B-tree, a WAL writer -- calling these as though true asynchronous I/O is always available, and it runs unchanged on every platform libxtc supports. Where the host has a native completion engine the call uses it; where it does not, the call uses a transparent fallback that presents the (the fiber parks, the loop keeps running other work, the call) returns bytes-transferred or a negative errno . A consumer never has to branch on the platform. Branching with #ifdef between a native-AIO and a non-AIO path is permitted if a consumer wants it, but is never required: one API that always appears to be AIO covers the common case.

The mechanism, chosen at run time, is:

  • On a loop whose I/O backend is io_uring, the operation is submitted natively (IORING_OP_READ, IORING_OP_WRITE, or IORING_OP_FSYNC); the fiber parks and the kernel's completion re-enqueues it. No worker thread is used.
  • On Windows (IOCP), () and () are native overlapped ()/() completions dequeued from the same completion port as socket events; xtc_aio_fsync() and xtc_aio_fdatasync() have no async form and take the offload path below.
  • On the kqueue backend (FreeBSD, macOS) the operation is submitted as POSIX AIO ((), (), ()) with completion delivered to the same kqueue via SIGEV_KEVENT and reaped as an EVFILT_AIO event -- no worker thread. A platform lacking either falls back to the offload path below.
  • On a readiness-only backend (epoll, poll, select, Solaris event ports, AIX) the operation is offloaded to the blocking thread pool (xtc_blocking(3)), because a regular file is not pollable -- a readiness poller cannot complete file I/O asynchronously, and a thread offload is the correct, behavior-preserving fallback elsewhere. The fiber still parks and the loop stays live, so the API behaves identically to the native path.
  • Off a loop entirely (no current fiber) the operation runs inline on the calling thread.

This is the layer that lets storage code use the fastest async file path a host offers without the caller choosing or knowing which one. In every case the call returns only once the operation has completed (or failed), so the caller may reuse buf immediately afterward.

The one behavioral caveat to size for: where the offload path is used, concurrency is bounded by the blocking-pool thread count (see xtc_blocking(3)), whereas the native engines admit many more in-flight ops. Code that issues a very high number of concurrent file ops should size the pool accordingly; correctness is identical either way.

() reads up to len bytes from fd at absolute offset off into buf; () writes len bytes from buf at off; () flushes the file's data and metadata durably; () flushes the data only (fdatasync semantics; the hot path for page and WAL flushes, and what) xtc_io(3)'s storage callers use. On macOS, which has no (), the data-only call falls back to (). None of them moves the file's seek pointer.

() and () are the async-fiber analogs of preadv(2) and pwritev(2): they read into, or write from, the iovcnt buffers described by iov at file offset off, as one positioned op, with the same fiber-parks / loop-stays-live semantics as the scalar calls. They exist for migration ergonomics: code that already assembles struct iovec arrays -- a WAL writer, PostgreSQL's / layer, any readv(2)/writev(2) consumer -- can move to libxtc without flattening its buffers or looping. Where the host has the op is submitted natively (IORING_OP_READV/WRITEV); elsewhere it offloads to the blocking pool (preadv(2)/pwritev(2) on a worker thread), identical observable behavior to the scalar API. iovcnt must be in [1, IOV_MAX]; out of range returns -EINVAL, so a caller cannot force an unbounded submission. These two are POSIX only -- Windows lacks preadv(2)/pwritev(2) and struct iovec, so they are not declared there; the scalar xtc_aio_pread()/xtc_aio_pwrite() remain available on every platform.

xtc_aio_pread() and xtc_aio_pwrite() return the number of bytes transferred (>= 0); a short read at end of file returns the partial count, not an error. xtc_aio_fsync() returns 0. On failure each returns a negative errno value (e.g. -EIO).

Call from a fiber running on a loop for the async path; off a loop the operation runs synchronously on the calling thread. The buffer must remain valid until the call returns, which is guaranteed simply by not returning from the call -- the fiber parks on this one operation's completion and arms no other waker, so the operation is never left in flight referencing a stack buffer that has gone away.

Page a 4 KiB block out and read it back, off the fiber's loop:

int n = xtc_aio_pwrite(fd, page, 4096, (int64_t)pid * 4096);
if (n != 4096) return -1;
if (xtc_aio_fsync(fd) != 0) return -1;
n = xtc_aio_pread(fd, page, 4096, (int64_t)pid * 4096);

xtc_io(3), xtc_blocking(3), xtc_loop(3)

The xtc project.

June 4, 2026 Debian

View the mdoc source