A minimal HTTP/1.1 server library in C on a thread-per-core io_uring runtime. You write
endpoints as plain functions, take a request, return a response, and the framework parses, routes,
serializes, and flushes. Under it, each connection is a stackful coroutine: the flush suspends it
and the io_uring completion resumes it, so the endpoint reads as linear code with no state machine.
Modelled on ioxide's TCP core; runtime design notes in
DESIGN.md.
For how it works — the io_uring setup, the coroutines, the worker loop, the HTTP engine and
the router — read ARCHITECTURE.md.
#include <ioma.h>
static ioma_response home(ioma_request *req) { (void)req; return ioma_text(200, "hello\n"); }
static ioma_response echo(ioma_request *req) { return ioma_bytes(200, "text/plain", req->body, req->body_len); }
int main(void) {
ioma_route("GET", "/", home);
ioma_route("POST", "/echo", echo);
return ioma_run(4, 8080); // 4 workers, one per core; blocks until Ctrl+C
}make # builds libioma.a, libioma.so, and the examples
./ioma-hello # the playground demo: 4 workers on :8080
curl http://127.0.0.1:8080/
curl -d hello http://127.0.0.1:8080/echo
python3 tests/smoke.py 8080 # routing, keep-alive, pipelining, 404, half-close, ...
wrk -t8 -c64 -d10s http://127.0.0.1:8080/health
IOMA_WORKERS and IOMA_PORT override the demo's defaults.
pkg-config (after sudo make install, default prefix /usr/local):
sudo make install
cc app.c $(pkg-config --cflags --libs ioma) -o app
CMake, either vendored as a subdirectory:
add_subdirectory(ioma)
target_link_libraries(myapp PRIVATE ioma::ioma)or installed and found:
find_package(ioma REQUIRED) # after: cmake --install <build> --prefix <prefix>
target_link_libraries(myapp PRIVATE ioma::ioma)Either way you #include <ioma.h>. Headers install under <prefix>/include/ioma/.
The framework surface is in include/http.h (pulled in by <ioma.h>):
- Endpoints return an
ioma_response:ioma_text/ioma_json/ioma_bytes, orioma_textfto format intoreq->scratch.ioma_header_set(&res, name, value)adds a response header. - Requests expose method, path, query, headers, and body as zero-copy slices into the read
buffer, plus case-insensitive
ioma_header_get. Valid only for the endpoint call. - Routing is exact
(method, path)viaioma_route, withioma_defaultfor the fallback (a built-in 404 otherwise). - Middleware wraps every handler:
ioma_use(mw)registers one; inside it, callioma_next_runto invoke the rest of the chain and the endpoint, or return without calling it to short-circuit. ioma_run(workers, port)starts the worker fleet and serves until SIGINT/SIGTERM.
For a non-HTTP protocol, include/proactor.h exposes the raw byte-level runtime
(await_recv / await_send); the HTTP layer is one handler on top of it.
Each accepted connection runs the serve loop on its own coroutine (src/http.c):
await_recvaccumulates bytes into a fixed request buffer (this suspends until data arrives).- picohttpparser parses the request line and headers, zero-copy, returning "incomplete" until the full head is in, so a split request just reads more.
- The body is read to its
Content-Length, or a chunked body is decoded in place (both fragmentation-safe). - The middleware chain runs, then the router matches
(method, path)and calls your endpoint. - The response head is built with memcpy of precomposed pieces plus a hand-rolled integer writer
(no snprintf), and
await_sendflushes it (this suspends until io_uring reports the send done). - Keep-alive loops for the next request on the same connection; otherwise the connection closes.
include/ public headers (ioma.h umbrella, http.h, proactor.h, coro.h, uring.h)
src/http.c parse, route, serialize, the connection serve loop, ioma_run
src/router.c the route table and middleware chain
src/proactor.c worker: buffer ring, listener, accept/recv/send, the proactor loop
src/coro.c switch_*.S stackful coroutines: guard-page stacks and the register swap
src/uring.c raw io_uring: setup, mmap, submit, batched CQ drain (no liburing)
playground/hello/ the demo server, built as ./ioma-hello
third_party/picohttpparser vendored HTTP request parser (MIT)
Deliberately small, to grow: chunked and Content-Length request bodies (both fragmentation-safe); the request head
plus body must fit a 16 KiB buffer (larger answers 413/431); routing is exact (method, path) with
no path parameters yet; the route and middleware tables are set once and then read-only.
Requires Linux 6.6+ (for NO_SQARRAY; it falls back on older kernels). Build with gcc -O2, no
external dependencies. MIT licensed.