#IO Reactor API (401 io api.h)
#define FIO_IO
#include FIO_INCLUDE_FILEFIO_IO adds the facil.io evented IO reactor, connection handles, protocol
callbacks, listener/client helpers, TLS transport hooks, per-connection
lifetime storage, timers, and optional async worker queues.
See [./400 io-overview.md](./400 io-overview.md) for the full IO / IPC / PubSub / HTTP stack. TLS backends are covered by [./405 openssl.md](./405 openssl.md) and [./405 tls13.md](./405 tls13.md). Socket helpers are documented in [./004 sock.md](./004 sock.md), timers and queues in [./102 queue.md](./102 queue.md), and lifecycle callbacks in [./004 state callbacks.md](./004 state callbacks.md).
#Configuration Macros
Define before including the header to override defaults.
| Macro | Default | Meaning |
|---|---|---|
FIO_IO_BUFFER_PER_WRITE |
65536U |
Stack buffer size used during write events. |
FIO_IO_THROTTLE_LIMIT |
2097152U |
on_data is throttled while outgoing backlog is large. |
FIO_IO_TIMEOUT_MAX |
300000 |
Maximum and default connection timeout, in milliseconds. |
FIO_IO_SHUTDOWN_TIMEOUT |
15000 |
Hard timeout for the reactor shutdown loop, in milliseconds. |
FIO_IO_COUNT_STORAGE |
1 in DEBUG, else 0 |
Enables IO byte-count storage when compiled in. |
#Core Types
| Type | Role |
|---|---|
fio_io_s |
Opaque connection handle. Use it instead of raw file descriptors inside protocol callbacks. |
fio_io_protocol_s |
Shared protocol callback table for a family of connections. Usually static / global and zero-initialized. |
fio_io_functions_s |
Optional transport vtable used to override socket IO, mainly for TLS. |
fio_io_tls_s |
Reference-counted TLS settings object consumed by transport backends. |
fio_io_listener_s |
Listener handle returned by fio_io_listen. |
fio_pubsub_msg_s |
Message delivered to on_pubsub callbacks; defined by the Pub/Sub module. |
fio_io_async_s |
Async worker queue attached to the reactor lifecycle. |
#Reactor Lifecycle and State
#Start / stop
void fio_io_start(int workers);
void fio_io_stop(void);
void fio_io_add_workers(int workers);
void fio_io_restart(int workers);
void fio_io_restart_on_signal(int signal);
size_t fio_io_shutdown_timeout(void);
size_t fio_io_shutdown_timeout_set(size_t milliseconds);fio_io_start(workers) starts the reactor and blocks until shutdown. A positive
workers value forks worker processes. fio_io_stop() asks the reactor to
stop. fio_io_add_workers and fio_io_restart are cluster/process helpers;
fio_io_restart_on_signal binds hot restart to a signal.
fio_io_shutdown_timeout_set changes the hard shutdown grace period and returns
the value that will be used.
#State queries
int fio_io_is_running(void);
int fio_io_is_master(void);
int fio_io_is_worker(void);
uint16_t fio_io_workers(int workers_requested);
int fio_io_pid(void);
int fio_io_root_pid(void);
int64_t fio_io_last_tick(void);
int64_t fio_io_last_tick_time(void);fio_io_last_tick() is the cached millisecond value from the last reactor poll.
fio_io_last_tick_time() is the cached wall-clock millisecond timestamp,
useful for approximate log and HTTP date values.
#Protocols
typedef struct fio_io_protocol_s fio_io_protocol_s;
struct fio_io_protocol_s {
struct { /* reserved; initialize to zero */ } reserved;
void (*on_attach)(fio_io_s *io);
void (*on_data)(fio_io_s *io);
void (*on_ready)(fio_io_s *io);
void (*on_shutdown)(fio_io_s *io);
void (*on_timeout)(fio_io_s *io);
void (*on_close)(void *iobuf, void *udata);
void (*on_pubsub)(fio_pubsub_msg_s *msg);
void (*on_user1)(fio_io_s *io, void *user_data);
void (*on_user2)(fio_io_s *io, void *user_data);
void (*on_user3)(fio_io_s *io, void *user_data);
void (*on_reserved)(fio_io_s *io, void *user_data);
fio_io_functions_s io_functions;
uint32_t timeout;
uint32_t buffer_size;
};Protocol structs define connection behavior. They are normally static/global and
shared by many IO handles. Initialize the full struct to zero and never mutate
reserved.
Callback flow:
on_attachafter an IO is attached to the protocol.on_datawhen incoming data can be read.on_readyafter all pending writes drain.on_timeoutwhen the connection timeout is reached.on_shutdownimmediately before reactor shutdown closes the connection.on_close(iobuf, udata)after the connection is closed.
All protocol callbacks return void. Use fio_io_close,
fio_io_close_now, or protocol state to control what happens next. Set
on_timeout = fio_io_touch for connections where idle timeout should be
ignored.
timeout is in milliseconds, capped by FIO_IO_TIMEOUT_MAX; 0 means the
maximum/default. buffer_size controls the per-connection protocol buffer
returned by fio_io_buffer.
#Iterating protocol IOs
size_t fio_io_protocol_each(fio_io_protocol_s *protocol,
void (*task)(fio_io_s *, void *udata2),
void *udata2);Runs task for each IO using protocol. Call only from the main IO thread;
use fio_io_defer when scheduling from another thread.
#Transport Functions / TLS Hooks
typedef struct fio_io_functions_s fio_io_functions_s;
struct fio_io_functions_s {
void *(*build_context)(fio_io_tls_s *tls, uint8_t is_client);
void (*free_context)(void *context);
void (*start)(fio_io_s *io);
ssize_t (*read)(fio_socket_i fd, void *buf, size_t len, void *context);
ssize_t (*write)(fio_socket_i fd, const void *buf, size_t len, void *context);
int (*flush)(fio_socket_i fd, void *context);
void (*finish)(fio_socket_i fd, void *context);
void (*cleanup)(void *context);
};The vtable lets a protocol replace plain socket IO with a transport layer such
as TLS. Functions receive the file descriptor but must not keep it or defer fd
operations; the fio_io_s handle is the long-lived identity.
Return conventions:
readbehaves like non-blockingread(2).writereturns plaintext bytes accepted (N > 0),0for zero-length input, or-1witherrnoset. If data was transformed (for example, encrypted), return success even if the underlying socket later blocks; useflushfor pending transformed bytes.flushreturns0only when all internal output is empty. Non-zero (N > 0or-1) means pending data remains and the reactor should keep watching for writability.finishruns before closing after output is sent;cleanupreleases the per-connection transport context after close.
Set default TLS transport functions before the reactor starts:
fio_io_functions_s fio_io_tls_default_functions(fio_io_functions_s *funcs);Passing NULL returns the current default. Passing a pointer sets a new
default and returns the selected functions.
#Listening and Connecting
#fio_io_listen
typedef struct fio_io_listen_args_s {
const char *url;
fio_io_protocol_s *protocol;
void *udata;
fio_io_tls_s *tls;
void (*on_start)(fio_io_protocol_s *protocol, void *udata);
void (*on_stop)(fio_io_protocol_s *protocol, void *udata);
fio_io_async_s *queue_for_accept;
uint8_t on_root;
uint8_t hide_from_log;
} fio_io_listen_args_s;
fio_io_listener_s *fio_io_listen(fio_io_listen_args_s args);
#define fio_io_listen(...) fio_io_listen((fio_io_listen_args_s){__VA_ARGS__})Creates a network listener and returns a self-destructible listener handle, or
NULL on error. The default URL is tcp://0.0.0.0:3000.
Call it before fio_io_start() for normal server setup. The header notes that
this schedules a task and should not be called from PRE_START or ON_START
state callbacks.
TLS can be supplied directly with .tls or inferred from the URL query:
fio_io_listen(.url = "0.0.0.0:3000/?tls", .protocol = &MY_PROTOCOL);
fio_io_listen(.url = "0.0.0.0:3000/?tls=./certs/", .protocol = &MY_PROTOCOL);
fio_io_listen(.url = "0.0.0.0:3000/?key=./key.pem&cert=./cert.pem",
.protocol = &MY_PROTOCOL);.tls ownership is moved to the listener. If you need to share a TLS settings
object, duplicate it first with fio_io_tls_dup and free your own references
with fio_io_tls_free.
Listener helpers:
void fio_io_listen_stop(fio_io_listener_s *l);
fio_io_protocol_s *fio_io_listener_protocol(fio_io_listener_s *l);
void *fio_io_listener_udata(fio_io_listener_s *l);
void *fio_io_listener_udata_set(fio_io_listener_s *l, void *new_udata);
fio_buf_info_s fio_io_listener_url(fio_io_listener_s *l);
int fio_io_listener_is_tls(fio_io_listener_s *l);#fio_io_connect
typedef struct {
const char *url;
fio_io_protocol_s *protocol;
void (*on_failed)(fio_io_protocol_s *protocol, void *udata);
void *udata;
fio_io_tls_s *tls;
uint32_t timeout;
} fio_io_connect_args_s;
fio_io_s *fio_io_connect(fio_io_connect_args_s args);
#define fio_io_connect(url_, ...) \
fio_io_connect((fio_io_connect_args_s){.url = url_, __VA_ARGS__})Connects to url as a client and returns the IO handle or NULL. The URL may
contain TLS hints. timeout defaults to 30 seconds. on_failed is the cleanup
hook for failed connection attempts; established connections use the protocol
callbacks.
#IO Handles and Operations
fio_io_s *fio_io_attach_fd(fio_socket_i fd,
fio_io_protocol_s *protocol,
void *udata,
void *tls);
fio_io_protocol_s *fio_io_protocol_set(fio_io_s *io,
fio_io_protocol_s *protocol);
fio_io_protocol_s *fio_io_protocol(fio_io_s *io);
void *fio_io_buffer(fio_io_s *io);
size_t fio_io_buffer_len(fio_io_s *io);
void *fio_io_udata_set(fio_io_s *io, void *udata);
void *fio_io_udata(fio_io_s *io);
void *fio_io_tls_set(fio_io_s *io, void *tls);
void *fio_io_tls(fio_io_s *io);
fio_socket_i fio_io_fd(fio_io_s *io);
void fio_io_touch(fio_io_s *io);
size_t fio_io_read(fio_io_s *io, void *buf, size_t len);
void fio_io_close(fio_io_s *io);
void fio_io_close_now(fio_io_s *io);
fio_io_s *fio_io_dup(fio_io_s *io);
void fio_io_free(fio_io_s *io);
void fio_io_suspend(fio_io_s *io);
void fio_io_unsuspend(fio_io_s *io);
int fio_io_is_suspended(fio_io_s *io);
int fio_io_is_open(fio_io_s *io);
size_t fio_io_backlog(fio_io_s *io);
void fio_io_noop(fio_io_s *io);fio_io_attach_fd adopts a valid socket into the reactor. It returns NULL on
error. The returned pointer must not be used arbitrarily; IO handles are valid
inside proper callbacks and scheduled tasks. If code must keep a handle outside
that context, call fio_io_dup and later fio_io_free. These two functions are
thread-safe.
fio_io_protocol_set installs a new protocol. NULL is a valid "only-write"
protocol. The accessor may temporarily return the old protocol while the change
is being attached.
fio_io_read returns bytes read. 0 is not EOF by itself; it can mean no data
was available on the non-blocking socket. Use close callbacks for final cleanup.
fio_io_close closes after scheduled data is sent. fio_io_close_now closes as
soon as possible. fio_io_suspend / fio_io_unsuspend control future
on_data delivery, and fio_io_backlog reports the approximate outgoing byte
count.
#Writing
typedef struct {
void *buf;
intptr_t fd;
size_t len;
size_t offset;
void (*dealloc)(void *);
uint8_t copy;
} fio_io_write_args_s;
void fio_io_write2(fio_io_s *io, fio_io_write_args_s args);
#define fio_io_write2(io, ...) \
fio_io_write2(io, (fio_io_write_args_s){__VA_ARGS__})
#define fio_io_write(io, buf_, len_) \
fio_io_write2(io, .buf = (buf_), .len = (len_), .copy = 1)
#define fio_io_sendfile(io, source_fd, offset_, bytes) \
fio_io_write2((io), .fd = (source_fd), .offset = (size_t)(offset_), .len = (bytes))fio_io_write2 schedules buffered output. If .buf is used and .copy is
non-zero, the data is copied immediately. If .copy == 0 and .dealloc is set,
the IO layer takes ownership of the buffer and calls dealloc later. If
.dealloc == NULL, the buffer is not freed by the IO layer.
For file output, pass .fd; .len == 0 means send the whole file. The
fio_io_sendfile helper closes source_fd after sending or on error.
#Scheduling Tasks and Timers
void fio_io_defer(void (*task)(void *, void *), void *udata1, void *udata2);
void fio_io_run_every(fio_timer_schedule_args_s args);
#define fio_io_run_every(...) \
fio_io_run_every((fio_timer_schedule_args_s){__VA_ARGS__})
fio_queue_s *fio_io_queue(void);fio_io_defer schedules a task on the IO reactor queue and is thread-safe.
Use it to move work from other threads back to the IO thread.
fio_io_run_every schedules a timer. It uses fio_timer_schedule_args_s from
the queue/timer API:
fnreturns non-zero to stop the timer.udata1andudata2are passed to callbacks.on_stopruns when the timer ends.everyis the interval in milliseconds.repetitionsis the number of runs;-1means indefinitely.
fio_io_queue() returns the reactor queue.
#Connection Environment
The IO environment links named objects to a connection lifetime. When the IO is
closed, stored on_close callbacks run automatically.
typedef struct {
intptr_t type;
fio_buf_info_s name;
void *udata;
void (*on_close)(void *data);
uint8_t const_name;
} fio_io_env_set_args_s;
typedef struct {
intptr_t type;
fio_buf_info_s name;
} fio_io_env_get_args_s;
void *fio_io_env_get(fio_io_s *io, fio_io_env_get_args_s args);
void fio_io_env_set(fio_io_s *io, fio_io_env_set_args_s args);
int fio_io_env_unset(fio_io_s *io, fio_io_env_get_args_s args);
int fio_io_env_remove(fio_io_s *io, fio_io_env_get_args_s args);
#define fio_io_env_get(io, ...) \
fio_io_env_get(io, (fio_io_env_get_args_s){__VA_ARGS__})
#define fio_io_env_set(io, ...) \
fio_io_env_set(io, (fio_io_env_set_args_s){__VA_ARGS__})
#define fio_io_env_unset(io, ...) \
fio_io_env_unset(io, (fio_io_env_get_args_s){__VA_ARGS__})
#define fio_io_env_remove(io, ...) \
fio_io_env_remove(io, (fio_io_env_get_args_s){__VA_ARGS__})type and name together identify an entry. Negative type values are
reserved. If const_name is set, the name string must outlive the environment.
If io == NULL, entries are stored in the global environment; their on_close
callbacks run when the process exits. unset detaches without calling
on_close; remove detaches and calls on_close as if the connection closed.
#TLS Settings Helpers
fio_io_tls_s *fio_io_tls_new(void);
fio_io_tls_s *fio_io_tls_from_url(fio_io_tls_s *target_or_null, fio_url_s url);
fio_io_tls_s *fio_io_tls_dup(fio_io_tls_s *tls);
void fio_io_tls_free(fio_io_tls_s *tls);
fio_io_tls_s *fio_io_tls_cert_add(fio_io_tls_s *tls,
const char *server_name,
const char *public_cert_file,
const char *private_key_file,
const char *pk_password);
fio_io_tls_s *fio_io_tls_alpn_add(fio_io_tls_s *tls,
const char *protocol_name,
void (*on_selected)(fio_io_s *));
int fio_io_tls_alpn_select(fio_io_tls_s *tls,
const char *protocol_name,
size_t name_length,
fio_io_s *io);
fio_io_tls_s *fio_io_tls_trust_add(fio_io_tls_s *tls,
const char *public_cert_file);
uintptr_t fio_io_tls_cert_count(fio_io_tls_s *tls);
uintptr_t fio_io_tls_alpn_count(fio_io_tls_s *tls);
uintptr_t fio_io_tls_trust_count(fio_io_tls_s *tls);fio_io_tls_s stores backend-neutral TLS instructions. Add certificates for
SNI, ALPN protocol callbacks, and trusted certificates / CA stores. Passing
NULL certificate paths can request a backend-provided self-signed certificate;
passing NULL to fio_io_tls_trust_add asks the backend to use system trust.
Backend behavior is documented in the TLS backend docs.
fio_io_tls_alpn_add silently ignores a NULL protocol name and replaces a
NULL callback with a no-op. The first ALPN protocol added is the default.
#Iterating TLS settings
typedef struct fio_io_tls_each_s {
fio_io_tls_s *tls;
void *udata;
void *udata2;
int (*each_cert)(struct fio_io_tls_each_s *,
const char *server_name,
const char *public_cert_file,
const char *private_key_file,
const char *pk_password);
int (*each_alpn)(struct fio_io_tls_each_s *,
const char *protocol_name,
void (*on_selected)(fio_io_s *));
int (*each_trust)(struct fio_io_tls_each_s *, const char *public_cert_file);
} fio_io_tls_each_s;
int fio_io_tls_each(fio_io_tls_each_s args);
#define fio_io_tls_each(tls_, ...) \
fio_io_tls_each(((fio_io_tls_each_s){.tls = tls_, __VA_ARGS__}))Transport backends use fio_io_tls_each to consume the stored instructions.
Iterator callbacks return int; check the backend using them for any additional
meaning assigned to non-zero return values.
#Peer certificate inspection (mTLS)
int fio_io_peer_info_next(fio_io_s *io, fio_x509_cert_s *dest);Trust configured with fio_io_tls_trust_add always refers to peer
verification: a server context verifies (and requires) client certificates,
a client context verifies the server certificate. An empty trust list means
no peer verification; passing NULL to fio_io_tls_trust_add selects the
system trust store.
Once the handshake completed, iterate the peer's certificate chain (leaf first) to implement application level client certificate authentication and authorization:
fio_x509_cert_s cert = {0}; /* zeroed = new loop */
while (fio_io_peer_info_next(io, &cert) == 0) {
if (!cert.verified)
continue; /* chain failed TLS-level verification (or was skipped) */
/* authorize by identity, e.g.: cert.cn,
* cert.subject, or pin cert.fingerprint (32-byte SHA-256) */
}The iterator is stateless — the position is identified from dest alone: a
zeroed dest (der.buf == NULL) starts a new loop; otherwise iteration
continues at dest->chain_index + 1. To restart a loop, zero the struct.
Multiple loops may iterate the same connection concurrently.
Iteration is capped at 128 certificates (chain_index 0..127) as a
deep-nesting / DoS guard — longer chains end the loop with -1.
Passing a NULL dest returns -1. The function returns
-1 when the iteration is done or when peer information is unavailable (no
TLS, handshake incomplete, peer sent no certificate, or the X509 module
missing).
Each call parses the next certificate into a fio_x509_cert_s (defined in
the X509 module, 156 x509.h). All certificate fields point directly into
memory owned by the TLS backend — nothing is copied or allocated. The views
stay valid until the next fio_io_peer_info_next call (on ANY connection)
or connection close, whichever comes first; copy anything that must outlive
the loop.
The verified flag reflects the TLS backend's verification of the whole
chain and is identical for every certificate in the chain. Connections that
skipped verification (empty trust list) report verified == 0.
#Async Worker Queues
typedef struct fio_io_async_s fio_io_async_s;
#define FIO_IO_ASYN_INIT ((fio_io_async_s){0})
fio_queue_s *fio_io_async_queue(fio_io_async_s *q);
void fio_io_async_attach(fio_io_async_s *q, uint32_t threads);
#define fio_io_async(q_, ...) fio_queue_push((q_)->q, __VA_ARGS__)
void fio_io_async_every(fio_io_async_s *q, fio_timer_schedule_args_s args);
#define fio_io_async_every(async, ...) \
fio_io_async_every(async, (fio_timer_schedule_args_s){__VA_ARGS__})Async queues are for non-IO work that must not block the reactor. Allocate the
fio_io_async_s object with static or otherwise reactor-long lifetime and
initialize it with FIO_IO_ASYN_INIT or zeroes before attaching it.
static fio_io_async_s SLOW_TASKS = FIO_IO_ASYN_INIT;
int main(void) {
fio_io_async_attach(&SLOW_TASKS, 32);
fio_io_start(0);
}The queue starts and stops with the IO reactor. Use fio_io_async_queue when a
raw fio_queue_s * is needed, fio_io_async to push tasks, and
fio_io_async_every for timers on that async queue.
#Minimal Echo Server
#define FIO_IO
#include "fio-stl/include.h"
static void echo_on_data(fio_io_s *io) {
char buf[4096];
size_t len;
while ((len = fio_io_read(io, buf, sizeof(buf))))
fio_io_write(io, buf, len); /* copies stack buffer */
}
static void echo_on_timeout(fio_io_s *io) { fio_io_close(io); }
static fio_io_protocol_s ECHO = {
.on_data = echo_on_data,
.on_timeout = echo_on_timeout,
.timeout = 30000,
};
int main(void) {
fio_io_listen(.url = "tcp://0.0.0.0:3000", .protocol = &ECHO);
fio_io_start(0);
}#Common Lifetime Rules
- Treat
fio_io_s *as callback/task scoped unless you hold a reference withfio_io_dup; release it withfio_io_free. - Do not keep raw file descriptors received by transport callbacks.
- Protocol objects should be zero-initialized and live at least as long as any IO that uses them.
- Listener
.tlsis moved to the listener; duplicate TLS settings before sharing them elsewhere. fio_io_writecopies the buffer;fio_io_write2can copy, borrow, or take ownership depending on.copyand.dealloc.- Environment
on_closecallbacks are the preferred cleanup point for objects tied to a connection lifetime.