Skip to content
c4punksPublic

Latest commit

 

History

2,776 Commits

Folders and files

Repository files navigation

CWIST logo

CWIST

C Web development Is Still Trustworthy

Discord Live in Production: oborona.zip API Reference

Write your routes and handlers once in C. Serve them from a native server, a WASI 0.2 edge runtime, a browser Service Worker, or bare-metal MCU firmware.

Bare-metal embedded support: raw-lwIP HTTP, optional mbedTLS HTTPS, static connection pools, and bounded response streaming on Cortex-M, validated on a physical STM32F769I-DISCO board.

CWIST is a C17 web framework and application server: routing, middleware, templates, sessions, validation and an embedded SQLite ORM on top of HTTP/1.1, HTTP/2, HTTP/3, WebSocket and WebTransport, with an io_uring/epoll/kqueue reactor.

Bare-metal firmware uses cwist_app_dispatch_memory() or the raw-lwIP adapter without BSD sockets, pthreads, epoll, or the hosted C1M reactor.

Dogfooded in production (maintainer-operated): oborona.zip (daily engineering blog) · fly.board

Supported platforms

Target Runtime and transport Validation and examples
Linux, macOS, BSD HTTP/1.1, HTTP/2, HTTP/3, TLS, WebSocket, WebTransport, io_uring/epoll/kqueue simple-server
WASI 0.2 and browser Service Worker The same routes and handlers on Wasmtime or in the browser wasip2-kv, wasm-service-worker
Bare-metal STM32 and Cortex-M Raw-lwIP HTTP, optional mbedTLS HTTPS, static connection pools, bounded streamed responses STM32 guide; STM32F769I-DISCO fortress (physical-board validated); NUCLEO-H743ZI2 example (reference integration: cross-build / QEMU)

Quick start

brew tap c4punks/cwist
brew install c4punks/cwist/cwist
#include <cwist/app.h>

static void hello(cwist_http_request *req, cwist_http_response *res) {
    (void)req;
    cwist_sstring_assign(res->body, "Hello from CWIST!");
}

int main(void) {
    cwist_app *app = cwist_app_create();
    cwist_app_get(app, "/", hello);
    cwist_app_listen(app, 8080);
    cwist_app_destroy(app);
    return 0;
}
gcc -o server main.c $(pkg-config --cflags --libs cwist)
./server

Or scaffold a hot-reloading project with cwist new project demo && cwist watcher. Next steps: the 30 modules in tutorials/ and the guides in docs/.

Same routes and handlers across four targets

A CWIST app registers routes and middleware on a cwist_app. The same route and handler definitions compile across four target environments, while transport and runtime capabilities follow each host:

Host Entry point What you get Example
Native server (Linux, macOS, BSD) cwist_app_listen(app, port) The full stack: HTTP/1.1–3, TLS, WebSocket, WebTransport, C1M reactor simple-server
Browser Service Worker CWIST_WASM_DEFINE_ENTRY(app), built with emcc Routing, zod validation, templates, cwist_db and signed-cookie sessions answering fetch inside the browser: no origin server, no framework JS wasm-service-worker
WASI 0.2 edge runtime (Wasmtime) cwist_app_listen on WASI sockets, built for wasm32-wasip2 The same app on Wasmtime; the host owns persistence as a KV-style blob wasip2-kv
Bare-metal STM32 firmware cwist_app_dispatch_memory() or the raw-lwIP adapter Routing, middleware, JSON helpers, and HTTP/1.1 without BSD sockets or pthreads; optional mbedTLS HTTPS and bounded streaming STM32 guide, fortress

The WASM hosts speak serialized HTTP/1.1 across a WIT interface, and the cwist-wasm npm package wraps the jco-transpiled component for JavaScript hosts. They are newer than the native server: read each example's README for what is and is not guaranteed yet (for example, the wasip2 demo does not promise crash-durable writes).

What CWIST includes

Layer What you get
Embedded / bare metal Cortex-M Thumb-2 builds, STM32F769I-DISCO (physical-board validated) and NUCLEO-H743ZI2 (reference integration) examples, raw-lwIP HTTP, optional mbedTLS HTTPS, static connection pools, bounded response streaming
Protocols HTTP/1.1, HTTP/2 (h2/h2c), HTTP/3 (QUIC, via lsquic), WebSocket, WebTransport, WebRTC data channels
TLS / Security BoringSSL, one-call hybrid post-quantum TLS (X25519MLKEM768), ECH, certificate hot reload, JWT, CSRF, WAF, rate limiting, secure headers, encrypted DB columns
Database SQLite3 + ORM and migrations, Nuke DB (in-memory SQLite synced to disk on COMMIT), PostgreSQL/MySQL/MariaDB detection by wire protocol
Routing Express-style :param routes, mux router, chainable middleware
Async handlers cwist_async_defer / cwist_async_respond: move blocking work to a job thread and answer later without stalling the reactor
gRPC / Protobuf Unary and streaming routes, health/reflection, cwist proto code generation, and a channel client with load balancing and gRFC A6 retries
Rendering HTML builder, scoped-CSS components, template engine, JSON builder
Observability Structured access logs, metrics endpoint, healthz
Tooling cwist CLI: scaffolding, incremental hot reload, OpenAPI 3.1 from Doxygen annotations, proto3 codegen
Bindings Rust and Zig

Performance, including where it loses

Every push and pull request runs CWIST against Actix, Axum, Gin and Spring Boot under wrk -t12 -c400 -d10s on a GitHub runner.

Latest completed CI benchmark (2026-11-06, commit b3834e0d, 4 vCPU AMD EPYC 9V74):

CWIST reactor CWIST Classic Actix Axum Gin Spring Boot
req/s 141,827 125,506 138,706 122,094 93,312 60,884
Mean latency 2.80 ms 1.82 ms 2.84 ms 3.13 ms 6.16 ms 6.54 ms
p50 (median) 2.60 ms 1.43 ms 2.63 ms 2.93 ms 4.05 ms 5.95 ms
p99 8.20 ms 6.91 ms 7.74 ms 7.91 ms 38.17 ms 14.25 ms
P99.999 18.15 ms 25.76 ms 15.87 ms 15.65 ms 84.59 ms 59.71 ms
RSS 22.2 MiB 58.4 MiB 12.0 MiB 14.8 MiB 27.5 MiB 1.22 GiB
Context switches 123,365 1,244,131 155,742 244,680 310,205 277,009

In this run the reactor is within runner variance of Actix on throughput (~2% apart) with fewer context switches, and the Classic pool answers the median request in about half of Axum's time and has the lowest p99. Actix and Axum have the tighter extreme tail (P99.999) and the smaller memory footprint; closing the tail gap is open work. The benchmark section below is regenerated by CI (Latency in that summary line is wrk mean latency), so it shows the most recent run, which may be newer than this table. Methodology and fairness settings are in docs/webserver-benchmark.md.

Two server modes

The mode is one environment variable; pick per workload.

  • CWIST reactor (default, CWIST_C1M_MODE unset or 1) takes throughput and connection density. Many connections share each event loop, so a held connection costs a reactor slot (about 5 KB over TLS) instead of a parked thread. Start here for APIs, web pages, webhooks, SSE fan-out, WebSocket chat and anything that holds many mostly idle connections.
  • CWIST Classic pool (CWIST_C1M_MODE=0) takes median latency. Each connection gets its own thread, so no request waits behind another in a batch. Pick it for short request bursts where the median matters more than connection count; it cannot reach C1M, because a million threads exceeds threads-max.

Holding a million connections

Measured on one 12-core machine (Ryzen 5 5600X, 62 GB, Linux 6.12, loopback):

Run Result Server memory
C1M cleartext, reactor, 12 workers (2026-09-29) 1,000,000 / 1,000,000 responded ~22.9 GiB peak RSS (~24 KB per connection)
C100K HTTPS, reactor, 4 workers (2026-10-17) 100,000 / 100,000 established, 0 dropped, all answered 4,975 B per open connection
C1M HTTPS, reactor (2026-10-18) 1,000,000 / 1,000,000 established, 0 dropped, all answered 4.56 GiB above idle (4,866 B per open connection)
C1M over TLS HTTP/1.1 and HTTP/2 via fly.board 1,000,000 / 1,000,000 held and served, zero failures 19.9 GB / 30.1 GB PSS

The HTTPS ramp is handshake-bound (~2,900 full P-256 handshakes/s, so ~340 s to open 1M connections). Kernel prerequisites, the client tooling fixes the runs needed, and the per-run details are in docs/performance/https-c100k.md and docs/c1m-file-limits.md.

On the async path, established HTTP/1.1 TLS connections are served on the C1M reactor by default (CWIST_HTTPS_REACTOR=1). TLS handshakes and HTTP/2 requests continue to use HTTPS pool threads; setting CWIST_HTTPS_REACTOR=0 restores pool-based HTTP/1.1 handling. A handler that writes faster than the socket drains waits inside the reactor thread (bounded by CWIST_HTTP_TIMEOUT_MS). Idle cleartext keep-alive connections are closed by a per-worker sweep timer once they wait longer than CWIST_HTTP_KEEP_ALIVE_TIMEOUT (default 15 s), counted in cwist_http_idle_reaped_total.

Why C, when Axum and Gin exist?

  1. Your C and C++ code is already the backend. Libraries for finance, game servers, ML inference or device control link straight into the server with no FFI layer, no serialization boundary and no second runtime.
  2. The same routes and handlers reach the browser, the edge, and MCUs. C compiles to WASM and bare-metal targets with mature toolchains, which lets shared handler logic run across the hosts above.
  3. Predictable footprint and start-up. No garbage collector, VM or JIT warm-up: the reactor's RSS is above the Rust frameworks but below Go and about 60× below the JVM.
  4. Competitive throughput and median latency, with the tail trade-off stated in the table above rather than hidden.

Memory safety and security in C

The first question about a C web server is buffer overflows, use-after-free and leaks. What CWIST does about it, and where each check lives:

  • Sanitizers on every push and PR. The test suite, tutorials and examples build with SANITIZE=address,undefined WERROR=1 and run under ASan/UBSan (asan.yml); ThreadSanitizer, CodeQL and cppcheck run in their own workflows.
  • Protocol conformance. An h2spec HTTP/2 run is diffed against a pinned baseline (scripts/ci/h2spec-baseline.txt); regressions fail the build.
  • Allocator gate. New raw malloc/free calls in the framework fail cwist audit --gate against scripts/ci/malloc-baseline.txt (malloc_lint.sh).
  • Fuzzing. libFuzzer + ASan/UBSan targets (make fuzz) exercise the sequenced-chunk assembler (tests/fuzz_seq.c), HTTP/1.1 request/query/cookie/date/multipart parsers (tests/fuzz_http1.c), HTTP/2 HPACK and frame handling (tests/fuzz_http2.c), HTTP/3 QPACK header validation and QUIC packet-in paths (tests/fuzz_http3.c), WebSocket frame and UTF-8 parsing (tests/fuzz_websocket.c), and reverse-proxy URL/header/body sanitization (tests/fuzz_proxy.c).
  • Deterministic lifetimes. Request buffers and cached replies live in libttak generational arenas and epoch-based reclamation critical sections. Deferred responses use an explicit ownership handoff, and CWIST_ASYNC_STRICT=1 turns misuse into an abort instead of a warning.
  • Built-in security controls and opt-in PQC TLS. Default HTTPS enforces TLS 1.2+ with AEAD cipher suites (X25519:P-256), while cwist_app_use_pqc_layer(app, true) opts into hybrid post-quantum key exchange (X25519MLKEM768:X25519:P-256) and a TLS 1.3 minimum. Built-in middleware and session helpers provide WAF checks, token-bucket rate limiting, timing-safe HMAC session verification, CSRF tokens and strict security headers (HSTS, COOP, Permissions-Policy).

Design decisions and their current status are recorded in docs/decisions/.

Install

CWIST vendors its dependencies (BoringSSL, lsquic, libttak, SQLite3), so a plain build works on a fresh Linux, macOS, or BSD machine:

git clone https://github.com/c4punks/CWIST.git
cd cwist
make
sudo make install        # optional, installs to /usr/local (override with PREFIX=/opt/cwist)

Homebrew

macOS and Linux (Linuxbrew) users can install the latest release from the CWIST tap:

brew tap c4punks/cwist
brew install cwist

The formula builds from the release source tarball (dist/cwist-<version>.tar.gz, vendored dependencies included) and installs libcwist.a, public headers, the cwist CLI, and cwist.pc pkg-config metadata under the Homebrew prefix.

Platform support

CWIST builds on Linux, macOS, and BSD. On Linux the reactor uses io_uring (raw syscalls, no liburing) and falls back to epoll if ring setup fails; macOS and BSD use kqueue. The HTTP/3 server and native client use non-blocking UDP sockets on every platform, and ECN metadata is enabled only when the host exposes the required socket options. Reactor internals (one-shot polls vs completion-staged receives, CWIST_RX_URING, latency probes) are described in docs/http-reactor-fairness.md and docs/runtime-config.md.

Bare-metal STM32

CWIST also provides a socket-free Cortex-M path for firmware that uses cwist_app_dispatch_memory() or the raw-lwIP adapter. The same routing, middleware, handlers, JSON helpers, and HTTP/1.1 parser are available without cwist_app_listen(), pthreads, epoll, or the C1M reactor.

Each board carries exactly one of three labels:

Board Status What backs it
STM32F769I-DISCO Physical-board validated Flashed and run on real hardware (example/stm32f769, fortress)
NUCLEO-H743ZI2 Reference integration (cross-build / QEMU) Cross-built; not yet run on a physical board (example/stm32)
STM32F429I-DISCO Planned Low-resource target; hardware on hand, not validated yet
  • Validated on STM32F769I-DISCO: raw-lwIP HTTP, mbedTLS HTTPS, static connection pools, and streamed responses through bounded TX buffers.
  • LCD and load demo: example/stm32f769-fortress/ exposes 288 real connection-pool slots on the 800x480 LCD and browser dashboard, with selectable responses up to 1 MiB.
  • Reference integration: NUCLEO-H743ZI2 application, HAL hooks, and lwIP options. It is cross-built only and has not been run on a physical board.
  • Automated checks (not hardware validation): baremetal.yml builds Cortex-M7 Thumb-2 firmware with arm-none-eabi-gcc and runs it under qemu-arm semihosting (make baremetal-smoke, make baremetal-size-gate); cross-compile.yml cross-builds fat-dist for embedded Linux triplets and link-smokes it under qemu-user. Passing either says nothing about a board's PHY, clock tree, NVIC, or RNG.

Build inputs and the lwIP/TLS integration are documented in docs/embedded/stm32.md. Board-specific wiring, cache constraints, and flashing instructions live in the example READMEs.

The cwist CLI and hot reload

make install also installs the cwist command line tool into $(PREFIX)/bin.

# Scaffold a project: src/main.c, Makefile, and a demo.cwpro manifest
cwist new project demo --directory ~/src
cd ~/src/demo

# Develop with hot reload (incremental rebuild + zero-downtime restart)
cwist watcher

# Generate OpenAPI 3.1 from Doxygen @openapi.* route annotations
cwist openapi

# Generate C models and gRPC method paths from proto3 definitions
cwist proto api.proto

# Inspect the project manifest and detected routes
cwist describe

The watcher rebuilds only the affected translation units and restarts the app only after a successful build, so the previous process keeps serving if a build fails. It wakes on inotify (Linux) or kqueue (BSD/macOS) with a polling fallback; --no-run rebuilds without running and --poll forces polling. The .cwpro manifest (cwist-project/v1) holds the entry point, include paths, debounce/stop timeouts and route discovery scope for the watcher and generators.

Linking

libcwist.a is a thin static archive: it contains only CWIST objects. make install installs the vendored libraries under $(PREFIX)/lib/cwist and their headers under $(PREFIX)/include/cwist/vendor. pkg-config resolves all of it:

gcc -o server main.c $(pkg-config --cflags --libs cwist)
# fully static:
gcc -o server main.c $(pkg-config --cflags --libs --static cwist)

Without pkg-config, link CWIST first and its dependencies after it:

gcc -I/opt/cwist/include -I/opt/cwist/include/cwist/vendor main.c \
    -L/opt/cwist/lib -L/opt/cwist/lib/cwist -lcwist \
    -llsquic -lssl -lcrypto -lnats_static -lttak -lcjson -luriparser \
    -lz -lzstd -lbrotlienc -lbrotlicommon -ldl -lpthread -lm -lstdc++

-lnghttp2 (HTTP/2) and -lcurl (RDBMS auto-mount probing) are optional. Brotli ships as libbrotli-dev on Debian/Ubuntu and brotli-devel on Fedora/RHEL; zstd as libzstd-dev / libzstd-devel. DESTDIR is supported for staged packages.

Configuration

CWIST reads .env files and prefixed environment variables:

cwist_config *cfg = cwist_config_create();
cwist_config_load_file(cfg, ".env");
cwist_config_load_env(cfg, "CWIST_");

const char *db_url = cwist_config_get(cfg, "DATABASE_URL");
int workers       = cwist_config_get_int(cfg, "WORKERS", 4);
bool debug        = cwist_config_get_bool(cfg, "DEBUG", false);

cwist_config_destroy(cfg);

The framework itself reads these without a prefix; every CWIST_* variable, with its default and v4 support status, is in docs/runtime-config.md.

Variable Default Description
CWIST_WORKERS 1 Worker processes to fork before entering the event loop.
CWIST_C1M_MODE true 1 selects the CWIST reactor, 0 the Classic pool.
CWIST_HTTP_BATCH 16 Pipelined HTTP/1.1 requests dispatched per event-loop turn (clamped to [1, 1024]).
CWIST_HTTP_YIELD_BATCH 16 Dispatch yield granularity within a batch before re-posting the connection.
CWIST_ASYNC_DEBUG unset Log rare reactor failure events (rearm/submit/SQ failures) to stderr.
CWIST_ASYNC_STRICT unset 1 aborts on deferred-response misuse instead of warning once.

Feature notes

PQC TLS. cwist_app_use_pqc_layer(app, true) forces X25519MLKEM768:X25519:P-256, sets TLS 1.3 as the minimum and strips legacy ciphers. Application code never touches OpenSSL directly.

WebTransport. cwist_app_use_webtransport(app, handler) handles the CONNECT negotiation over HTTP/3 and exposes cwist_webtransport_read/write/flush/close/open_bidi/open_uni.

Nuke DB. cwist_nuke_init("data.db", 5000) loads an on-disk SQLite file into memory with sqlite3_deserialize, runs PRAGMA integrity_check, serves every query from RAM and syncs to disk on COMMIT. If bootstrap fails, it falls back to read-only disk protection mode.

RDBMS auto-mount. cwist_app_auto_rdbms(app, 5432) classifies the server by wire protocol (a PostgreSQL StartupMessage or a MySQL handshake packet) instead of guessing from the port number.

libttak. The in-tree allocator/reactor toolkit provides generational arenas, epoch-based reclamation, signal-safe detachable arenas for TLS and WebSocket buffers, and a lock-free job queue.

Latest benchmark run

CI regenerates this block from each benchmark run.

Latest Web Server Benchmark (wrk -t12 -c400 -d10s (after 10s warmup, warmup discarded)):

  • CWIST Classic pool: 114167 req/s | Latency 2.01ms (P50 1.65ms, P90 4.06ms, P99 7.26ms, P99.999 22.56ms) | RSS 60364KiB | Csw 1118408
  • CWIST reactor: 142108 req/s | Latency 2.79ms (P50 2.57ms, P90 5.08ms, P99 7.99ms, P99.999 14.29ms) | RSS 22340KiB | Csw 124995
  • Actix: 128508 req/s | Latency 3.05ms (P50 2.85ms, P90 5.05ms, P99 7.67ms, P99.999 15.36ms) | RSS 12384KiB | Csw 166962
  • Axum: 109270 req/s | Latency 3.61ms (P50 3.41ms, P90 6.03ms, P99 8.89ms, P99.999 15.29ms) | RSS 16024KiB | Csw 252103
  • Gin (Go): 78282 req/s | Latency 8.06ms (P50 4.88ms, P90 20.48ms, P99 52.06ms, P99.999 120.58ms) | RSS 27956KiB | Csw 329310
  • Spring Boot: 44854 req/s | Latency 8.91ms (P50 8.19ms, P90 11.54ms, P99 19.76ms, P99.999 75.68ms) | RSS 1312444KiB | Csw 234822

Settled CWIST-internal tuning variants (glibc arena cap from PR #35, cooperative drain chunk from issue #25) are recorded in docs/cooperative-queuing.md and benchmarks/webserver.json; they are no longer rendered as comparison rows.

Spring runtime environment

  • JDK: openjdk version "25.0.5" 2026-10-20 LTS
  • Spring Boot: 3.2.3
  • Stack: Spring WebFlux + Reactor Netty on native epoll (G1GC, JDK 25 Leyden AOT, virtual threads disabled)
  • Virtual threads: disabled

JVM options

-Xms1024m
-Xmx1024m
-XX:+UseG1GC
-XX:GCTimeRatio=99
-XX:G1HeapRegionSize=1m
-XX:+AlwaysPreTouch
-XX:CompileThreshold=1500
-XX:CICompilerCount=4
-Djava.security.egd=file:/dev/urandom
-Djava.net.preferIPv4Stack=true
-Dio.netty.allocator.type=pooled
-Dio.netty.leakDetection.level=disabled
-Dio.netty.buffer.checkBounds=false
-Dio.netty.buffer.checkAccessible=false
-Dreactor.netty.ioWorkerCount=4
-Xlog:gc*:file=/tmp/spring_gc.log:time,uptime,level,tags
-XX:+AOTClassLinking
-XX:AOTCache=/tmp/spring_bench/app.aot (JEP 483 + JEP 514 single-step AOT)

Warmup/profile

wrk -t12 -c400 -d10s (after 10s warmup, warmup discarded)

Web Server Benchmark Trends

Latency distribution (density curve reconstructed from each server's percentiles - shows the shape of the tail, not just its P99.999 number):

Web Server Latency Distribution

Methodology, JVM options, and fairness settings: docs/webserver-benchmark.md

Stability and conformance

  • Versioning: v4 is the first LTS line. Draft-level features cycle through dev and may appear or disappear between tags; pin an exact tag in production. See docs/versioning.md.
  • Conformance gates: ASan/UBSan with -Werror on every push, and an h2spec HTTP/2 diff against a pinned baseline; known gaps are tracked in scripts/ci/h2spec-baseline.txt.

Documentation

The full map is docs/README.md. In suggested reading order:

Examples

Runnable demos under example/: a minimal server, step-by-step HTTP, SQLite and encrypted-column DB, JWT auth, a WebSocket Othello game, WebRTC, WebTransport, the WASM hosts (Service Worker, wasip2), Rust and Zig hello-worlds, and the rps-showcase throughput demo. The examples table lists them all.

Maintainer-operated production deployments built on CWIST: oborona.zip (live daily engineering blog) and fly.board.

Community

The official CWIST Discord: https://discord.gg/6F8HDmNAPg. Questions, design discussion and contribution coordination happen there; see CONTRIBUTING.md to send a patch.

Third-party licenses

CWIST vendors its dependencies under lib/; each keeps its own license (summary in NOTICE.md, authoritative text in each submodule's license file):

Component License
BoringSSL Apache-2.0
lsquic MIT (some Chromium-derived parts BSD-3-Clause)
cJSON, multipart-parser-c MIT
libttak BSD-3-Clause
SQLite Public Domain
cnats Apache-2.0
uriparser BSD-3-Clause (library only; its test suite is LGPL-2.1-or-later and is not linked)

Static linking propagates each component's license obligations to linked binaries; review NOTICE.md when distributing.