diff --git a/CHANGELOG.md b/CHANGELOG.md index a4f9c10..927758a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 + `_static_(...)`, 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 diff --git a/README.md b/README.md index a704df1..5e845bb 100644 --- a/README.md +++ b/README.md @@ -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 `_static_`, not `_`, for the same reason a +method's is `_ctx_`: `_` 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 diff --git a/examples/echo/c_abi_bindings/echo.h b/examples/echo/c_abi_bindings/echo.h index 27a821c..0365f73 100644 --- a/examples/echo/c_abi_bindings/echo.h +++ b/examples/echo/c_abi_bindings/echo.h @@ -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 */ diff --git a/examples/echo/c_bindings/echo.h b/examples/echo/c_bindings/echo.h index 4fa7f11..6ff9410 100644 --- a/examples/echo/c_bindings/echo.h +++ b/examples/echo/c_bindings/echo.h @@ -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 */ diff --git a/examples/echo/cpp_bindings/echo.hpp b/examples/echo/cpp_bindings/echo.hpp index db98b76..b4acea8 100644 --- a/examples/echo/cpp_bindings/echo.hpp +++ b/examples/echo/cpp_bindings/echo.hpp @@ -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 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::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::err(ffi_raw_.error()); + return decodeCborFFI(ffi_raw_.value()); + } + + static std::future> lib_versionAsync(std::chrono::milliseconds timeout = std::chrono::seconds{30}) { + return std::async(std::launch::async, [timeout]() { return lib_version(timeout); }); + } + + static Result 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::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::err(ffi_raw_.error()); + return decodeCborFFI(ffi_raw_.value()); + } + + static std::future> 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_; diff --git a/examples/echo/echo.nim b/examples/echo/echo.nim index 991e370..b5746d8 100644 --- a/examples/echo/echo.nim +++ b/examples/echo/echo.nim @@ -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 diff --git a/examples/timer/c_bindings/my_timer.h b/examples/timer/c_bindings/my_timer.h index 470d84c..45e453a 100644 --- a/examples/timer/c_bindings/my_timer.h +++ b/examples/timer/c_bindings/my_timer.h @@ -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 */ diff --git a/examples/timer/cddl_bindings/my_timer.cddl b/examples/timer/cddl_bindings/my_timer.cddl index 7adbd70..516384c 100644 --- a/examples/timer/cddl_bindings/my_timer.cddl +++ b/examples/timer/cddl_bindings/my_timer.cddl @@ -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 diff --git a/examples/timer/cpp_bindings/my_timer.hpp b/examples/timer/cpp_bindings/my_timer.hpp index ded2cbf..57b71b4 100644 --- a/examples/timer/cpp_bindings/my_timer.hpp +++ b/examples/timer/cpp_bindings/my_timer.hpp @@ -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 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::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::err(ffi_raw_.error()); + return decodeCborFFI(ffi_raw_.value()); + } + + static std::future> 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; diff --git a/examples/timer/rust_bindings/examples/main.rs b/examples/timer/rust_bindings/examples/main.rs index 132b809..f52e1e9 100644 --- a/examples/timer/rust_bindings/examples/main.rs +++ b/examples/timer/rust_bindings/examples/main.rs @@ -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), diff --git a/examples/timer/rust_bindings/src/api.rs b/examples/timer/rust_bindings/src/api.rs index fb05b7c..cf02916 100644 --- a/examples/timer/rust_bindings/src/api.rs +++ b/examples/timer/rust_bindings/src/api.rs @@ -305,4 +305,22 @@ impl MyTimerCtx { decode_cbor::(&raw_bytes) } + pub fn lib_version(timeout: Duration) -> Result { + 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::(&raw_bytes) + } + + pub async fn lib_version_async(timeout: Duration) -> Result { + 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::(&raw_bytes) + } + } diff --git a/examples/timer/rust_bindings/src/ffi.rs b/examples/timer/rust_bindings/src/ffi.rs index 3a6cf73..188ce74 100644 --- a/examples/timer/rust_bindings/src/ffi.rs +++ b/examples/timer/rust_bindings/src/ffi.rs @@ -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; diff --git a/examples/timer/rust_bindings/src/types.rs b/examples/timer/rust_bindings/src/types.rs index 8d5c9b7..21c2a5a 100644 --- a/examples/timer/rust_bindings/src/types.rs +++ b/examples/timer/rust_bindings/src/types.rs @@ -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, diff --git a/examples/timer/timer.nim b/examples/timer/timer.nim index fb83133..c405952 100644 --- a/examples/timer/timer.nim +++ b/examples/timer/timer.nim @@ -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.} = diff --git a/ffi/codegen/c.nim b/ffi/codegen/c.nim index e4a2b5b..fd26a92 100644 --- a/ffi/codegen/c.nim +++ b/ffi/codegen/c.nim @@ -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: `_ctx_` for a method, + ## `_static_` for a static. `_` 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 `.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 `_static_`. + 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" diff --git a/ffi/codegen/c_cpp_common.nim b/ffi/codegen/c_cpp_common.nim index b23484c..6d734ba 100644 --- a/ffi/codegen/c_cpp_common.nim +++ b/ffi/codegen/c_cpp_common.nim @@ -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: diff --git a/ffi/codegen/cddl.nim b/ffi/codegen/cddl.nim index 62cadd7..c05f870 100644 --- a/ffi/codegen/cddl.nim +++ b/ffi/codegen/cddl.nim @@ -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: diff --git a/ffi/codegen/cpp.nim b/ffi/codegen/cpp.nim index 2b52a31..fd8806d 100644 --- a/ffi/codegen/cpp.nim +++ b/ffi/codegen/cpp.nim @@ -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:") diff --git a/ffi/codegen/meta.nim b/ffi/codegen/meta.nim index 7886d9a..b6af5da 100644 --- a/ffi/codegen/meta.nim +++ b/ffi/codegen/meta.nim @@ -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 diff --git a/ffi/codegen/rust.nim b/ffi/codegen/rust.nim index 4efe46e..8c7ee42 100644 --- a/ffi/codegen/rust.nim +++ b/ffi/codegen/rust.nim @@ -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]) diff --git a/ffi/ffi_context_pool.nim b/ffi/ffi_context_pool.nim index ac8ab0c..089aede 100644 --- a/ffi/ffi_context_pool.nim +++ b/ffi/ffi_context_pool.nim @@ -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(): diff --git a/ffi/internal/c_macro_helpers.nim b/ffi/internal/c_macro_helpers.nim index 59ed6e6..16fa5e1 100644 --- a/ffi/internal/c_macro_helpers.nim +++ b/ffi/internal/c_macro_helpers.nim @@ -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 diff --git a/ffi/internal/ffi_macro.nim b/ffi/internal/ffi_macro.nim index e46fda2..075408f 100644 --- a/ffi/internal/ffi_macro.nim +++ b/ffi/internal/ffi_macro.nim @@ -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 = diff --git a/tests/e2e/c_abi/test_echo_c_abi.c b/tests/e2e/c_abi/test_echo_c_abi.c index e153b09..3747a8c 100644 --- a/tests/e2e/c_abi/test_echo_c_abi.c +++ b/tests/e2e/c_abi/test_echo_c_abi.c @@ -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; } diff --git a/tests/e2e/cpp/test_timer_e2e.cpp b/tests/e2e/cpp/test_timer_e2e.cpp index 469de9c..840afe3 100644 --- a/tests/e2e/cpp/test_timer_e2e.cpp +++ b/tests/e2e/cpp/test_timer_e2e.cpp @@ -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>> 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"); diff --git a/tests/unit/fixtures/ffistatic_abi_c_scalar_fixture.nim b/tests/unit/fixtures/ffistatic_abi_c_scalar_fixture.nim new file mode 100644 index 0000000..14c7dbb --- /dev/null +++ b/tests/unit/fixtures/ffistatic_abi_c_scalar_fixture.nim @@ -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() diff --git a/tests/unit/fixtures/ffistatic_handle_param_fixture.nim b/tests/unit/fixtures/ffistatic_handle_param_fixture.nim new file mode 100644 index 0000000..577bc73 --- /dev/null +++ b/tests/unit/fixtures/ffistatic_handle_param_fixture.nim @@ -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() diff --git a/tests/unit/fixtures/ffistatic_handle_return_fixture.nim b/tests/unit/fixtures/ffistatic_handle_return_fixture.nim new file mode 100644 index 0000000..c6c795c --- /dev/null +++ b/tests/unit/fixtures/ffistatic_handle_return_fixture.nim @@ -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() diff --git a/tests/unit/fixtures/ffistatic_ok_fixture.nim b/tests/unit/fixtures/ffistatic_ok_fixture.nim new file mode 100644 index 0000000..5146af4 --- /dev/null +++ b/tests/unit/fixtures/ffistatic_ok_fixture.nim @@ -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() diff --git a/tests/unit/test_c_abi_codegen.nim b/tests/unit/test_c_abi_codegen.nim index 8df85d0..a83faf7 100644 --- a/tests/unit/test_c_abi_codegen.nim +++ b/tests/unit/test_c_abi_codegen.nim @@ -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( diff --git a/tests/unit/test_c_codegen.nim b/tests/unit/test_c_codegen.nim index 972a8f5..c8504ae 100644 --- a/tests/unit/test_c_codegen.nim +++ b/tests/unit/test_c_codegen.nim @@ -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 = @[ diff --git a/tests/unit/test_ffi_context.nim b/tests/unit/test_ffi_context.nim index 247142e..5c3e8b9 100644 --- a/tests/unit/test_ffi_context.nim +++ b/tests/unit/test_ffi_context.nim @@ -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 diff --git a/tests/unit/test_ffistatic_reject.nim b/tests/unit/test_ffistatic_reject.nim new file mode 100644 index 0000000..c41c79d --- /dev/null +++ b/tests/unit/test_ffistatic_reject.nim @@ -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")