diff --git a/examples/timer/c_bindings/.gitignore b/examples/timer/c_bindings/.gitignore new file mode 100644 index 0000000..15097d5 --- /dev/null +++ b/examples/timer/c_bindings/.gitignore @@ -0,0 +1,3 @@ +/example +/libmy_timer.dylib +/libmy_timer.so diff --git a/examples/timer/c_bindings/Makefile b/examples/timer/c_bindings/Makefile new file mode 100644 index 0000000..843082a --- /dev/null +++ b/examples/timer/c_bindings/Makefile @@ -0,0 +1,40 @@ +# Build the native (same-process) C example for the timer library. +# +# make run # build the Nim dylib + the C driver, then run it +# make clean +# +# The Nim library is compiled from the repository root so its vendored Nimble +# dependencies resolve, exactly like the CMake-based C++ example does. + +REPO_ROOT := $(abspath ../../..) +NIM_SRC := $(REPO_ROOT)/examples/timer/timer.nim + +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 +CFLAGS ?= -std=c11 -Wall -Wextra -O2 +NIMFLAGS := --mm:orc -d:chronicles_log_level=WARN --app:lib --noMain \ + --nimMainPrefix:libmy_timer + +.PHONY: all run clean + +all: example + +$(LIBNAME): + cd $(REPO_ROOT) && nim c $(NIMFLAGS) -o:$(CURDIR)/$(LIBNAME) $(NIM_SRC) + +example: example.c my_timer.h $(LIBNAME) + $(CC) $(CFLAGS) -I. example.c -L. -lmy_timer $(RPATH) -o example + +run: example + ./example + +clean: + rm -f example $(LIBNAME) diff --git a/examples/timer/c_bindings/README.md b/examples/timer/c_bindings/README.md new file mode 100644 index 0000000..39b7ac3 --- /dev/null +++ b/examples/timer/c_bindings/README.md @@ -0,0 +1,39 @@ +# C bindings — native (same-process) example + +Generated C headers for the timer library plus a small driver that links the +library directly and calls the **native** (zero-serialization) ABI. + +## Files + +| File | Description | +|------|-------------| +| `my_timer.h` | Native ABI: each `{.ffi.}` type is a plain C `struct`, passed by value to `int (ctx, cb, ud, )`. Results arrive on the callback. Best for same-process callers — no serialization. | +| `my_timer_cbor.h` | CBOR ABI (`_cbor`): request/response as CBOR bytes. Use this when the call crosses a process or machine boundary. See [`../ipc`](../ipc). | +| `example.c` | Native same-process driver: create → version → echo → complex → destroy. | +| `Makefile` | Builds the Nim dylib (from the repo root) and the driver. | + +The headers are regenerated by `nimble genbindings_c` (run from the repo root) +and overwritten each time — don't edit them by hand. + +## Build & run + +```sh +cd examples/timer/c_bindings +make run +``` + +This compiles `libmy_timer.{dylib,so}` and runs `./example`, which prints the +library version and the round-tripped echo/complex responses. Every call is +dispatched on the library's FFI thread, so the driver blocks on a condvar-backed +callback for each result. + +## Native vs CBOR + +The native path passes `{.ffi.}` structs as flat C-POD values (`const char*` for +strings, `{ T* ptr; size_t len }` for sequences, `{ int present; T }` for +options). Arguments are **deep-copied** across the FFI-thread boundary, so the C +caller's buffers can be freed immediately after the call returns. String returns +come back raw; struct returns are CBOR-encoded inside the callback payload. + +For the cross-process / cross-machine path, the same library is reached over a +socket using the CBOR ABI — see [`../ipc`](../ipc). diff --git a/examples/timer/c_bindings/example.c b/examples/timer/c_bindings/example.c new file mode 100644 index 0000000..8cc23da --- /dev/null +++ b/examples/timer/c_bindings/example.c @@ -0,0 +1,118 @@ +// Native (zero-serialization, same-process) C example for the timer library. +// +// This is the in-process path: the C host links libmy_timer directly and calls +// the native `` entry points, passing `{.ffi.}` types as plain C structs +// by value. No CBOR — arguments are deep-copied across the FFI thread boundary +// as flat C-POD graphs. Each call delivers its result to a callback that we +// block on with a condvar (every call is dispatched on the library's FFI +// thread, so the result is not ready until the callback fires). +// +// For the cross-process / cross-machine path (CBOR over a socket), see +// ../ipc/. +#include "my_timer.h" +#include +#include +#include + +// --- one-shot blocking response capture ------------------------------------- +typedef struct { + int ret; + char buf[2048]; + 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); +} + +// Native ABI: on RET_OK (msg, len) is the raw return value (for a string- +// returning proc, the bytes; for a struct-returning proc, its CBOR encoding); +// on RET_ERR it is the raw error text. We copy it so it outlives the callback. +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; + size_t n = len < sizeof(r->buf) - 1 ? len : sizeof(r->buf) - 1; + if (msg && n) memcpy(r->buf, msg, n); + r->buf[n] = '\0'; + 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); +} + +int main(void) { + // 1) Construct the library context. The ctor takes a TimerConfig by value; + // its `name: string` field is a plain `const char*` on the C side. + Resp cr; + resp_init(&cr); + TimerConfig cfg = {.name = "c-native-demo"}; + void *ctx = my_timer_create(cfg, on_result, &cr); + resp_wait(&cr); + if (!ctx || cr.ret != RET_OK) { + fprintf(stderr, "create failed (ret=%d): %s\n", cr.ret, cr.buf); + return 1; + } + printf("created timer ctx=%p\n", ctx); + + // 2) Synchronous-shaped call: version returns a plain string, delivered raw. + Resp vr; + resp_init(&vr); + if (my_timer_version(ctx, on_result, &vr) == RET_OK) { + resp_wait(&vr); + printf("version: %s\n", vr.buf); + } + + // 3) Struct param by value: EchoRequest { const char* message; int64 delayMs }. + // The library sleeps delayMs on its chronos loop, then echoes the message. + Resp er; + resp_init(&er); + EchoRequest req = {.message = "hello from C", .delayMs = 5}; + if (my_timer_echo(ctx, on_result, &er, req) == RET_OK) { + resp_wait(&er); + // EchoResponse is a struct return, delivered as CBOR on the native path; + // the echoed message appears verbatim inside the payload bytes. + printf("echo ret=%d (%zu-byte response, contains \"%s\")\n", er.ret, er.len, + strstr(er.buf, "hello from C") ? "hello from C" : ""); + } + + // 4) Deeply nested struct: seq, seq, Option, Option. + // Demonstrates that the whole graph is deep-copied across the boundary. + Resp xr; + resp_init(&xr); + EchoRequest msgs[2] = { + {.message = "one", .delayMs = 0}, + {.message = "two", .delayMs = 0}, + }; + const char *tags[1] = {"demo"}; + ComplexRequest creq = { + .messages = msgs, + .messages_len = 2, + .tags = tags, + .tags_len = 1, + .note_present = 1, + .note = "a note", + .retries_present = 1, + .retries = 3, + }; + if (my_timer_complex(ctx, on_result, &xr, creq) == RET_OK) { + resp_wait(&xr); + printf("complex ret=%d (%zu-byte response)\n", xr.ret, xr.len); + } + + // 5) Tear down the context (joins the FFI thread). + my_timer_destroy(ctx); + printf("destroyed; done.\n"); + return 0; +} diff --git a/examples/timer/c_bindings/my_timer.h b/examples/timer/c_bindings/my_timer.h new file mode 100644 index 0000000..d5ad5fa --- /dev/null +++ b/examples/timer/c_bindings/my_timer.h @@ -0,0 +1,116 @@ +// Generated by nim-ffi C codegen. Do not edit by hand. +// +// Native (zero-serialization) C ABI. Each call delivers its result to the +// callback: on RET_OK, (msg, len) is the raw return value (for string-returning +// procs, the string bytes — not NUL-terminated; use len); on RET_ERR, (msg, len) +// is the raw error text. A `_cbor` variant of each proc also exists for +// generic/cross-language callers that prefer a CBOR request/response. +#ifndef NIM_FFI_GEN_MY_TIMER_H +#define NIM_FFI_GEN_MY_TIMER_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef NIM_FFI_RET_CODES +#define NIM_FFI_RET_CODES +#define RET_OK 0 +#define RET_ERR 1 +#define RET_MISSING_CALLBACK 2 +#endif + +#ifndef NIM_FFI_CALLBACK_T +#define NIM_FFI_CALLBACK_T +typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData); +#endif + + +// --- {.ffi.}-annotated types, exposed as C structs ---------- +typedef struct { + const char* name; +} TimerConfig; + +typedef struct { + const char* message; + int64_t delayMs; +} EchoRequest; + +typedef struct { + const char* echoed; + const char* timerName; +} EchoResponse; + +typedef struct { + EchoRequest *messages; + size_t messages_len; + const char* *tags; + size_t tags_len; + int note_present; + const char* note; + int retries_present; + int64_t retries; +} ComplexRequest; + +typedef struct { + const char* summary; + int64_t itemCount; + int hasNote; +} ComplexResponse; + +typedef struct { + const char* message; + int64_t echoCount; +} EchoEvent; + +typedef struct { + const char* name; + const char* *payload; + size_t payload_len; + int64_t priority; +} JobSpec; + +typedef struct { + int64_t maxAttempts; + int64_t backoffMs; + const char* *retryOn; + size_t retryOn_len; +} RetryPolicy; + +typedef struct { + int64_t startAtMs; + int64_t intervalMs; + int jitter_present; + int64_t jitter; +} ScheduleConfig; + +typedef struct { + const char* jobId; + int64_t willRunCount; + int64_t firstRunAtMs; + int64_t effectiveBackoffMs; +} ScheduleResult; + + +void *my_timer_create(TimerConfig config, FFICallBack callback, void *userData); + +int my_timer_echo(void *ctx, FFICallBack callback, void *userData, EchoRequest req); + +int my_timer_version(void *ctx, FFICallBack callback, void *userData); + +int my_timer_complex(void *ctx, FFICallBack callback, void *userData, ComplexRequest req); + +int my_timer_schedule(void *ctx, FFICallBack callback, void *userData, JobSpec job, RetryPolicy retry, ScheduleConfig schedule); + +int my_timer_destroy(void *ctx); + +uint64_t my_timer_add_event_listener(void *ctx, const char *eventName, FFICallBack callback, void *userData); +int my_timer_remove_event_listener(void *ctx, uint64_t listenerId); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif /* NIM_FFI_GEN_MY_TIMER_H */ \ No newline at end of file diff --git a/examples/timer/c_bindings/my_timer_cbor.h b/examples/timer/c_bindings/my_timer_cbor.h new file mode 100644 index 0000000..04e8c93 --- /dev/null +++ b/examples/timer/c_bindings/my_timer_cbor.h @@ -0,0 +1,178 @@ +// Generated by nim-ffi C codegen. Do not edit by hand. +// +// CBOR ABI (`_cbor`). Use this for callers that cross a process or machine +// boundary (the request has to be serialized anyway) or any generic / cross- +// language caller. Build the request with the FfiCbor helpers below — a CBOR map +// whose keys are the Nim parameter names (listed per proc) — call the matching +// `_cbor`, and decode the RET_OK response (a CBOR-encoded value; for +// string-returning procs a CBOR text string) with ffi_decode_text. RET_ERR +// delivers raw error text. For same-process callers, prefer the native `` +// ABI in the companion .h header. +#ifndef NIM_FFI_GEN_MY_TIMER_CBOR_H +#define NIM_FFI_GEN_MY_TIMER_CBOR_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef NIM_FFI_RET_CODES +#define NIM_FFI_RET_CODES +#define RET_OK 0 +#define RET_ERR 1 +#define RET_MISSING_CALLBACK 2 +#endif + +#ifndef NIM_FFI_CALLBACK_T +#define NIM_FFI_CALLBACK_T +typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData); +#endif + +#ifndef NIM_FFI_CBOR_HELPERS +#define NIM_FFI_CBOR_HELPERS +// --- minimal growable CBOR request encoder -------------------------------- +typedef struct { + uint8_t *buf; + size_t cap; + size_t len; +} FfiCbor; + +static inline FfiCbor ffi_cbor_new(void) { + FfiCbor c; + c.cap = 256; + c.len = 0; + c.buf = (uint8_t *)malloc(c.cap); + return c; +} +static inline void ffi_cbor_free(FfiCbor *c) { + free(c->buf); + c->buf = NULL; +} +static inline void ffi_cbor_put(FfiCbor *c, uint8_t b) { + if (c->len >= c->cap) { + c->cap *= 2; + c->buf = (uint8_t *)realloc(c->buf, c->cap); + } + c->buf[c->len++] = b; +} +static inline void ffi_cbor_head(FfiCbor *c, uint8_t major, uint64_t arg) { + uint8_t mt = (uint8_t)(major << 5); + if (arg < 24) { + ffi_cbor_put(c, mt | (uint8_t)arg); + } else if (arg <= 0xff) { + ffi_cbor_put(c, mt | 24); + ffi_cbor_put(c, (uint8_t)arg); + } else if (arg <= 0xffff) { + ffi_cbor_put(c, mt | 25); + ffi_cbor_put(c, (uint8_t)(arg >> 8)); + ffi_cbor_put(c, (uint8_t)arg); + } else if (arg <= 0xffffffffULL) { + ffi_cbor_put(c, mt | 26); + ffi_cbor_put(c, (uint8_t)(arg >> 24)); + ffi_cbor_put(c, (uint8_t)(arg >> 16)); + ffi_cbor_put(c, (uint8_t)(arg >> 8)); + ffi_cbor_put(c, (uint8_t)arg); + } else { + ffi_cbor_put(c, mt | 27); + for (int s = 56; s >= 0; s -= 8) ffi_cbor_put(c, (uint8_t)(arg >> s)); + } +} +static inline void ffi_cbor_map(FfiCbor *c, size_t n) { ffi_cbor_head(c, 5, n); } +static inline void ffi_cbor_text(FfiCbor *c, const char *s) { + size_t n = s ? strlen(s) : 0; + ffi_cbor_head(c, 3, n); + for (size_t i = 0; i < n; i++) ffi_cbor_put(c, (uint8_t)s[i]); +} +static inline void ffi_cbor_kv_text(FfiCbor *c, const char *k, const char *v) { + ffi_cbor_text(c, k); + ffi_cbor_text(c, v); +} +static inline void ffi_cbor_kv_uint(FfiCbor *c, const char *k, uint64_t v) { + ffi_cbor_text(c, k); + ffi_cbor_head(c, 0, v); +} +static inline void ffi_cbor_kv_int(FfiCbor *c, const char *k, int64_t v) { + ffi_cbor_text(c, k); + if (v >= 0) + ffi_cbor_head(c, 0, (uint64_t)v); + else + ffi_cbor_head(c, 1, (uint64_t)(-(v + 1))); +} + +// --- response decoding ----------------------------------------------------- +// Zero-copy view of a top-level CBOR text string (the RET_OK payload). Sets +// *out/*outLen to point INTO `data` (no allocation; valid only while `data` is) +// and returns 1; returns 0 for a non-text-string payload. +static inline int ffi_text_view(const uint8_t *data, size_t len, + const uint8_t **out, size_t *outLen) { + if (len < 1 || (data[0] >> 5) != 3) return 0; + uint8_t info = data[0] & 0x1f; + size_t p = 1; + uint64_t slen = 0; + if (info < 24) { + slen = info; + } else if (info == 24) { + if (len < p + 1) return 0; + slen = data[p++]; + } else if (info == 25) { + if (len < p + 2) return 0; + slen = ((uint64_t)data[p] << 8) | data[p + 1]; + p += 2; + } else if (info == 26) { + if (len < p + 4) return 0; + slen = ((uint64_t)data[p] << 24) | ((uint64_t)data[p + 1] << 16) | + ((uint64_t)data[p + 2] << 8) | data[p + 3]; + p += 4; + } else { + return 0; + } + if (len < p + slen) return 0; + *out = data + p; + *outLen = (size_t)slen; + return 1; +} + +// Owning variant: malloc a NUL-terminated copy. NULL for a non-text payload. +// Caller frees. +static inline char *ffi_decode_text(const uint8_t *data, size_t len) { + const uint8_t *view; + size_t slen; + if (!ffi_text_view(data, len, &view, &slen)) return NULL; + char *out = (char *)malloc(slen + 1); + if (!out) return NULL; + memcpy(out, view, slen); + out[slen] = '\0'; + return out; +} +#endif // NIM_FFI_CBOR_HELPERS + + +// request map keys: {"config": TimerConfig} +void *my_timer_create_cbor(const uint8_t *reqCbor, size_t reqCborLen, FFICallBack callback, void *userData); + +// request map keys: {"req": EchoRequest} +int my_timer_echo_cbor(void *ctx, FFICallBack callback, void *userData, const uint8_t *reqCbor, size_t reqCborLen); + +// request: empty CBOR map (0xA0) +int my_timer_version_cbor(void *ctx, FFICallBack callback, void *userData, const uint8_t *reqCbor, size_t reqCborLen); + +// request map keys: {"req": ComplexRequest} +int my_timer_complex_cbor(void *ctx, FFICallBack callback, void *userData, const uint8_t *reqCbor, size_t reqCborLen); + +// request map keys: {"job": JobSpec, "retry": RetryPolicy, "schedule": ScheduleConfig} +int my_timer_schedule_cbor(void *ctx, FFICallBack callback, void *userData, const uint8_t *reqCbor, size_t reqCborLen); + +int my_timer_destroy(void *ctx); + +uint64_t my_timer_add_event_listener(void *ctx, const char *eventName, FFICallBack callback, void *userData); +int my_timer_remove_event_listener(void *ctx, uint64_t listenerId); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif /* NIM_FFI_GEN_MY_TIMER_CBOR_H */ \ No newline at end of file