Keel is the default event loop and transport layer for Hull. It is swappable, but it is the first-class path for HTTP, WebSocket, SSE, async clients, file descriptors, timers, and worker wakeups over epoll, kqueue, io_uring, or poll.
#include <keel/keel.h> static void handle_run(KlRequest *req, KlResponse *res, void *ctx) { (void)req; (void)ctx; static const char body[] = "{\"ok\":true,\"transport\":\"keel\"}"; kl_response_json(res, 200, body, sizeof(body) - 1); } int main(void) { KlServer s; KlConfig cfg = { .port = 7070 }; kl_server_init(&s, &cfg); kl_server_route(&s, "GET", "/run", handle_run, NULL, NULL); kl_server_run(&s); kl_server_free(&s); }
Keel does not replace Hull's capability boundary. It is the default event and transport substrate beneath Hull: a narrow, auditable path to sockets, timers, local native services, side-car binaries, device bridges, and high-throughput protocol edges. OTTO and other Artalis systems use the same substrate where C eventing and transport matter.
Keel builds as a static C library. Use kqueue on macOS and BSD, epoll on Linux, io_uring where it makes sense, and poll when portability matters more than edge-triggered performance.
# Build libkeel.a with the platform default backend. $ make # Linux 5.6+: readiness events through io_uring. $ make BACKEND=iouring # Universal POSIX fallback. $ make BACKEND=poll # Run the test suite and example programs. $ make test $ make examples
Benchmark context: make bench runs a dedicated local benchmark server across four endpoints with wrk latency reporting. Defaults are 4 threads, 100 connections, and 10s duration; the benchmark workflow also runs Linux epoll, Linux io_uring, and macOS kqueue with 2 threads, 50 connections, and 5s duration.
A minimal C11 event loop and protocol substrate with async server handlers, sync/async clients, WebSocket, SSE, timers, thread-pool wakeups, body readers, streaming, and connection pooling. Hull uses it by default; OTTO and other Artalis systems reuse it where they need the same C-level eventing and transport.
Not a web framework, not an application runtime, not a policy engine, not an RPC religion. Hull owns capabilities. Keel is the default byte-moving layer, and remains replaceable at the boundary.
Local-first and embedded systems need a predictable event loop and network edge that can be compiled, audited, fuzzed, sandboxed, and carried into constrained deployments.
Beneath Hull's capability-limited orchestration layer, in native Artalis services, and between Hull and side-cars that perform routing, optimization, inference, device IO, or other compute-heavy work.
Allocator, parser, body reader, TLS, resolver, router, client, and server modules are independently testable and replaceable.
Buffer, file, and streaming response modes let services avoid copying when sendfile, writev, chunking, or backpressure are the right tool.
The init/run split, fixed resource limits, explicit vtables, and narrow file descriptor model make syscall lockdown natural.
Hull should not need to embed every native capability directly into the scripting runtime. Keel is the default event loop and local transport path to native binaries that can be sandboxed, restarted, versioned, audited, and replaced independently.
The important architectural point is the seam: Keel is the standard substrate, but Hull does not hard-code trust into it. Capability checks stay above; side-car process isolation stays below. The same eventing and transport layer can be reused directly by native services that do not need the Hull scripting runtime.
In Hull, Keel is wired into server startup, route dispatch, middleware, timers, SSE, async work, and static-file serving. See Hull's architecture notes and dependency table.
+-------------------------------------------------------+
| Hull app code |
| Lua / JS orchestration, routes, workflows |
+--------------------------+----------------------------+
|
| manifest-declared calls
v
+-------------------------------------------------------+
| Hull runtime boundary |
| capabilities, audit, sandbox, WASM compute |
| |
| Keel default event + transport layer |
| HTTP / WS / SSE / clients / timers / watchers |
+--------------------------+----------------------------+
|
| explicit local transport
v
+-------------------------------------------------------+
| Sandboxed native side-car |
| optimizer, router, inference, device bridge |
+-------------------------------------------------------+
Keel is deliberately closer to the metal than application servers, but more protocol-aware than a generic event loop.
| Capability | Keel | libuv | Nginx | gRPC |
|---|---|---|---|---|
| Embeddable C library | yes | yes | no | part |
| HTTP server and client in one substrate | yes | no | server | rpc |
| Pluggable TLS/parser/allocator/body reader | yes | part | no | no |
| Designed for side-car sandbox boundaries | yes | generic | edge | service |
| Single small static-library deployment | yes | yes | no | heavy |
Keel's center of gravity is event-loop integration plus HTTP/1.1 transport. WebSocket, SSE, client pooling, compression, proxying, redirects, generic FD watchers, timers, and HTTP/2 hooks extend that core without turning Keel into a monolithic application server.
| Surface | Status | Notes |
|---|---|---|
| Event loop core | stable | epoll, kqueue, io_uring poll-add, and POSIX poll behind one small API; exercised by server, client, timers, and watcher tests. |
| Timers and worker wakeups | stable | One-shot timers, thread-pool resume, and pipe-based event-loop wakeup with public examples. |
| HTTP/1.1 server | stable | Routing, middleware, keep-alive, timeouts, buffered/file/stream responses; covered by the main test and example suite. |
| HTTP/1.1 client | stable | Blocking and async APIs, TLS hooks, proxy support, redirects, connection pooling, and decompression support. |
| WebSocket | tested | RFC 6455 server and client, shared frame parser, masked client frames, and dedicated fuzz corpus. |
| Server-Sent Events | stable | Zero-alloc SSE framing over chunked streaming responses. |
| Multipart form-data | tested | RFC 2046 parser with part, total-size, and part-count limits plus dedicated fuzz targets. |
| Compression / decompression | stable | Pluggable vtables; miniz gzip backend ships in-tree. |
| HTTP proxy / CONNECT | stable | HTTP forwarding and HTTPS tunneling for client-side transport. |
| HTTP/2 | pluggable | Server and client surfaces use session vtables; backend choice remains explicit. |
| TLS | bring your own | Keel defines the transport vtable and avoids vendoring TLS policy. |
| Async DNS | bring your own | Resolver vtable plus caching decorator; c-ares or thread-pool wrappers fit here. |
| Generic FD watchers | stable | Register arbitrary file descriptors for callbacks on the same event context. |
| Side-car local transport | default | Hull's default route to sandboxed native binaries; replaceable at explicit boundaries. |
| OTTO/native service substrate | in use | Reusable event loop and transport library for C services that sit outside Hull. |
The composable event context owns readiness, watchers, timers, and allocator state. Server, client, thread pool, SSE, WebSocket, and side-car bridges sit on top of that same loop.
Edge-triggered epoll/kqueue, io_uring poll-add, and POSIX poll expose the same small event API to higher layers.
Async handlers can suspend a connection and resume it from a file descriptor watcher, timer, client callback, or worker completion.
Request bodies are routed into explicit readers: buffer, multipart, stream-oriented custom parsers, or application-specific sinks.
Chunked responses, SSE, WebSocket frames, backpressure drain buffers, and client pools keep long-lived local services cheap.
Keel is not the authorization layer. Hull decides what an app can do. Keel keeps the local transport path small enough to test, fuzz, constrain, and place behind kernel sandboxing.
Header size, body size, multipart part limits, drain buffers, connection timeouts, idle timers, and route-level readers make overload behavior explicit.
Parser, response parser, multipart, and WebSocket paths have dedicated fuzz targets and corpus fixtures.
TLS, allocators, DNS, compression, file IO, and parsers are vtables, so production users can choose the backend appropriate for their threat model.
Keel is intentionally boring in the places that matter: C11, static library, CI, sanitizers, fuzzing, OpenSSF checks, and examples that compile against the public API.
$ make debug # ASan + UBSan $ make test # unit and integration suites $ make analyze # clang static analyzer $ make cppcheck # static analysis $ make fuzz # parser and protocol fuzz targets