feat: propagate comments (#136)

This commit is contained in:
Gabriel Cruz 2026-07-22 17:04:35 -03:00 committed by GitHub
parent 98fb7da9e2
commit c58e2bcdd2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 546 additions and 15 deletions

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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));

View File

@ -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(); });
}

View File

@ -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()

View File

@ -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));

View File

@ -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

View File

@ -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); });
}

View File

@ -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)?;

View File

@ -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;

View File

@ -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*(

View File

@ -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)

View File

@ -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 =

View File

@ -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))

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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 = ""

View File

@ -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),
)
)

View 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 == ""

View 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

View File

@ -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", " */"]