feat(ffi): {.ffiStatic.} for context-independent procs (#138)

This commit is contained in:
Gabriel Cruz 2026-07-24 12:35:11 -03:00 committed by GitHub
parent 20fe91ae45
commit 41bb2b36a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1205 additions and 200 deletions

View File

@ -46,6 +46,19 @@ All notable changes to this project are documented in this file.
where `-install_name` requires `-dynamiclib`.
### Added
- **`{.ffiStatic.}`**: exports a context-independent proc — no library param, and
no `ctx` in its wrapper, 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>(...)`, C++ and Rust 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 held for the rest of the
process, so that call starts a thread pair nothing tears down —
`destroyFFIContext` refuses it; `destroyStaticFFIContext` is the Nim-side
teardown for process shutdown and tests, with no foreign equivalent. 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 +82,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,44 @@ 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, and it holds
one of the pool's slots. It has no `myLib`, which is why a static proc cannot
take the library value.
There is no foreign teardown for it. From Nim, `destroyStaticFFIContext(pool)`
stops the thread pair and frees the slot; it is only sound once nothing will call
a `{.ffiStatic.}` proc again, so it is meant for process shutdown and tests.
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")
# No `Echo` param, so the 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 + wrapper: `<lib>_ctx_<name>`, or `<lib>_static_<name>` for a
## static; `<lib>_<name>` itself is the raw symbol the dylib exports.
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,8 +809,8 @@ proc monomorphiseAll(
discard ensureCType(reg, n)
reqTypes.add(n)
var respTypes: seq[string] = @[]
for m in methods:
respTypes.add(cReturnType(reg, m))
for p in replyProcs:
respTypes.add(cReturnType(reg, p))
for ev in events:
discard ensureCType(reg, ev.payloadTypeName)
return (reqTypes, respTypes)
@ -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,23 @@ proc emitAbiCtxAndCtor(
lines.add("}")
lines.add("")
proc emitAbiMethod(
proc emitAbiProcWrapper(
lines: var seq[string],
reg: var AbiReg,
ctxType, libName, libType: string,
m: FFIProcMeta,
) =
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 +1351,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 +1526,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 +1534,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

@ -1,6 +1,6 @@
## Helpers shared by the C/C++ binding generators (cpp.nim, c.nim).
import std/[strutils, options]
import std/strutils
import ./meta, ./string_helpers
proc stripLibPrefix*(procName, libName: string): string =
@ -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("")
# `this->methodName(...)` so a same-named param can't shadow the call target.
# A method calls `this->methodName(...)` so a same-named param can't shadow
# the call target; a static has no `this` and forwards its own `timeout`.
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,42 @@ var ffiEnumTypeNames* {.compileTime.}: seq[string]
proc isFFIEnumTypeName*(name: string): bool {.compileTime.} =
name in ffiEnumTypeNames
func isStatic*(p: FFIProcMeta): bool =
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,9 @@ 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:
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 +304,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 +691,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 +709,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 +726,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 +751,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

@ -83,11 +83,6 @@ proc deinitContextResources*[T](ctx: ptr FFIContext[T]): Result[void, string] =
closeAndNil(ctx.eventThreadExitSignal)
ok()
proc cleanUpResources[T](ctx: ptr FFIContext[T]): Result[void, string] =
defer:
freeShared(ctx)
ctx.deinitContextResources()
template newSignalOrErr(field: untyped, name: string) =
field = ThreadSignalPtr.new().valueOr:
return err("couldn't create ThreadSignalPtr: " & name & ": " & $error)
@ -112,7 +107,8 @@ proc initContextResources*[T](ctx: ptr FFIContext[T]): Result[void, string] =
var success = false
defer:
if not success:
ctx.cleanUpResources().isOkOr:
# `ctx` is a pool slot the caller owns; close what was opened, never free it.
ctx.deinitContextResources().isOkOr:
error "failed to clean up resources after createFFIContext failure",
error = error
@ -186,10 +182,3 @@ proc stopAndJoinThreads*[T](ctx: ptr FFIContext[T]): Result[void, string] =
?ctx.eventThreadExitSignal.waitExitOrErr("event thread", ThreadExitTimeout)
joinThread(ctx.eventThread)
ok()
proc clearContext[T](ctx: ptr FFIContext[T]): Result[void, string] =
ctx.stopAndJoinThreads().isOkOr:
return err("clearContext: " & $error)
ctx.cleanUpResources().isOkOr:
return err("cleanUpResources failed: " & $error)
ok()

View File

@ -1,13 +1,26 @@
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
StaticCtxDestroying
StaticCtxReady
FFIContextPool*[T] = object
## Fixed pool of FFI contexts, plus the one `{.ffiStatic.}` context.
# Each live context holds 5 ThreadSignalPtrs — one fd each on Linux, two (a
# socketpair) elsewhere. Under refc a destroyed context cannot close them
# (see `deinitContextResources`), so churn leaks fds unbounded.
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:
@ -32,10 +45,19 @@ proc createFFIContext*[T](
return err("createFFIContext: initContextResources failed: " & $error)
ok(ctx)
proc isStaticCtx[T](pool: var FFIContextPool[T], ctx: ptr FFIContext[T]): bool =
## True while `ctx` is the pool's static context, including mid-teardown.
# `staticCtx` is cleared only once the slot is released, so matching on the
# pointer covers `Destroying` too.
pool.staticCtx.load() == cast[pointer](ctx)
proc destroyFFIContext*[T](
pool: var FFIContextPool[T], ctx: ptr FFIContext[T]
): Result[void, string] =
## On thread-exit timeout the slot is leaked; closing live-thread resources is unsafe.
# Destroying it would release the slot while `staticState` still points at it.
if pool.isStaticCtx(ctx):
return err("destroyFFIContext(pool): the {.ffiStatic.} context outlives every ctx")
ctx.stopAndJoinThreads().isOkOr:
return err("destroyFFIContext(pool): " & $error)
# Required: next acquisition would otherwise re-init a live lock (UB).
@ -45,6 +67,52 @@ 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, created on first use: a static proc has
## no ctx of its own, but its handler still needs an FFI thread.
# Holds its slot until `destroyStaticFFIContext`, so `pool` must outlive its
# threads: only call this on the global `declareLibrary` emits. `myLib` stays
# the zero value. A failed create resets to `StaticCtxNone` so waiters retry.
while true:
case pool.staticState.load()
of StaticCtxReady:
return ok(cast[ptr FFIContext[T]](pool.staticCtx.load()))
of StaticCtxCreating, StaticCtxDestroying:
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 destroyStaticFFIContext*[T](pool: var FFIContextPool[T]): Result[void, string] =
## Teardown counterpart to `staticFFIContext`: stops the static context's
## threads and frees its slot. A no-op when there is no static context.
# Claiming `Ready -> Destroying` serialises concurrent teardowns; it does not
# make teardown safe against a static call already in flight.
var expected = StaticCtxReady
if not pool.staticState.compareExchange(expected, StaticCtxDestroying):
return ok()
let ctx = cast[ptr FFIContext[T]](pool.staticCtx.load())
ctx.stopAndJoinThreads().isOkOr:
# Threads are still live: leak the slot rather than free resources under them.
pool.staticState.store(StaticCtxReady)
return err("destroyStaticFFIContext: " & $error)
let deinitRes = ctx.deinitContextResources()
pool.releaseSlot(ctx)
pool.staticCtx.store(nil)
pool.staticState.store(StaticCtxNone)
deinitRes.isOkOr:
return err("destroyStaticFFIContext: " & $error)
ok()
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(
@ -703,13 +706,48 @@ proc stringTrampBody(boxName: NimNode): NimNode =
except CatchableError as e:
box.fn(RET_ERR, "".cstring, e.msg.cstring, box.ud)
proc exportedMethodProc(
spec: CAbiSpec, boxName, envWire, trampName, poolIdent, cbType: NimNode
proc ctxBindingGuard(
poolIdent, emptyReply, ctxIdent: NimNode, isStatic: bool
): NimNode {.compileTime.} =
## Prologue that binds `ctxIdent`: a method validates the ctx it was handed, a
## static resolves the library's shared one.
if not isStatic:
return quote:
if onReply.isNil():
return RET_MISSING_CALLBACK
if not `poolIdent`.isValidCtx(cast[pointer](`ctxIdent`)):
onReply(
RET_ERR, `emptyReply`, "ctx is not a valid FFI context".cstring, userData
)
return RET_ERR
let guard = quote:
if onReply.isNil():
return RET_MISSING_CALLBACK
let `ctxIdent` = `poolIdent`.staticFFIContext().valueOr:
let errStr = "ffiStatic: " & error
onReply(RET_ERR, `emptyReply`, errStr.cstring, userData)
return RET_ERR
# A static call may be the host's first entry into the library. Raw AST, not
# `quote`: `when declared` over an undeclared symbol inside `quote` ICEs.
guard.insert(
0,
nnkWhenStmt.newTree(
nnkElifBranch.newTree(
newCall(ident("declared"), ident("initializeLibrary")),
newStmtList(newCall(ident("initializeLibrary"))),
)
),
)
guard
proc exportedProc(
spec: CAbiSpec,
boxName, envWire, trampName, poolIdent, cbType: NimNode,
isStatic: bool,
): NimNode =
# No `foreignThreadGc`: `cwireUnpack`/`cwirePack` alloc on the calling thread (already GC-registered); wrapping would free its live ORC heap.
let envName = spec.envelope
let libFFICtx =
nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), spec.libType))
let ctxIdent = ident("ctx")
# String reply: empty non-nil cstring on error; object reply: nil ptr gated by err_code.
let emptyReply =
if isStringType(spec.respType):
@ -717,11 +755,6 @@ proc exportedMethodProc(
else:
newNilLit()
let body = quote:
if onReply.isNil():
return RET_MISSING_CALLBACK
if not `poolIdent`.isValidCtx(cast[pointer](ctx)):
onReply(RET_ERR, `emptyReply`, "ctx is not a valid FFI context".cstring, userData)
return RET_ERR
var ownedWire: `envWire`
cwirePack(ownedWire, cwireUnpack(req[]))
let ownedCopy = cwireOwnedCopy(ownedWire)
@ -739,7 +772,7 @@ proc exportedMethodProc(
)
let sendRes =
try:
ffi_context.sendRequestToFFIThread(ctx, reqPtr)
ffi_context.sendRequestToFFIThread(`ctxIdent`, reqPtr)
except Exception as e:
Result[void, string].err("sendRequestToFFIThread exception: " & e.msg)
if sendRes.isErr():
@ -750,16 +783,26 @@ proc exportedMethodProc(
onReply(RET_ERR, `emptyReply`, sendRes.error.cstring, userData)
return RET_ERR
return RET_OK
let fullBody = ctxBindingGuard(poolIdent, emptyReply, ctxIdent, isStatic)
for stmt in body:
fullBody.add(stmt)
var params = @[
ident("cint"),
newIdentDefs(ident("onReply"), cbType),
newIdentDefs(ident("userData"), ident("pointer")),
newIdentDefs(ident("req"), nnkPtrTy.newTree(envWire)),
]
if not isStatic:
let libFFICtx =
nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), spec.libType))
params.insert(newIdentDefs(ctxIdent, libFFICtx), 1)
newProc(
name = ident($envName & "CAbiExport"),
params = @[
ident("cint"),
newIdentDefs(ident("ctx"), libFFICtx),
newIdentDefs(ident("onReply"), cbType),
newIdentDefs(ident("userData"), ident("pointer")),
newIdentDefs(ident("req"), nnkPtrTy.newTree(envWire)),
],
body = body,
params = params,
body = fullBody,
pragmas = nnkPragma.newTree(
ident("dynlib"),
nnkExprColonExpr.newTree(ident("exportc"), newStrLitNode(spec.exportName)),
@ -874,26 +917,30 @@ 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 isStatic = spec.kind == cakStatic
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)
exportedProc(spec, boxName, envWire, trampName, poolIdent, cbType, isStatic)
)
elif rt.kind == nnkIdent:
# `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)
exportedProc(spec, boxName, envWire, trampName, poolIdent, cbType, isStatic)
)
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,37 @@ 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:
let why =
if isStatic: " takes no library param" else: " has an {.ffiHandle.} receiver"
error(
"`.ffi.` proc " & $procName & " has an {.ffiHandle.} receiver but no " &
"library is declared; call declareLibrary(name, LibType) first"
where & " proc " & $procName & why & " but no library is declared; " &
"call declareLibrary(name, LibType) first"
)
# A handle receiver carries no library type, so fall back to the declared one.
# Neither carries a library type, so fall back to the declared one.
let libTypeName =
if firstIsHandle:
if firstIsHandle or isStatic:
ident(currentLibType)
else:
recvType
@ -852,31 +847,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 +934,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 +961,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 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 +1009,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 +1038,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 +1063,17 @@ 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: the guard's `let ctx` must be a sibling of the send 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 +1094,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 +1131,38 @@ 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 `{.ffi.}`: no library receiver, and no `ctx` in the C
## wrapper, so a host calls it without constructing the library.
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,35 @@ static void test_version(EchoCtx* ctx) {
assert(strcmp(w.text_a, "nim-echo v0.1.0") == 0);
}
/* No EchoCtx: this call creates the library's static context. 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");
}
// No EchoCtx is constructed anywhere in this test.
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,22 @@
## Must fail: no `abi = c` reply shape for a static's scalar return, and the error
## must say so rather than die on an undeclared `int_CWire`.
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: the same shapes, with no handle crossing a static's boundary.
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,12 @@ registerReqFFI(HeavyRefAllocRequest, lib: ptr TestLib):
await sleepAsync(10.milliseconds)
return ok("heavy-done")
# Global, as declareLibrary emits it: a static ctx's threads may outlive any scope.
# One pool for every slot-accounting case below — under refc a destroyed context
# can't close its five ThreadSignalPtrs, so a second 32-slot fill would put the
# suite over the 1024-fd limit.
var staticPool: FFIContextPool[TestLib]
suite "FFIContextPool":
test "create and destroy via pool succeeds":
var pool: FFIContextPool[TestLib]
@ -129,18 +135,48 @@ suite "FFIContextPool":
check pool.destroyFFIContext(ctx2).isOk()
check ctx1 == ctx2
test "pool exhaustion returns error":
var pool: FFIContextPool[TestLib]
var ctxs: array[MaxFFIContexts, ptr FFIContext[TestLib]]
for i in 0 ..< MaxFFIContexts:
ctxs[i] = pool.createFFIContext().valueOr:
for j in 0 ..< i:
discard pool.destroyFFIContext(ctxs[j])
assert false, "createFFIContext(pool) failed at slot " & $i & ": " & $error
return
check pool.createFFIContext().isErr()
for i in 0 ..< MaxFFIContexts:
discard pool.destroyFFIContext(ctxs[i])
# Each static case tears its pool back down on every exit path: left running
# under refc the threads race later suites' allocation and GC (macOS SIGSEGV).
test "staticFFIContext returns one shared context and refuses destruction":
defer:
check staticPool.destroyStaticFFIContext().isOk()
let first = staticPool.staticFFIContext().valueOr:
assert false, "staticFFIContext failed: " & $error
return
check staticPool.staticFFIContext().tryGet() == first
# Occupies a pool slot like any other context.
check staticPool.isValidCtx(first)
check staticPool.destroyFFIContext(first).isErr()
# Still live, and still the same context.
check staticPool.staticFFIContext().tryGet() == first
test "pool exhaustion errors and leaves staticFFIContext retryable":
var filler: seq[ptr FFIContext[TestLib]]
defer:
check staticPool.destroyStaticFFIContext().isOk()
for c in filler:
check staticPool.destroyFFIContext(c).isOk()
check staticPool.staticFFIContext().isOk()
var c = staticPool.createFFIContext()
while c.isOk():
filler.add(c.tryGet())
c = staticPool.createFFIContext()
# The static ctx holds a slot, so only MaxFFIContexts-1 were left.
check filler.len == MaxFFIContexts - 1
check staticPool.createFFIContext().isErr()
# Hand the static ctx's slot straight to a plain one, so the retry below has
# to fail on a genuinely full pool.
check staticPool.destroyStaticFFIContext().isOk()
let reclaimed = staticPool.createFFIContext().valueOr:
assert false, "createFFIContext(pool) failed on the freed static slot: " & $error
return
filler.add(reclaimed)
# No slot free: the create fails and must reset the state, not latch it.
check staticPool.staticFFIContext().isErr()
check staticPool.destroyFFIContext(filler.pop()).isOk()
check staticPool.staticFFIContext().isOk()
test "requests are processed via pool context":
var pool: FFIContextPool[TestLib]

View File

@ -0,0 +1,48 @@
## Asserts `{.ffiStatic.}` rejects an {.ffiHandle.} param/return and an `abi = c`
## scalar return. Each fixture compiles in a child `nim check` so its expected
## failure is an assertion rather than 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")