facil.io

#IO Reactor API (401 io api.h)

c
#define FIO_IO
#include FIO_INCLUDE_FILE

FIO_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

c
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

c
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

c
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:

  1. on_attach after an IO is attached to the protocol.
  2. on_data when incoming data can be read.
  3. on_ready after all pending writes drain.
  4. on_timeout when the connection timeout is reached.
  5. on_shutdown immediately before reactor shutdown closes the connection.
  6. 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

c
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

c
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:

  • read behaves like non-blocking read(2).
  • write returns plaintext bytes accepted (N > 0), 0 for zero-length input, or -1 with errno set. If data was transformed (for example, encrypted), return success even if the underlying socket later blocks; use flush for pending transformed bytes.
  • flush returns 0 only when all internal output is empty. Non-zero (N > 0 or -1) means pending data remains and the reactor should keep watching for writability.
  • finish runs before closing after output is sent; cleanup releases the per-connection transport context after close.

Set default TLS transport functions before the reactor starts:

c
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

c
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:

c
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:

c
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

c
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

c
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

c
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

c
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:

  • fn returns non-zero to stop the timer.
  • udata1 and udata2 are passed to callbacks.
  • on_stop runs when the timer ends.
  • every is the interval in milliseconds.
  • repetitions is the number of runs; -1 means 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.

c
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

c
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

c
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)

c
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:

c
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

c
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.

c
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

c
#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 with fio_io_dup; release it with fio_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 .tls is moved to the listener; duplicate TLS settings before sharing them elsewhere.
  • fio_io_write copies the buffer; fio_io_write2 can copy, borrow, or take ownership depending on .copy and .dealloc.
  • Environment on_close callbacks are the preferred cleanup point for objects tied to a connection lifetime.