A lightweight, cross-platform reactor event-driven library written in C99.
git clone --recurse-submodules https://github.com/CandyMi/ccev.git
cd ccev
cmake -B build
cmake --build build
ctest --test-dir build --output-on-failureccev provides two levels of socket abstraction:
-
ccev_sock_t— raw fd + event callbacks (reactor primitive).
Create viaccev_sock_create(),ccev_listen(), orccev_connect().
Register interest withccev_sock_read_start()/ccev_sock_write_start(). -
ccev_stream_t— buffered I/O + stream reader (protocol primitive).
Upgrade a sock viaccev_stream_open()(same address, backed byccev_sock_any_t).
Providesccev_stream_write(),ccev_stream_read(),ccev_stream_readline(),ccev_stream_readnum(),ccev_stream_sendfile().
flowchart LR
subgraph Public["Public API"]
L["ccev_listen() → ccev_sock_t*"]
C["ccev_connect() → ccev_sock_t*"]
S["ccev_sock_create(loop, fd, udata)\n→ ccev_sock_t*"]
end
subgraph Low["Low-level (ccev_sock_t)"]
RS["sock_read_start / _stop"]
WS["sock_write_start / _stop"]
SC["sock_set_close_cb"]
CL["sock_close"]
end
subgraph High["High-level (ccev_stream_t)"]
O["ccev_stream_open(sock)\n→ stream"]
W["stream_write / write_batch / flush"]
SF["stream_sendfile"]
RL["stream_readline / readnum\n/ read_stop"]
end
Public --> Low
Public --> High
O -. "cast via ccev_sock_any_t\n(same address)" .-> Low
flowchart TD
subgraph Run["ccev_loop_run(loop, mode) — one iteration"]
direction TB
ENTER["entry"] --> Q0{"stop_flag?"}
Q0 -->|"Yes"| EXIT0["return 0"]
Q0 -->|"No"| WIN["Win32: ccev__signal_dispatch"]
WIN --> T1["ccev__timer_process(now)\n→ next_ms (-1 if none)"]
T1 --> Q1{"stop_flag?"}
Q1 -->|"Yes"| EXIT
Q1 -->|"No"| TO["Compute epoll timeout"]
TO --> TOC{{"mode?"}}
TOC -->|"RUN_ONCE"| TO0["timeout = 0"]
TOC -->|"RUN_FOREVER\n+ no timers"| TOM1["timeout = -1\n(block indefinitely)"]
TOC -->|"RUN_FOREVER\n+ next timer"| TOM2["timeout = next_ms"]
TO0 --> EW["epoll_wait(EINTR retry)\n→ n events"]
TOM1 --> EW
TOM2 --> EW
EW --> DISPATCH["For each of n events..."]
DISPATCH --> EVT{"ev dispatch"}
EVT -->|"null / closed"| SKIP["continue"]
EVT -->|"wake_sock"| WAKE["drain pipe\ncontinue"]
EVT -->|"EPOLLERR|HUP"| HUP["mode dispatch\nCONNECT→is_connected+cb\nINIT→rcb(HUP)+close\nLISTEN→close"]
EVT -->|"LISTEN + IN"| LISTEN["batch accept (≤128)\nfor each: ccev_sock_create\n→ listen_cb\nre-arm EPOLLIN\ncontinue"]
EVT -->|"CONNECT + OUT"| CONN["ccsocket_is_connected"]
CONN -->|"CC_CONNECTED"| CONNOK["mode = INIT\ndel connect timer\nconnect_cb(OK)\ncontinue"]
CONN -->|"CC_CONNERROR"| CONNERR["connect_cb(ERR)\nschedule_close\ncontinue"]
EVT -->|"normal IN"| READ["fire rcb(sock, events)"]
EVT -->|"normal OUT"| WRITE["fire wcb(sock, events)"]
READ --> REARM["ccev__sock_rearm\n(if rcb/wcb present)"]
WRITE --> REARM
EW --> POST["Post-dispatch"]
POST --> RWAKE["Re-arm wake_sock\nepoll_ctl MOD\n(EPOLLIN|ONESHOT)"]
RWAKE --> CLOSING["ccev__process_closing()"]
CLOSING --> Q2{"stop_flag?"}
Q2 -->|"No"| Q3{"mode ==\nFOREVER?"}
Q3 -->|"Yes"| WIN
end
Q2 -->|"Yes"| EXIT["Clear stop_flag\nreturn (ONCE?n:0)"]
Q3 -->|"No"| EXIT
flowchart LR
subgraph states["Socket state machine"]
INIT["CCEV_SOCK_INIT\n(active data)"] -->|"ccev_sock_close"| CLOSED["sock->closed = true"]
INIT -->|"ccev_listen"| LISTEN["CCEV_SOCK_LISTEN"]
INIT -.->|"ccev_stream_open"| STREAM["(ccev_stream_t)"]
LISTEN -->|"accept → client"| INIT
end
subgraph close["Deferred close flow"]
CLOSED -->|"ccev__sock_schedule_close"| CLIST["move to closing list\nepoll_ctl DEL"]
CLIST -->|"ccev__process_closing\n(next loop iteration)"| FIRE["fire close_cb"]
FIRE --> FREE["ccev__sock_free\nclose(fd) + free"]
end
- Phase 0 (Win32): Poll
sig_pendingflag. - Phase 1 — Timers:
ccev__timer_process()pops expired timers from the 4-ary heap and fires callbacks before I/O dispatch. - Phase 2 — Timeout: 0 for RUN_ONCE, -1 (block) when no timers, or
next_msto meet the earliest timer. - Phase 3 — Poll:
epoll_wait()with EINTR retry. - Phase 4 — Dispatch: Route each event by socket mode:
wake_sock: drain pipe, skip re-arm.HUP/ERR: Mode-based dispatch — CONNECT→ccsocket_is_connected+connect_cb, INIT→rcb(HUP)+close, LISTEN→close.LISTEN: batch accept (≤128), firelisten_cbper client, re-arm.CONNECT:ccsocket_is_connected()—CC_CONNECTED→OK,CC_CONNERROR→ERR+close.- Normal I/O: fire
rcb/wcb, thenccev__sock_rearm().
- Phase 5: Unconditionally re-arm
wake_sock(epoll_ctl MOD, EPOLLIN|ONESHOT). - Phase 6:
ccev__process_closing()— fireclose_cband free. - Check
stop_flag: break if set, loop ifCCEV_RUN_FOREVER.
#include "ccev.h"
#include <stdio.h>
#include <string.h>
/* Forward declarations */
static void on_readable(ccev_sock_t *sock, int events);
static void on_sent(void *udata) { (void)udata; }
static void on_readable(ccev_sock_t *sock, int events) {
(void)events;
char buf[4096];
int n;
ccsocket_stcode_t rc = ccsocket_recv(ccev_sock_get_fd(sock),
buf, sizeof(buf), &n);
if (rc == CC_OPCODE_OK && n > 0) {
ccev_stream_t *st = (ccev_stream_t *)ccev_sock_get_udata(sock);
ccev_stream_write(st, buf, (size_t)n, on_sent, NULL);
} else if (rc == CC_OPCODE_ERROR && n == 0) {
ccev_sock_close(sock);
}
}
static void on_accept(void *udata, ccev_sock_t *client,
const char *ip, int port) {
printf("accept: %s:%d\n", ip, port);
/* Upgrade to stream for buffered write */
ccev_stream_t *st = ccev_stream_open(client);
ccev_sock_set_udata(client, st);
ccev_sock_read_start(client, on_readable);
}
int main(void) {
ccev_loop_t *loop = ccev_loop_create(1024);
if (!loop) return 1;
ccev_sock_t *listener = ccev_listen(loop, "0.0.0.0", 8080, 128,
CCEV_REUSEADDR, on_accept, NULL);
if (!listener) { ccev_loop_destroy(loop); return 1; }
printf("echo server on 0.0.0.0:8080\n");
ccev_loop_run(loop, CCEV_RUN_FOREVER);
ccev_loop_destroy(loop);
return 0;
}See docs/ for full documentation.
MIT