mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-22 01:10:03 +00:00
docs(examples): C example reads typed struct returns
Update the native C example to cast each struct return's callback msg to its `const <Type>*` and read it in-callback (EchoResponse, ComplexResponse), instead of scanning opaque bytes. Regenerate the headers with the new return-shape note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9cf4bf0127
commit
98e8e8829f
@ -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 <Type>*`** 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).
|
||||
|
||||
@ -2,24 +2,26 @@
|
||||
//
|
||||
// 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).
|
||||
// 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 <Type>*`.
|
||||
//
|
||||
// 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 <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
// --- 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" : "<not found>");
|
||||
printf("echo: %s\n", er.text);
|
||||
}
|
||||
|
||||
// 4) Deeply nested struct: seq<struct>, seq<string>, Option<string>, Option<int>.
|
||||
// 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).
|
||||
|
||||
@ -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 `<name>_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 <Type>*` (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 `<name>_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
|
||||
|
||||
|
||||
@ -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 `<name>_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 <Type>*` (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 `<name>_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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user