Skip to content

Commit bcd911f

Browse files
committed
feat: allow no-ctx ffi procs
1 parent e60f771 commit bcd911f

32 files changed

Lines changed: 1145 additions & 129 deletions

CHANGELOG.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,26 @@ All notable changes to this project are documented in this file.
2020
where `-install_name` requires `-dynamiclib`.
2121

2222
### Added
23+
- **`{.ffiStatic.}`**: exports a context-independent proc. It takes no library
24+
param, and its wrapper takes no `ctx`, so a host can call a stateless utility
25+
(key generation, parsing, a version string) without constructing the library
26+
— previously the only way to expose one was an `{.ffi.}` method, forcing the
27+
caller to build and tear down an instance just to reach it
28+
([#134](https://github.com/logos-messaging/nim-ffi/issues/134)). Wired for
29+
both the `cbor` and `c` ABIs and all four backends: the C header emits
30+
`<lib>_static_<proc>(...)` (`<lib>_ctx_<proc>` is the method form and
31+
`<lib>_<proc>` the raw symbol), while C++ and Rust emit an associated function
32+
on the ctx type taking the `timeout` a method reads from its ctx. Handlers run
33+
on the library's *static context*, created on the first such call and alive for
34+
the rest of the process, so the first call starts a thread pair that is never
35+
torn down. An `{.ffiHandle.}` parameter or return is rejected at macro time: a
36+
handle belongs to the context that created it, which a static proc cannot reach.
2337
- `{.ffiEvent.}` no longer requires an explicit wire-name string: when omitted
2438
it is derived from the proc name via `camelToSnakeCase`
2539
(`onPeerConnected``on_peer_connected`), matching how `{.ffi.}` derives its
2640
C export symbol. Pass a string literal only to override it.
27-
- FFI annotations (`{.ffi.}`, `{.ffiCtor.}`, `{.ffiDtor.}`, `{.ffiEvent.}`,
28-
`{.ffiHandle.}`, `{.ffiRaw.}`) that expand after `genBindings()` now produce a
41+
- FFI annotations (`{.ffi.}`, `{.ffiStatic.}`, `{.ffiCtor.}`, `{.ffiDtor.}`,
42+
`{.ffiEvent.}`, `{.ffiHandle.}`, `{.ffiRaw.}`) that expand after `genBindings()` now produce a
2943
loud compile error instead of being silently dropped from the generated
3044
bindings.
3145
- **C binding generator** (`-d:targetLang=c`): emits a header-only C binding

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ requires "https://github.com/logos-messaging/nim-ffi >= 0.2.0"
1919

2020
- You **declare a library** once with `declareLibrary(name, LibType)`.
2121
- You **annotate procs and types** with pragmas (`{.ffi.}`, `{.ffiCtor.}`,
22-
`{.ffiDtor.}`, `{.ffiEvent.}`).
22+
`{.ffiDtor.}`, `{.ffiStatic.}`, `{.ffiEvent.}`).
2323
- You **call `genBindings()` last**, which emits the foreign bindings.
2424

2525
By default, every request/response crosses the boundary as a single CBOR blob
@@ -78,6 +78,7 @@ The generated C export names are the snake_case form of the proc names, e.g.
7878
| `declareLibrary(name, LibType[, defaultABIFormat])` | call | Registers the library, its state type, and the default wire format. Must run before any annotation. |
7979
| `{.ffi.}` on a `type` | `object` | Registers the type for binding generation; it serializes via the library's ABI format (CBOR by default). |
8080
| `{.ffi.}` on a `proc` | proc | Exposes a method. First param is the library value, then typed params; returns `Future[Result[T, string]]`. |
81+
| `{.ffiStatic.}` | proc | Exposes a context-independent proc: no library param, and its wrapper takes no ctx — see below. |
8182
| `{.ffiCtor.}` | proc | The constructor. Returns `Future[Result[LibType, string]]`; creates the FFI context. |
8283
| `{.ffiDtor.}` | proc | The destructor. Exactly one param `(x: LibType)`; tears the context down. |
8384
| `{.ffiEvent[: "wire_name"].}` | proc (empty body) | A library-initiated callback. Call the proc from any `{.ffi.}` handler to fire it. The wire name is optional — see below. |
@@ -91,6 +92,44 @@ Every `{.ffi.}` / `{.ffiCtor.}` proc must have an explicit
9192
`return ok(...)` without awaiting). The `Result`'s error string is delivered to
9293
the foreign caller as the failure message.
9394

95+
### Context-independent procs
96+
97+
A `{.ffi.}` proc is a method: it takes the library value and its wrapper takes a
98+
`ctx`, so the host must construct the library to call it. A stateless utility
99+
shouldn't have to pay for that. Annotate it `{.ffiStatic.}` instead — drop the
100+
library param, and the ctx disappears from the generated wrapper:
101+
102+
```nim
103+
proc counterParse*(text: string): Future[Result[BumpRequest, string]] {.ffiStatic.} =
104+
return ok(BumpRequest(by: text.parseInt()))
105+
```
106+
107+
```c
108+
/* {.ffi.} method */ counter_ctx_bump(ctx, &req, on_reply, ud);
109+
/* {.ffiStatic.} — no ctx */ counter_static_parse(text, on_reply, ud);
110+
```
111+
112+
The wrapper is `<lib>_static_<proc>` rather than `<lib>_<proc>` for the same
113+
reason a method's is `<lib>_ctx_<proc>`: `<lib>_<proc>` is the raw symbol the
114+
dylib exports. In C++ and Rust a static is an associated function on the ctx
115+
type (`EchoCtx::lib_version()`), so it takes the `timeout` a method reads from
116+
its ctx.
117+
118+
The handler still runs on an FFI thread — the library's **static context**,
119+
created on the first `{.ffiStatic.}` call and alive for the rest of the process.
120+
So the first such call starts a thread pair that is never torn down (there is no
121+
ctx whose destructor could own them). It has no `myLib`, which is why a static
122+
proc cannot take the library value.
123+
124+
Two things cannot cross a `{.ffiStatic.}` proc, and the macro rejects both: an
125+
`{.ffiHandle.}` parameter and an `{.ffiHandle.}` return. A handle is registered
126+
in the context that created it, and a static proc has no access to the caller's.
127+
128+
Under `abi = c` a static replies with a `string` or an `{.ffi.}` object type. A
129+
scalar return is wired only for an all-scalar `{.ffi.}` method, which rides the
130+
[CBOR-free fast path](#abi-format) — that path resolves the library value
131+
through the ctx, which a static proc doesn't have.
132+
94133
### The result callback contract
95134

96135
Each request carries a result callback. It receives one of these status codes

examples/echo/c_abi_bindings/echo.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,17 @@ typedef struct {
3232
typedef struct {
3333
ShoutRequest req;
3434
} EchoShoutReq;
35+
typedef struct {
36+
uint8_t _placeholder; /* C forbids empty structs */
37+
} EchoLibVersionReq;
38+
typedef struct {
39+
ShoutRequest req;
40+
} EchoShoutAnonReq;
3541

3642
typedef void (*EchoShoutReplyFn)(int err_code, const ShoutResponse* reply, const char* err_msg, void* user_data);
3743
typedef void (*EchoVersionReplyFn)(int err_code, const char* reply, const char* err_msg, void* user_data);
44+
typedef void (*EchoLibVersionReplyFn)(int err_code, const char* reply, const char* err_msg, void* user_data);
45+
typedef void (*EchoShoutAnonReplyFn)(int err_code, const ShoutResponse* reply, const char* err_msg, void* user_data);
3846

3947
typedef void (*EchoCreateRawFn)(int err_code, const char* ctx_addr, const char* err_msg, void* user_data);
4048
/* Raw reply of a scalar-fast-path export: `msg`/`len` are bytes (a string
@@ -61,6 +69,8 @@ extern "C" {
6169
void* echo_create(const EchoCreateCtorReq* req, EchoCreateRawFn on_created, void* user_data);
6270
int echo_shout(void* ctx, EchoShoutReplyFn on_reply, void* user_data, const EchoShoutReq* req);
6371
int echo_version(void* ctx, EchoScalarRawFn callback, void* user_data);
72+
int echo_lib_version(EchoLibVersionReplyFn on_reply, void* user_data, const EchoLibVersionReq* req);
73+
int echo_shout_anon(EchoShoutAnonReplyFn on_reply, void* user_data, const EchoShoutAnonReq* req);
6474
int echo_destroy(void* ctx);
6575

6676
#ifdef __cplusplus
@@ -165,4 +175,17 @@ static inline int echo_ctx_version(const EchoCtx* ctx, EchoVersionReplyFn on_rep
165175
return echo_version(ctx->ptr, echo_version_scalar_reply, box);
166176
}
167177

178+
static inline int echo_static_lib_version(EchoLibVersionReplyFn on_reply, void* user_data) {
179+
EchoLibVersionReq ffi_req;
180+
memset(&ffi_req, 0, sizeof(ffi_req));
181+
return echo_lib_version(on_reply, user_data, &ffi_req);
182+
}
183+
184+
static inline int echo_static_shout_anon(const ShoutRequest* req, EchoShoutAnonReplyFn on_reply, void* user_data) {
185+
EchoShoutAnonReq ffi_req;
186+
memset(&ffi_req, 0, sizeof(ffi_req));
187+
ffi_req.req = *req;
188+
return echo_shout_anon(on_reply, user_data, &ffi_req);
189+
}
190+
168191
#endif /* NIM_FFI_LIB_ECHO_C_ABI_H_INCLUDED */

examples/echo/c_bindings/echo.h

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ typedef struct {
2525
typedef struct {
2626
char _nimffi_empty; /* C forbids empty structs */
2727
} EchoVersionReq;
28+
typedef struct {
29+
char _nimffi_empty; /* C forbids empty structs */
30+
} EchoLibVersionReq;
31+
typedef struct {
32+
ShoutRequest req;
33+
} EchoShoutAnonReq;
2834

2935
static inline CborError echo_enc_EchoConfig(
3036
CborEncoder* e, const EchoConfig* v) {
@@ -185,6 +191,47 @@ static inline CborError echo_dec_EchoVersionReq(
185191
(void)out;
186192
return cbor_value_advance(it);
187193
}
194+
static inline CborError echo_enc_EchoLibVersionReq(
195+
CborEncoder* e, const EchoLibVersionReq* v) {
196+
(void)v;
197+
CborEncoder m;
198+
CborError err = cbor_encoder_create_map(e, &m, 0);
199+
if (err) return err;
200+
return cbor_encoder_close_container(e, &m);
201+
}
202+
static inline CborError echo_dec_EchoLibVersionReq(
203+
CborValue* it, EchoLibVersionReq* out) {
204+
if (!cbor_value_is_map(it)) return CborErrorImproperValue;
205+
(void)out;
206+
return cbor_value_advance(it);
207+
}
208+
static inline CborError echo_enc_EchoShoutAnonReq(
209+
CborEncoder* e, const EchoShoutAnonReq* v) {
210+
CborEncoder m;
211+
CborError err = cbor_encoder_create_map(e, &m, 1);
212+
if (err) return err;
213+
err = cbor_encode_text_stringz(&m, "req");
214+
if (err) return err;
215+
err = echo_enc_ShoutRequest(&m, &v->req);
216+
if (err) return err;
217+
return cbor_encoder_close_container(e, &m);
218+
}
219+
static inline CborError echo_dec_EchoShoutAnonReq(
220+
CborValue* it, EchoShoutAnonReq* out) {
221+
if (!cbor_value_is_map(it)) return CborErrorImproperValue;
222+
CborValue field;
223+
CborError err;
224+
err = cbor_value_map_find_value(it, "req", &field);
225+
if (err) return err;
226+
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
227+
err = echo_dec_ShoutRequest(&field, &out->req);
228+
if (err) return err;
229+
return cbor_value_advance(it);
230+
}
231+
static inline void echo_free_EchoShoutAnonReq(EchoShoutAnonReq* v) {
232+
if (!v) return;
233+
echo_free_ShoutRequest(&v->req);
234+
}
188235

189236
/* ============================================================ */
190237
/* C ABI declarations (symbols exported by the Nim dylib) */
@@ -196,6 +243,8 @@ extern "C" {
196243
void* echo_create(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback callback, void* user_data);
197244
int echo_shout(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
198245
int echo_version(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
246+
int echo_lib_version(FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
247+
int echo_shout_anon(FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
199248
int echo_destroy(void* ctx);
200249
uint64_t echo_add_event_listener(void* ctx, const char* event_name, FFICallback callback, void* user_data);
201250
int echo_remove_event_listener(void* ctx, uint64_t listener_id);
@@ -208,6 +257,8 @@ int echo_remove_event_listener(void* ctx, uint64_t listener_id);
208257
static inline CborError echo_encv_EchoCreateCtorReq(CborEncoder* e, const void* v) { return echo_enc_EchoCreateCtorReq(e, (const EchoCreateCtorReq*)v); }
209258
static inline CborError echo_encv_EchoShoutReq(CborEncoder* e, const void* v) { return echo_enc_EchoShoutReq(e, (const EchoShoutReq*)v); }
210259
static inline CborError echo_encv_EchoVersionReq(CborEncoder* e, const void* v) { return echo_enc_EchoVersionReq(e, (const EchoVersionReq*)v); }
260+
static inline CborError echo_encv_EchoLibVersionReq(CborEncoder* e, const void* v) { return echo_enc_EchoLibVersionReq(e, (const EchoLibVersionReq*)v); }
261+
static inline CborError echo_encv_EchoShoutAnonReq(CborEncoder* e, const void* v) { return echo_enc_EchoShoutAnonReq(e, (const EchoShoutAnonReq*)v); }
211262
static inline CborError echo_decv_ShoutResponse(CborValue* it, void* v) { return echo_dec_ShoutResponse(it, (ShoutResponse*)v); }
212263
static inline CborError echo_decv_Str(CborValue* it, void* v) { return nimffi_dec_str(it, (NimFfiStr*)v); }
213264

@@ -418,4 +469,127 @@ static inline int echo_ctx_version(const EchoCtx* ctx, EchoVersionReplyFn on_rep
418469
return 0;
419470
}
420471

472+
typedef void (*EchoLibVersionReplyFn)(int err_code, const NimFfiStr* reply, const char* err_msg, void* user_data);
473+
typedef struct { EchoLibVersionReplyFn fn; void* user_data; } EchoLibVersionCallBox;
474+
static void echo_lib_version_reply_trampoline(int ret, const char* msg, size_t len, void* ud) {
475+
EchoLibVersionCallBox* box = (EchoLibVersionCallBox*)ud;
476+
/* Non-terminal progress ping: keep the box for the terminal reply. */
477+
if (ret == NIMFFI_RET_STALE_WARN) return;
478+
if (!box->fn) {
479+
free(box);
480+
return;
481+
}
482+
if (ret != 0) {
483+
char* em = nimffi_dup_cstr_n(msg ? msg : "", msg ? len : 0);
484+
box->fn(ret, NULL, em ? em : "FFI call failed", box->user_data);
485+
free(em);
486+
free(box);
487+
return;
488+
}
489+
char* err = NULL;
490+
NimFfiStr out;
491+
memset(&out, 0, sizeof(out));
492+
int dec = nimffi_decode_from_buf(echo_decv_Str, (const uint8_t*)msg, len, &out, &err);
493+
if (dec != 0) {
494+
box->fn(-1, NULL, err ? err : "decode failed", box->user_data);
495+
free(err);
496+
nimffi_free_str(&out);
497+
free(box);
498+
return;
499+
}
500+
box->fn(NIMFFI_RET_OK, &out, NULL, box->user_data);
501+
nimffi_free_str(&out);
502+
free(box);
503+
}
504+
static inline int echo_static_lib_version(EchoLibVersionReplyFn on_reply, void* user_data) {
505+
EchoLibVersionReq ffi_req;
506+
memset(&ffi_req, 0, sizeof(ffi_req));
507+
uint8_t* req_buf = NULL;
508+
size_t req_len = 0;
509+
char* err = NULL;
510+
if (nimffi_encode_to_buf(echo_encv_EchoLibVersionReq, &ffi_req, &req_buf, &req_len, &err) != 0) {
511+
if (on_reply) on_reply(-1, NULL, err ? err : "encode failed", user_data);
512+
free(err);
513+
return -1;
514+
}
515+
EchoLibVersionCallBox* box = (EchoLibVersionCallBox*)malloc(sizeof(EchoLibVersionCallBox));
516+
if (!box) {
517+
free(req_buf);
518+
if (on_reply) on_reply(-1, NULL, "out of memory", user_data);
519+
return -1;
520+
}
521+
box->fn = on_reply;
522+
box->user_data = user_data;
523+
int ret = echo_lib_version(echo_lib_version_reply_trampoline, box, req_buf, req_len);
524+
free(req_buf);
525+
if (ret == NIMFFI_RET_MISSING_CALLBACK) {
526+
if (on_reply) on_reply(-1, NULL, "RET_MISSING_CALLBACK (internal error)", user_data);
527+
free(box);
528+
return -1;
529+
}
530+
return 0;
531+
}
532+
533+
typedef void (*EchoShoutAnonReplyFn)(int err_code, const ShoutResponse* reply, const char* err_msg, void* user_data);
534+
typedef struct { EchoShoutAnonReplyFn fn; void* user_data; } EchoShoutAnonCallBox;
535+
static void echo_shout_anon_reply_trampoline(int ret, const char* msg, size_t len, void* ud) {
536+
EchoShoutAnonCallBox* box = (EchoShoutAnonCallBox*)ud;
537+
/* Non-terminal progress ping: keep the box for the terminal reply. */
538+
if (ret == NIMFFI_RET_STALE_WARN) return;
539+
if (!box->fn) {
540+
free(box);
541+
return;
542+
}
543+
if (ret != 0) {
544+
char* em = nimffi_dup_cstr_n(msg ? msg : "", msg ? len : 0);
545+
box->fn(ret, NULL, em ? em : "FFI call failed", box->user_data);
546+
free(em);
547+
free(box);
548+
return;
549+
}
550+
char* err = NULL;
551+
ShoutResponse out;
552+
memset(&out, 0, sizeof(out));
553+
int dec = nimffi_decode_from_buf(echo_decv_ShoutResponse, (const uint8_t*)msg, len, &out, &err);
554+
if (dec != 0) {
555+
box->fn(-1, NULL, err ? err : "decode failed", box->user_data);
556+
free(err);
557+
echo_free_ShoutResponse(&out);
558+
free(box);
559+
return;
560+
}
561+
box->fn(NIMFFI_RET_OK, &out, NULL, box->user_data);
562+
echo_free_ShoutResponse(&out);
563+
free(box);
564+
}
565+
static inline int echo_static_shout_anon(const ShoutRequest* req, EchoShoutAnonReplyFn on_reply, void* user_data) {
566+
EchoShoutAnonReq ffi_req;
567+
memset(&ffi_req, 0, sizeof(ffi_req));
568+
ffi_req.req = *req;
569+
uint8_t* req_buf = NULL;
570+
size_t req_len = 0;
571+
char* err = NULL;
572+
if (nimffi_encode_to_buf(echo_encv_EchoShoutAnonReq, &ffi_req, &req_buf, &req_len, &err) != 0) {
573+
if (on_reply) on_reply(-1, NULL, err ? err : "encode failed", user_data);
574+
free(err);
575+
return -1;
576+
}
577+
EchoShoutAnonCallBox* box = (EchoShoutAnonCallBox*)malloc(sizeof(EchoShoutAnonCallBox));
578+
if (!box) {
579+
free(req_buf);
580+
if (on_reply) on_reply(-1, NULL, "out of memory", user_data);
581+
return -1;
582+
}
583+
box->fn = on_reply;
584+
box->user_data = user_data;
585+
int ret = echo_shout_anon(echo_shout_anon_reply_trampoline, box, req_buf, req_len);
586+
free(req_buf);
587+
if (ret == NIMFFI_RET_MISSING_CALLBACK) {
588+
if (on_reply) on_reply(-1, NULL, "RET_MISSING_CALLBACK (internal error)", user_data);
589+
free(box);
590+
return -1;
591+
}
592+
return 0;
593+
}
594+
421595
#endif /* NIM_FFI_LIB_ECHO_H_INCLUDED */

0 commit comments

Comments
 (0)