Small but capable: spawn/await/cancel fibers, fiber-blocking I/O (epoll/kqueue), bounded channels, pooled fiber stacks, and a global or explicit runtime API with deterministic scheduling.
- Fibers with explicit
async_yield. - Fiber-blocking I/O via epoll/kqueue.
- Global runtime by default; custom runtime optional.
- No hidden allocations in hot paths (fiber stacks are pooled).
- macOS: arm64, x86_64 (kqueue)
- Linux: x86_64 (epoll)
#include "async.h"
static void worker(void *user)
{
for (int i = 0; i <= 10; ++i) {
printf("%d ", i);
async_yield();
}
}
int main(void)
{
async_spawn(worker, NULL);
async_spawn(worker, NULL);
return async_run();
}async_run(); /* runs until idle */
async_spawn(worker, NULL); /* Will use the global runtime */
async_spawn_with(&rt, worker, NULL); /* _with functions allow specifying a custom runtime */
async_await(handle);
async_yield();Simple IO API for basic operations, read, write, accept, etc.
async_io_t io = {0};
async_io_register(&io, fd);
async_read(&io, buf, len, &n);
async_write(&io, buf, len, &n);
async_accept_io(&io, &client);
async_io_close(&io);See examples/ for usage.
I/O calls yield the current fiber until the fd is ready.
async_io_t must be zero-initialized before use:
async_io_t io = {0};
async_io_register(&io, fd);Read once from a registered fd:
static void reader(void *user)
{
async_io_t *io = (async_io_t *)user;
char buf[128];
size_t n = 0;
if (async_read(io, buf, sizeof(buf), &n) != ASYNC_OK) {
fprintf(stderr, "read failed\n");
}
}Write once to a registered fd:
static void writer(void *user)
{
async_io_t *io = (async_io_t *)user;
const char msg[] = "hello\n";
size_t n = 0;
if (async_write(io, msg, sizeof(msg) - 1, &n) != ASYNC_OK) {
fprintf(stderr, "write failed\n");
}
}Bounded pointer channels for fiber-to-fiber messaging.
async_channel_t ch;
async_global_init(NULL);
async_channel_init(&ch, async_global(), 64, 64);
async_channel_send(&ch, payload);
async_channel_recv(&ch, &payload);
async_channel_close(&ch);
async_channel_shutdown(&ch);Create a custom runtime
Example in examples/custom_runtime.c.
async_config_t cfg = {0};
async_runtime_t rt;
if (async_runtime_init(&rt, &cfg) != ASYNC_OK) {
return 1;
}
async_fiber_handle_t handle = async_spawn_with(&rt, worker, NULL);
if (handle.status != ASYNC_OK) {
async_runtime_shutdown(&rt);
return 1;
}
int result = async_runtime_run(&rt);
async_runtime_shutdown(&rt);
return result == ASYNC_OK ? 0 : 1;- task_capacity = 256
- fiber_capacity = 64
- fiber_stack_size = 64 * 1024
- max_events = 64
- Fiber stacks are guard-paged when allocated by the runtime pool. (User-provided stacks are not guarded.)
- x87/MMX/SSE state is preserved across fiber swaps by default. (can be disabled with
-DASYNC_FIBER_SAVE_X87=0.) - CET shadow stacks are supported ONLY on Linux x86_64 when enabled with
-DASYNC_FIBER_SHADOW_STACK=1.
make
make examples
make testsmake install PREFIX=/usr/local
make uninstall PREFIX=/usr/localexamples/counter_async.cexamples/await_basic.cexamples/io_pipe.cexamples/http_server.cexamples/custom_runtime.cexamples/channel_basic.c
- To validate guard pages during tests, set
ASYNC_TEST_GUARD_PAGES=1(triggers an intentional SIGSEGV).