diff --git a/examples/timer/c_bindings/README.md b/examples/timer/c_bindings/README.md index 39b7ac3..757f972 100644 --- a/examples/timer/c_bindings/README.md +++ b/examples/timer/c_bindings/README.md @@ -33,7 +33,9 @@ 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. +arrive as raw bytes; **struct returns arrive as a typed `const *`** in the +callback (cast and read it there — it is valid only for the callback's lifetime, +and the library deep-frees it afterwards, so copy out anything you need). 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 index 8cc23da..ac43a0f 100644 --- a/examples/timer/c_bindings/example.c +++ b/examples/timer/c_bindings/example.c @@ -2,24 +2,26 @@ // // 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). +// by value. No CBOR — arguments are deep-copied across the FFI thread boundary, +// and a struct return arrives at the callback as a typed `const *`. // -// For the cross-process / cross-machine path (CBOR over a socket), see -// ../ipc/. +// 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). A struct return is valid only for the +// callback's lifetime — read it there; the library frees it afterwards. +// +// 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 ------------------------------------- +// --- one-shot blocking result capture --------------------------------------- typedef struct { - int ret; - char buf[2048]; - size_t len; - int done; + int ret, done; + char text[512]; // a string return, or copied-out struct fields + long itemCount; + int hasNote; pthread_mutex_t mu; pthread_cond_t cv; } Resp; @@ -29,66 +31,98 @@ static void resp_init(Resp *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; +static void resp_done(Resp *r) { 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); } +// Acknowledges a call with no payload we care about (ctor) or an error. +static void cb_ack(int ret, const char *msg, size_t len, void *ud) { + Resp *r = ud; + pthread_mutex_lock(&r->mu); + r->ret = ret; + if (ret == RET_ERR && msg) { + size_t n = len < sizeof(r->text) - 1 ? len : sizeof(r->text) - 1; + memcpy(r->text, msg, n); + r->text[n] = 0; + } + resp_done(r); +} +// String return (version): msg is the raw bytes. +static void cb_string(int ret, const char *msg, size_t len, void *ud) { + Resp *r = ud; + pthread_mutex_lock(&r->mu); + r->ret = ret; + size_t n = len < sizeof(r->text) - 1 ? len : sizeof(r->text) - 1; + if (msg) memcpy(r->text, msg, n); + r->text[n] = 0; + resp_done(r); +} +// EchoResponse return: msg is a `const EchoResponse*` (read it here). +static void cb_echo(int ret, const char *msg, size_t len, void *ud) { + Resp *r = ud; + pthread_mutex_lock(&r->mu); + r->ret = ret; + if (ret == RET_OK) { + const EchoResponse *e = (const EchoResponse *)msg; + snprintf(r->text, sizeof(r->text), "echoed=%s timerName=%s", e->echoed, + e->timerName); + } + resp_done(r); + (void)len; +} +// ComplexResponse return: msg is a `const ComplexResponse*`. +static void cb_complex(int ret, const char *msg, size_t len, void *ud) { + Resp *r = ud; + pthread_mutex_lock(&r->mu); + r->ret = ret; + if (ret == RET_OK) { + const ComplexResponse *c = (const ComplexResponse *)msg; + snprintf(r->text, sizeof(r->text), "%s", c->summary); + r->itemCount = c->itemCount; + r->hasNote = c->hasNote; + } + resp_done(r); + (void)len; +} + 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. + // 1) Construct: TimerConfig by value (its `name: string` is a `const char*`). Resp cr; resp_init(&cr); TimerConfig cfg = {.name = "c-native-demo"}; - void *ctx = my_timer_create(cfg, on_result, &cr); + void *ctx = my_timer_create(cfg, cb_ack, &cr); resp_wait(&cr); if (!ctx || cr.ret != RET_OK) { - fprintf(stderr, "create failed (ret=%d): %s\n", cr.ret, cr.buf); + fprintf(stderr, "create failed (ret=%d): %s\n", cr.ret, cr.text); return 1; } printf("created timer ctx=%p\n", ctx); - // 2) Synchronous-shaped call: version returns a plain string, delivered raw. + // 2) String return. Resp vr; resp_init(&vr); - if (my_timer_version(ctx, on_result, &vr) == RET_OK) { + if (my_timer_version(ctx, cb_string, &vr) == RET_OK) { resp_wait(&vr); - printf("version: %s\n", vr.buf); + printf("version: %s\n", vr.text); } - // 3) Struct param by value: EchoRequest { const char* message; int64 delayMs }. - // The library sleeps delayMs on its chronos loop, then echoes the message. + // 3) Struct param + typed struct return (EchoResponse). Resp er; resp_init(&er); EchoRequest req = {.message = "hello from C", .delayMs = 5}; - if (my_timer_echo(ctx, on_result, &er, req) == RET_OK) { + if (my_timer_echo(ctx, cb_echo, &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" : ""); + printf("echo: %s\n", er.text); } - // 4) Deeply nested struct: seq, seq, Option, Option. - // Demonstrates that the whole graph is deep-copied across the boundary. + // 4) Deeply nested param + typed struct return (ComplexResponse). Resp xr; resp_init(&xr); EchoRequest msgs[2] = { @@ -106,9 +140,10 @@ int main(void) { .retries_present = 1, .retries = 3, }; - if (my_timer_complex(ctx, on_result, &xr, creq) == RET_OK) { + if (my_timer_complex(ctx, cb_complex, &xr, creq) == RET_OK) { resp_wait(&xr); - printf("complex ret=%d (%zu-byte response)\n", xr.ret, xr.len); + printf("complex: itemCount=%ld hasNote=%d summary=\"%s\"\n", xr.itemCount, + xr.hasNote, xr.text); } // 5) Tear down the context (joins the FFI thread). diff --git a/examples/timer/c_bindings/my_timer.h b/examples/timer/c_bindings/my_timer.h index d5ad5fa..cd50e82 100644 --- a/examples/timer/c_bindings/my_timer.h +++ b/examples/timer/c_bindings/my_timer.h @@ -1,10 +1,15 @@ // 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. +// callback. On RET_OK: +// - string-returning procs: (msg, len) is the raw string bytes (not +// NUL-terminated; use len). +// - struct-returning procs: msg is a pointer to the returned C struct — cast +// it to `const *` (len is sizeof). It is valid ONLY for the duration +// of the callback; copy out anything you need before returning. The library +// deep-frees it right after the callback (you free nothing). +// 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 CBOR. #ifndef NIM_FFI_GEN_MY_TIMER_H #define NIM_FFI_GEN_MY_TIMER_H diff --git a/examples/timer/go_bindings/my_timer.h b/examples/timer/go_bindings/my_timer.h index d5ad5fa..cd50e82 100644 --- a/examples/timer/go_bindings/my_timer.h +++ b/examples/timer/go_bindings/my_timer.h @@ -1,10 +1,15 @@ // 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. +// callback. On RET_OK: +// - string-returning procs: (msg, len) is the raw string bytes (not +// NUL-terminated; use len). +// - struct-returning procs: msg is a pointer to the returned C struct — cast +// it to `const *` (len is sizeof). It is valid ONLY for the duration +// of the callback; copy out anything you need before returning. The library +// deep-frees it right after the callback (you free nothing). +// 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 CBOR. #ifndef NIM_FFI_GEN_MY_TIMER_H #define NIM_FFI_GEN_MY_TIMER_H