mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-07-27 01:53:26 +00:00
feat: propagate comments (#136)
This commit is contained in:
parent
98fb7da9e2
commit
c58e2bcdd2
@ -31,6 +31,14 @@ All notable changes to this project are documented in this file.
|
||||
where `-install_name` requires `-dynamiclib`.
|
||||
|
||||
### Added
|
||||
- Doc comments (`##`) on `{.ffi.}` / `{.ffiCtor.}` / `{.ffiDtor.}` procs are now
|
||||
propagated to the generated bindings — `/** ... */` on the C declarations,
|
||||
`///` on the C++ class methods and Rust `pub fn`s, and `;` comments in the
|
||||
CDDL schema — so the exported API is documented once, in the Nim source
|
||||
([#127](https://github.com/logos-messaging/nim-ffi/issues/127)). Editing a
|
||||
`##` comment now changes the generated bindings, so `nimble check_bindings`
|
||||
flags them stale until regenerated; an undocumented proc still generates
|
||||
byte-identical output.
|
||||
- `{.ffiEvent.}` no longer requires an explicit wire-name string: when omitted
|
||||
it is derived from the proc name via `camelToSnakeCase`
|
||||
(`onPeerConnected` → `on_peer_connected`), matching how `{.ffi.}` derives its
|
||||
|
||||
22
README.md
22
README.md
@ -84,6 +84,28 @@ The generated C export names are the snake_case form of the proc names, e.g.
|
||||
| `{.ffiHandle.}` | `ref object` | Marks a type as an opaque handle: it stays server-side and crosses the wire as a `uint64` id. |
|
||||
| `genBindings()` | call | Emits the bindings. Must be the **last** FFI call in the compilation root. |
|
||||
|
||||
### Doc comments
|
||||
|
||||
A `##` doc comment on an annotated proc is carried through to every generated
|
||||
binding, so the exported API is documented once, at the source:
|
||||
|
||||
```nim
|
||||
proc myTimerEcho*(
|
||||
timer: MyTimer, req: EchoRequest
|
||||
): Future[Result[EchoResponse, string]] {.ffi.} =
|
||||
## Sleeps `delayMs` then echoes the message back.
|
||||
...
|
||||
```
|
||||
|
||||
becomes ``/** Sleeps `delayMs` then echoes the message back. */`` above both the
|
||||
exported symbol and the `<lib>_ctx_echo` wrapper in the C header, `/// ...` on
|
||||
the C++ class method and the Rust `pub fn`, and a `;` comment in the CDDL
|
||||
schema. Multi-line doc comments are preserved as-is.
|
||||
|
||||
Only `##` doc comments are propagated, and only on procs — a plain `#` comment
|
||||
above the proc, and any comment on a `{.ffi.}` type or its fields, is dropped by
|
||||
Nim's parser before the macro can see it.
|
||||
|
||||
### The return-type contract
|
||||
|
||||
Every `{.ffi.}` / `{.ffiCtor.}` proc must have an explicit
|
||||
|
||||
@ -58,9 +58,13 @@ static inline char* nimffi_abi_dup_cstr_n(const char* s, size_t n) {
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Creates an echo context that prefixes every reply with `config.prefix`. */
|
||||
void* echo_create(const EchoCreateCtorReq* req, EchoCreateRawFn on_created, void* user_data);
|
||||
/** Upper-cases `req.text` and returns it behind the context's prefix. */
|
||||
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);
|
||||
/** Releases the echo context. */
|
||||
int echo_destroy(void* ctx);
|
||||
|
||||
#ifdef __cplusplus
|
||||
@ -103,6 +107,7 @@ static void echo_create_trampoline(int ret, const char* ctx_addr, const char* er
|
||||
free(box);
|
||||
}
|
||||
|
||||
/** Creates an echo context that prefixes every reply with `config.prefix`. */
|
||||
static inline int echo_ctx_create(const EchoConfig* config, EchoCreateFn on_created, void* user_data) {
|
||||
EchoCreateCtorReq ffi_req;
|
||||
memset(&ffi_req, 0, sizeof(ffi_req));
|
||||
@ -118,6 +123,7 @@ static inline int echo_ctx_create(const EchoConfig* config, EchoCreateFn on_crea
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Releases the echo context. */
|
||||
static inline int echo_ctx_destroy(EchoCtx* ctx) {
|
||||
if (!ctx) return NIMFFI_RET_OK;
|
||||
int rc = NIMFFI_RET_OK;
|
||||
@ -126,6 +132,7 @@ static inline int echo_ctx_destroy(EchoCtx* ctx) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
/** Upper-cases `req.text` and returns it behind the context's prefix. */
|
||||
static inline int echo_ctx_shout(const EchoCtx* ctx, const ShoutRequest* req, EchoShoutReplyFn on_reply, void* user_data) {
|
||||
EchoShoutReq ffi_req;
|
||||
memset(&ffi_req, 0, sizeof(ffi_req));
|
||||
@ -156,6 +163,7 @@ static void echo_version_scalar_reply(int caller_ret, char* msg, size_t len, voi
|
||||
free(reply);
|
||||
}
|
||||
|
||||
/** Returns the library's version string. */
|
||||
static inline int echo_ctx_version(const EchoCtx* ctx, EchoVersionReplyFn on_reply, void* user_data) {
|
||||
EchoVersionScalarBox* box = (EchoVersionScalarBox*)malloc(sizeof(EchoVersionScalarBox));
|
||||
if (!box) {
|
||||
|
||||
@ -193,9 +193,13 @@ static inline CborError echo_dec_EchoVersionReq(
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Creates an echo context that prefixes every reply with `config.prefix`. */
|
||||
void* echo_create(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback callback, void* user_data);
|
||||
/** Upper-cases `req.text` and returns it behind the context's prefix. */
|
||||
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);
|
||||
/** 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);
|
||||
int echo_remove_event_listener(void* ctx, uint64_t listener_id);
|
||||
@ -264,6 +268,7 @@ static void echo_create_trampoline(int ret, const char* msg, size_t len, void* u
|
||||
free(box);
|
||||
}
|
||||
|
||||
/** Creates an echo context that prefixes every reply with `config.prefix`. */
|
||||
static inline int echo_ctx_create(const EchoConfig* config, EchoCreateFn on_created, void* user_data) {
|
||||
EchoCreateCtorReq ffi_req;
|
||||
memset(&ffi_req, 0, sizeof(ffi_req));
|
||||
@ -289,6 +294,7 @@ static inline int echo_ctx_create(const EchoConfig* config, EchoCreateFn on_crea
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Releases the echo context. */
|
||||
static inline int echo_ctx_destroy(EchoCtx* ctx) {
|
||||
if (!ctx) return NIMFFI_RET_OK;
|
||||
int rc = NIMFFI_RET_OK;
|
||||
@ -329,6 +335,7 @@ static void echo_shout_reply_trampoline(int ret, const char* msg, size_t len, vo
|
||||
echo_free_ShoutResponse(&out);
|
||||
free(box);
|
||||
}
|
||||
/** Upper-cases `req.text` and returns it behind the context's prefix. */
|
||||
static inline int echo_ctx_shout(const EchoCtx* ctx, const ShoutRequest* req, EchoShoutReplyFn on_reply, void* user_data) {
|
||||
EchoShoutReq ffi_req;
|
||||
memset(&ffi_req, 0, sizeof(ffi_req));
|
||||
@ -391,6 +398,7 @@ static void echo_version_reply_trampoline(int ret, const char* msg, size_t len,
|
||||
nimffi_free_str(&out);
|
||||
free(box);
|
||||
}
|
||||
/** Returns the library's version string. */
|
||||
static inline int echo_ctx_version(const EchoCtx* ctx, EchoVersionReplyFn on_reply, void* user_data) {
|
||||
EchoVersionReq ffi_req;
|
||||
memset(&ffi_req, 0, sizeof(ffi_req));
|
||||
|
||||
@ -431,9 +431,13 @@ inline CborError decode_cbor(CborValue& it, EchoVersionReq&) {
|
||||
extern "C" {
|
||||
typedef void (*FFICallback)(int ret, const char* msg, size_t len, void* user_data);
|
||||
|
||||
/** Creates an echo context that prefixes every reply with `config.prefix`. */
|
||||
void* echo_create(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback callback, void* user_data);
|
||||
/** Upper-cases `req.text` and returns it behind the context's prefix. */
|
||||
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);
|
||||
/** 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);
|
||||
int echo_remove_event_listener(void* ctx, uint64_t listener_id);
|
||||
@ -512,6 +516,7 @@ inline Result<std::vector<std::uint8_t>> ffi_call_(
|
||||
|
||||
class EchoCtx {
|
||||
public:
|
||||
/// Creates an echo context that prefixes every reply with `config.prefix`.
|
||||
static Result<std::unique_ptr<EchoCtx>> create(const EchoConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
|
||||
const auto ffi_req_ = EchoCreateCtorReq{config};
|
||||
auto ffi_enc_ = encodeCborFFI(ffi_req_);
|
||||
@ -535,6 +540,7 @@ public:
|
||||
return Result<std::unique_ptr<EchoCtx>>::ok(std::unique_ptr<EchoCtx>(new EchoCtx(reinterpret_cast<void*>(static_cast<uintptr_t>(addr)), timeout)));
|
||||
}
|
||||
|
||||
/// Creates an echo context that prefixes every reply with `config.prefix`.
|
||||
static std::future<Result<std::unique_ptr<EchoCtx>>> createAsync(const EchoConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
|
||||
return std::async(std::launch::async, [config, timeout]() { return create(config, timeout); });
|
||||
}
|
||||
@ -560,6 +566,7 @@ public:
|
||||
EchoCtx(EchoCtx&&) = delete;
|
||||
EchoCtx& operator=(EchoCtx&&) = delete;
|
||||
|
||||
/// Upper-cases `req.text` and returns it behind the context's prefix.
|
||||
Result<ShoutResponse> shout(const ShoutRequest& req) const {
|
||||
const auto ffi_req_ = EchoShoutReq{req};
|
||||
auto ffi_enc_ = encodeCborFFI(ffi_req_);
|
||||
@ -572,10 +579,12 @@ public:
|
||||
return decodeCborFFI<ShoutResponse>(ffi_raw_.value());
|
||||
}
|
||||
|
||||
/// Upper-cases `req.text` and returns it behind the context's prefix.
|
||||
std::future<Result<ShoutResponse>> shoutAsync(const ShoutRequest& req) const {
|
||||
return std::async(std::launch::async, [this, req]() { return this->shout(req); });
|
||||
}
|
||||
|
||||
/// Returns the library's version string.
|
||||
Result<std::string> version() const {
|
||||
const auto ffi_req_ = EchoVersionReq{};
|
||||
auto ffi_enc_ = encodeCborFFI(ffi_req_);
|
||||
@ -588,6 +597,7 @@ public:
|
||||
return decodeCborFFI<std::string>(ffi_raw_.value());
|
||||
}
|
||||
|
||||
/// Returns the library's version string.
|
||||
std::future<Result<std::string>> versionAsync() const {
|
||||
return std::async(std::launch::async, [this]() { return this->version(); });
|
||||
}
|
||||
|
||||
@ -23,20 +23,24 @@ type ShoutResponse {.ffi.} = object
|
||||
prefix: string
|
||||
|
||||
proc echoCreate*(config: EchoConfig): Future[Result[Echo, string]] {.ffiCtor.} =
|
||||
## Creates an echo context that prefixes every reply with `config.prefix`.
|
||||
await sleepAsync(1.milliseconds)
|
||||
return ok(Echo(prefix: config.prefix))
|
||||
|
||||
proc echoShout*(
|
||||
e: Echo, req: ShoutRequest
|
||||
): Future[Result[ShoutResponse, string]] {.ffi.} =
|
||||
## Upper-cases `req.text` and returns it behind the context's prefix.
|
||||
await sleepAsync(1.milliseconds)
|
||||
let upper = req.text.toUpperAscii
|
||||
return ok(ShoutResponse(shouted: e.prefix & ": " & upper, prefix: e.prefix))
|
||||
|
||||
proc echoVersion*(e: Echo): Future[Result[string, string]] {.ffi.} =
|
||||
## Returns the library's version string.
|
||||
return ok("nim-echo v0.1.0")
|
||||
|
||||
proc echo_destroy*(e: Echo) {.ffiDtor.} =
|
||||
## Releases the echo context.
|
||||
discard
|
||||
|
||||
genBindings()
|
||||
|
||||
@ -766,11 +766,16 @@ static inline void my_timer_free_MyTimerScheduleReq(MyTimerScheduleReq* v) {
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Creates the FFIContext + MyTimer; async via chronos. */
|
||||
void* my_timer_create(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback callback, void* user_data);
|
||||
/** Sleeps `delayMs` then echoes the message back, firing `on_echo_fired`. */
|
||||
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_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);
|
||||
/** Tears down the FFI context; blocks until FFI + watchdog threads join. */
|
||||
int my_timer_destroy(void* ctx);
|
||||
uint64_t my_timer_add_event_listener(void* ctx, const char* event_name, FFICallback callback, void* user_data);
|
||||
int my_timer_remove_event_listener(void* ctx, uint64_t listener_id);
|
||||
@ -871,6 +876,7 @@ static void my_timer_create_trampoline(int ret, const char* msg, size_t len, voi
|
||||
free(box);
|
||||
}
|
||||
|
||||
/** Creates the FFIContext + MyTimer; async via chronos. */
|
||||
static inline int my_timer_ctx_create(const TimerConfig* config, MyTimerCreateFn on_created, void* user_data) {
|
||||
MyTimerCreateCtorReq ffi_req;
|
||||
memset(&ffi_req, 0, sizeof(ffi_req));
|
||||
@ -896,6 +902,7 @@ static inline int my_timer_ctx_create(const TimerConfig* config, MyTimerCreateFn
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Tears down the FFI context; blocks until FFI + watchdog threads join. */
|
||||
static inline int my_timer_ctx_destroy(MyTimerCtx* ctx) {
|
||||
if (!ctx) return NIMFFI_RET_OK;
|
||||
int rc = NIMFFI_RET_OK;
|
||||
@ -908,6 +915,7 @@ static inline int my_timer_ctx_destroy(MyTimerCtx* ctx) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
/** Fired by `myTimerEcho` once the reply is ready. */
|
||||
static inline uint64_t my_timer_ctx_add_on_echo_fired_listener(MyTimerCtx* ctx, MyTimerOnEchoFiredFn fn, void* user_data) {
|
||||
MyTimerOnEchoFiredBox* box = (MyTimerOnEchoFiredBox*)malloc(sizeof(MyTimerOnEchoFiredBox));
|
||||
if (!box) return 0;
|
||||
@ -974,6 +982,7 @@ static void my_timer_echo_reply_trampoline(int ret, const char* msg, size_t len,
|
||||
my_timer_free_EchoResponse(&out);
|
||||
free(box);
|
||||
}
|
||||
/** Sleeps `delayMs` then echoes the message back, firing `on_echo_fired`. */
|
||||
static inline int my_timer_ctx_echo(const MyTimerCtx* ctx, const EchoRequest* req, MyTimerEchoReplyFn on_reply, void* user_data) {
|
||||
MyTimerEchoReq ffi_req;
|
||||
memset(&ffi_req, 0, sizeof(ffi_req));
|
||||
@ -1036,6 +1045,7 @@ static void my_timer_version_reply_trampoline(int ret, const char* msg, size_t l
|
||||
nimffi_free_str(&out);
|
||||
free(box);
|
||||
}
|
||||
/** Returns the library's version string. */
|
||||
static inline int my_timer_ctx_version(const MyTimerCtx* ctx, MyTimerVersionReplyFn on_reply, void* user_data) {
|
||||
MyTimerVersionReq ffi_req;
|
||||
memset(&ffi_req, 0, sizeof(ffi_req));
|
||||
@ -1159,6 +1169,7 @@ static void my_timer_schedule_reply_trampoline(int ret, const char* msg, size_t
|
||||
my_timer_free_ScheduleResult(&out);
|
||||
free(box);
|
||||
}
|
||||
/** Three object-typed params (`job`, `retry`, `schedule`) packed into one CBOR envelope. */
|
||||
static inline int my_timer_ctx_schedule(const MyTimerCtx* ctx, const JobSpec* job, const RetryPolicy* retry, const ScheduleConfig* schedule, MyTimerScheduleReplyFn on_reply, void* user_data) {
|
||||
MyTimerScheduleReq ffi_req;
|
||||
memset(&ffi_req, 0, sizeof(ffi_req));
|
||||
|
||||
@ -23,14 +23,17 @@ MyTimerScheduleReq = { job: JobSpec, retry: RetryPolicy, schedule: ScheduleConfi
|
||||
|
||||
; ─── Procs ─────────────────────────────────────────────────────────
|
||||
; my_timer_create (ctor)
|
||||
; Creates the FFIContext + MyTimer; async via chronos.
|
||||
my_timer_create-request = MyTimerCreateCtorReq
|
||||
my_timer_create-response = tstr
|
||||
|
||||
; my_timer_echo (ffi)
|
||||
; Sleeps `delayMs` then echoes the message back, firing `on_echo_fired`.
|
||||
my_timer_echo-request = MyTimerEchoReq
|
||||
my_timer_echo-response = EchoResponse
|
||||
|
||||
; my_timer_version (ffi)
|
||||
; Returns the library's version string.
|
||||
my_timer_version-request = MyTimerVersionReq
|
||||
my_timer_version-response = tstr
|
||||
|
||||
@ -39,8 +42,10 @@ my_timer_complex-request = MyTimerComplexReq
|
||||
my_timer_complex-response = ComplexResponse
|
||||
|
||||
; my_timer_schedule (ffi)
|
||||
; Three object-typed params (`job`, `retry`, `schedule`) packed into one CBOR envelope.
|
||||
my_timer_schedule-request = MyTimerScheduleReq
|
||||
my_timer_schedule-response = ScheduleResult
|
||||
|
||||
; my_timer_destroy (dtor)
|
||||
; Tears down the FFI context; blocks until FFI + watchdog threads join.
|
||||
my_timer_destroy-response = nil
|
||||
|
||||
@ -729,11 +729,16 @@ inline CborError decode_cbor(CborValue& it, MyTimerScheduleReq& v) {
|
||||
extern "C" {
|
||||
typedef void (*FFICallback)(int ret, const char* msg, size_t len, void* user_data);
|
||||
|
||||
/** Creates the FFIContext + MyTimer; async via chronos. */
|
||||
void* my_timer_create(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback callback, void* user_data);
|
||||
/** Sleeps `delayMs` then echoes the message back, firing `on_echo_fired`. */
|
||||
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_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);
|
||||
/** Tears down the FFI context; blocks until FFI + watchdog threads join. */
|
||||
int my_timer_destroy(void* ctx);
|
||||
uint64_t my_timer_add_event_listener(void* ctx, const char* event_name, FFICallback callback, void* user_data);
|
||||
int my_timer_remove_event_listener(void* ctx, uint64_t listener_id);
|
||||
@ -812,6 +817,7 @@ inline Result<std::vector<std::uint8_t>> ffi_call_(
|
||||
|
||||
class MyTimerCtx {
|
||||
public:
|
||||
/// Creates the FFIContext + MyTimer; async via chronos.
|
||||
static Result<std::unique_ptr<MyTimerCtx>> create(const TimerConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
|
||||
const auto ffi_req_ = MyTimerCreateCtorReq{config};
|
||||
auto ffi_enc_ = encodeCborFFI(ffi_req_);
|
||||
@ -835,6 +841,7 @@ public:
|
||||
return Result<std::unique_ptr<MyTimerCtx>>::ok(std::unique_ptr<MyTimerCtx>(new MyTimerCtx(reinterpret_cast<void*>(static_cast<uintptr_t>(addr)), timeout)));
|
||||
}
|
||||
|
||||
/// Creates the FFIContext + MyTimer; async via chronos.
|
||||
static std::future<Result<std::unique_ptr<MyTimerCtx>>> createAsync(const TimerConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
|
||||
return std::async(std::launch::async, [config, timeout]() { return create(config, timeout); });
|
||||
}
|
||||
@ -863,6 +870,7 @@ public:
|
||||
// ── Event listener API ──────────────────────────────────
|
||||
struct ListenerHandle { std::uint64_t id = 0; };
|
||||
|
||||
/// Fired by `myTimerEcho` once the reply is ready.
|
||||
ListenerHandle addOnEchoFiredListener(std::function<void(const EchoEvent&)> handler) {
|
||||
auto owned = std::make_unique<TypedListener<EchoEvent>>(std::move(handler));
|
||||
auto* raw = owned.get();
|
||||
@ -880,6 +888,7 @@ public:
|
||||
return rc == 0;
|
||||
}
|
||||
|
||||
/// Sleeps `delayMs` then echoes the message back, firing `on_echo_fired`.
|
||||
Result<EchoResponse> echo(const EchoRequest& req) const {
|
||||
const auto ffi_req_ = MyTimerEchoReq{req};
|
||||
auto ffi_enc_ = encodeCborFFI(ffi_req_);
|
||||
@ -892,10 +901,12 @@ public:
|
||||
return decodeCborFFI<EchoResponse>(ffi_raw_.value());
|
||||
}
|
||||
|
||||
/// Sleeps `delayMs` then echoes the message back, firing `on_echo_fired`.
|
||||
std::future<Result<EchoResponse>> echoAsync(const EchoRequest& req) const {
|
||||
return std::async(std::launch::async, [this, req]() { return this->echo(req); });
|
||||
}
|
||||
|
||||
/// Returns the library's version string.
|
||||
Result<std::string> version() const {
|
||||
const auto ffi_req_ = MyTimerVersionReq{};
|
||||
auto ffi_enc_ = encodeCborFFI(ffi_req_);
|
||||
@ -908,6 +919,7 @@ public:
|
||||
return decodeCborFFI<std::string>(ffi_raw_.value());
|
||||
}
|
||||
|
||||
/// Returns the library's version string.
|
||||
std::future<Result<std::string>> versionAsync() const {
|
||||
return std::async(std::launch::async, [this]() { return this->version(); });
|
||||
}
|
||||
@ -928,6 +940,7 @@ public:
|
||||
return std::async(std::launch::async, [this, req]() { return this->complex(req); });
|
||||
}
|
||||
|
||||
/// Three object-typed params (`job`, `retry`, `schedule`) packed into one CBOR envelope.
|
||||
Result<ScheduleResult> schedule(const JobSpec& job, const RetryPolicy& retry, const ScheduleConfig& schedule) const {
|
||||
const auto ffi_req_ = MyTimerScheduleReq{job, retry, schedule};
|
||||
auto ffi_enc_ = encodeCborFFI(ffi_req_);
|
||||
@ -940,6 +953,7 @@ public:
|
||||
return decodeCborFFI<ScheduleResult>(ffi_raw_.value());
|
||||
}
|
||||
|
||||
/// Three object-typed params (`job`, `retry`, `schedule`) packed into one CBOR envelope.
|
||||
std::future<Result<ScheduleResult>> scheduleAsync(const JobSpec& job, const RetryPolicy& retry, const ScheduleConfig& schedule) const {
|
||||
return std::async(std::launch::async, [this, job, retry, schedule]() { return this->schedule(job, retry, schedule); });
|
||||
}
|
||||
|
||||
@ -159,6 +159,7 @@ impl Drop for MyTimerCtx {
|
||||
}
|
||||
|
||||
impl MyTimerCtx {
|
||||
/// Creates the FFIContext + MyTimer; async via chronos.
|
||||
pub fn create(config: TimerConfig, timeout: Duration) -> Result<Self, String> {
|
||||
let req = MyTimerCreateCtorReq { config };
|
||||
let req_bytes = encode_cbor(&req)?;
|
||||
@ -171,6 +172,7 @@ impl MyTimerCtx {
|
||||
Ok(Self { ptr: addr as *mut c_void, timeout, listeners: std::sync::Mutex::new(std::collections::HashMap::new()) })
|
||||
}
|
||||
|
||||
/// Creates the FFIContext + MyTimer; async via chronos.
|
||||
pub async fn new_async(config: TimerConfig, timeout: Duration) -> Result<Self, String> {
|
||||
let req = MyTimerCreateCtorReq { config };
|
||||
let req_bytes = encode_cbor(&req)?;
|
||||
@ -199,6 +201,7 @@ impl MyTimerCtx {
|
||||
ListenerHandle { id }
|
||||
}
|
||||
|
||||
/// Fired by `myTimerEcho` once the reply is ready.
|
||||
/// Register a typed listener for `on_echo_fired`. The returned handle can be
|
||||
/// passed to `remove_event_listener` to unregister.
|
||||
pub fn add_on_echo_fired_listener<F>(&self, handler: F) -> ListenerHandle
|
||||
@ -220,6 +223,7 @@ impl MyTimerCtx {
|
||||
rc == 0
|
||||
}
|
||||
|
||||
/// Sleeps `delayMs` then echoes the message back, firing `on_echo_fired`.
|
||||
pub fn echo(&self, req: EchoRequest) -> Result<EchoResponse, String> {
|
||||
let req = MyTimerEchoReq { req };
|
||||
let req_bytes = encode_cbor(&req)?;
|
||||
@ -229,6 +233,7 @@ impl MyTimerCtx {
|
||||
decode_cbor::<EchoResponse>(&raw_bytes)
|
||||
}
|
||||
|
||||
/// Sleeps `delayMs` then echoes the message back, firing `on_echo_fired`.
|
||||
pub async fn echo_async(&self, req: EchoRequest) -> Result<EchoResponse, String> {
|
||||
let req = MyTimerEchoReq { req };
|
||||
let req_bytes = encode_cbor(&req)?;
|
||||
@ -239,6 +244,7 @@ impl MyTimerCtx {
|
||||
decode_cbor::<EchoResponse>(&raw_bytes)
|
||||
}
|
||||
|
||||
/// Returns the library's version string.
|
||||
pub fn version(&self) -> Result<String, String> {
|
||||
let req = MyTimerVersionReq {};
|
||||
let req_bytes = encode_cbor(&req)?;
|
||||
@ -248,6 +254,7 @@ impl MyTimerCtx {
|
||||
decode_cbor::<String>(&raw_bytes)
|
||||
}
|
||||
|
||||
/// Returns the library's version string.
|
||||
pub async fn version_async(&self) -> Result<String, String> {
|
||||
let req = MyTimerVersionReq {};
|
||||
let req_bytes = encode_cbor(&req)?;
|
||||
@ -277,6 +284,7 @@ impl MyTimerCtx {
|
||||
decode_cbor::<ComplexResponse>(&raw_bytes)
|
||||
}
|
||||
|
||||
/// Three object-typed params (`job`, `retry`, `schedule`) packed into one CBOR envelope.
|
||||
pub fn schedule(&self, job: JobSpec, retry: RetryPolicy, schedule: ScheduleConfig) -> Result<ScheduleResult, String> {
|
||||
let req = MyTimerScheduleReq { job, retry, schedule };
|
||||
let req_bytes = encode_cbor(&req)?;
|
||||
@ -286,6 +294,7 @@ impl MyTimerCtx {
|
||||
decode_cbor::<ScheduleResult>(&raw_bytes)
|
||||
}
|
||||
|
||||
/// Three object-typed params (`job`, `retry`, `schedule`) packed into one CBOR envelope.
|
||||
pub async fn schedule_async(&self, job: JobSpec, retry: RetryPolicy, schedule: ScheduleConfig) -> Result<ScheduleResult, String> {
|
||||
let req = MyTimerScheduleReq { job, retry, schedule };
|
||||
let req_bytes = encode_cbor(&req)?;
|
||||
|
||||
@ -9,11 +9,16 @@ pub type FFICallback = unsafe extern "C" fn(
|
||||
|
||||
#[link(name = "my_timer")]
|
||||
extern "C" {
|
||||
/// Creates the FFIContext + MyTimer; async via chronos.
|
||||
pub fn my_timer_create(req_cbor: *const u8, req_cbor_len: usize, callback: FFICallback, user_data: *mut c_void) -> *mut c_void;
|
||||
/// Sleeps `delayMs` then echoes the message back, firing `on_echo_fired`.
|
||||
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_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;
|
||||
/// Tears down the FFI context; blocks until FFI + watchdog threads join.
|
||||
pub fn my_timer_destroy(ctx: *mut c_void) -> c_int;
|
||||
pub fn my_timer_add_event_listener(ctx: *mut c_void, event_name: *const c_char, callback: FFICallback, user_data: *mut c_void) -> u64;
|
||||
pub fn my_timer_remove_event_listener(ctx: *mut c_void, listener_id: u64) -> c_int;
|
||||
|
||||
@ -36,23 +36,25 @@ type EchoEvent {.ffi.} = object
|
||||
message: string
|
||||
echoCount: int
|
||||
|
||||
proc onEchoFired*(evt: EchoEvent) {.ffiEvent: "on_echo_fired".}
|
||||
proc onEchoFired*(evt: EchoEvent) {.ffiEvent: "on_echo_fired".} =
|
||||
## Fired by `myTimerEcho` once the reply is ready.
|
||||
|
||||
# Constructor: creates the FFIContext + MyTimer; async via chronos.
|
||||
proc myTimerCreate*(config: TimerConfig): Future[Result[MyTimer, string]] {.ffiCtor.} =
|
||||
## Creates the FFIContext + MyTimer; async via chronos.
|
||||
await sleepAsync(1.milliseconds) # proves chronos is live on the FFI thread
|
||||
return ok(MyTimer(name: config.name))
|
||||
|
||||
# Async method: sleeps `delayMs` then echoes the message back.
|
||||
proc myTimerEcho*(
|
||||
timer: MyTimer, req: EchoRequest
|
||||
): Future[Result[EchoResponse, string]] {.ffi.} =
|
||||
## Sleeps `delayMs` then echoes the message back, firing `on_echo_fired`.
|
||||
await sleepAsync(req.delayMs.milliseconds)
|
||||
onEchoFired(EchoEvent(message: req.message, echoCount: 1))
|
||||
return ok(EchoResponse(echoed: req.message, timerName: timer.name))
|
||||
|
||||
# Sync method: no await, so the macro fires the callback inline.
|
||||
proc myTimerVersion*(timer: MyTimer): Future[Result[string, string]] {.ffi.} =
|
||||
## Returns the library's version string.
|
||||
return ok("nim-timer v0.1.0")
|
||||
|
||||
proc myTimerComplex*(
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
## `abi = c` emits one header whose structs are the C ABI directly. Lacking
|
||||
## generics, each distinct `seq[T]`/`Option[T]` is monomorphised per type.
|
||||
|
||||
import std/[os, strutils, tables, sets]
|
||||
import std/[os, strutils, tables, sets, options]
|
||||
import ./meta, ./string_helpers, ./c_cpp_common, ./types_ir
|
||||
|
||||
## Fixed 64-bit wire type for any Nim `ptr T`/`pointer` (mirrors CppPtrType).
|
||||
@ -487,6 +487,7 @@ proc emitConstructors(
|
||||
head & params.join(", ") & ", " & fnType & " on_created, void* user_data) {"
|
||||
else:
|
||||
head & fnType & " on_created, void* user_data) {"
|
||||
lines.add(renderBlockDocComment(ctor.doc))
|
||||
lines.add(sig)
|
||||
lines.add(" " & reqName & " ffi_req;")
|
||||
lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));")
|
||||
@ -525,15 +526,19 @@ proc emitConstructors(
|
||||
|
||||
proc emitDestructor(
|
||||
lines: var seq[string],
|
||||
ctxType, libName, dtorProcName: string,
|
||||
ctxType, libName: string,
|
||||
dtor: Option[FFIProcMeta],
|
||||
events: seq[FFIEventMeta],
|
||||
) =
|
||||
if dtor.isSome():
|
||||
lines.add(renderBlockDocComment(dtor.get().doc))
|
||||
lines.add("static inline int " & libName & "_ctx_destroy(" & ctxType & "* ctx) {")
|
||||
lines.add(" if (!ctx) return NIMFFI_RET_OK;")
|
||||
lines.add(" int rc = NIMFFI_RET_OK;")
|
||||
if dtorProcName.len > 0:
|
||||
if dtor.isSome():
|
||||
lines.add(
|
||||
" if (ctx->ptr) { rc = " & dtorProcName & "(ctx->ptr); ctx->ptr = NULL; }"
|
||||
" if (ctx->ptr) { rc = " & dtor.get().procName &
|
||||
"(ctx->ptr); ctx->ptr = NULL; }"
|
||||
)
|
||||
if events.len > 0:
|
||||
# A failed teardown leaves the worker threads live (ffi_context.nim:
|
||||
@ -557,6 +562,7 @@ proc emitListenerApi(
|
||||
return
|
||||
for ev in events:
|
||||
let n = evNames(libType, libName, ev)
|
||||
lines.add(renderBlockDocComment(ev.doc))
|
||||
lines.add(
|
||||
"static inline uint64_t " & n.regName & "(" & ctxType & "* ctx, " & n.fnType &
|
||||
" fn, void* user_data) {"
|
||||
@ -661,6 +667,7 @@ proc emitMethod(
|
||||
head & params.join(", ") & ", " & fnType & " on_reply, void* user_data) {"
|
||||
else:
|
||||
head & fnType & " on_reply, void* user_data) {"
|
||||
lines.add(renderBlockDocComment(m.doc))
|
||||
lines.add(sig)
|
||||
lines.add(" " & reqName & " ffi_req;")
|
||||
lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));")
|
||||
@ -789,6 +796,7 @@ proc generateCLibHeader*(
|
||||
lines.add("#endif")
|
||||
lines.add("")
|
||||
for p in procs:
|
||||
lines.add(renderBlockDocComment(p.doc))
|
||||
case p.kind
|
||||
of FFIKind.FFI:
|
||||
lines.add(
|
||||
@ -842,7 +850,7 @@ proc generateCLibHeader*(
|
||||
emitEventMachinery(lines, reg, libType, libName, events)
|
||||
emitContextStruct(lines, ctxType, events)
|
||||
emitConstructors(lines, reg, ctxType, libType, libName, ctors)
|
||||
emitDestructor(lines, ctxType, libName, classified.dtorProcName, events)
|
||||
emitDestructor(lines, ctxType, libName, classified.dtor, events)
|
||||
emitListenerApi(lines, ctxType, libType, libName, events)
|
||||
for m in methods:
|
||||
emitMethod(lines, reg, ctxType, libType, libName, m)
|
||||
@ -1085,6 +1093,7 @@ proc emitAbiExternDecls(
|
||||
lines.add("#endif")
|
||||
lines.add("")
|
||||
for p in procs:
|
||||
lines.add(renderBlockDocComment(p.doc))
|
||||
case p.kind
|
||||
of FFIKind.FFI:
|
||||
if p.scalarFastPath:
|
||||
@ -1181,6 +1190,7 @@ proc emitAbiCtxAndCtor(
|
||||
head & params.join(", ") & ", " & createFn & " on_created, void* user_data) {"
|
||||
else:
|
||||
head & createFn & " on_created, void* user_data) {"
|
||||
lines.add(renderBlockDocComment(ctor.doc))
|
||||
lines.add(sig)
|
||||
lines.add(" " & reqStruct & " ffi_req;")
|
||||
lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));")
|
||||
@ -1220,6 +1230,7 @@ proc emitAbiMethod(
|
||||
head & params.join(", ") & ", " & info.fnType & " on_reply, void* user_data) {"
|
||||
else:
|
||||
head & info.fnType & " on_reply, void* user_data) {"
|
||||
lines.add(renderBlockDocComment(m.doc))
|
||||
lines.add(sig)
|
||||
lines.add(" " & reqStruct & " ffi_req;")
|
||||
lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));")
|
||||
@ -1326,6 +1337,7 @@ proc emitAbiScalarMethod(
|
||||
head & params.join(", ") & ", " & info.fnType & " on_reply, void* user_data) {"
|
||||
else:
|
||||
head & info.fnType & " on_reply, void* user_data) {"
|
||||
lines.add(renderBlockDocComment(m.doc))
|
||||
lines.add(sig)
|
||||
lines.add(
|
||||
" " & boxType & "* box = (" & boxType & "*)malloc(sizeof(" & boxType & "));"
|
||||
@ -1401,7 +1413,7 @@ proc generateCAbiLibHeader*(
|
||||
lines.add("/* High-level context wrapper */")
|
||||
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.dtorProcName, @[])
|
||||
emitDestructor(lines, ctxType, libName, classified.dtor, @[])
|
||||
for m in classified.methods:
|
||||
if m.scalarFastPath:
|
||||
emitAbiScalarMethod(lines, reg, ctxType, libName, libType, m)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
## Helpers shared by the C/C++ binding generators (cpp.nim, c.nim).
|
||||
|
||||
import std/strutils
|
||||
import std/[strutils, options]
|
||||
import ./meta, ./string_helpers
|
||||
|
||||
proc stripLibPrefix*(procName, libName: string): string =
|
||||
@ -21,7 +21,7 @@ proc reqStructName*(p: FFIProcMeta): string =
|
||||
type ClassifiedProcs* = object
|
||||
ctors*: seq[FFIProcMeta]
|
||||
methods*: seq[FFIProcMeta]
|
||||
dtorProcName*: string
|
||||
dtor*: Option[FFIProcMeta]
|
||||
|
||||
proc classifyProcs*(procs: seq[FFIProcMeta]): ClassifiedProcs =
|
||||
## Splits the registry into constructors, methods and the first destructor.
|
||||
@ -33,8 +33,8 @@ proc classifyProcs*(procs: seq[FFIProcMeta]): ClassifiedProcs =
|
||||
of FFIKind.FFI:
|
||||
c.methods.add(p)
|
||||
of FFIKind.DTOR:
|
||||
if c.dtorProcName.len == 0:
|
||||
c.dtorProcName = p.procName
|
||||
if c.dtor.isNone():
|
||||
c.dtor = some(p)
|
||||
c
|
||||
|
||||
proc libTypeName*(ctors: seq[FFIProcMeta], libName: string): string =
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
## ffi/cbor_serial.nim: types become rules, procs get request/response rules.
|
||||
|
||||
import std/[os, strutils, unicode]
|
||||
import ./meta
|
||||
import ./meta, ./string_helpers
|
||||
|
||||
proc innerOf(typeName, prefix: string): string =
|
||||
if typeName.startsWith(prefix) and typeName.endsWith("]"):
|
||||
@ -154,6 +154,7 @@ proc generateCddlSchema*(
|
||||
of FFIKind.DTOR: "dtor"
|
||||
of FFIKind.FFI: "ffi"
|
||||
L.add("; " & p.procName & " (" & kindTag & ")")
|
||||
L.add(renderDocComment(p.doc, "", "; "))
|
||||
if p.kind != FFIKind.DTOR:
|
||||
L.add(p.procName & "-request = " & reqStructName(p))
|
||||
L.add(p.procName & "-response = " & responseRule(p))
|
||||
|
||||
@ -112,6 +112,7 @@ proc emitEventDispatcher(
|
||||
for ev in events:
|
||||
let methodName =
|
||||
"addOn" & capitalizeFirstLetter(ev.nimProcName).substr(2) & "Listener"
|
||||
lines.add(renderMemberDocComment(ev.doc))
|
||||
lines.add(
|
||||
" ListenerHandle $1(std::function<void(const $2&)> handler) {" %
|
||||
[methodName, ev.payloadTypeName]
|
||||
@ -251,6 +252,7 @@ proc generateCppHeader*(
|
||||
)
|
||||
lines.add("")
|
||||
for p in procs:
|
||||
lines.add(renderBlockDocComment(p.doc))
|
||||
case p.kind
|
||||
of FFIKind.FFI:
|
||||
lines.add(
|
||||
@ -312,6 +314,7 @@ proc generateCppHeader*(
|
||||
|
||||
# `create` yields the ctx via the callback's CBOR address (sync void* return discarded), owned as a unique_ptr since the class forbids copy/move.
|
||||
let createRet = "Result<std::unique_ptr<$1>>" % [ctxTypeName]
|
||||
lines.add(renderMemberDocComment(ctor.doc))
|
||||
lines.add(" static $1 create($2) {" % [createRet, ctorParamsWithTimeout])
|
||||
lines.add(" const auto ffi_req_ = $1;" % [reqInit])
|
||||
lines.add(" auto ffi_enc_ = encodeCborFFI(ffi_req_);")
|
||||
@ -363,6 +366,7 @@ proc generateCppHeader*(
|
||||
epNames.join(", ") & ", timeout"
|
||||
else:
|
||||
"timeout"
|
||||
lines.add(renderMemberDocComment(ctor.doc))
|
||||
lines.add(
|
||||
" static std::future<Result<std::unique_ptr<$1>>> createAsync($2) {" %
|
||||
[ctxTypeName, ctorParamsWithTimeout]
|
||||
@ -405,6 +409,7 @@ proc generateCppHeader*(
|
||||
let reqInit = cppBracedInit(reqName, methParamNames)
|
||||
|
||||
let methRet = "Result<$1>" % [retCppType]
|
||||
lines.add(renderMemberDocComment(m.doc))
|
||||
lines.add(" $1 $2($3) const {" % [methRet, methodName, methParamsStr])
|
||||
lines.add(" const auto ffi_req_ = $1;" % [reqInit])
|
||||
lines.add(" auto ffi_enc_ = encodeCborFFI(ffi_req_);")
|
||||
@ -425,6 +430,7 @@ proc generateCppHeader*(
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
# `this->methodName(...)` so a same-named param can't shadow the call target.
|
||||
lines.add(renderMemberDocComment(m.doc))
|
||||
if methParamsStr.len > 0:
|
||||
lines.add(
|
||||
" std::future<$1> $2Async($3) const {" % [methRet, methodName, methParamsStr]
|
||||
|
||||
@ -26,6 +26,7 @@ type
|
||||
libName*: string
|
||||
kind*: FFIKind
|
||||
libTypeName*: string
|
||||
doc*: string
|
||||
extraParams*: seq[FFIParamMeta] # all params except the lib param
|
||||
returnTypeName*: string
|
||||
returnIsPtr*: bool
|
||||
@ -52,6 +53,7 @@ type
|
||||
libName*: string
|
||||
payloadTypeName*: string
|
||||
abiFormat*: ABIFormat
|
||||
doc*: string
|
||||
|
||||
var ffiProcRegistry* {.compileTime.}: seq[FFIProcMeta]
|
||||
var ffiTypeRegistry* {.compileTime.}: seq[FFITypeMeta]
|
||||
|
||||
@ -183,6 +183,7 @@ proc generateFFIRs*(procs: seq[FFIProcMeta]): string =
|
||||
|
||||
for p in procs:
|
||||
var params: seq[string] = @[]
|
||||
lines.add(renderMemberDocComment(p.doc))
|
||||
case p.kind
|
||||
of FFIKind.FFI:
|
||||
# Method-style: ctx first.
|
||||
@ -544,6 +545,7 @@ proc generateApiRs*(
|
||||
else:
|
||||
reqName & " {}"
|
||||
|
||||
lines.add(renderMemberDocComment(ctor.doc))
|
||||
lines.add(" pub fn create($1) -> Result<Self, String> {" % [ctorParamsStr])
|
||||
lines.add(" let req = $1;" % [reqLit])
|
||||
lines.add(" let req_bytes = encode_cbor(&req)?;")
|
||||
@ -569,6 +571,7 @@ proc generateApiRs*(
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
|
||||
lines.add(renderMemberDocComment(ctor.doc))
|
||||
lines.add(
|
||||
" pub async fn new_async($1) -> Result<Self, String> {" % [ctorParamsStr]
|
||||
)
|
||||
@ -621,6 +624,7 @@ proc generateApiRs*(
|
||||
let methodName = "add_" & camelToSnakeCase(ev.nimProcName) & "_listener"
|
||||
let handlerStruct = capitalizeFirstLetter(ev.nimProcName) & "Handler"
|
||||
let trampolineName = camelToSnakeCase(ev.nimProcName) & "_trampoline"
|
||||
lines.add(renderMemberDocComment(ev.doc))
|
||||
lines.add(
|
||||
" /// Register a typed listener for `$1`. The returned handle can be" %
|
||||
[ev.wireName]
|
||||
@ -690,6 +694,7 @@ proc generateApiRs*(
|
||||
|
||||
let retTypeForApi = if m.returnRidesAsPtr(): RustPtrType else: retRustType
|
||||
|
||||
lines.add(renderMemberDocComment(m.doc))
|
||||
lines.add(
|
||||
" pub fn $1(&self$2) -> Result<$3, String> {" %
|
||||
[methodName, paramsStr, retTypeForApi]
|
||||
@ -707,6 +712,7 @@ proc generateApiRs*(
|
||||
lines.add("")
|
||||
|
||||
# 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> {" %
|
||||
[methodName, paramsStr, retTypeForApi]
|
||||
|
||||
@ -1,7 +1,48 @@
|
||||
## Unicode-aware identifier-casing helpers shared by codegen and the FFI macro.
|
||||
## Unicode-aware identifier casing and doc-comment rendering, shared by codegen
|
||||
## and the FFI macro.
|
||||
|
||||
import std/[strutils, unicode]
|
||||
|
||||
func docLines(doc: string): seq[string] =
|
||||
## `doc` split into lines, trailing blank ones dropped.
|
||||
if doc.strip().len == 0:
|
||||
return @[]
|
||||
var lines = doc.splitLines()
|
||||
while lines.len > 0 and lines[^1].strip().len == 0:
|
||||
lines.setLen(lines.len - 1)
|
||||
return lines
|
||||
|
||||
func renderDocComment*(doc, indent, prefix: string): seq[string] =
|
||||
## `doc` as one `prefix`-led line comment per source line, at `indent`.
|
||||
var rendered: seq[string] = @[]
|
||||
for line in docLines(doc):
|
||||
# A trailing `\` would splice the next generated line into a `//` comment.
|
||||
rendered.add(
|
||||
indent & (prefix & line).strip(leading = false, chars = Whitespace + {'\\'})
|
||||
)
|
||||
return rendered
|
||||
|
||||
func renderMemberDocComment*(doc: string): seq[string] =
|
||||
## `///` at the indent C++ class members and Rust `impl` items sit at.
|
||||
return doc.renderDocComment(" ", "/// ")
|
||||
|
||||
func escapeBlockComment(line: string): string =
|
||||
## `*/` would close the comment early and splice the rest in as code.
|
||||
return line.replace("*/", "* /")
|
||||
|
||||
func renderBlockDocComment*(doc: string, indent = ""): seq[string] =
|
||||
## `doc` as a `/** ... */` block at `indent`; one-liners stay on one line.
|
||||
let lines = docLines(doc)
|
||||
if lines.len == 0:
|
||||
return @[]
|
||||
if lines.len == 1:
|
||||
return @[indent & "/** " & escapeBlockComment(lines[0].strip()) & " */"]
|
||||
var rendered = @[indent & "/**"]
|
||||
for line in lines:
|
||||
rendered.add((indent & " * " & escapeBlockComment(line)).strip(leading = false))
|
||||
rendered.add(indent & " */")
|
||||
return rendered
|
||||
|
||||
proc toLower*(s: string): string =
|
||||
## Unicode-aware lowercase for an entire string.
|
||||
var buf = ""
|
||||
|
||||
@ -143,6 +143,16 @@ proc registerFFITypeInfo(
|
||||
)
|
||||
return typeDef
|
||||
|
||||
func extractDocComment(prc: NimNode): string {.compileTime.} =
|
||||
## The proc's leading `##`, or "". Nim drops comments outside a proc body, so
|
||||
## types and fields are unreachable from here.
|
||||
let body = prc[^1]
|
||||
if body.kind != nnkStmtList or body.len == 0:
|
||||
return ""
|
||||
if body[0].kind != nnkCommentStmt:
|
||||
return ""
|
||||
return body[0].strVal
|
||||
|
||||
proc nimTypeNameRepr(typ: NimNode): string =
|
||||
## Stringifies a parameter or field type for the registry.
|
||||
case typ.kind
|
||||
@ -760,6 +770,7 @@ macro ffi*(args: varargs[untyped]): untyped =
|
||||
returnIsPtr: retIsPtr,
|
||||
returnIsHandle: retIsHandle,
|
||||
abiFormat: abiFormat,
|
||||
doc: extractDocComment(prc),
|
||||
)
|
||||
|
||||
# CBOR-free scalar fast path: only `abi = c` with all-scalar params/return that fit the inline slots; non-scalar `abi = c` rides the `_CWire` C-dispatch.
|
||||
@ -1294,6 +1305,7 @@ macro ffiCtor*(args: varargs[untyped]): untyped =
|
||||
returnTypeName: $libTypeName,
|
||||
returnIsPtr: false,
|
||||
abiFormat: abiFormat,
|
||||
doc: extractDocComment(prc),
|
||||
)
|
||||
)
|
||||
|
||||
@ -1422,6 +1434,7 @@ macro ffiDtor*(args: varargs[untyped]): untyped =
|
||||
returnTypeName: "",
|
||||
returnIsPtr: false,
|
||||
abiFormat: abiFormat,
|
||||
doc: extractDocComment(prc),
|
||||
)
|
||||
)
|
||||
|
||||
@ -1511,6 +1524,7 @@ macro ffiEvent*(args: varargs[untyped]): untyped =
|
||||
libName: currentLibName,
|
||||
payloadTypeName: payloadTypeNameStr,
|
||||
abiFormat: abiFormat,
|
||||
doc: extractDocComment(prc),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
98
tests/unit/test_doc_extraction.nim
Normal file
98
tests/unit/test_doc_extraction.nim
Normal file
@ -0,0 +1,98 @@
|
||||
## `##` comments on annotated procs → the registry. Codegen: test_doc_propagation.nim.
|
||||
|
||||
import unittest2
|
||||
import results
|
||||
import ffi
|
||||
import ffi/codegen/meta
|
||||
|
||||
type DocLib = object
|
||||
|
||||
# Stub the importc NimMain declareLibrary emits (plain-exe link).
|
||||
{.emit: "void libdoctestNimMain(void) {}".}
|
||||
|
||||
declareLibrary("doctest", DocLib)
|
||||
|
||||
type DocConfig {.ffi.} = object
|
||||
v: int
|
||||
|
||||
type Tocked {.ffi.} = object
|
||||
n: int
|
||||
|
||||
proc doctest_create*(c: DocConfig): Future[Result[DocLib, string]] {.ffiCtor.} =
|
||||
## Builds the lib.
|
||||
return ok(DocLib())
|
||||
|
||||
proc doctest_documented*(lib: DocLib): Future[Result[string, string]] {.ffi.} =
|
||||
## First line.
|
||||
## Second line.
|
||||
return ok("ok")
|
||||
|
||||
proc doctest_undocumented*(lib: DocLib): Future[Result[string, string]] {.ffi.} =
|
||||
return ok("ok")
|
||||
|
||||
proc doctest_hash_commented*(lib: DocLib): Future[Result[string, string]] {.ffi.} =
|
||||
# not a doc comment
|
||||
return ok("ok")
|
||||
|
||||
proc doctest_spec_documented*(
|
||||
lib: DocLib, n: int
|
||||
): Future[Result[int, string]] {.ffi: "abi = cbor".} =
|
||||
## Doc survives alongside a pragma spec.
|
||||
return ok(n)
|
||||
|
||||
proc doctest_destroy*(lib: DocLib) {.ffiDtor.} =
|
||||
## Tears it down.
|
||||
discard
|
||||
|
||||
proc doctest_tocked*(p: Tocked) {.ffiEvent: "on_tocked".} =
|
||||
## Fires on every tock.
|
||||
|
||||
proc doctest_plain*(p: Tocked) {.ffiEvent: "on_plain".}
|
||||
|
||||
# The registries are compile-time only, so snapshot them into consts.
|
||||
proc docOf(procName: string): string {.compileTime.} =
|
||||
for p in ffiProcRegistry:
|
||||
if p.procName == procName:
|
||||
return p.doc
|
||||
doAssert false, "no proc " & procName & " in ffiProcRegistry"
|
||||
|
||||
proc eventDocOf(wireName: string): string {.compileTime.} =
|
||||
for e in ffiEventRegistry:
|
||||
if e.wireName == wireName:
|
||||
return e.doc
|
||||
doAssert false, "no event " & wireName & " in ffiEventRegistry"
|
||||
|
||||
const
|
||||
CreateDoc = docOf("doctest_create")
|
||||
DocumentedDoc = docOf("doctest_documented")
|
||||
UndocumentedDoc = docOf("doctest_undocumented")
|
||||
HashCommentedDoc = docOf("doctest_hash_commented")
|
||||
SpecDocumentedDoc = docOf("doctest_spec_documented")
|
||||
DestroyDoc = docOf("doctest_destroy")
|
||||
TockedDoc = eventDocOf("on_tocked")
|
||||
PlainEventDoc = eventDocOf("on_plain")
|
||||
|
||||
suite "extractDocComment: what lands on the registry":
|
||||
test "a single-line doc comment is captured":
|
||||
check CreateDoc == "Builds the lib."
|
||||
|
||||
test "a multi-line doc comment keeps its line breaks":
|
||||
check DocumentedDoc == "First line.\nSecond line."
|
||||
|
||||
test "an undocumented proc gets an empty doc":
|
||||
check UndocumentedDoc == ""
|
||||
|
||||
test "a plain `#` comment is not captured":
|
||||
check HashCommentedDoc == ""
|
||||
|
||||
test "a doc comment survives a pragma spec argument":
|
||||
check SpecDocumentedDoc == "Doc survives alongside a pragma spec."
|
||||
|
||||
test "the destructor's doc is captured":
|
||||
check DestroyDoc == "Tears it down."
|
||||
|
||||
test "an event proc's doc comment is captured":
|
||||
check TockedDoc == "Fires on every tock."
|
||||
|
||||
test "an undocumented event proc gets an empty doc":
|
||||
check PlainEventDoc == ""
|
||||
195
tests/unit/test_doc_propagation.nim
Normal file
195
tests/unit/test_doc_propagation.nim
Normal file
@ -0,0 +1,195 @@
|
||||
## FFIProcMeta.doc → each target's comment syntax. Extraction: test_doc_extraction.nim.
|
||||
|
||||
import std/strutils
|
||||
import unittest2
|
||||
import ffi/codegen/[meta, c, cpp, rust, cddl]
|
||||
|
||||
const
|
||||
CtorDoc = "Builds a widget from `config`."
|
||||
MethodDoc = "Echoes `req` back.\nSecond line."
|
||||
DtorDoc = "Releases the widget."
|
||||
EventDoc = "Fires once the echo lands."
|
||||
ScalarDoc = "Adds two numbers, no CBOR in sight."
|
||||
|
||||
let types = @[
|
||||
FFITypeMeta(
|
||||
name: "EchoRequest", fields: @[FFIFieldMeta(name: "message", typeName: "string")]
|
||||
),
|
||||
FFITypeMeta(
|
||||
name: "EchoResponse", fields: @[FFIFieldMeta(name: "echoed", typeName: "string")]
|
||||
),
|
||||
]
|
||||
|
||||
let procs = @[
|
||||
FFIProcMeta(
|
||||
procName: "widget_create",
|
||||
libName: "widget",
|
||||
kind: FFIKind.CTOR,
|
||||
libTypeName: "Widget",
|
||||
extraParams: @[FFIParamMeta(name: "config", typeName: "EchoRequest")],
|
||||
returnTypeName: "Widget",
|
||||
doc: CtorDoc,
|
||||
),
|
||||
FFIProcMeta(
|
||||
procName: "widget_echo",
|
||||
libName: "widget",
|
||||
kind: FFIKind.FFI,
|
||||
libTypeName: "Widget",
|
||||
extraParams: @[FFIParamMeta(name: "req", typeName: "EchoRequest")],
|
||||
returnTypeName: "EchoResponse",
|
||||
doc: MethodDoc,
|
||||
),
|
||||
FFIProcMeta(
|
||||
procName: "widget_destroy",
|
||||
libName: "widget",
|
||||
kind: FFIKind.DTOR,
|
||||
libTypeName: "Widget",
|
||||
returnTypeName: "",
|
||||
doc: DtorDoc,
|
||||
),
|
||||
]
|
||||
|
||||
let events = @[
|
||||
FFIEventMeta(
|
||||
wireName: "on_echoed",
|
||||
nimProcName: "onEchoed",
|
||||
libName: "widget",
|
||||
payloadTypeName: "EchoResponse",
|
||||
doc: EventDoc,
|
||||
)
|
||||
]
|
||||
|
||||
let undocumented = block:
|
||||
var stripped = procs
|
||||
for p in stripped.mitems:
|
||||
p.doc = ""
|
||||
stripped
|
||||
|
||||
func withoutDocLines(s: string): string =
|
||||
## Drops `///` lines by content, so a generator's own hardcoded ones survive.
|
||||
var rendered: seq[string] = @[]
|
||||
for doc in [CtorDoc, MethodDoc, DtorDoc]:
|
||||
for line in doc.splitLines():
|
||||
rendered.add(("/// " & line).strip())
|
||||
var kept: seq[string] = @[]
|
||||
for line in s.splitLines():
|
||||
if line.strip() notin rendered:
|
||||
kept.add(line)
|
||||
return kept.join("\n")
|
||||
|
||||
proc checkNoDocText(rendered: string) =
|
||||
for doc in [CtorDoc, MethodDoc, DtorDoc]:
|
||||
for line in doc.splitLines():
|
||||
check line notin rendered
|
||||
|
||||
suite "doc comments reach the C header":
|
||||
setup:
|
||||
let header = generateCLibHeader(procs, types, "widget")
|
||||
|
||||
test "the exported symbol carries the doc":
|
||||
check "/** " & CtorDoc & " */\nvoid* widget_create(" in header
|
||||
|
||||
test "the high-level wrapper carries the doc":
|
||||
check "/** " & CtorDoc & " */\nstatic inline int widget_ctx_create(" in header
|
||||
check "/** " & DtorDoc & " */\nstatic inline int widget_ctx_destroy(" in header
|
||||
|
||||
test "a multi-line doc becomes a star block":
|
||||
check "/**\n * Echoes `req` back.\n * Second line.\n */\nstatic inline int widget_ctx_echo(" in
|
||||
header
|
||||
|
||||
test "the listener registration carries the event's doc":
|
||||
let withEvents = generateCLibHeader(procs, types, "widget", events)
|
||||
check "/** " & EventDoc &
|
||||
" */\nstatic inline uint64_t widget_ctx_add_on_echoed_listener(" in withEvents
|
||||
|
||||
test "an undocumented registry leaks no doc text":
|
||||
checkNoDocText(generateCLibHeader(undocumented, types, "widget"))
|
||||
|
||||
suite "doc comments reach the abi = c header":
|
||||
setup:
|
||||
var abiProcs = procs
|
||||
abiProcs.add(
|
||||
FFIProcMeta(
|
||||
procName: "widget_add",
|
||||
libName: "widget",
|
||||
kind: FFIKind.FFI,
|
||||
libTypeName: "Widget",
|
||||
extraParams: @[
|
||||
FFIParamMeta(name: "a", typeName: "int"),
|
||||
FFIParamMeta(name: "b", typeName: "int"),
|
||||
],
|
||||
returnTypeName: "int",
|
||||
scalarFastPath: true,
|
||||
doc: ScalarDoc,
|
||||
)
|
||||
)
|
||||
for p in abiProcs.mitems:
|
||||
p.abiFormat = ABIFormat.C
|
||||
var abiTypes = types
|
||||
for t in abiTypes.mitems:
|
||||
t.abiFormat = ABIFormat.C
|
||||
let header = generateCAbiLibHeader(abiProcs, abiTypes, "widget")
|
||||
|
||||
test "the exported symbol carries the doc":
|
||||
check "/** " & CtorDoc & " */\nvoid* widget_create(" in header
|
||||
|
||||
test "the high-level wrapper carries the doc":
|
||||
check "/** " & CtorDoc & " */\nstatic inline int widget_ctx_create(" in header
|
||||
check "/** " & DtorDoc & " */\nstatic inline int widget_ctx_destroy(" in header
|
||||
|
||||
test "the scalar fast path carries the doc":
|
||||
check "/** " & ScalarDoc & " */\nstatic inline int widget_ctx_add(" in header
|
||||
|
||||
suite "doc comments reach the C++ header":
|
||||
setup:
|
||||
let header = generateCppHeader(procs, types, "widget")
|
||||
|
||||
test "the extern \"C\" declaration carries the doc":
|
||||
check "/** " & CtorDoc & " */\nvoid* widget_create(" in header
|
||||
|
||||
test "class members carry an indented /// doc":
|
||||
check " /// " & CtorDoc &
|
||||
"\n static Result<std::unique_ptr<WidgetCtx>> create(" in header
|
||||
check " /// Echoes `req` back.\n /// Second line.\n Result<EchoResponse> echo(" in
|
||||
header
|
||||
|
||||
test "the async variant repeats the doc":
|
||||
check " /// Echoes `req` back.\n /// Second line.\n std::future<Result<EchoResponse>> echoAsync(" in
|
||||
header
|
||||
|
||||
test "the listener registration carries the event's doc":
|
||||
check " /// " & EventDoc & "\n ListenerHandle addOnEchoedListener(" in
|
||||
generateCppHeader(procs, types, "widget", events)
|
||||
|
||||
test "an undocumented registry leaks no doc text":
|
||||
checkNoDocText(generateCppHeader(undocumented, types, "widget"))
|
||||
|
||||
suite "doc comments reach the Rust bindings":
|
||||
test "the extern \"C\" block carries the doc":
|
||||
let ffiRs = generateFFIRs(procs)
|
||||
check " /// " & CtorDoc & "\n pub fn widget_create(" in ffiRs
|
||||
check " /// Echoes `req` back.\n /// Second line.\n pub fn widget_echo(" in
|
||||
ffiRs
|
||||
|
||||
test "the high-level api carries the doc":
|
||||
let apiRs = generateApiRs(procs, "widget")
|
||||
check " /// " & CtorDoc & "\n pub fn create(" in apiRs
|
||||
check " /// Echoes `req` back.\n /// Second line.\n pub fn echo(" in apiRs
|
||||
check " /// Echoes `req` back.\n /// Second line.\n pub async fn echo_async(" in
|
||||
apiRs
|
||||
|
||||
test "the listener registration carries the event's doc above the generated one":
|
||||
check " /// " & EventDoc & "\n /// Register a typed listener for `on_echoed`" in
|
||||
generateApiRs(procs, "widget", events)
|
||||
|
||||
test "docs are purely additive — stripping them reproduces the undocumented output":
|
||||
check withoutDocLines(generateFFIRs(procs)) == generateFFIRs(undocumented)
|
||||
check withoutDocLines(generateApiRs(procs, "widget")) ==
|
||||
generateApiRs(undocumented, "widget")
|
||||
|
||||
suite "doc comments reach the CDDL schema":
|
||||
test "each documented rule gets a `;` comment under its header":
|
||||
let schema = generateCddlSchema(procs, types, "widget", "widget.nim")
|
||||
check "; widget_create (ctor)\n; " & CtorDoc & "\n" in schema
|
||||
check "; widget_echo (ffi)\n; Echoes `req` back.\n; Second line.\n" in schema
|
||||
check "; widget_destroy (dtor)\n; " & DtorDoc & "\n" in schema
|
||||
@ -81,3 +81,53 @@ suite "snakeToPascalCase":
|
||||
test "already-mixed parts preserve their existing case after the first":
|
||||
# Existing caps after the first letter of each part are preserved.
|
||||
check snakeToPascalCase("already_HasCaps") == "AlreadyHasCaps"
|
||||
|
||||
suite "renderDocComment":
|
||||
test "empty doc renders nothing":
|
||||
check renderDocComment("", "", "/// ").len == 0
|
||||
|
||||
test "blank doc renders nothing":
|
||||
check renderDocComment(" \n ", "", "/// ").len == 0
|
||||
|
||||
test "single line gets the prefix":
|
||||
check renderDocComment("does a thing", "", "/// ") == @["/// does a thing"]
|
||||
|
||||
test "each line gets prefix and indent":
|
||||
check renderDocComment("one\ntwo", " ", "/// ") == @[
|
||||
" /// one", " /// two"
|
||||
]
|
||||
|
||||
test "a blank interior line keeps no trailing whitespace":
|
||||
check renderDocComment("one\n\ntwo", "", "/// ") == @["/// one", "///", "/// two"]
|
||||
|
||||
test "trailing blank lines are dropped":
|
||||
check renderDocComment("one\n\n", "", "; ") == @["; one"]
|
||||
|
||||
test "a trailing backslash cannot splice the next line into the comment":
|
||||
check renderDocComment("one \\\ntwo", "", "/// ") == @["/// one", "/// two"]
|
||||
|
||||
suite "renderBlockDocComment":
|
||||
test "empty doc renders nothing":
|
||||
check renderBlockDocComment("", "").len == 0
|
||||
|
||||
test "single line collapses to a one-line block":
|
||||
check renderBlockDocComment("does a thing", "") == @["/** does a thing */"]
|
||||
|
||||
test "multiple lines render a star block":
|
||||
check renderBlockDocComment("one\ntwo", "") == @["/**", " * one", " * two", " */"]
|
||||
|
||||
test "indent applies to every line of the block":
|
||||
check renderBlockDocComment("one\ntwo", " ") ==
|
||||
@[" /**", " * one", " * two", " */"]
|
||||
|
||||
test "a blank interior line keeps no trailing whitespace":
|
||||
check renderBlockDocComment("one\n\ntwo", "") ==
|
||||
@["/**", " * one", " *", " * two", " */"]
|
||||
|
||||
test "a one-liner cannot close its own comment early":
|
||||
check renderBlockDocComment("Frees it. */ #define OOPS 1 /* x", "") ==
|
||||
@["/** Frees it. * / #define OOPS 1 /* x */"]
|
||||
|
||||
test "a star block cannot close its own comment early":
|
||||
check renderBlockDocComment("one */ #define OOPS 1\ntwo", "") ==
|
||||
@["/**", " * one * / #define OOPS 1", " * two", " */"]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user