feat: allow no-ctx ffi procs

This commit is contained in:
Gabriel Cruz 2026-07-16 17:05:59 -03:00
parent 20fe91ae45
commit 2abfc57b73
No known key found for this signature in database
GPG Key ID: 2E467754A6BA9BA5
33 changed files with 1167 additions and 158 deletions

View File

@ -46,6 +46,17 @@ All notable changes to this project are documented in this file.
where `-install_name` requires `-dynamiclib`.
### Added
- **`{.ffiStatic.}`**: exports a context-independent proc. It takes no library
param and its wrapper takes no `ctx`, so a host can call a stateless utility
(key generation, parsing, a version string) without constructing the library
([#134](https://github.com/logos-messaging/nim-ffi/issues/134)). Wired for both
the `cbor` and `c` ABIs across all four backends: the C header emits
`<lib>_static_<proc>(...)`, while C++ and Rust emit an associated function on
the ctx type taking the `timeout` a method reads from its ctx. Handlers run on
the library's *static context*, created on the first such call and alive for the
rest of the process, so that call starts a thread pair that is never torn down.
An `{.ffiHandle.}` parameter or return is rejected at macro time: a handle
belongs to the context that created it, which a static proc cannot reach.
- `{.ffi.}` now accepts an `enum` type, emitting a native enum in every target
(C `enum`, C++ `enum class`, Rust enum, CDDL string choice). Values cross the
wire as the text `$value` yields — the associated string if declared, else the
@ -69,8 +80,8 @@ All notable changes to this project are documented in this file.
`##` comment now changes the generated bindings, so `nimble check_bindings`
flags them stale until regenerated; an undocumented proc still generates
byte-identical output.
- FFI annotations (`{.ffi.}`, `{.ffiCtor.}`, `{.ffiDtor.}`, `{.ffiEvent.}`,
`{.ffiHandle.}`, `{.ffiRaw.}`) that expand after `genBindings()` now produce a
- FFI annotations (`{.ffi.}`, `{.ffiStatic.}`, `{.ffiCtor.}`, `{.ffiDtor.}`,
`{.ffiEvent.}`, `{.ffiHandle.}`, `{.ffiRaw.}`) that expand after `genBindings()` now produce a
loud compile error instead of being silently dropped from the generated
bindings.
- **C binding generator** (`-d:targetLang=c`): emits a header-only C binding

View File

@ -19,7 +19,7 @@ requires "https://github.com/logos-messaging/nim-ffi >= 0.2.0"
- You **declare a library** once with `declareLibrary(name, LibType)`.
- You **annotate procs and types** with pragmas (`{.ffi.}`, `{.ffiCtor.}`,
`{.ffiDtor.}`, `{.ffiEvent.}`).
`{.ffiDtor.}`, `{.ffiStatic.}`, `{.ffiEvent.}`).
- You **call `genBindings()` last**, which emits the foreign bindings.
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.
| `declareLibrary(name, LibType[, defaultABIFormat])` | call | Registers the library, its state type, and the default wire format. Must run before any annotation. |
| `{.ffi.}` on a `type` | `object`, `enum` | Registers the type for binding generation; it serializes via the library's ABI format (CBOR by default). Enums are CBOR-only — see below. |
| `{.ffi.}` on a `proc` | proc | Exposes a method. First param is the library value, then typed params; returns `Future[Result[T, string]]`. |
| `{.ffiStatic.}` | proc | Exposes a context-independent proc: no library param, and its wrapper takes no ctx — see below. |
| `{.ffiCtor.}` | proc | The constructor. Returns `Future[Result[LibType, string]]`; creates the FFI context. |
| `{.ffiDtor.}` | proc | The destructor. Exactly one param `(x: LibType)`; tears the context down. |
| `{.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. |
@ -175,6 +176,39 @@ Every `{.ffi.}` / `{.ffiCtor.}` proc must have an explicit
`return ok(...)` without awaiting). The `Result`'s error string is delivered to
the foreign caller as the failure message.
### Context-independent procs
A `{.ffi.}` proc is a method: it takes the library value, and its wrapper takes a
`ctx`, so the host must construct the library to call it. A stateless utility
shouldn't have to pay for that. Annotate it `{.ffiStatic.}` instead — drop the
library param, and the ctx disappears from the generated wrapper:
```nim
proc counterParse*(text: string): Future[Result[BumpRequest, string]] {.ffiStatic.} =
return ok(BumpRequest(by: text.parseInt()))
```
```c
/* {.ffi.} method */ counter_ctx_bump(ctx, &req, on_reply, ud);
/* {.ffiStatic.} — no ctx */ counter_static_parse(text, on_reply, ud);
```
The wrapper is `<lib>_static_<proc>`, not `<lib>_<proc>`, for the same reason a
method's is `<lib>_ctx_<proc>`: `<lib>_<proc>` is the raw symbol the dylib
exports. In C++ and Rust a static is an associated function on the ctx type
(`EchoCtx::lib_version()`), taking the `timeout` a method reads from its ctx.
The handler still needs an FFI thread, so it runs on the library's **static
context**: created on the first `{.ffiStatic.}` call, then alive for the rest of
the process — no ctx owns it, so nothing tears its thread pair down. It has no
`myLib`, which is why a static proc cannot take the library value.
The macro rejects an `{.ffiHandle.}` parameter or return: a handle is registered
in the context that created it, which a static proc cannot reach. Under
`abi = c` a static replies with a `string` or an `{.ffi.}` object type — a scalar
return is wired only for an all-scalar `{.ffi.}` method, which rides the
[CBOR-free fast path](#abi-format) through the ctx a static doesn't have.
### The result callback contract
Each request carries a result callback. It receives one of these status codes

View File

@ -38,9 +38,17 @@ typedef struct {
typedef struct {
ShoutRequest req;
} EchoShoutReq;
typedef struct {
uint8_t _placeholder; /* C forbids empty structs */
} EchoLibVersionReq;
typedef struct {
ShoutRequest req;
} EchoShoutAnonReq;
typedef void (*EchoShoutReplyFn)(int err_code, const ShoutResponse* reply, const char* err_msg, void* user_data);
typedef void (*EchoVersionReplyFn)(int err_code, const char* reply, const char* err_msg, void* user_data);
typedef void (*EchoLibVersionReplyFn)(int err_code, const char* reply, const char* err_msg, void* user_data);
typedef void (*EchoShoutAnonReplyFn)(int err_code, const ShoutResponse* reply, const char* err_msg, void* user_data);
typedef void (*EchoCreateRawFn)(int err_code, const char* ctx_addr, const char* err_msg, void* user_data);
/* Raw reply of a scalar-fast-path export: `msg`/`len` are bytes (a string
@ -70,6 +78,8 @@ void* echo_create(const EchoCreateCtorReq* req, EchoCreateRawFn on_created, void
int echo_shout(void* ctx, EchoShoutReplyFn on_reply, void* user_data, const EchoShoutReq* req);
/** Returns the library's version string. */
int echo_version(void* ctx, EchoScalarRawFn callback, void* user_data);
int echo_lib_version(EchoLibVersionReplyFn on_reply, void* user_data, const EchoLibVersionReq* req);
int echo_shout_anon(EchoShoutAnonReplyFn on_reply, void* user_data, const EchoShoutAnonReq* req);
/** Releases the echo context. */
int echo_destroy(void* ctx);
@ -181,4 +191,17 @@ static inline int echo_ctx_version(const EchoCtx* ctx, EchoVersionReplyFn on_rep
return echo_version(ctx->ptr, echo_version_scalar_reply, box);
}
static inline int echo_static_lib_version(EchoLibVersionReplyFn on_reply, void* user_data) {
EchoLibVersionReq ffi_req;
memset(&ffi_req, 0, sizeof(ffi_req));
return echo_lib_version(on_reply, user_data, &ffi_req);
}
static inline int echo_static_shout_anon(const ShoutRequest* req, EchoShoutAnonReplyFn on_reply, void* user_data) {
EchoShoutAnonReq ffi_req;
memset(&ffi_req, 0, sizeof(ffi_req));
ffi_req.req = *req;
return echo_shout_anon(on_reply, user_data, &ffi_req);
}
#endif /* NIM_FFI_LIB_ECHO_C_ABI_H_INCLUDED */

View File

@ -31,6 +31,12 @@ typedef struct {
typedef struct {
char _nimffi_empty; /* C forbids empty structs */
} EchoVersionReq;
typedef struct {
char _nimffi_empty; /* C forbids empty structs */
} EchoLibVersionReq;
typedef struct {
ShoutRequest req;
} EchoShoutAnonReq;
static inline CborError echo_enc_EchoConfig(
CborEncoder* e, const EchoConfig* v) {
@ -191,6 +197,47 @@ static inline CborError echo_dec_EchoVersionReq(
(void)out;
return cbor_value_advance(it);
}
static inline CborError echo_enc_EchoLibVersionReq(
CborEncoder* e, const EchoLibVersionReq* v) {
(void)v;
CborEncoder m;
CborError err = cbor_encoder_create_map(e, &m, 0);
if (err) return err;
return cbor_encoder_close_container(e, &m);
}
static inline CborError echo_dec_EchoLibVersionReq(
CborValue* it, EchoLibVersionReq* out) {
if (!cbor_value_is_map(it)) return CborErrorImproperValue;
(void)out;
return cbor_value_advance(it);
}
static inline CborError echo_enc_EchoShoutAnonReq(
CborEncoder* e, const EchoShoutAnonReq* v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(e, &m, 1);
if (err) return err;
err = cbor_encode_text_stringz(&m, "req");
if (err) return err;
err = echo_enc_ShoutRequest(&m, &v->req);
if (err) return err;
return cbor_encoder_close_container(e, &m);
}
static inline CborError echo_dec_EchoShoutAnonReq(
CborValue* it, EchoShoutAnonReq* out) {
if (!cbor_value_is_map(it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(it, "req", &field);
if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = echo_dec_ShoutRequest(&field, &out->req);
if (err) return err;
return cbor_value_advance(it);
}
static inline void echo_free_EchoShoutAnonReq(EchoShoutAnonReq* v) {
if (!v) return;
echo_free_ShoutRequest(&v->req);
}
/* ============================================================ */
/* C ABI declarations (symbols exported by the Nim dylib) */
@ -205,6 +252,8 @@ void* echo_create(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback call
int echo_shout(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
/** Returns the library's version string. */
int echo_version(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
int echo_lib_version(FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
int echo_shout_anon(FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
/** Releases the echo context. */
int echo_destroy(void* ctx);
uint64_t echo_add_event_listener(void* ctx, const char* event_name, FFICallback callback, void* user_data);
@ -218,6 +267,8 @@ int echo_remove_event_listener(void* ctx, uint64_t listener_id);
static inline CborError echo_encv_EchoCreateCtorReq(CborEncoder* e, const void* v) { return echo_enc_EchoCreateCtorReq(e, (const EchoCreateCtorReq*)v); }
static inline CborError echo_encv_EchoShoutReq(CborEncoder* e, const void* v) { return echo_enc_EchoShoutReq(e, (const EchoShoutReq*)v); }
static inline CborError echo_encv_EchoVersionReq(CborEncoder* e, const void* v) { return echo_enc_EchoVersionReq(e, (const EchoVersionReq*)v); }
static inline CborError echo_encv_EchoLibVersionReq(CborEncoder* e, const void* v) { return echo_enc_EchoLibVersionReq(e, (const EchoLibVersionReq*)v); }
static inline CborError echo_encv_EchoShoutAnonReq(CborEncoder* e, const void* v) { return echo_enc_EchoShoutAnonReq(e, (const EchoShoutAnonReq*)v); }
static inline CborError echo_decv_ShoutResponse(CborValue* it, void* v) { return echo_dec_ShoutResponse(it, (ShoutResponse*)v); }
static inline CborError echo_decv_Str(CborValue* it, void* v) { return nimffi_dec_str(it, (NimFfiStr*)v); }
@ -434,4 +485,127 @@ static inline int echo_ctx_version(const EchoCtx* ctx, EchoVersionReplyFn on_rep
return 0;
}
typedef void (*EchoLibVersionReplyFn)(int err_code, const NimFfiStr* reply, const char* err_msg, void* user_data);
typedef struct { EchoLibVersionReplyFn fn; void* user_data; } EchoLibVersionCallBox;
static void echo_lib_version_reply_trampoline(int ret, const char* msg, size_t len, void* ud) {
EchoLibVersionCallBox* box = (EchoLibVersionCallBox*)ud;
/* Non-terminal progress ping: keep the box for the terminal reply. */
if (ret == NIMFFI_RET_STALE_WARN) return;
if (!box->fn) {
free(box);
return;
}
if (ret != 0) {
char* em = nimffi_dup_cstr_n(msg ? msg : "", msg ? len : 0);
box->fn(ret, NULL, em ? em : "FFI call failed", box->user_data);
free(em);
free(box);
return;
}
char* err = NULL;
NimFfiStr out;
memset(&out, 0, sizeof(out));
int dec = nimffi_decode_from_buf(echo_decv_Str, (const uint8_t*)msg, len, &out, &err);
if (dec != 0) {
box->fn(-1, NULL, err ? err : "decode failed", box->user_data);
free(err);
nimffi_free_str(&out);
free(box);
return;
}
box->fn(NIMFFI_RET_OK, &out, NULL, box->user_data);
nimffi_free_str(&out);
free(box);
}
static inline int echo_static_lib_version(EchoLibVersionReplyFn on_reply, void* user_data) {
EchoLibVersionReq ffi_req;
memset(&ffi_req, 0, sizeof(ffi_req));
uint8_t* req_buf = NULL;
size_t req_len = 0;
char* err = NULL;
if (nimffi_encode_to_buf(echo_encv_EchoLibVersionReq, &ffi_req, &req_buf, &req_len, &err) != 0) {
if (on_reply) on_reply(-1, NULL, err ? err : "encode failed", user_data);
free(err);
return -1;
}
EchoLibVersionCallBox* box = (EchoLibVersionCallBox*)malloc(sizeof(EchoLibVersionCallBox));
if (!box) {
free(req_buf);
if (on_reply) on_reply(-1, NULL, "out of memory", user_data);
return -1;
}
box->fn = on_reply;
box->user_data = user_data;
int ret = echo_lib_version(echo_lib_version_reply_trampoline, box, req_buf, req_len);
free(req_buf);
if (ret == NIMFFI_RET_MISSING_CALLBACK) {
if (on_reply) on_reply(-1, NULL, "RET_MISSING_CALLBACK (internal error)", user_data);
free(box);
return -1;
}
return 0;
}
typedef void (*EchoShoutAnonReplyFn)(int err_code, const ShoutResponse* reply, const char* err_msg, void* user_data);
typedef struct { EchoShoutAnonReplyFn fn; void* user_data; } EchoShoutAnonCallBox;
static void echo_shout_anon_reply_trampoline(int ret, const char* msg, size_t len, void* ud) {
EchoShoutAnonCallBox* box = (EchoShoutAnonCallBox*)ud;
/* Non-terminal progress ping: keep the box for the terminal reply. */
if (ret == NIMFFI_RET_STALE_WARN) return;
if (!box->fn) {
free(box);
return;
}
if (ret != 0) {
char* em = nimffi_dup_cstr_n(msg ? msg : "", msg ? len : 0);
box->fn(ret, NULL, em ? em : "FFI call failed", box->user_data);
free(em);
free(box);
return;
}
char* err = NULL;
ShoutResponse out;
memset(&out, 0, sizeof(out));
int dec = nimffi_decode_from_buf(echo_decv_ShoutResponse, (const uint8_t*)msg, len, &out, &err);
if (dec != 0) {
box->fn(-1, NULL, err ? err : "decode failed", box->user_data);
free(err);
echo_free_ShoutResponse(&out);
free(box);
return;
}
box->fn(NIMFFI_RET_OK, &out, NULL, box->user_data);
echo_free_ShoutResponse(&out);
free(box);
}
static inline int echo_static_shout_anon(const ShoutRequest* req, EchoShoutAnonReplyFn on_reply, void* user_data) {
EchoShoutAnonReq ffi_req;
memset(&ffi_req, 0, sizeof(ffi_req));
ffi_req.req = *req;
uint8_t* req_buf = NULL;
size_t req_len = 0;
char* err = NULL;
if (nimffi_encode_to_buf(echo_encv_EchoShoutAnonReq, &ffi_req, &req_buf, &req_len, &err) != 0) {
if (on_reply) on_reply(-1, NULL, err ? err : "encode failed", user_data);
free(err);
return -1;
}
EchoShoutAnonCallBox* box = (EchoShoutAnonCallBox*)malloc(sizeof(EchoShoutAnonCallBox));
if (!box) {
free(req_buf);
if (on_reply) on_reply(-1, NULL, "out of memory", user_data);
return -1;
}
box->fn = on_reply;
box->user_data = user_data;
int ret = echo_shout_anon(echo_shout_anon_reply_trampoline, box, req_buf, req_len);
free(req_buf);
if (ret == NIMFFI_RET_MISSING_CALLBACK) {
if (on_reply) on_reply(-1, NULL, "RET_MISSING_CALLBACK (internal error)", user_data);
free(box);
return -1;
}
return 0;
}
#endif /* NIM_FFI_LIB_ECHO_H_INCLUDED */

View File

@ -430,6 +430,40 @@ inline CborError decode_cbor(CborValue& it, EchoVersionReq&) {
return cbor_value_advance(&it);
}
struct EchoLibVersionReq {
};
inline CborError encode_cbor(CborEncoder& e, const EchoLibVersionReq&) {
CborEncoder m;
CborError err = cbor_encoder_create_map(&e, &m, 0);
if (err) return err;
return cbor_encoder_close_container(&e, &m);
}
inline CborError decode_cbor(CborValue& it, EchoLibVersionReq&) {
if (!cbor_value_is_map(&it)) return CborErrorImproperValue;
return cbor_value_advance(&it);
}
struct EchoShoutAnonReq {
ShoutRequest req;
};
inline CborError encode_cbor(CborEncoder& e, const EchoShoutAnonReq& v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(&e, &m, 1);
if (err) return err;
err = cbor_encode_text_stringz(&m, "req"); if (err) return err;
err = encode_cbor(m, v.req); if (err) return err;
return cbor_encoder_close_container(&e, &m);
}
inline CborError decode_cbor(CborValue& it, EchoShoutAnonReq& v) {
if (!cbor_value_is_map(&it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(&it, "req", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.req); if (err) return err;
return cbor_value_advance(&it);
}
// ============================================================
// C FFI declarations
// ============================================================
@ -443,6 +477,8 @@ void* echo_create(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback call
int echo_shout(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
/** Returns the library's version string. */
int echo_version(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
int echo_lib_version(FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
int echo_shout_anon(FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
/** Releases the echo context. */
int echo_destroy(void* ctx);
uint64_t echo_add_event_listener(void* ctx, const char* event_name, FFICallback callback, void* user_data);
@ -608,6 +644,38 @@ public:
return std::async(std::launch::async, [this]() { return this->version(); });
}
static Result<std::string> lib_version(std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
const auto ffi_req_ = EchoLibVersionReq{};
auto ffi_enc_ = encodeCborFFI(ffi_req_);
if (ffi_enc_.isErr()) return Result<std::string>::err(ffi_enc_.error());
const auto& ffi_req_bytes_ = ffi_enc_.value();
auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {
return echo_lib_version(cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size());
}, timeout);
if (ffi_raw_.isErr()) return Result<std::string>::err(ffi_raw_.error());
return decodeCborFFI<std::string>(ffi_raw_.value());
}
static std::future<Result<std::string>> lib_versionAsync(std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
return std::async(std::launch::async, [timeout]() { return lib_version(timeout); });
}
static Result<ShoutResponse> shout_anon(const ShoutRequest& req, std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
const auto ffi_req_ = EchoShoutAnonReq{req};
auto ffi_enc_ = encodeCborFFI(ffi_req_);
if (ffi_enc_.isErr()) return Result<ShoutResponse>::err(ffi_enc_.error());
const auto& ffi_req_bytes_ = ffi_enc_.value();
auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {
return echo_shout_anon(cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size());
}, timeout);
if (ffi_raw_.isErr()) return Result<ShoutResponse>::err(ffi_raw_.error());
return decodeCborFFI<ShoutResponse>(ffi_raw_.value());
}
static std::future<Result<ShoutResponse>> shout_anonAsync(const ShoutRequest& req, std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
return std::async(std::launch::async, [req, timeout]() { return shout_anon(req, timeout); });
}
private:
void* ptr_;
std::chrono::milliseconds timeout_;

View File

@ -44,6 +44,16 @@ proc echoVersion*(e: Echo): Future[Result[string, string]] {.ffi.} =
## Returns the library's version string.
return ok("nim-echo v0.1.0")
# The two below need no `Echo`, so their wrappers take no ctx.
proc echoLibVersion*(): Future[Result[string, string]] {.ffiStatic.} =
return ok("nim-echo v0.1.0")
proc echoShoutAnon*(
req: ShoutRequest
): Future[Result[ShoutResponse, string]] {.ffiStatic.} =
await sleepAsync(1.milliseconds)
return ok(ShoutResponse(shouted: req.text.toUpperAscii, prefix: ""))
proc echo_destroy*(e: Echo) {.ffiDtor.} =
## Releases the echo context.
discard

View File

@ -92,6 +92,9 @@ typedef struct {
typedef struct {
char _nimffi_empty; /* C forbids empty structs */
} MyTimerVersionReq;
typedef struct {
char _nimffi_empty; /* C forbids empty structs */
} MyTimerLibVersionReq;
typedef struct {
ComplexRequest req;
} MyTimerComplexReq;
@ -734,6 +737,20 @@ static inline CborError my_timer_dec_MyTimerVersionReq(
(void)out;
return cbor_value_advance(it);
}
static inline CborError my_timer_enc_MyTimerLibVersionReq(
CborEncoder* e, const MyTimerLibVersionReq* v) {
(void)v;
CborEncoder m;
CborError err = cbor_encoder_create_map(e, &m, 0);
if (err) return err;
return cbor_encoder_close_container(e, &m);
}
static inline CborError my_timer_dec_MyTimerLibVersionReq(
CborValue* it, MyTimerLibVersionReq* out) {
if (!cbor_value_is_map(it)) return CborErrorImproperValue;
(void)out;
return cbor_value_advance(it);
}
static inline CborError my_timer_enc_MyTimerComplexReq(
CborEncoder* e, const MyTimerComplexReq* v) {
CborEncoder m;
@ -821,6 +838,7 @@ void* my_timer_create(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback
int my_timer_echo(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
/** Returns the library's version string. */
int my_timer_version(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
int my_timer_lib_version(FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
int my_timer_complex(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
/** Three object-typed params (`job`, `retry`, `schedule`) packed into one CBOR envelope. */
int my_timer_schedule(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
@ -837,6 +855,7 @@ int my_timer_remove_event_listener(void* ctx, uint64_t listener_id);
static inline CborError my_timer_encv_MyTimerCreateCtorReq(CborEncoder* e, const void* v) { return my_timer_enc_MyTimerCreateCtorReq(e, (const MyTimerCreateCtorReq*)v); }
static inline CborError my_timer_encv_MyTimerEchoReq(CborEncoder* e, const void* v) { return my_timer_enc_MyTimerEchoReq(e, (const MyTimerEchoReq*)v); }
static inline CborError my_timer_encv_MyTimerVersionReq(CborEncoder* e, const void* v) { return my_timer_enc_MyTimerVersionReq(e, (const MyTimerVersionReq*)v); }
static inline CborError my_timer_encv_MyTimerLibVersionReq(CborEncoder* e, const void* v) { return my_timer_enc_MyTimerLibVersionReq(e, (const MyTimerLibVersionReq*)v); }
static inline CborError my_timer_encv_MyTimerComplexReq(CborEncoder* e, const void* v) { return my_timer_enc_MyTimerComplexReq(e, (const MyTimerComplexReq*)v); }
static inline CborError my_timer_encv_MyTimerScheduleReq(CborEncoder* e, const void* v) { return my_timer_enc_MyTimerScheduleReq(e, (const MyTimerScheduleReq*)v); }
static inline CborError my_timer_decv_EchoResponse(CborValue* it, void* v) { return my_timer_dec_EchoResponse(it, (EchoResponse*)v); }
@ -1251,4 +1270,65 @@ static inline int my_timer_ctx_schedule(const MyTimerCtx* ctx, const JobSpec* jo
return 0;
}
typedef void (*MyTimerLibVersionReplyFn)(int err_code, const NimFfiStr* reply, const char* err_msg, void* user_data);
typedef struct { MyTimerLibVersionReplyFn fn; void* user_data; } MyTimerLibVersionCallBox;
static void my_timer_lib_version_reply_trampoline(int ret, const char* msg, size_t len, void* ud) {
MyTimerLibVersionCallBox* box = (MyTimerLibVersionCallBox*)ud;
/* Non-terminal progress ping: keep the box for the terminal reply. */
if (ret == NIMFFI_RET_STALE_WARN) return;
if (!box->fn) {
free(box);
return;
}
if (ret != 0) {
char* em = nimffi_dup_cstr_n(msg ? msg : "", msg ? len : 0);
box->fn(ret, NULL, em ? em : "FFI call failed", box->user_data);
free(em);
free(box);
return;
}
char* err = NULL;
NimFfiStr out;
memset(&out, 0, sizeof(out));
int dec = nimffi_decode_from_buf(my_timer_decv_Str, (const uint8_t*)msg, len, &out, &err);
if (dec != 0) {
box->fn(-1, NULL, err ? err : "decode failed", box->user_data);
free(err);
nimffi_free_str(&out);
free(box);
return;
}
box->fn(NIMFFI_RET_OK, &out, NULL, box->user_data);
nimffi_free_str(&out);
free(box);
}
static inline int my_timer_static_lib_version(MyTimerLibVersionReplyFn on_reply, void* user_data) {
MyTimerLibVersionReq ffi_req;
memset(&ffi_req, 0, sizeof(ffi_req));
uint8_t* req_buf = NULL;
size_t req_len = 0;
char* err = NULL;
if (nimffi_encode_to_buf(my_timer_encv_MyTimerLibVersionReq, &ffi_req, &req_buf, &req_len, &err) != 0) {
if (on_reply) on_reply(-1, NULL, err ? err : "encode failed", user_data);
free(err);
return -1;
}
MyTimerLibVersionCallBox* box = (MyTimerLibVersionCallBox*)malloc(sizeof(MyTimerLibVersionCallBox));
if (!box) {
free(req_buf);
if (on_reply) on_reply(-1, NULL, "out of memory", user_data);
return -1;
}
box->fn = on_reply;
box->user_data = user_data;
int ret = my_timer_lib_version(my_timer_lib_version_reply_trampoline, box, req_buf, req_len);
free(req_buf);
if (ret == NIMFFI_RET_MISSING_CALLBACK) {
if (on_reply) on_reply(-1, NULL, "RET_MISSING_CALLBACK (internal error)", user_data);
free(box);
return -1;
}
return 0;
}
#endif /* NIM_FFI_LIB_MY_TIMER_H_INCLUDED */

View File

@ -19,6 +19,7 @@ ScheduleResult = { jobId: tstr, willRunCount: int, firstRunAtMs: int, effectiveB
MyTimerCreateCtorReq = { config: TimerConfig }
MyTimerEchoReq = { req: EchoRequest }
MyTimerVersionReq = { }
MyTimerLibVersionReq = { }
MyTimerComplexReq = { req: ComplexRequest }
MyTimerScheduleReq = { job: JobSpec, retry: RetryPolicy, schedule: ScheduleConfig }
@ -38,6 +39,10 @@ my_timer_echo-response = EchoResponse
my_timer_version-request = MyTimerVersionReq
my_timer_version-response = tstr
; my_timer_lib_version (ffiStatic)
my_timer_lib_version-request = MyTimerLibVersionReq
my_timer_lib_version-response = tstr
; my_timer_complex (ffi)
my_timer_complex-request = MyTimerComplexReq
my_timer_complex-response = ComplexResponse

View File

@ -705,6 +705,19 @@ inline CborError decode_cbor(CborValue& it, MyTimerVersionReq&) {
return cbor_value_advance(&it);
}
struct MyTimerLibVersionReq {
};
inline CborError encode_cbor(CborEncoder& e, const MyTimerLibVersionReq&) {
CborEncoder m;
CborError err = cbor_encoder_create_map(&e, &m, 0);
if (err) return err;
return cbor_encoder_close_container(&e, &m);
}
inline CborError decode_cbor(CborValue& it, MyTimerLibVersionReq&) {
if (!cbor_value_is_map(&it)) return CborErrorImproperValue;
return cbor_value_advance(&it);
}
struct MyTimerComplexReq {
ComplexRequest req;
};
@ -772,6 +785,7 @@ void* my_timer_create(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback
int my_timer_echo(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
/** Returns the library's version string. */
int my_timer_version(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
int my_timer_lib_version(FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
int my_timer_complex(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
/** Three object-typed params (`job`, `retry`, `schedule`) packed into one CBOR envelope. */
int my_timer_schedule(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
@ -995,6 +1009,22 @@ public:
return std::async(std::launch::async, [this, job, retry, schedule]() { return this->schedule(job, retry, schedule); });
}
static Result<std::string> lib_version(std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
const auto ffi_req_ = MyTimerLibVersionReq{};
auto ffi_enc_ = encodeCborFFI(ffi_req_);
if (ffi_enc_.isErr()) return Result<std::string>::err(ffi_enc_.error());
const auto& ffi_req_bytes_ = ffi_enc_.value();
auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {
return my_timer_lib_version(cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size());
}, timeout);
if (ffi_raw_.isErr()) return Result<std::string>::err(ffi_raw_.error());
return decodeCborFFI<std::string>(ffi_raw_.value());
}
static std::future<Result<std::string>> lib_versionAsync(std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
return std::async(std::launch::async, [timeout]() { return lib_version(timeout); });
}
private:
struct ListenerBase {
virtual ~ListenerBase() = default;

View File

@ -7,6 +7,9 @@ use std::sync::mpsc;
use std::time::Duration;
fn main() -> Result<(), String> {
// `myTimerLibVersion` is {.ffiStatic.}: an associated fn, no ctx needed.
println!("lib version: {}", MyTimerCtx::lib_version(Duration::from_secs(5))?);
let ctx = MyTimerCtx::create(
TimerConfig { name: "rust-sync-demo".into() },
Duration::from_secs(5),

View File

@ -305,4 +305,22 @@ impl MyTimerCtx {
decode_cbor::<ScheduleResult>(&raw_bytes)
}
pub fn lib_version(timeout: Duration) -> Result<String, String> {
let req = MyTimerLibVersionReq {};
let req_bytes = encode_cbor(&req)?;
let raw_bytes = ffi_call_sync(timeout, |cb, ud| unsafe {
ffi::my_timer_lib_version(cb, ud, req_bytes.as_ptr(), req_bytes.len())
})?;
decode_cbor::<String>(&raw_bytes)
}
pub async fn lib_version_async(timeout: Duration) -> Result<String, String> {
let req = MyTimerLibVersionReq {};
let req_bytes = encode_cbor(&req)?;
let raw_bytes = ffi_call_async(timeout, move |cb, ud| unsafe {
ffi::my_timer_lib_version(cb, ud, req_bytes.as_ptr(), req_bytes.len())
}).await?;
decode_cbor::<String>(&raw_bytes)
}
}

View File

@ -15,6 +15,7 @@ extern "C" {
pub fn my_timer_echo(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int;
/// Returns the library's version string.
pub fn my_timer_version(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int;
pub fn my_timer_lib_version(callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int;
pub fn my_timer_complex(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int;
/// Three object-typed params (`job`, `retry`, `schedule`) packed into one CBOR envelope.
pub fn my_timer_schedule(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int;

View File

@ -109,6 +109,9 @@ pub struct MyTimerEchoReq {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MyTimerVersionReq {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MyTimerLibVersionReq {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MyTimerComplexReq {
pub req: ComplexRequest,

View File

@ -65,6 +65,10 @@ proc myTimerVersion*(timer: MyTimer): Future[Result[string, string]] {.ffi.} =
## Returns the library's version string.
return ok(TimerVersion)
# No `timer` param, so its wrapper takes no ctx.
proc myTimerLibVersion*(): Future[Result[string, string]] {.ffiStatic.} =
return ok("nim-timer v0.1.0")
proc myTimerComplex*(
timer: MyTimer, req: ComplexRequest
): Future[Result[ComplexResponse, string]] {.ffi.} =

View File

@ -677,12 +677,15 @@ proc emitListenerApi(
lines.add("}")
lines.add("")
proc emitMethod(
proc emitProcWrapper(
lines: var seq[string],
reg: var CTypeReg,
ctxType, libType, libName: string,
m: FFIProcMeta,
) =
## Reply trampoline + CBOR-encoding wrapper: `<lib>_ctx_<name>` for a method,
## `<lib>_static_<name>` for a static. `<lib>_<name>` is the raw dylib symbol.
let isStatic = m.isStatic()
let stripped = stripLibPrefix(m.procName, libName)
let reqName = reqStructName(m)
let retC = cReturnType(reg, m)
@ -722,7 +725,11 @@ proc emitMethod(
lines.add("}")
let head =
"static inline int " & libName & "_ctx_" & stripped & "(const " & ctxType & "* ctx, "
if isStatic:
"static inline int " & libName & "_static_" & stripped & "("
else:
"static inline int " & libName & "_ctx_" & stripped & "(const " & ctxType &
"* ctx, "
let sig =
if params.len > 0:
head & params.join(", ") & ", " & fnType & " on_reply, void* user_data) {"
@ -757,8 +764,9 @@ proc emitMethod(
lines.add(" }")
lines.add(" box->fn = on_reply;")
lines.add(" box->user_data = user_data;")
let ctxArg = if isStatic: "" else: "ctx->ptr, "
lines.add(
" int ret = " & m.procName & "(ctx->ptr, " & tramp & ", box, req_buf, req_len);"
" int ret = " & m.procName & "(" & ctxArg & tramp & ", box, req_buf, req_len);"
)
lines.add(" free(req_buf);")
lines.add(" if (ret == NIMFFI_RET_MISSING_CALLBACK) {")
@ -787,7 +795,7 @@ proc newCTypeReg(
proc monomorphiseAll(
reg: var CTypeReg,
types: seq[FFITypeMeta],
procs, methods: seq[FFIProcMeta],
procs, replyProcs: seq[FFIProcMeta],
events: seq[FFIEventMeta],
): tuple[reqTypes, respTypes: seq[string]] =
## Runs every type, Req, return type and event payload through ensureCType,
@ -801,7 +809,7 @@ proc monomorphiseAll(
discard ensureCType(reg, n)
reqTypes.add(n)
var respTypes: seq[string] = @[]
for m in methods:
for m in replyProcs:
respTypes.add(cReturnType(reg, m))
for ev in events:
discard ensureCType(reg, ev.payloadTypeName)
@ -852,12 +860,12 @@ proc generateCLibHeader*(
## The `<lib>.h` header: library structs, monomorphised codecs and async API.
let classified = classifyProcs(procs)
let ctors = classified.ctors
let methods = classified.methods
let libType = libTypeName(ctors, libName)
let ctxType = libType & "Ctx"
var reg = newCTypeReg(libName, libType, types, procs)
let (reqTypes, respTypes) = monomorphiseAll(reg, types, procs, methods, events)
let (reqTypes, respTypes) =
monomorphiseAll(reg, types, procs, classified.replyProcs(), events)
let guard = "NIM_FFI_LIB_" & libName.toUpperAscii() & "_H_INCLUDED"
var lines: seq[string] = @[]
@ -894,6 +902,11 @@ proc generateCLibHeader*(
"int " & p.procName & "(void* ctx, FFICallback callback, void* user_data, " &
"const uint8_t* req_cbor, size_t req_cbor_len);"
)
of FFIKind.STATIC:
lines.add(
"int " & p.procName & "(FFICallback callback, void* user_data, " &
"const uint8_t* req_cbor, size_t req_cbor_len);"
)
of FFIKind.CTOR:
lines.add(
"void* " & p.procName & "(const uint8_t* req_cbor, size_t req_cbor_len, " &
@ -943,8 +956,8 @@ proc generateCLibHeader*(
emitConstructors(lines, reg, ctxType, libType, libName, ctors)
emitDestructor(lines, ctxType, libName, classified.dtor, events)
emitListenerApi(lines, ctxType, libType, libName, events)
for m in methods:
emitMethod(lines, reg, ctxType, libType, libName, m)
for m in classified.replyProcs():
emitProcWrapper(lines, reg, ctxType, libType, libName, m)
lines.add("#endif /* " & guard & " */")
return lines.join("\n") & "\n"
@ -1198,6 +1211,12 @@ proc emitAbiExternDecls(
"int " & p.procName & "(void* ctx, " & info.fnType &
" on_reply, void* user_data, const " & reqStructName(p) & "* req);"
)
of FFIKind.STATIC:
let info = abiMethodReplyInfo(reg, libType, p)
lines.add(
"int " & p.procName & "(" & info.fnType & " on_reply, void* user_data, const " &
reqStructName(p) & "* req);"
)
of FFIKind.CTOR:
lines.add(
"void* " & p.procName & "(const " & reqStructName(p) & "* req, " & createRawFn &
@ -1304,18 +1323,24 @@ proc emitAbiCtxAndCtor(
lines.add("}")
lines.add("")
proc emitAbiMethod(
proc emitAbiProcWrapper(
lines: var seq[string],
reg: var AbiReg,
ctxType, libName, libType: string,
m: FFIProcMeta,
) =
## See `emitProcWrapper` for why a static's wrapper is `<lib>_static_<name>`.
let isStatic = m.isStatic()
let stripped = stripLibPrefix(m.procName, m.libName)
let reqStruct = reqStructName(m)
let info = abiMethodReplyInfo(reg, libType, m)
let (params, assigns) = abiReqParamsAndAssigns(reg, m.extraParams)
let head =
"static inline int " & libName & "_ctx_" & stripped & "(const " & ctxType & "* ctx, "
if isStatic:
"static inline int " & libName & "_static_" & stripped & "("
else:
"static inline int " & libName & "_ctx_" & stripped & "(const " & ctxType &
"* ctx, "
let sig =
if params.len > 0:
head & params.join(", ") & ", " & info.fnType & " on_reply, void* user_data) {"
@ -1327,7 +1352,10 @@ proc emitAbiMethod(
lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));")
for a in assigns:
lines.add(a)
lines.add(" return " & m.procName & "(ctx->ptr, on_reply, user_data, &ffi_req);")
let ctxArg = if isStatic: "" else: "ctx->ptr, "
lines.add(
" return " & m.procName & "(" & ctxArg & "on_reply, user_data, &ffi_req);"
)
lines.add("}")
lines.add("")
@ -1499,7 +1527,7 @@ proc generateCAbiLibHeader*(
lines.add(decl)
lines.add("")
emitAbiReplyTypedefs(lines, reg, libType, classified.methods)
emitAbiReplyTypedefs(lines, reg, libType, classified.replyProcs())
lines.add("")
emitAbiExternDecls(lines, reg, libName, libType, procs)
@ -1507,11 +1535,12 @@ proc generateCAbiLibHeader*(
emitAbiCtxAndCtor(lines, reg, libName, libType, ctxType, classified.ctors)
# abi = c has no events, so the destructor is the CBOR one minus the listener sweep.
emitDestructor(lines, ctxType, libName, classified.dtor, @[])
for m in classified.methods:
# A static is never scalar-fast-path (`isScalarOnly` gates on FFIKind.FFI).
for m in classified.replyProcs():
if m.scalarFastPath:
emitAbiScalarMethod(lines, reg, ctxType, libName, libType, m)
else:
emitAbiMethod(lines, reg, ctxType, libName, libType, m)
emitAbiProcWrapper(lines, reg, ctxType, libName, libType, m)
lines.add("#endif /* " & guard & " */")
return lines.join("\n") & "\n"

View File

@ -18,25 +18,6 @@ proc reqStructName*(p: FFIProcMeta): string =
else:
camel & "Req"
type ClassifiedProcs* = object
ctors*: seq[FFIProcMeta]
methods*: seq[FFIProcMeta]
dtor*: Option[FFIProcMeta]
proc classifyProcs*(procs: seq[FFIProcMeta]): ClassifiedProcs =
## Splits the registry into constructors, methods and the first destructor.
var c: ClassifiedProcs
for p in procs:
case p.kind
of FFIKind.CTOR:
c.ctors.add(p)
of FFIKind.FFI:
c.methods.add(p)
of FFIKind.DTOR:
if c.dtor.isNone():
c.dtor = some(p)
c
proc libTypeName*(ctors: seq[FFIProcMeta], libName: string): string =
## The library type name, from the first ctor or derived from `libName`.
if ctors.len > 0:

View File

@ -110,7 +110,7 @@ proc responseRule(p: FFIProcMeta): string =
of FFIKind.DTOR:
# Dtor payload is a CBOR null sentinel.
"nil"
of FFIKind.FFI:
of FFIKind.FFI, FFIKind.STATIC:
if p.returnRidesAsPtr():
"uint"
else:
@ -166,6 +166,7 @@ proc generateCddlSchema*(
of FFIKind.CTOR: "ctor"
of FFIKind.DTOR: "dtor"
of FFIKind.FFI: "ffi"
of FFIKind.STATIC: "ffiStatic"
L.add("; " & p.procName & " (" & kindTag & ")")
L.add(renderDocComment(p.doc, "", "; "))
if p.kind != FFIKind.DTOR:

View File

@ -6,6 +6,9 @@ import ./meta, ./string_helpers, ./c_cpp_common, ./types_ir, ./consts
## Fixed 64-bit wire type for any Nim `ptr T` / `pointer`.
const CppPtrType* = "uint64_t"
## Trailing param of every call that can't inherit a ctx's `timeout_`.
const CppTimeoutParam = "std::chrono::milliseconds timeout = std::chrono::seconds{30}"
const
HeaderPreludeTpl = staticRead("templates/cpp/header_prelude.hpp.tpl")
ResultTpl = staticRead("templates/cpp/result.hpp.tpl")
@ -319,6 +322,11 @@ proc generateCppHeader*(
"int $1(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);" %
[p.procName]
)
of FFIKind.STATIC:
lines.add(
"int $1(FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);" %
[p.procName]
)
of FFIKind.CTOR:
lines.add(
"void* $1(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback callback, void* user_data);" %
@ -341,7 +349,6 @@ proc generateCppHeader*(
let classified = classifyProcs(procs)
let ctors = classified.ctors
let methods = classified.methods
let ctxTypeName = libTypeName(ctors, libName) & "Ctx"
lines.add("// ============================================================")
@ -363,12 +370,11 @@ proc generateCppHeader*(
nimTypeToCpp(ep.typeName)
ctorParams.add("const $1& $2" % [cppType, ep.name])
epNames.add(ep.name)
let timeoutParam = "std::chrono::milliseconds timeout = std::chrono::seconds{30}"
let ctorParamsWithTimeout =
if ctorParams.len > 0:
ctorParams.join(", ") & ", " & timeoutParam
ctorParams.join(", ") & ", " & CppTimeoutParam
else:
timeoutParam
CppTimeoutParam
let reqInit = cppBracedInit(reqName, epNames)
@ -444,7 +450,9 @@ proc generateCppHeader*(
emitEventDispatcher(lines, ctxTypeName, libName, events)
for m in methods:
# A static has no ctx to inherit `timeout_` from, so it takes its own `timeout`.
for m in classified.replyProcs():
let isStatic = m.isStatic()
let methodName = stripLibPrefix(m.procName, libName)
let retCppType =
if m.returnRidesAsPtr():
@ -463,14 +471,21 @@ proc generateCppHeader*(
nimTypeToCpp(ep.typeName)
methParams.add("const $1& $2" % [cppType, ep.name])
methParamNames.add(ep.name)
let methParamsStr = methParams.join(", ")
let methParamNamesStr = methParamNames.join(", ")
let methParamsStr =
if not isStatic:
methParams.join(", ")
elif methParams.len > 0:
methParams.join(", ") & ", " & CppTimeoutParam
else:
CppTimeoutParam
let reqInit = cppBracedInit(reqName, methParamNames)
let methRet = "Result<$1>" % [retCppType]
lines.add(renderMemberDocComment(m.doc))
lines.add(" $1 $2($3) const {" % [methRet, methodName, methParamsStr])
let decl = if isStatic: " static $1 $2($3) {" else: " $1 $2($3) const {"
lines.add(decl % [methRet, methodName, methParamsStr])
lines.add(" const auto ffi_req_ = $1;" % [reqInit])
lines.add(" auto ffi_enc_ = encodeCborFFI(ffi_req_);")
lines.add(
@ -478,35 +493,46 @@ proc generateCppHeader*(
)
lines.add(" const auto& ffi_req_bytes_ = ffi_enc_.value();")
lines.add(" auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {")
let ctxArg = if isStatic: "" else: "ptr_, "
lines.add(
" return $1(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size());" %
[m.procName]
" return $1($2cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size());" %
[m.procName, ctxArg]
)
lines.add(" }, timeout_);")
lines.add(" }, $1);" % [if isStatic: "timeout" else: "timeout_"])
lines.add(
" if (ffi_raw_.isErr()) return $1::err(ffi_raw_.error());" % [methRet]
)
lines.add(" return decodeCborFFI<$1>(ffi_raw_.value());" % [retCppType])
lines.add(" }")
lines.add("")
# A static forwards its own `timeout`; a method captures `this` and calls
# `this->methodName(...)` so a same-named param can't shadow the call target.
let staticArgs =
if methParamNames.len > 0:
methParamNamesStr & ", timeout"
else:
"timeout"
let asyncArgs = if isStatic: staticArgs else: methParamNamesStr
let asyncCapture =
if isStatic:
staticArgs
elif methParamNamesStr.len > 0:
"this, " & methParamNamesStr
else:
"this"
let asyncDecl =
if isStatic:
" static std::future<$1> $2Async($3) {"
else:
" std::future<$1> $2Async($3) const {"
lines.add(renderMemberDocComment(m.doc))
if methParamsStr.len > 0:
lines.add(
" std::future<$1> $2Async($3) const {" % [methRet, methodName, methParamsStr]
)
lines.add(
" return std::async(std::launch::async, [this, $1]() { return this->$2($3); });" %
[methParamNamesStr, methodName, methParamNamesStr]
)
lines.add(" }")
else:
lines.add(" std::future<$1> $2Async() const {" % [methRet, methodName])
lines.add(
" return std::async(std::launch::async, [this]() { return this->$1(); });" %
[methodName]
)
lines.add(" }")
lines.add(asyncDecl % [methRet, methodName, methParamsStr])
lines.add(
" return std::async(std::launch::async, [$1]() { return $2$3($4); });" %
[asyncCapture, (if isStatic: "" else: "this->"), methodName, asyncArgs]
)
lines.add(" }")
lines.add("")
lines.add("private:")

View File

@ -1,7 +1,7 @@
## Compile-time metadata types for FFI binding generation, populated by the
## {.ffiCtor.}/{.ffi.} macros and consumed by codegen.
import std/strutils
import std/[strutils, options]
type
ABIFormat* {.pure.} = enum
@ -20,6 +20,7 @@ type
FFI
CTOR
DTOR
STATIC ## `{.ffiStatic.}`: context-independent, its wrapper takes no `ctx`
FFIProcMeta* = object
procName*: string
@ -146,6 +147,40 @@ var ffiEnumTypeNames* {.compileTime.}: seq[string]
proc isFFIEnumTypeName*(name: string): bool {.compileTime.} =
name in ffiEnumTypeNames
func isStatic*(p: FFIProcMeta): bool =
## True for a `{.ffiStatic.}` proc: no library receiver, no ctx in its wrapper.
p.kind == FFIKind.STATIC
type ClassifiedProcs* = object
ctors*: seq[FFIProcMeta]
methods*: seq[FFIProcMeta]
statics*: seq[FFIProcMeta]
dtor*: Option[FFIProcMeta]
func classifyProcs*(procs: seq[FFIProcMeta]): ClassifiedProcs =
## Splits the registry into constructors, methods, statics and the first destructor.
var c: ClassifiedProcs
for p in procs:
case p.kind
of FFIKind.CTOR:
c.ctors.add(p)
of FFIKind.FFI:
c.methods.add(p)
of FFIKind.STATIC:
c.statics.add(p)
of FFIKind.DTOR:
if c.dtor.isNone():
c.dtor = some(p)
c
func dtorProcName*(c: ClassifiedProcs): string =
## The destructor's proc name, or "" when the library has no destructor.
if c.dtor.isSome(): c.dtor.get().procName else: ""
func replyProcs*(c: ClassifiedProcs): seq[FFIProcMeta] =
## Procs that reply with a decoded value: methods and statics.
c.methods & c.statics
proc ridesAsPtr*(ep: FFIParamMeta): bool =
## True if the param crosses the wire as an opaque uint64 (raw ptr or handle).
ep.isPtr or ep.isHandle

View File

@ -185,9 +185,10 @@ proc generateFFIRs*(procs: seq[FFIProcMeta]): string =
var params: seq[string] = @[]
lines.add(renderMemberDocComment(p.doc))
case p.kind
of FFIKind.FFI:
# Method-style: ctx first.
params.add("ctx: *mut c_void")
of FFIKind.FFI, FFIKind.STATIC:
# Method-style: ctx first. A static is the same shape, minus the ctx.
if not p.isStatic():
params.add("ctx: *mut c_void")
params.add("callback: FFICallback")
params.add("user_data: *mut c_void")
params.add("req_cbor: *const u8")
@ -304,18 +305,9 @@ proc generateApiRs*(
## Requests/responses are CBOR (ciborium); errors are raw UTF-8 strings.
var lines: seq[string] = @[]
var ctors: seq[FFIProcMeta] = @[]
var methods: seq[FFIProcMeta] = @[]
var dtorProcName = ""
for p in procs:
case p.kind
of FFIKind.CTOR:
ctors.add(p)
of FFIKind.FFI:
methods.add(p)
of FFIKind.DTOR:
if dtorProcName.len == 0:
dtorProcName = p.procName
let classified = classifyProcs(procs)
let ctors = classified.ctors
let dtorProcName = classified.dtorProcName
var libTypeName = ""
if ctors.len > 0:
@ -700,7 +692,9 @@ proc generateApiRs*(
lines.add(" }")
lines.add("")
for m in methods:
# A static is an associated fn: no `&self` to read `timeout` from, so it takes one.
for m in classified.replyProcs():
let isStatic = m.isStatic()
let methodName = stripLibPrefix(m.procName, libName)
let retRustType = nimTypeToRust(m.returnTypeName)
let reqName = reqStructName(m)
@ -716,11 +710,15 @@ proc generateApiRs*(
nimTypeToRust(ep.typeName)
paramsList.add("$1: $2" % [snake, rustType])
fieldInits.add(snake)
if isStatic:
paramsList.add("timeout: Duration")
let paramsStr =
if paramsList.len > 0:
", " & paramsList.join(", ")
if isStatic:
paramsList.join(", ")
elif paramsList.len > 0:
"&self, " & paramsList.join(", ")
else:
""
"&self"
let reqLit =
if fieldInits.len > 0:
@ -729,18 +727,22 @@ proc generateApiRs*(
reqName & " {}"
let retTypeForApi = if m.returnRidesAsPtr(): RustPtrType else: retRustType
let timeoutExpr = if isStatic: "timeout" else: "self.timeout"
let ctxArg = if isStatic: "" else: "self.ptr, "
lines.add(renderMemberDocComment(m.doc))
lines.add(
" pub fn $1(&self$2) -> Result<$3, String> {" %
" pub fn $1($2) -> Result<$3, String> {" %
[methodName, paramsStr, retTypeForApi]
)
lines.add(" let req = $1;" % [reqLit])
lines.add(" let req_bytes = encode_cbor(&req)?;")
lines.add(" let raw_bytes = ffi_call_sync(self.timeout, |cb, ud| unsafe {")
lines.add(
" ffi::$1(self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len())" %
[m.procName]
" let raw_bytes = ffi_call_sync($1, |cb, ud| unsafe {" % [timeoutExpr]
)
lines.add(
" ffi::$1($2cb, ud, req_bytes.as_ptr(), req_bytes.len())" %
[m.procName, ctxArg]
)
lines.add(" })?;")
lines.add(" decode_cbor::<$1>(&raw_bytes)" % [retTypeForApi])
@ -750,18 +752,21 @@ proc generateApiRs*(
# async method: ptr cast to usize (Copy + Send) keeps the move closure and returned future Send for multi-threaded tokio runtimes.
lines.add(renderMemberDocComment(m.doc))
lines.add(
" pub async fn $1_async(&self$2) -> Result<$3, String> {" %
" pub async fn $1_async($2) -> Result<$3, String> {" %
[methodName, paramsStr, retTypeForApi]
)
lines.add(" let req = $1;" % [reqLit])
lines.add(" let req_bytes = encode_cbor(&req)?;")
lines.add(" let ptr = self.ptr as usize;")
if not isStatic:
lines.add(" let ptr = self.ptr as usize;")
lines.add(
" let raw_bytes = ffi_call_async(self.timeout, move |cb, ud| unsafe {"
" let raw_bytes = ffi_call_async($1, move |cb, ud| unsafe {" % [
timeoutExpr
]
)
lines.add(
" ffi::$1(ptr as *mut c_void, cb, ud, req_bytes.as_ptr(), req_bytes.len())" %
[m.procName]
" ffi::$1($2cb, ud, req_bytes.as_ptr(), req_bytes.len())" %
[m.procName, if isStatic: "" else: "ptr as *mut c_void, "]
)
lines.add(" }).await?;")
lines.add(" decode_cbor::<$1>(&raw_bytes)" % [retTypeForApi])

View File

@ -1,13 +1,22 @@
import std/atomics
import std/[atomics, sysatomics]
import results
import ./ffi_context
const MaxFFIContexts* = 32
type FFIContextPool*[T] = object
## Fixed pool. Bounds ThreadSignalPtr fds at MaxFFIContexts * 2.
slots: array[MaxFFIContexts, FFIContext[T]]
inUse: array[MaxFFIContexts, Atomic[bool]]
type
StaticCtxState = enum
## Lifecycle of the pool's `{.ffiStatic.}` context; see `staticFFIContext`.
StaticCtxNone
StaticCtxCreating
StaticCtxReady
FFIContextPool*[T] = object
## Fixed pool. Bounds ThreadSignalPtr fds at MaxFFIContexts * 2.
slots: array[MaxFFIContexts, FFIContext[T]]
inUse: array[MaxFFIContexts, Atomic[bool]]
staticCtx: Atomic[pointer]
staticState: Atomic[StaticCtxState]
proc acquireSlot[T](pool: var FFIContextPool[T]): Result[ptr FFIContext[T], string] =
for i in 0 ..< MaxFFIContexts:
@ -45,6 +54,32 @@ proc destroyFFIContext*[T](
return err("destroyFFIContext(pool): " & $error)
ok()
proc staticFFIContext*[T](
pool: var FFIContextPool[T]
): Result[ptr FFIContext[T], string] =
## The pool's `{.ffiStatic.}` context: a static proc has no ctx of its own, but
## its handler still needs an FFI thread. Created on first use and never
## destroyed, so it holds a slot for good and `pool` must outlive its threads —
## only ever call this on the global `declareLibrary` emits. `myLib` stays the
## zero value; a static handler must not touch it. A failed create resets to
## `StaticCtxNone` so the spinning losers retry instead of hanging.
while true:
case pool.staticState.load()
of StaticCtxReady:
return ok(cast[ptr FFIContext[T]](pool.staticCtx.load()))
of StaticCtxCreating:
cpuRelax()
of StaticCtxNone:
var expected = StaticCtxNone
if not pool.staticState.compareExchange(expected, StaticCtxCreating):
continue
let ctx = pool.createFFIContext().valueOr:
pool.staticState.store(StaticCtxNone)
return err("staticFFIContext: " & error)
pool.staticCtx.store(cast[pointer](ctx))
pool.staticState.store(StaticCtxReady)
return ok(ctx)
proc isValidCtx*[T](pool: var FFIContextPool[T], ctx: pointer): bool =
## Rejects nil / dangling pointers at the API boundary.
if ctx.isNil():

View File

@ -540,6 +540,7 @@ type
CAbiKind = enum
cakMethod
cakCtor
cakStatic
CAbiSpec = object
kind: CAbiKind
@ -561,18 +562,20 @@ proc copyTypes(types: seq[NimNode]): seq[NimNode] {.compileTime.} =
res.add(t.copyNimTree())
res
proc registerCAbiMethod*(
proc registerCAbiProc*(
isStatic: bool,
exportName: string,
libType, envelope: NimNode,
paramNames: seq[string],
paramTypes: seq[NimNode],
respType, handler: NimNode,
) {.compileTime.} =
## Record an `abi = c` method for `flushCAbiDispatch`. Nodes are `copyNimTree`
## frozen: reusing the Req section's originals (bound to `nnkSym`) would ICE.
## Record an `abi = c` method (or `{.ffiStatic.}` proc) for `flushCAbiDispatch`.
## Nodes are `copyNimTree` frozen: reusing the Req section's originals (bound to
## `nnkSym`) would ICE.
cAbiSpecs.add(
CAbiSpec(
kind: cakMethod,
kind: if isStatic: cakStatic else: cakMethod,
exportName: exportName,
libType: libType.copyNimTree(),
envelope: envelope.copyNimTree(),
@ -590,7 +593,7 @@ proc registerCAbiCtor*(
paramTypes: seq[NimNode],
handler: NimNode,
) {.compileTime.} =
## Record an `abi = c` ctor for `flushCAbiDispatch`; see `registerCAbiMethod`
## Record an `abi = c` ctor for `flushCAbiDispatch`; see `registerCAbiProc`
## for why nodes are `copyNimTree` frozen.
cAbiSpecs.add(
CAbiSpec(
@ -750,6 +753,7 @@ proc exportedMethodProc(
onReply(RET_ERR, `emptyReply`, sendRes.error.cstring, userData)
return RET_ERR
return RET_OK
newProc(
name = ident($envName & "CAbiExport"),
params = @[
@ -768,6 +772,76 @@ proc exportedMethodProc(
),
)
proc exportedStaticProc(
spec: CAbiSpec, boxName, envWire, trampName, poolIdent, cbType: NimNode
): NimNode =
## Ctx-less twin of `exportedMethodProc`: binds the library's static context
## instead of taking one. `initGuard` is raw AST because a `when declared` over
## an undeclared symbol inside `quote` ICEs (see `exportedCtorProc`).
let envName = spec.envelope
let emptyReply =
if isStringType(spec.respType):
newDotExpr(newLit(""), ident("cstring"))
else:
newNilLit()
let initGuard = nnkWhenStmt.newTree(
nnkElifBranch.newTree(
newCall(ident("declared"), ident("initializeLibrary")),
newStmtList(newCall(ident("initializeLibrary"))),
)
)
let body = quote:
if onReply.isNil():
return RET_MISSING_CALLBACK
let ctx = `poolIdent`.staticFFIContext().valueOr:
let errStr = "ffiStatic: " & error
onReply(RET_ERR, `emptyReply`, errStr.cstring, userData)
return RET_ERR
var ownedWire: `envWire`
cwirePack(ownedWire, cwireUnpack(req[]))
let ownedCopy = cwireOwnedCopy(ownedWire)
if ownedCopy.isNil():
cwireFree(ownedWire)
onReply(RET_ERR, `emptyReply`, "out of memory".cstring, userData)
return RET_ERR
let reqBuf = cast[ptr UncheckedArray[byte]](ownedCopy)
let box = cast[ptr `boxName`](allocBox(sizeof(`boxName`)))
box.fn = onReply
box.ud = userData
let typeStr = $`envName`
let reqPtr = FFIThreadRequest.initFromOwnedShared(
`trampName`, box, typeStr.cstring, reqBuf, sizeof(`envWire`), rawReply = true
)
let sendRes =
try:
ffi_context.sendRequestToFFIThread(ctx, reqPtr)
except Exception as e:
Result[void, string].err("sendRequestToFFIThread exception: " & e.msg)
if sendRes.isErr():
# See exportedMethodProc: the rejected send freed the struct copy, not the
# field buffers `ownedWire` still aliases.
cwireFree(ownedWire)
onReply(RET_ERR, `emptyReply`, sendRes.error.cstring, userData)
return RET_ERR
return RET_OK
body.insert(0, initGuard)
newProc(
name = ident($envName & "CAbiExport"),
params = @[
ident("cint"),
newIdentDefs(ident("onReply"), cbType),
newIdentDefs(ident("userData"), ident("pointer")),
newIdentDefs(ident("req"), nnkPtrTy.newTree(envWire)),
],
body = body,
pragmas = nnkPragma.newTree(
ident("dynlib"),
nnkExprColonExpr.newTree(ident("exportc"), newStrLitNode(spec.exportName)),
ident("cdecl"),
nnkExprColonExpr.newTree(ident("raises"), nnkBracket.newTree()),
),
)
proc exportedCtorProc(
spec: CAbiSpec, boxName, envWire, trampName, poolIdent, cbType: NimNode
): NimNode =
@ -874,26 +948,27 @@ proc flushCAbiDispatch*(): NimNode {.compileTime.} =
sink.add(boxTypeDef(boxName, cbType))
sink.add(replyTrampProc(trampName, stringTrampBody(boxName)))
sink.add(exportedCtorProc(spec, boxName, envWire, trampName, poolIdent, cbType))
of cakMethod:
of cakMethod, cakStatic:
let emitExport =
if spec.kind == cakStatic: exportedStaticProc else: exportedMethodProc
let rt = spec.respType
if isStringType(rt):
let cbType = cAbiCbType(ident("cstring"))
sink.add(boxTypeDef(boxName, cbType))
sink.add(replyTrampProc(trampName, stringTrampBody(boxName)))
sink.add(
exportedMethodProc(spec, boxName, envWire, trampName, poolIdent, cbType)
)
elif rt.kind == nnkIdent:
sink.add(emitExport(spec, boxName, envWire, trampName, poolIdent, cbType))
# `isKnownFFIType`, not just `nnkIdent`: a bare `int` is an ident too, and
# would otherwise reach for a `int_CWire` companion that is never emitted.
elif rt.kind == nnkIdent and isKnownFFIType($rt):
let respWire = ident(cwireTypeName($rt))
let cbType = cAbiCbType(nnkPtrTy.newTree(respWire))
sink.add(boxTypeDef(boxName, cbType))
sink.add(replyTrampProc(trampName, objectTrampBody(boxName, respWire)))
sink.add(
exportedMethodProc(spec, boxName, envWire, trampName, poolIdent, cbType)
)
sink.add(emitExport(spec, boxName, envWire, trampName, poolIdent, cbType))
else:
error(
"abi = c: unsupported response type for proc '" & spec.exportName & "': " &
rt.repr & " (only object and string returns are wired)"
rt.repr & " — reply with a `string` or an `{.ffi.}` object type. " &
"A scalar return is wired only for an all-scalar `{.ffi.}` method."
)
sink

View File

@ -271,11 +271,13 @@ proc unpackHandleField*(
return err(`errPrefix` & error)
cast[`userType`](ffiH)
proc cExportedParams(ctxType: NimNode): seq[NimNode] =
## C-exported wrapper param list (cint; ctx, callback, userData, reqCbor, reqCborLen).
proc cExportedParams(ctxType: NimNode, withCtx = true): seq[NimNode] =
## C-exported wrapper param list (cint; ctx, callback, userData, reqCbor,
## reqCborLen). A `{.ffiStatic.}` wrapper drops the leading `ctx`.
var params: seq[NimNode] = @[]
params.add(ident("cint"))
params.add(newIdentDefs(ident("ctx"), ctxType))
if withCtx:
params.add(newIdentDefs(ident("ctx"), ctxType))
params.add(newIdentDefs(ident("callback"), ident("FFICallBack")))
params.add(newIdentDefs(ident("userData"), ident("pointer")))
params.add(newIdentDefs(ident("reqCbor"), nnkPtrTy.newTree(ident("byte"))))
@ -807,44 +809,35 @@ macro ffiConst*(args: varargs[untyped]): untyped =
echo stmts.repr
return stmts
macro ffi*(args: varargs[untyped]): untyped =
## Simplified FFI macro for procs or types: a type registers for binding gen; a
## proc takes a library-type param plus optional Nim params, returns
## Future[Result[RetType, string]], and gets a C wrapper taking one CBOR buffer.
requireBeforeGenBindings("`.ffi.`")
# Annotated node is the last vararg; leading args are `"abi = ..."` specs.
let prc = args[^1]
let abiFormat = resolveFFISpecs(args[0 ..^ 2])
# A value type stands alone (no library required); its `c` companion is emitted later by `genBindings()`, since a type-pragma macro can only return a TypeDef.
if prc.kind == nnkTypeDef:
gateFFITypeABIFormat(abiFormat, "`.ffi.` type")
var cleanTypeDef = prc.copyNimTree()
if cleanTypeDef[0].kind == nnkPragmaExpr:
cleanTypeDef[0] = cleanTypeDef[0][0]
return registerFFITypeInfo(cleanTypeDef, abiFormat)
requireLibraryDeclared("`.ffi.`")
proc buildFFIProc(
prc: NimNode, abiFormat: ABIFormat, isStatic: bool
): NimNode {.compileTime.} =
## Shared body of `{.ffi.}` and `{.ffiStatic.}`. A static has no library receiver:
## its wire params start at param 1 and its C wrapper binds the static context.
let where = if isStatic: "`.ffiStatic.`" else: "`.ffi.`"
let procName = prc[0]
let formalParams = prc[3]
let bodyNode = prc[^1]
if formalParams.len < 2:
if not isStatic and formalParams.len < 2:
error("`.ffi.` procs require at least 1 parameter (the library type)")
let firstParam = formalParams[1]
let recvName = firstParam[0]
let recvType = firstParam[1]
let firstIsHandle = isHandleType(recvType)
if firstIsHandle and currentLibType.len == 0:
var recvName, recvType: NimNode = newEmptyNode()
var firstIsHandle = false
if not isStatic:
let firstParam = formalParams[1]
recvName = firstParam[0]
recvType = firstParam[1]
firstIsHandle = isHandleType(recvType)
if (firstIsHandle or isStatic) and currentLibType.len == 0:
error(
"`.ffi.` proc " & $procName & " has an {.ffiHandle.} receiver but no " &
"library is declared; call declareLibrary(name, LibType) first"
where & " proc " & $procName & " carries no library type but no library is " &
"declared; call declareLibrary(name, LibType) first"
)
# A handle receiver carries no library type, so fall back to the declared one.
# A static proc and a handle receiver both carry no library type, so fall back to the declared one.
let libTypeName =
if firstIsHandle:
if firstIsHandle or isStatic:
ident(currentLibType)
else:
recvType
@ -852,31 +845,43 @@ macro ffi*(args: varargs[untyped]): untyped =
let retTypeNode = formalParams[0]
if retTypeNode.kind == nnkEmpty:
error(
"`.ffi.` proc must have an explicit return type Future[Result[RetType, string]]"
where & " proc must have an explicit return type Future[Result[RetType, string]]"
)
if retTypeNode.kind != nnkBracketExpr or $retTypeNode[0] != "Future":
error(
"`.ffi.` return type must be Future[Result[RetType, string]], got: " &
where & " return type must be Future[Result[RetType, string]], got: " &
retTypeNode.repr
)
let resultInner = retTypeNode[1]
if resultInner.kind != nnkBracketExpr or $resultInner[0] != "Result":
error(
"`.ffi.` return type must be Future[Result[RetType, string]], got: " &
where & " return type must be Future[Result[RetType, string]], got: " &
retTypeNode.repr
)
let resultRetType = resultInner[1]
rejectRawPtrType(resultRetType, "`.ffi.` proc " & $procName & " return type")
rejectRawPtrType(resultRetType, where & " proc " & $procName & " return type")
# An {.ffiHandle.} lives in one ctx's registry, which a static proc cannot reach.
if isStatic and isHandleType(resultRetType):
error(
where & " proc " & $procName & " returns the {.ffiHandle.} type " & $resultRetType &
"; a handle belongs to a context. Make it an `{.ffi.}` method instead."
)
# A handle receiver rides the wire; a value-type lib receiver binds to ctx.myLib.
var extraParamNames: seq[string] = @[]
var extraParamTypes: seq[NimNode] = @[]
let wireStart = if firstIsHandle: 1 else: 2
let wireStart = if isStatic or firstIsHandle: 1 else: 2
for i in wireStart ..< formalParams.len:
let p = formalParams[i]
for j in 0 ..< p.len - 2:
rejectRawPtrType(p[^2], "`.ffi.` proc " & $procName & " parameter " & $p[j])
rejectRawPtrType(p[^2], where & " proc " & $procName & " parameter " & $p[j])
if isStatic and isHandleType(p[^2]):
error(
where & " proc " & $procName & " takes the {.ffiHandle.} parameter " & $p[j] &
": " & $p[^2] & "; a handle belongs to a context. " &
"Make it an `{.ffi.}` method instead."
)
extraParamNames.add($p[j])
extraParamTypes.add(p[^2])
@ -927,7 +932,7 @@ macro ffi*(args: varargs[untyped]): untyped =
let procMeta = FFIProcMeta(
procName: cExportName,
libName: currentLibName,
kind: FFIKind.FFI,
kind: if isStatic: FFIKind.STATIC else: FFIKind.FFI,
libTypeName: $libTypeName,
extraParams: wireParamMetas,
returnTypeName: retTn,
@ -954,6 +959,20 @@ macro ffi*(args: varargs[untyped]): untyped =
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
return RET_ERR
proc buildStaticCtxGuard(): NimNode =
## Binds the library's static context: a static wrapper takes no `ctx`, and a
## static call may be the host's first entry (hence `initializeLibrary`).
## `ctxIdent` is substituted so the send below sees it (`quote` gensyms).
let ctxIdent = ident("ctx")
quote:
initializeLibrary()
if callback.isNil():
return RET_MISSING_CALLBACK
let `ctxIdent` = `poolIdent`.staticFFIContext().valueOr:
let errStr = "ffiStatic: " & error
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
return RET_ERR
proc buildSendAndReply(reqPtrIdent: NimNode): NimNode =
## Hands `reqPtrIdent` to the FFI thread and maps the outcome to a C return code.
let sendResIdent = genSym(nskLet, "sendRes")
@ -988,8 +1007,10 @@ macro ffi*(args: varargs[untyped]): untyped =
## Reproduces the user's exact signature so it stays callable from Nim.
var helperParams = newSeq[NimNode]()
helperParams.add(retTypeNode)
helperParams.add(newIdentDefs(recvName, recvType))
for i in 2 ..< formalParams.len:
let helperStart = if isStatic: 1 else: 2
if not isStatic:
helperParams.add(newIdentDefs(recvName, recvType))
for i in helperStart ..< formalParams.len:
let p = formalParams[i]
for j in 0 ..< p.len - 2:
helperParams.add(newIdentDefs(p[j], p[^2]))
@ -1015,7 +1036,7 @@ macro ffi*(args: varargs[untyped]): untyped =
lambdaParams.add(newIdentDefs(ident(extraParamNames[i]), extraParamTypes[i]))
let helperCall = newTree(nnkCall, userProcName)
if not firstIsHandle:
if not firstIsHandle and not isStatic:
let ctxMyLib = newDotExpr(newTree(nnkDerefExpr, ctxHandlerName), ident("myLib"))
helperCall.add(newTree(nnkDerefExpr, ctxMyLib))
for name in extraParamNames:
@ -1040,10 +1061,18 @@ macro ffi*(args: varargs[untyped]): untyped =
`lambdaNode`
# C-exported wrapper: (ctx, callback, userData, reqCbor, reqCborLen).
let exportedParams = cExportedParams(ctxType)
let exportedParams = cExportedParams(ctxType, withCtx = not isStatic)
let ffiBody = newStmtList()
ffiBody.add buildCtxGuard()
# Flattened, not nested: the static guard's `let ctx` has to be a sibling of
# the send below for it to be in scope.
let guard =
if isStatic:
buildStaticCtxGuard()
else:
buildCtxGuard()
for stmt in guard:
ffiBody.add(stmt)
let reqPtrIdent = genSym(nskLet, "reqPtr")
ffiBody.add quote do:
@ -1064,9 +1093,9 @@ macro ffi*(args: varargs[untyped]): untyped =
buildProcessFFIRequestProc(reqTypeName, handlerParam, lambdaNode, ABIFormat.C),
addNewRequestToRegistry(reqTypeName, handlerParam, resultRetType, ABIFormat.C),
)
registerCAbiMethod(
cExportName, libTypeName, reqTypeName, extraParamNames, extraParamTypes,
resultRetType, handler,
registerCAbiProc(
isStatic, cExportName, libTypeName, reqTypeName, extraParamNames,
extraParamTypes, resultRetType, handler,
)
return newStmtList(helperProc, buildRequestType(reqTypeName, lambdaNode))
@ -1101,6 +1130,40 @@ macro ffi*(args: varargs[untyped]): untyped =
echo stmts.repr
return stmts
macro ffi*(args: varargs[untyped]): untyped =
## Simplified FFI macro for procs or types: a type registers for binding gen; a
## proc takes a library-type param plus optional Nim params, returns
## Future[Result[RetType, string]], and gets a C wrapper taking one CBOR buffer.
requireBeforeGenBindings("`.ffi.`")
# Annotated node is the last vararg; leading args are `"abi = ..."` specs.
let prc = args[^1]
let abiFormat = resolveFFISpecs(args[0 ..^ 2])
# A value type stands alone (no library required); its `c` companion is emitted later by `genBindings()`, since a type-pragma macro can only return a TypeDef.
if prc.kind == nnkTypeDef:
gateFFITypeABIFormat(abiFormat, "`.ffi.` type")
var cleanTypeDef = prc.copyNimTree()
if cleanTypeDef[0].kind == nnkPragmaExpr:
cleanTypeDef[0] = cleanTypeDef[0][0]
return registerFFITypeInfo(cleanTypeDef, abiFormat)
requireLibraryDeclared("`.ffi.`")
return buildFFIProc(prc, abiFormat, isStatic = false)
macro ffiStatic*(args: varargs[untyped]): untyped =
## Context-independent twin of `{.ffi.}`: the proc takes no library receiver and
## its C wrapper takes no `ctx`, so a host can call it without constructing the
## library. Handlers still run on an FFI thread — the library's static context,
## created on the first such call and alive for the rest of the process.
requireBeforeGenBindings("`.ffiStatic.`")
requireLibraryDeclared("`.ffiStatic.`")
let prc = args[^1]
let abiFormat = resolveFFISpecs(args[0 ..^ 2])
gateABIFormat(abiFormat, "`.ffiStatic.` proc")
if prc.kind notin {nnkProcDef, nnkFuncDef}:
error("`.ffiStatic.` must be applied to a proc definition")
return buildFFIProc(prc, abiFormat, isStatic = true)
proc buildCtorRequestType(
reqTypeName: NimNode, paramNames: seq[string], paramTypes: seq[NimNode]
): NimNode =

View File

@ -118,12 +118,36 @@ static void test_version(EchoCtx* ctx) {
assert(strcmp(w.text_a, "nim-echo v0.1.0") == 0);
}
/* {.ffiStatic.}: no EchoCtx, and the library's static context is created by
* this very call. Runs before make_ctx() so nothing else has initialised the
* Nim runtime first. */
static void test_static_no_ctx(void) {
ReplyWaiter v;
memset(&v, 0, sizeof(v));
echo_static_lib_version(on_version, &v);
wait_done(&v.done);
assert(v.err_code == 0);
assert(strcmp(v.text_a, "nim-echo v0.1.0") == 0);
ReplyWaiter s;
memset(&s, 0, sizeof(s));
ShoutRequest req = {"hello"};
echo_static_shout_anon(&req, on_shout, &s);
wait_done(&s.done);
assert(s.err_code == 0);
assert(strcmp(s.text_a, "HELLO") == 0);
assert(strcmp(s.text_b, "") == 0);
}
int main(void) {
test_static_no_ctx();
EchoCtx* ctx = make_ctx();
test_shout(ctx);
test_shout_too_long(ctx);
test_version(ctx);
assert(echo_ctx_destroy(ctx) == NIMFFI_RET_OK);
/* A static still works after every ctx is gone: its context is its own. */
test_static_no_ctx();
printf("all abi=c echo e2e checks passed\n");
return 0;
}

View File

@ -247,6 +247,38 @@ TEST(TimerE2E, CrossLibrary) {
EXPECT_EQ(e.shouted, "X-ECHO: ASYNC-E");
}
// The whole point of {.ffiStatic.}: no EchoCtx is ever constructed here.
TEST(TimerE2E, StaticProcNeedsNoContext) {
EXPECT_EQ(mustOk(EchoCtx::lib_version()), "nim-echo v0.1.0");
const auto resp = mustOk(EchoCtx::shout_anon(ShoutRequest{"hello"}));
EXPECT_EQ(resp.shouted, "HELLO");
EXPECT_EQ(resp.prefix, "");
}
// The static context is created once, on demand, and shared by every caller.
TEST(TimerE2E, StaticProcConcurrentFirstCall) {
constexpr int kThreads = 8;
std::vector<std::future<Result<ShoutResponse>>> futs;
futs.reserve(kThreads);
for (int i = 0; i < kThreads; ++i) {
futs.push_back(EchoCtx::shout_anonAsync(ShoutRequest{"race" + std::to_string(i)}));
}
for (int i = 0; i < kThreads; ++i) {
EXPECT_EQ(mustOk(futs[i].get()).shouted, "RACE" + std::to_string(i));
}
}
// A static call and a ctx call must not disturb each other's state.
TEST(TimerE2E, StaticProcCoexistsWithContext) {
auto ctx = mustOk(EchoCtx::create(EchoConfig{"WITH-CTX"}));
EXPECT_EQ(mustOk(ctx->shout(ShoutRequest{"a"})).prefix, "WITH-CTX");
EXPECT_EQ(mustOk(EchoCtx::shout_anon(ShoutRequest{"b"})).prefix, "");
EXPECT_EQ(mustOk(ctx->shout(ShoutRequest{"c"})).prefix, "WITH-CTX");
}
// Chained async calls A->B->C must preserve ordering and payload across hops.
TEST(TimerE2E, TriplePipeline) {
auto ctx = makeCtx("pipeline");

View File

@ -0,0 +1,23 @@
## Must fail: `{.ffiStatic.}` never rides the ctx-bound all-scalar fast path, so a
## scalar return has no `abi = c` reply shape. The error must say so, not die on an
## undeclared `int_CWire` (see tests/unit/test_ffistatic_reject.nim).
import ffi, chronos
type ScalarLib = object
base: int
declareLibrary("staticscalar", ScalarLib, defaultABIFormat = "c")
type ScalarConfig {.ffi.} = object
base: int
proc staticscalarCreate*(
cfg: ScalarConfig
): Future[Result[ScalarLib, string]] {.ffiCtor.} =
return ok(ScalarLib(base: cfg.base))
proc staticscalarAdd*(a: int, b: int): Future[Result[int, string]] {.ffiStatic.} =
return ok(a + b)
genBindings()

View File

@ -0,0 +1,16 @@
## Must fail: `{.ffiStatic.}` handle param (see tests/unit/test_ffistatic_reject.nim).
import ffi, chronos
type StaticLib = object
base: int
declareLibrary("staticrej", StaticLib)
type Session {.ffiHandle.} = ref object
id: int
proc staticrejBad*(s: Session): Future[Result[int, string]] {.ffiStatic.} =
return ok(s.id)
genBindings()

View File

@ -0,0 +1,16 @@
## Must fail: `{.ffiStatic.}` handle return (see tests/unit/test_ffistatic_reject.nim).
import ffi, chronos
type StaticLib = object
base: int
declareLibrary("staticrej", StaticLib)
type Session {.ffiHandle.} = ref object
id: int
proc staticrejBad*(): Future[Result[Session, string]] {.ffiStatic.} =
return ok(Session(id: 1))
genBindings()

View File

@ -0,0 +1,20 @@
## Must compile: proves the rejections are about handles, not the static shape.
import ffi, chronos
type StaticLib = object
base: int
declareLibrary("staticrej", StaticLib)
type Session {.ffiHandle.} = ref object
id: int
proc staticrejFine*(n: int): Future[Result[int, string]] {.ffiStatic.} =
return ok(n + 1)
proc staticrejOpen*(lib: StaticLib): Future[Result[Session, string]] {.ffi.} =
## A handle is fine on a method: it lives in the caller's own context.
return ok(Session(id: lib.base))
genBindings()

View File

@ -77,6 +77,14 @@ suite "generateCAbiLibHeader":
returnTypeName: "float32",
scalarFastPath: true,
),
FFIProcMeta(
procName: "timer_parse",
libName: "timer",
kind: FFIKind.STATIC,
libTypeName: "Timer",
extraParams: @[param("req", "EchoRequest")],
returnTypeName: "EchoResponse",
),
FFIProcMeta(
procName: "timer_destroy",
libName: "timer",
@ -180,6 +188,17 @@ static inline int timer_ctx_destroy(TimerCtx* ctx) {
header
check "if (n == SIZE_MAX) return NULL;" in header
test "a static's raw symbol and wrapper both drop the ctx":
check "int timer_parse(TimerParseReplyFn on_reply, void* user_data, " &
"const TimerParseReq* req);" in header
check "timer_static_parse(const EchoRequest* req, TimerParseReplyFn on_reply, void* user_data)" in
header
check "return timer_parse(on_reply, user_data, &ffi_req);" in header
test "a static replies through the same typed ReplyFn surface as a method":
check "typedef void (*TimerParseReplyFn)(int err_code, const EchoResponse* reply," in
header
test "events are rejected (CBOR-only for now)":
expect ValueError:
discard generateCAbiLibHeader(

View File

@ -161,6 +161,65 @@ static inline int timer_ctx_destroy(TimerCtx* ctx) {
test "an empty request envelope still encodes a (zero-length) map":
check "_nimffi_empty" in header
suite "generateCLibHeader: context-independent procs":
setup:
let procs = @[
FFIProcMeta(
procName: "timer_create",
libName: "timer",
kind: FFIKind.CTOR,
libTypeName: "Timer",
extraParams: @[param("config", "EchoRequest")],
returnTypeName: "Timer",
),
FFIProcMeta(
procName: "timer_version",
libName: "timer",
kind: FFIKind.FFI,
libTypeName: "Timer",
extraParams: @[],
returnTypeName: "string",
),
FFIProcMeta(
procName: "timer_parse",
libName: "timer",
kind: FFIKind.STATIC,
libTypeName: "Timer",
extraParams: @[param("req", "EchoRequest")],
returnTypeName: "EchoResponse",
),
]
let types = @[
FFITypeMeta(name: "EchoRequest", fields: @[field("m", "string")]),
FFITypeMeta(name: "EchoResponse", fields: @[field("echoed", "string")]),
]
let header = generateCLibHeader(procs, types, "timer")
test "the static's raw symbol takes no ctx":
check "int timer_parse(FFICallback callback, void* user_data, " &
"const uint8_t* req_cbor, size_t req_cbor_len);" in header
test "its wrapper is _static_-namespaced and takes neither ctx nor timeout":
check "timer_static_parse(const EchoRequest* req, TimerParseReplyFn on_reply, void* user_data)" in
header
check "timer_ctx_parse(" notin header
test "the wrapper calls the raw symbol without a ctx argument":
check "timer_parse(timer_parse_reply_trampoline, box, req_buf, req_len);" in header
test "a static gets the same reply machinery as a method":
check "typedef void (*TimerParseReplyFn)(int err_code, const EchoResponse* reply, " &
"const char* err_msg, void* user_data);" in header
check "TimerParseCallBox" in header
check "timer_parse_reply_trampoline(" in header
test "its return type is monomorphised into the codecs":
check "timer_decv_EchoResponse" in header
test "methods keep their ctx":
check "int timer_version(void* ctx, FFICallback callback" in header
check "timer_ctx_version(const TimerCtx* ctx," in header
suite "generateCLibHeader: events":
setup:
let procs = @[

View File

@ -109,6 +109,11 @@ registerReqFFI(HeavyRefAllocRequest, lib: ptr TestLib):
await sleepAsync(10.milliseconds)
return ok("heavy-done")
# Globals, as declareLibrary emits them: a static ctx is never destroyed, so its
# threads outlive any scope and the pool must outlive them.
var sharedPool: FFIContextPool[TestLib]
var retryPool: FFIContextPool[TestLib]
suite "FFIContextPool":
test "create and destroy via pool succeeds":
var pool: FFIContextPool[TestLib]
@ -142,6 +147,35 @@ suite "FFIContextPool":
for i in 0 ..< MaxFFIContexts:
discard pool.destroyFFIContext(ctxs[i])
test "staticFFIContext returns one shared context and holds its slot":
let first = sharedPool.staticFFIContext().valueOr:
assert false, "staticFFIContext failed: " & $error
return
check sharedPool.staticFFIContext().tryGet() == first
# The static ctx owns a slot for good: only MaxFFIContexts-1 are left.
var ctxs: seq[ptr FFIContext[TestLib]] = @[]
for _ in 0 ..< MaxFFIContexts - 1:
let c = sharedPool.createFFIContext().valueOr:
assert false, "createFFIContext(pool) failed: " & $error
return
ctxs.add(c)
check sharedPool.createFFIContext().isErr()
for c in ctxs:
discard sharedPool.destroyFFIContext(c)
test "a failed create leaves staticFFIContext retryable":
var ctxs: array[MaxFFIContexts, ptr FFIContext[TestLib]]
for i in 0 ..< MaxFFIContexts:
ctxs[i] = retryPool.createFFIContext().valueOr:
assert false, "createFFIContext(pool) failed at slot " & $i & ": " & $error
return
# No slot free: the create fails and must reset the state, not latch it.
check retryPool.staticFFIContext().isErr()
discard retryPool.destroyFFIContext(ctxs[0])
check retryPool.staticFFIContext().isOk()
for i in 1 ..< MaxFFIContexts:
discard retryPool.destroyFFIContext(ctxs[i])
test "requests are processed via pool context":
var pool: FFIContextPool[TestLib]
var d: CallbackData

View File

@ -0,0 +1,52 @@
## Asserts `{.ffiStatic.}` rejects what cannot cross a context-independent proc:
## an {.ffiHandle.} parameter or return (both are resolved against the context
## that owns them), and an `abi = c` scalar return (no reply shape without the
## ctx-bound fast path).
##
## Each fixture compiles in a child `nim check` so its expected failure is a test
## assertion, not this file's own compile error.
import std/[os, osproc, strutils, compilesettings]
import unittest2
const
fixtureDir = currentSourcePath().parentDir() / "fixtures"
nimExe = getCurrentCompilerExe()
ffiSearchPaths = querySettingSeq(searchPaths)
proc checkFixture(name: string): tuple[output: string, exitCode: int] =
let cacheDir = getTempDir() / "ffi_ffistatic_reject_cache" / name
var cmd = quoteShell(nimExe) & " check --hints:off --warnings:off"
for p in ffiSearchPaths:
cmd.add(" --path:" & quoteShell(p))
cmd.add(" --nimcache:" & quoteShell(cacheDir))
cmd.add(" " & quoteShell(fixtureDir / (name & "_fixture.nim")))
execCmdEx(cmd)
suite "{.ffiStatic.} rejects handles at macro time":
test "an {.ffiHandle.} parameter fails the build, naming the proc and the fix":
let (output, code) = checkFixture("ffistatic_handle_param")
check code != 0
check output.contains("staticrejBad")
check output.contains("Session")
check output.contains("`{.ffi.}` method instead")
test "an {.ffiHandle.} return fails the build, naming the proc and the fix":
let (output, code) = checkFixture("ffistatic_handle_return")
check code != 0
check output.contains("staticrejBad")
check output.contains("Session")
check output.contains("`{.ffi.}` method instead")
test "the same shapes without handles compile":
let (output, code) = checkFixture("ffistatic_ok")
check code == 0
check not output.contains("Error")
suite "{.ffiStatic.} rejects an abi = c scalar return":
test "the error names the proc and the wired reply shapes, not int_CWire":
let (output, code) = checkFixture("ffistatic_abi_c_scalar")
check code != 0
check output.contains("staticscalar_add")
check output.contains("unsupported response type")
check not output.contains("int_CWire")