diff --git a/examples/timer/ipc/.gitignore b/examples/timer/ipc/.gitignore new file mode 100644 index 0000000..a3d2cf3 --- /dev/null +++ b/examples/timer/ipc/.gitignore @@ -0,0 +1,5 @@ +/server +/client +/libmy_timer.dylib +/libmy_timer.so +/.srv.pid diff --git a/examples/timer/ipc/Makefile b/examples/timer/ipc/Makefile new file mode 100644 index 0000000..e2aaa0c --- /dev/null +++ b/examples/timer/ipc/Makefile @@ -0,0 +1,54 @@ +# Build the IPC (CBOR-over-socket) timer example. +# +# make # build the Nim dylib, the server, and the client +# make demo # run a full same-machine round-trip over a unix socket +# make clean +# +# The server links libmy_timer; the client does not (it only needs the CBOR +# encoder/decoder in my_timer_cbor.h). The Nim library is compiled from the +# repository root so its vendored Nimble dependencies resolve. + +# Generated my_timer.h / my_timer_cbor.h live in ../c_bindings. +REPO_ROOT := $(abspath ../../..) +NIM_SRC := $(REPO_ROOT)/examples/timer/timer.nim +HDR_DIR := ../c_bindings + +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Darwin) + LIBNAME := libmy_timer.dylib + RPATH := -Wl,-rpath,. +else + LIBNAME := libmy_timer.so + RPATH := -Wl,-rpath,'$$ORIGIN' +endif + +CC ?= cc +# proto.h is a shared static-inline header; each TU uses a subset of it, so +# unused-function warnings are expected and silenced. +CFLAGS ?= -std=c11 -Wall -Wextra -Wno-unused-function -O2 -I. -I$(HDR_DIR) +NIMFLAGS := --mm:orc -d:chronicles_log_level=WARN --app:lib --noMain \ + --nimMainPrefix:libmy_timer +SOCK := /tmp/my_timer.sock + +.PHONY: all demo clean + +all: server client + +$(LIBNAME): + cd $(REPO_ROOT) && nim c $(NIMFLAGS) -o:$(CURDIR)/$(LIBNAME) $(NIM_SRC) + +server: server.c proto.h $(HDR_DIR)/my_timer_cbor.h $(LIBNAME) + $(CC) $(CFLAGS) server.c -L. -lmy_timer -lpthread $(RPATH) -o server + +client: client.c proto.h $(HDR_DIR)/my_timer_cbor.h + $(CC) $(CFLAGS) client.c -o client + +# Same-machine demo: start the server on a unix socket, run the client, stop. +demo: all + @./server --unix $(SOCK) & echo $$! > .srv.pid; \ + sleep 1; \ + ./client --unix $(SOCK); \ + kill `cat .srv.pid` 2>/dev/null; rm -f .srv.pid $(SOCK) + +clean: + rm -f server client $(LIBNAME) .srv.pid $(SOCK) diff --git a/examples/timer/ipc/README.md b/examples/timer/ipc/README.md new file mode 100644 index 0000000..451ca33 --- /dev/null +++ b/examples/timer/ipc/README.md @@ -0,0 +1,91 @@ +# IPC example — CBOR over a socket (cross-process / cross-machine) + +The native ABI in [`../c_bindings`](../c_bindings) is for callers that link the +library **in the same process**. When the caller lives in a *different process* +— possibly on a *different machine* — there is no shared address space, so the +request has to be serialized. That is exactly what the **CBOR ABI** +(`_cbor`, declared in `my_timer_cbor.h`) is for. + +This example wires that ABI across a socket: + +- **`server`** links `libmy_timer`, creates one timer context at startup, and + serves method calls. It owns the `ctx` pointer — which is meaningful only + inside its own address space and never travels over the wire. +- **`client`** does **not** link the library. It only builds CBOR request + payloads (with the `FfiCbor` encoder bundled in `my_timer_cbor.h`) and parses + CBOR responses. It could be written in any language with a CBOR codec. + +``` + client process server process + ┌─────────────┐ method + CBOR req ┌──────────────────────────┐ + │ build CBOR │ ─────────────────────▶ │ my_timer__cbor(ctx,…) │ + │ parse CBOR │ ◀───────────────────── │ libmy_timer (FFI thread) │ + └─────────────┘ ret + CBOR response └──────────────────────────┘ +``` + +Wire framing (network byte order, so endianness never matters): + +``` +request: [u32 method_len][method][u32 payload_len][cbor payload] +response: [i32 ret ][u32 resp_len][cbor/raw response] +``` + +## Build + +```sh +cd examples/timer/ipc +make # builds libmy_timer + server + client +``` + +## Scenario A — same machine (two processes, AF_UNIX) + +A Unix-domain socket is the right transport when both ends are on one host. + +```sh +make demo # starts the server, runs the client, cleans up +``` + +or manually, in two terminals: + +```sh +# terminal 1 +./server --unix /tmp/my_timer.sock + +# terminal 2 +./client --unix /tmp/my_timer.sock +``` + +Expected client output: + +``` +[client] version = nim-timer v0.1.0 +[client] echo.echoed= hello over the wire +[client] echo.timer = ipc-server # proves the server's context state round-tripped +``` + +## Scenario B — separate machines (AF_INET / TCP) + +The exact same binaries, over TCP. Run the server on host A and the client on +host B; only the address changes. + +```sh +# host A (the server, e.g. 192.168.1.20) +./server --tcp 0.0.0.0 9099 + +# host B (the client) +./client --tcp 192.168.1.20 9099 +``` + +Because the wire is self-describing CBOR with network-byte-order framing, the +two machines may differ in OS, architecture, or endianness. The client needs +only `my_timer_cbor.h` (or a CBOR library in its own language) — not the +compiled timer library. + +## Notes + +- Every `{.ffi.}` call is dispatched on the library's FFI thread, so the server + blocks on a condvar-backed callback for each result before replying. +- The client demonstrates `version` (empty request → text response) and `echo` + (nested request → `EchoResponse` map). `proto.h` includes a small CBOR reader + to pull text fields out of the response map; a real client would use its + language's CBOR library. diff --git a/examples/timer/ipc/client.c b/examples/timer/ipc/client.c new file mode 100644 index 0000000..f9b9b10 --- /dev/null +++ b/examples/timer/ipc/client.c @@ -0,0 +1,111 @@ +// Timer IPC client. Does NOT link the library — it only builds CBOR requests +// (with the FfiCbor encoder from my_timer_cbor.h) and parses CBOR responses. +// The actual timer runs in the server process, reached over a socket. +// +// ./client --unix /tmp/my_timer.sock # same machine +// ./client --tcp 192.168.1.20 9099 # separate machine +#include "proto.h" + +#include +#include +#include +#include + +static int connect_unix(const char *path) { + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) return -1; + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1); + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) { + close(fd); + return -1; + } + return fd; +} +static int connect_tcp(const char *host, uint16_t port) { + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) return -1; + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) { + close(fd); + return -1; + } + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) { + close(fd); + return -1; + } + return fd; +} + +// Send one request frame, read one response frame. Returns ret code (or -1 on +// transport error); response bytes (malloc'd, caller frees) via out params. +static int call(int fd, const char *method, FfiCbor req, uint8_t **resp, + uint32_t *resp_len) { + if (proto_send_request(fd, method, req.buf, (uint32_t)req.len) != 0) return -1; + int32_t ret = -1; + if (proto_recv_response(fd, &ret, resp, resp_len) != 0) return -1; + return ret; +} + +int main(int argc, char **argv) { + int fd = -1; + if (argc == 3 && strcmp(argv[1], "--unix") == 0) { + fd = connect_unix(argv[2]); + } else if (argc == 4 && strcmp(argv[1], "--tcp") == 0) { + fd = connect_tcp(argv[2], (uint16_t)atoi(argv[3])); + } else { + fprintf(stderr, "usage: %s --unix | --tcp \n", argv[0]); + return 2; + } + if (fd < 0) { + perror("connect"); + return 1; + } + printf("[client] connected\n"); + + uint8_t *resp = NULL; + uint32_t rlen = 0; + + // 1) version — empty request, response is a CBOR text string. + { + FfiCbor req = req_version(); + int ret = call(fd, "version", req, &resp, &rlen); + ffi_cbor_free(&req); + if (ret == RET_OK) { + char *s = ffi_decode_text(resp, rlen); + printf("[client] version = %s\n", s ? s : ""); + free(s); + } else { + printf("[client] version failed (ret=%d)\n", ret); + } + free(resp); + resp = NULL; + } + + // 2) echo — nested request, response is an EchoResponse map. + { + FfiCbor req = req_echo("hello over the wire", 5); + int ret = call(fd, "echo", req, &resp, &rlen); + ffi_cbor_free(&req); + if (ret == RET_OK) { + char echoed[256] = {0}, timer_name[256] = {0}; + cbor_map_get_text(resp, rlen, "echoed", echoed, sizeof(echoed)); + cbor_map_get_text(resp, rlen, "timerName", timer_name, sizeof(timer_name)); + printf("[client] echo.echoed= %s\n", echoed); + printf("[client] echo.timer = %s\n", timer_name); + } else { + printf("[client] echo failed (ret=%d)\n", ret); + } + free(resp); + resp = NULL; + } + + close(fd); + printf("[client] done\n"); + return 0; +} diff --git a/examples/timer/ipc/proto.h b/examples/timer/ipc/proto.h new file mode 100644 index 0000000..ba8ce33 --- /dev/null +++ b/examples/timer/ipc/proto.h @@ -0,0 +1,203 @@ +// Shared wire protocol for the timer IPC example (client <-> server). +// +// The library lives in the *server* process; the `ctx` it returns is a pointer +// into the server's address space and never crosses the wire. A client sends a +// method name + a CBOR request payload; the server routes it to the matching +// `_cbor` entry point on its own context and ships the CBOR response back. +// +// All framing uses network byte order so the same client/server work whether +// they are two processes on one machine (AF_UNIX) or two machines (AF_INET). +// +// Request frame: [u32 method_len][method][u32 payload_len][cbor payload] +// Response frame: [ i32 ret ][u32 resp_len][cbor/raw response] +#ifndef TIMER_IPC_PROTO_H +#define TIMER_IPC_PROTO_H + +#include "my_timer_cbor.h" // FfiCbor encoder + ffi_decode_text (pure C, no lib) + +#include +#include +#include +#include +#include + +// --- reliable read/write (handle short reads/writes) ------------------------ +static int io_write_all(int fd, const void *buf, size_t n) { + const uint8_t *p = (const uint8_t *)buf; + while (n) { + ssize_t w = write(fd, p, n); + if (w <= 0) return -1; + p += (size_t)w; + n -= (size_t)w; + } + return 0; +} +static int io_read_all(int fd, void *buf, size_t n) { + uint8_t *p = (uint8_t *)buf; + while (n) { + ssize_t r = read(fd, p, n); + if (r <= 0) return -1; // 0 = peer closed + p += (size_t)r; + n -= (size_t)r; + } + return 0; +} + +static int io_write_u32(int fd, uint32_t v) { + uint32_t be = htonl(v); + return io_write_all(fd, &be, 4); +} +static int io_read_u32(int fd, uint32_t *out) { + uint32_t be; + if (io_read_all(fd, &be, 4) != 0) return -1; + *out = ntohl(be); + return 0; +} + +// --- request frame ---------------------------------------------------------- +static int proto_send_request(int fd, const char *method, const uint8_t *payload, + uint32_t payload_len) { + uint32_t mlen = (uint32_t)strlen(method); + if (io_write_u32(fd, mlen) != 0) return -1; + if (io_write_all(fd, method, mlen) != 0) return -1; + if (io_write_u32(fd, payload_len) != 0) return -1; + if (payload_len && io_write_all(fd, payload, payload_len) != 0) return -1; + return 0; +} + +// Reads a request frame. `method` is filled (NUL-terminated, <= method_cap-1). +// `*payload` is malloc'd (caller frees) or NULL when empty. +static int proto_recv_request(int fd, char *method, size_t method_cap, + uint8_t **payload, uint32_t *payload_len) { + uint32_t mlen; + if (io_read_u32(fd, &mlen) != 0) return -1; + if (mlen >= method_cap) return -1; + if (io_read_all(fd, method, mlen) != 0) return -1; + method[mlen] = '\0'; + if (io_read_u32(fd, payload_len) != 0) return -1; + *payload = NULL; + if (*payload_len) { + *payload = (uint8_t *)malloc(*payload_len); + if (!*payload || io_read_all(fd, *payload, *payload_len) != 0) { + free(*payload); + return -1; + } + } + return 0; +} + +// --- response frame --------------------------------------------------------- +static int proto_send_response(int fd, int32_t ret, const uint8_t *resp, + uint32_t resp_len) { + if (io_write_u32(fd, (uint32_t)ret) != 0) return -1; + if (io_write_u32(fd, resp_len) != 0) return -1; + if (resp_len && io_write_all(fd, resp, resp_len) != 0) return -1; + return 0; +} +static int proto_recv_response(int fd, int32_t *ret, uint8_t **resp, + uint32_t *resp_len) { + uint32_t r; + if (io_read_u32(fd, &r) != 0) return -1; + *ret = (int32_t)r; + if (io_read_u32(fd, resp_len) != 0) return -1; + *resp = NULL; + if (*resp_len) { + *resp = (uint8_t *)malloc(*resp_len); + if (!*resp || io_read_all(fd, *resp, *resp_len) != 0) { + free(*resp); + return -1; + } + } + return 0; +} + +// --- CBOR request builders (use the FfiCbor encoder from my_timer_cbor.h) ---- +// version: empty map. request map keys: (none) +static FfiCbor req_version(void) { + FfiCbor c = ffi_cbor_new(); + ffi_cbor_map(&c, 0); + return c; +} +// echo: { "req": { "message": , "delayMs": } } +static FfiCbor req_echo(const char *message, int64_t delay_ms) { + FfiCbor c = ffi_cbor_new(); + ffi_cbor_map(&c, 1); + ffi_cbor_text(&c, "req"); + ffi_cbor_map(&c, 2); + ffi_cbor_kv_text(&c, "message", message); + ffi_cbor_kv_int(&c, "delayMs", delay_ms); + return c; +} + +// --- minimal CBOR response reader ------------------------------------------- +// Enough to walk the definite-length maps the library emits (text/int/bool/ +// nested). `cbor_item_len` returns the byte length of one item at `p`, 0 on err. +static size_t cbor_item_len(const uint8_t *p, size_t len) { + if (len < 1) return 0; + uint8_t major = p[0] >> 5, info = p[0] & 0x1f; + size_t head = 1; + uint64_t arg = info; + if (info == 24) { if (len < 2) return 0; arg = p[1]; head = 2; } + else if (info == 25) { if (len < 3) return 0; arg = ((uint64_t)p[1] << 8) | p[2]; head = 3; } + else if (info == 26) { if (len < 5) return 0; arg = ((uint64_t)p[1] << 24) | ((uint64_t)p[2] << 16) | ((uint64_t)p[3] << 8) | p[4]; head = 5; } + else if (info == 27) { if (len < 9) return 0; arg = 0; for (int i = 1; i <= 8; i++) arg = (arg << 8) | p[i]; head = 9; } + else if (info >= 28) return 0; + switch (major) { + case 0: case 1: case 7: return head; // uint / negint / simple+float + case 2: case 3: return head + (size_t)arg; // bytes / text + case 4: { // array: arg items + size_t off = head; + for (uint64_t i = 0; i < arg; i++) { + size_t n = cbor_item_len(p + off, len - off); + if (!n) return 0; + off += n; + } + return off; + } + case 5: { // map: 2*arg items + size_t off = head; + for (uint64_t i = 0; i < arg * 2; i++) { + size_t n = cbor_item_len(p + off, len - off); + if (!n) return 0; + off += n; + } + return off; + } + default: return 0; + } +} + +// Find text value for `key` in a top-level definite-length map. Copies into +// `out` (NUL-terminated). Returns 1 on success, 0 if not found / not text. +static int cbor_map_get_text(const uint8_t *p, size_t len, const char *key, + char *out, size_t out_cap) { + if (len < 1 || (p[0] >> 5) != 5) return 0; + uint8_t info = p[0] & 0x1f; + if (info >= 24) return 0; // only small maps in this example + uint64_t pairs = info; + size_t off = 1, klen = strlen(key); + for (uint64_t i = 0; i < pairs; i++) { + const uint8_t *k = p + off; + if (off >= len || (k[0] >> 5) != 3) return 0; + uint8_t kinfo = k[0] & 0x1f; + if (kinfo >= 24) return 0; + size_t kn = 1 + kinfo, vlen_off = off + kn; + const uint8_t *v = p + vlen_off; + int match = (kinfo == klen) && memcmp(k + 1, key, klen) == 0; + if (match && (v[0] >> 5) == 3) { + uint8_t vinfo = v[0] & 0x1f; + if (vinfo >= 24) return 0; + size_t vn = vinfo; + size_t cp = vn < out_cap - 1 ? vn : out_cap - 1; + memcpy(out, v + 1, cp); + out[cp] = '\0'; + return 1; + } + size_t vfull = cbor_item_len(v, len - vlen_off); + if (!vfull) return 0; + off = vlen_off + vfull; + } + return 0; +} + +#endif // TIMER_IPC_PROTO_H diff --git a/examples/timer/ipc/server.c b/examples/timer/ipc/server.c new file mode 100644 index 0000000..cd6df00 --- /dev/null +++ b/examples/timer/ipc/server.c @@ -0,0 +1,171 @@ +// Timer IPC server: links libmy_timer, owns the FFI context, and serves method +// calls over a socket using the CBOR ABI. One context is created at startup and +// shared by every client connection (the library is internally thread-safe and +// serializes work on its own FFI thread). +// +// ./server --unix /tmp/my_timer.sock # same machine (AF_UNIX) +// ./server --tcp 0.0.0.0 9099 # separate machines (AF_INET) +#include "proto.h" + +#include +#include +#include +#include +#include + +// --- blocking response capture for the async CBOR entry points -------------- +typedef struct { + int ret; + uint8_t *bytes; + size_t len; + int done; + pthread_mutex_t mu; + pthread_cond_t cv; +} Resp; + +static void resp_init(Resp *r) { + memset(r, 0, sizeof(*r)); + pthread_mutex_init(&r->mu, NULL); + pthread_cond_init(&r->cv, NULL); +} +static void resp_reset(Resp *r) { + free(r->bytes); + r->bytes = NULL; + r->len = 0; + r->ret = 0; + r->done = 0; +} +static void on_result(int ret, const char *msg, size_t len, void *ud) { + Resp *r = (Resp *)ud; + pthread_mutex_lock(&r->mu); + r->ret = ret; + r->bytes = (uint8_t *)malloc(len ? len : 1); + if (msg && len) memcpy(r->bytes, msg, len); + r->len = len; + r->done = 1; + pthread_cond_signal(&r->cv); + pthread_mutex_unlock(&r->mu); +} +static void resp_wait(Resp *r) { + pthread_mutex_lock(&r->mu); + while (!r->done) pthread_cond_wait(&r->cv, &r->mu); + pthread_mutex_unlock(&r->mu); +} + +// --- socket setup ----------------------------------------------------------- +static int make_unix_listener(const char *path) { + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) return -1; + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1); + unlink(path); + if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0 || listen(fd, 8) != 0) { + close(fd); + return -1; + } + return fd; +} +static int make_tcp_listener(const char *host, uint16_t port) { + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) return -1; + int yes = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) { + close(fd); + return -1; + } + if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0 || listen(fd, 8) != 0) { + close(fd); + return -1; + } + return fd; +} + +// --- dispatch one method on the shared context ------------------------------ +static int dispatch(void *ctx, const char *method, const uint8_t *payload, + uint32_t plen, Resp *r) { + resp_reset(r); + if (strcmp(method, "version") == 0) + return my_timer_version_cbor(ctx, on_result, r, payload, plen); + if (strcmp(method, "echo") == 0) + return my_timer_echo_cbor(ctx, on_result, r, payload, plen); + if (strcmp(method, "complex") == 0) + return my_timer_complex_cbor(ctx, on_result, r, payload, plen); + if (strcmp(method, "schedule") == 0) + return my_timer_schedule_cbor(ctx, on_result, r, payload, plen); + return -1; // unknown method +} + +static void serve_conn(int conn, void *ctx, Resp *r) { + for (;;) { + char method[64]; + uint8_t *payload = NULL; + uint32_t plen = 0; + if (proto_recv_request(conn, method, sizeof(method), &payload, &plen) != 0) + break; // peer closed + int rc = dispatch(ctx, method, payload, plen, r); + free(payload); + if (rc == RET_OK) + resp_wait(r); + int32_t ret = (rc == RET_OK) ? r->ret : RET_ERR; + const uint8_t *body = (rc == RET_OK) ? r->bytes : (const uint8_t *)method; + uint32_t blen = (rc == RET_OK) ? (uint32_t)r->len : 0; + printf("[server] %-9s -> ret=%d (%u bytes)\n", method, ret, blen); + if (proto_send_response(conn, ret, body, blen) != 0) break; + } +} + +int main(int argc, char **argv) { + setvbuf(stdout, NULL, _IOLBF, 0); // flush server logs line-by-line + int listener = -1; + if (argc == 3 && strcmp(argv[1], "--unix") == 0) { + listener = make_unix_listener(argv[2]); + printf("[server] listening on unix:%s\n", argv[2]); + } else if (argc == 4 && strcmp(argv[1], "--tcp") == 0) { + listener = make_tcp_listener(argv[2], (uint16_t)atoi(argv[3])); + printf("[server] listening on tcp:%s:%s\n", argv[2], argv[3]); + } else { + fprintf(stderr, "usage: %s --unix | --tcp \n", argv[0]); + return 2; + } + if (listener < 0) { + perror("listen"); + return 1; + } + + // Create the library context once, up front, with a fixed config. + Resp r; + resp_init(&r); + FfiCbor cfg = ffi_cbor_new(); + ffi_cbor_map(&cfg, 1); + ffi_cbor_text(&cfg, "config"); + ffi_cbor_map(&cfg, 1); + ffi_cbor_kv_text(&cfg, "name", "ipc-server"); + void *ctx = my_timer_create_cbor(cfg.buf, cfg.len, on_result, &r); + ffi_cbor_free(&cfg); + resp_wait(&r); + if (!ctx) { + fprintf(stderr, "[server] create failed\n"); + return 1; + } + printf("[server] timer context ready\n"); + + for (;;) { + int conn = accept(listener, NULL, NULL); + if (conn < 0) break; + printf("[server] client connected\n"); + serve_conn(conn, ctx, &r); + close(conn); + printf("[server] client disconnected\n"); + } + + my_timer_destroy(ctx); + close(listener); + return 0; +}