mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-07-27 01:53:26 +00:00
feat(ffi): {.ffiStatic.} for context-independent procs (#138)
This commit is contained in:
parent
20fe91ae45
commit
41bb2b36a7
17
CHANGELOG.md
17
CHANGELOG.md
@ -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
|
||||
|
||||
41
README.md
41
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,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
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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_;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.} =
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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");
|
||||
|
||||
22
tests/unit/fixtures/ffistatic_abi_c_scalar_fixture.nim
Normal file
22
tests/unit/fixtures/ffistatic_abi_c_scalar_fixture.nim
Normal 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()
|
||||
16
tests/unit/fixtures/ffistatic_handle_param_fixture.nim
Normal file
16
tests/unit/fixtures/ffistatic_handle_param_fixture.nim
Normal 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()
|
||||
16
tests/unit/fixtures/ffistatic_handle_return_fixture.nim
Normal file
16
tests/unit/fixtures/ffistatic_handle_return_fixture.nim
Normal 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()
|
||||
20
tests/unit/fixtures/ffistatic_ok_fixture.nim
Normal file
20
tests/unit/fixtures/ffistatic_ok_fixture.nim
Normal 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()
|
||||
@ -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(
|
||||
|
||||
@ -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 = @[
|
||||
|
||||
@ -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]
|
||||
|
||||
48
tests/unit/test_ffistatic_reject.nim
Normal file
48
tests/unit/test_ffistatic_reject.nim
Normal 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")
|
||||
Loading…
x
Reference in New Issue
Block a user