mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-20 16:29:31 +00:00
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>
172 lines
5.2 KiB
C
172 lines
5.2 KiB
C
// 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;
|
|
}
|