diff --git a/README.md b/README.md new file mode 100644 index 0000000..f0c3c56 --- /dev/null +++ b/README.md @@ -0,0 +1,328 @@ +# Wirecall + +Wirecall is a small C RPC library for making named procedure calls over TCP. +It gives you a compact binary protocol, typed primitive arguments, dynamic +routes, sync and coroutine-backed async handlers, and a socket event backend +that can use the native IO primitive for the host platform. + +The goal is simple: your application should mostly read like this: + +```text +server: init, add routes, bind, run +client: connect, call, close +``` + +**Project Status: young and usable.** Wirecall is early, but it already has +the core pieces: TCP server/client support, typed payloads, name-based calls, +dynamic route replacement, packet integrity checks, tracing, benchmarks, and +platform backend selection. The API may still move while the library finds its +sharpest shape. + +## Features + +**Named RPC calls.** Procedures are registered by name and called by name. The +library maps names to stable internal IDs so the hot path stays table-friendly. + +**Compact binary packets.** Each packet has an operation, flags, procedure ID, +payload size, call ID, checksum, and optional keyed MAC. Payloads are typed +primitive values. + +**Typed primitive payloads.** Wirecall supports `null`, `bool`, `i64`, `u64`, +`f64`, `bytes`, and `string`. Handlers validate the arity and types they expect. + +**Sync and async handlers.** Fast sync handlers run without creating a +coroutine. Async handlers use vendored `minicoro` and can yield while external +work completes. + +**Dynamic routes.** Routes can be added, replaced, and removed at runtime. +Route state can have finalizers, which makes embedding into runtimes less +awkward. + +**Low-latency server loop.** Connections and calls use mmap-backed fixed +arenas, route lookup is direct and paged, response writes are batched, and +tracing is disabled by default behind an unlikely runtime flag. + +**Native event backends.** macOS and BSD use `kqueue`, Linux uses an +`io_uring` backend, and Windows uses a WSA event backend behind the same +interface. The public API does not change with the backend. + +**Embedding hooks.** A process-wide allocator hook lets a host runtime route +Wirecall allocations through its own allocator, accounting, leak tracker, or +arena policy. + +## Example + +This pair registers an `add` procedure on the server and calls it from a +client. + + + + + + + + + + +
ServerClient
+ +```c +#include "wirecall/server.h" + +#include + +static int add( + wirecall_ctx *ctx, + const wirecall_value *args, + size_t argc, + wirecall_writer *out, + void *user_data +) { + if (argc != 2 || + args[0].type != WIRECALL_TYPE_I64 || + args[1].type != WIRECALL_TYPE_I64) { + return -1; + } + + return wirecall_writer_i64(out, args[0].as.i64 + args[1].as.i64); +} + +int main(void) { + wirecall_server *server = NULL; + + if (wirecall_server_init(&server) != 0 || + wirecall_server_add_route_name(server, "add", add, NULL) != 0 || + wirecall_server_bind(server, "127.0.0.1", "7000") != 0 || + wirecall_server_listen(server) != 0) { + fprintf(stderr, "server setup failed\n"); + wirecall_server_destroy(server); + return 1; + } + + int rc = wirecall_server_run(server); + wirecall_server_destroy(server); + return rc == 0 ? 0 : 1; +} +``` + + + +```c +#include "wirecall/client.h" + +#include + +int main(void) { + wirecall_client *client = NULL; + if (wirecall_client_connect(&client, "127.0.0.1", "7000") != 0) { + return 1; + } + + wirecall_writer args; + wirecall_writer_init(&args); + wirecall_writer_i64(&args, 20); + wirecall_writer_i64(&args, 22); + + wirecall_value *result = NULL; + size_t result_count = 0; + int rc = wirecall_client_call_name( + client, + "add", + &args, + &result, + &result_count + ); + + if (rc == 0 && + result_count == 1 && + result[0].type == WIRECALL_TYPE_I64) { + printf("20 + 22 = %lld\n", (long long)result[0].as.i64); + } + + wirecall_values_free(result); + wirecall_writer_free(&args); + wirecall_client_close(client); + return rc == 0 ? 0 : 1; +} +``` + +
+ +## Async Handlers + +Async routes are opt-in. A sync route stays on the fast path and does not pay +for coroutine creation. An async route can yield: + +```c +static int wait_for_job( + wirecall_ctx *ctx, + const wirecall_value *args, + size_t argc, + wirecall_writer *out, + void *user_data +) { + job *j = enqueue_job(user_data, args, argc); + while (!job_done(j)) { + wirecall_ctx_yield(ctx); + } + return wirecall_writer_i64(out, job_result(j)); +} + +wirecall_server_add_async_route_name(server, "job.add", wait_for_job, queue); +``` + +See `demos/async/server.c` for a complete queue-backed example. + +## Packet Integrity + +Wirecall packets include integrity fields in the packet header. By default the +library uses a cheap checksum. A keyed MAC can be enabled when both sides share +a 16-byte key: + +```c +uint8_t key[16] = { + 0x00, 0x01, 0x02, 0x03, + 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, + 0x0c, 0x0d, 0x0e, 0x0f, +}; + +wirecall_server_set_integrity( + server, + WIRECALL_INTEGRITY_CHECKSUM | WIRECALL_INTEGRITY_MAC, + key +); + +wirecall_client_set_integrity( + client, + WIRECALL_INTEGRITY_CHECKSUM | WIRECALL_INTEGRITY_MAC, + key +); +``` + +Integrity can be disabled for raw benchmarking with `WIRECALL_INTEGRITY_NONE`. + +## Embedding + +Wirecall is meant to be easy to drop into another runtime. The two main hooks +are allocator replacement and route finalizers: + +```c +wirecall_allocator allocator = { + .ctx = runtime, + .alloc = runtime_alloc, + .realloc = runtime_realloc, + .free = runtime_free, +}; + +wirecall_set_allocator(&allocator); + +wirecall_server_add_route_name_ex( + server, + "vm.call", + vm_call, + closure, + closure_release +); +``` + +Set the allocator before creating Wirecall objects and do not swap it while the +library is live. See `demos/embed.c` for a small embedding demo with allocation +stats and route state finalization. + +## Build + +Wirecall uses Meson and C23 with GNU extensions. + +```sh +meson setup build +meson compile -C build +``` + +The build produces: + +```text +wirecall-server +wirecall-client +wirecall-async-server +wirecall-async-client +wirecall-embed +wirecall-bench +``` + +Run the test suite with: + +```sh +meson test -C build --print-errorlogs +``` + +Format the code with: + +```sh +meson compile -C build format +``` + +## Benchmark + +The benchmark starts an in-process server and drives pipelined client traffic +against it: + +```sh +./build/wirecall-bench 1000000 40 100 4 8 +``` + +Arguments are: + +```text +requests clients warmup server_workers pipeline_depth [trace] [integrity] [mac] +``` + +For example, this enables tracing: + +```sh +./build/wirecall-bench 1000000 40 100 4 8 1 +``` + +Trace output includes client send/receive timing, server read/parse/write +timing, route lookup, worker distribution, CPU time, context switches, RSS, and +virtual memory size. + +## Backend Selection + +Backend selection happens at build time: + +```text +macOS, BSD -> kqueue +Linux -> io_uring +Windows -> WSA event backend +``` + +The backend boundary is internal. Public code should include +`wirecall/client.h`, `wirecall/server.h`, `wirecall/protocol.h`, or +`wirecall/trace.h`. + +## Design Notes + +Wirecall is intentionally small. The main tradeoff is that v1 uses a built-in +typed primitive format instead of user codecs or schema generation. That keeps +the library easy to embed and keeps the packet path predictable. + +The fast path is tuned around a few rules: + +- decode packets without allocating where practical +- keep sync handlers out of the coroutine scheduler +- batch response writes per connection +- use table-shaped route lookup +- keep tracing compiled in but cold until enabled +- isolate platform IO behind a strict backend interface + +## Roadmap + +Things that still deserve love: + +- broader Linux validation for the `io_uring` backend +- deeper Windows backend testing +- TLS or transport hooks +- cancellation and deadline APIs +- stronger documentation for embedding into language runtimes +- fuzzing for packet and payload decoding diff --git a/meson.build b/meson.build index 0835d3e..33a6ebc 100644 --- a/meson.build +++ b/meson.build @@ -12,6 +12,43 @@ include = include_directories('include') clang_format = find_program('clang-format', required: false) c = meson.get_compiler('c') +if clang_format.found() + format_sources = files( + 'include/arena.h', + 'include/backend.h', + 'include/memory.h', + 'include/proc.h', + 'include/routes.h', + 'include/scheduler.h', + 'include/wirecall/client.h', + 'include/wirecall/protocol.h', + 'include/wirecall/server.h', + 'include/wirecall/trace.h', + 'src/backend/io_uring.c', + 'src/backend/kqueue.c', + 'src/backend/windows.c', + 'src/client.c', + 'src/memory.c', + 'src/payload.c', + 'src/protocol.c', + 'src/routes.c', + 'src/scheduler.c', + 'src/server.c', + 'src/trace.c', + 'demos/async/client.c', + 'demos/async/server.c', + 'demos/bench.c', + 'demos/client.c', + 'demos/embed.c', + 'demos/server.c', + 'tests/test_integration.c', + 'tests/test_protocol.c', + 'tests/test_routes.c', + 'tests/test_scheduler.c', + ) + run_target('format', command: [clang_format, '-i', format_sources]) +endif + system = host_machine.system() backend_sources = [] platform_deps = []