# 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, coroutine, and deferred handlers.** Fast sync handlers run without
creating a coroutine. Coroutine handlers use vendored `minicoro` and can yield.
Deferred handlers hand completion to a foreign scheduler, such as an embedded
runtime.
**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.
| Server |
Client |
|
```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. A coroutine 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
```
To consume wirecall from another Meson project, copy `wirecall.wrap` into that
project's `subprojects/` directory and use:
```meson
wirecall = dependency('wirecall')
executable(
'my-rpc-client',
'main.c',
dependencies: wirecall,
)
```
When wirecall is used as a subproject, demos and tests are not built by default.
They can be enabled explicitly:
```sh
meson setup build -Dwirecall:demos=enabled -Dwirecall:tests=enabled
```
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