docs(examples): add native (same-process) C example

The C codegen already emitted `my_timer.h` / `my_timer_cbor.h`, but the example
had no runnable driver. Add `example.c` exercising the native ABI end-to-end
(ctor with a struct param, string-returning version, struct-param echo, and a
deeply nested ComplexRequest), plus a Makefile that builds the Nim dylib from
the repo root — where the vendored Nimble deps resolve — and links the driver.

Native is the same-process path; the companion CBOR headers are for crossing a
process/machine boundary (see the forthcoming ipc example).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ivan FB 2026-05-31 10:47:27 +02:00
parent ad493d6f9d
commit c5c7c373b4
No known key found for this signature in database
GPG Key ID: DF0C67A04C543270
6 changed files with 494 additions and 0 deletions

3
examples/timer/c_bindings/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/example
/libmy_timer.dylib
/libmy_timer.so

View File

@ -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)

View File

@ -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 <name>(ctx, cb, ud, <args…>)`. Results arrive on the callback. Best for same-process callers — no serialization. |
| `my_timer_cbor.h` | CBOR ABI (`<name>_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).

View File

@ -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 `<name>` 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 <pthread.h>
#include <stdio.h>
#include <string.h>
// --- 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" : "<not found>");
}
// 4) Deeply nested struct: seq<struct>, seq<string>, Option<string>, Option<int>.
// 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;
}

View File

@ -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 `<name>_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 <stddef.h>
#include <stdint.h>
#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 */

View File

@ -0,0 +1,178 @@
// Generated by nim-ffi C codegen. Do not edit by hand.
//
// CBOR ABI (`<name>_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
// `<name>_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 `<name>`
// ABI in the companion <lib>.h header.
#ifndef NIM_FFI_GEN_MY_TIMER_CBOR_H
#define NIM_FFI_GEN_MY_TIMER_CBOR_H
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#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 */