mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-22 09:20:13 +00:00
docs(examples): add CBOR-over-socket IPC example (same + separate machines)
The native C ABI only works in-process. This example demonstrates the other half — the CBOR ABI crossing a process (and machine) boundary — since the `ctx` pointer is process-local and cannot travel over the wire. A server links libmy_timer, owns one context, and serves method calls; a client links nothing (it only needs the FfiCbor encoder/ffi_decode_text in my_timer_cbor.h) and speaks the same CBOR over a socket. Both binaries accept `--unix <path>` for two processes on one host and `--tcp <host> <port>` for two machines — the only difference is the socket family, so one client/server pair covers both scenarios. Framing is length-prefixed in network byte order so the endpoints may differ in OS, arch, or endianness. `proto.h` carries the shared framing, the CBOR request builders, and a small CBOR map reader so the client can pull text fields out of a response without a full CBOR library. Verified end-to-end over both AF_UNIX and TCP loopback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c5c7c373b4
commit
efadf11660
5
examples/timer/ipc/.gitignore
vendored
Normal file
5
examples/timer/ipc/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/server
|
||||
/client
|
||||
/libmy_timer.dylib
|
||||
/libmy_timer.so
|
||||
/.srv.pid
|
||||
54
examples/timer/ipc/Makefile
Normal file
54
examples/timer/ipc/Makefile
Normal file
@ -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)
|
||||
91
examples/timer/ipc/README.md
Normal file
91
examples/timer/ipc/README.md
Normal file
@ -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**
|
||||
(`<name>_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_<m>_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.
|
||||
111
examples/timer/ipc/client.c
Normal file
111
examples/timer/ipc/client.c
Normal file
@ -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 <netinet/in.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
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 <path> | --tcp <host> <port>\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 : "<decode failed>");
|
||||
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;
|
||||
}
|
||||
203
examples/timer/ipc/proto.h
Normal file
203
examples/timer/ipc/proto.h
Normal file
@ -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
|
||||
// `<name>_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 <arpa/inet.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// --- 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": <str>, "delayMs": <int> } }
|
||||
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
|
||||
171
examples/timer/ipc/server.c
Normal file
171
examples/timer/ipc/server.c
Normal file
@ -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 <netinet/in.h>
|
||||
#include <pthread.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
// --- 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 <path> | --tcp <host> <port>\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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user