mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-07-27 01:53:26 +00:00
feat(codegen): {.ffiConst.} constants and {.ffi.} enums for C/C++/Rust (#140)
This commit is contained in:
parent
c58e2bcdd2
commit
64cb4bfce4
19
CHANGELOG.md
19
CHANGELOG.md
@ -31,6 +31,21 @@ All notable changes to this project are documented in this file.
|
||||
where `-install_name` requires `-dynamiclib`.
|
||||
|
||||
### Added
|
||||
- `{.ffi.}` now accepts an `enum` type, emitting a native enum in every target
|
||||
(C `enum`, C++ `enum class`, Rust enum, CDDL string choice). Values cross the
|
||||
wire as the text `$value` yields — the associated string if declared, else the
|
||||
symbol name — matching what `cbor_serialization` writes. Enums are supported
|
||||
on the CBOR wire only; reaching one from an `abi = c` type or proc is now a
|
||||
compile error naming the type, where it previously registered as a
|
||||
fieldless struct and silently dropped the value.
|
||||
- `{.ffiConst.}` exposes a Nim `const` to every generated binding as a native
|
||||
constant (`static const` in C, `constexpr` in C++, `pub const` in Rust).
|
||||
Integer, float, `bool` and `string` values are supported, computed
|
||||
expressions arrive folded, and names are re-cased to `UPPER_SNAKE`.
|
||||
- `{.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
|
||||
C export symbol. Pass a string literal only to override it.
|
||||
- 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
|
||||
@ -39,10 +54,6 @@ All notable changes to this project are documented in this file.
|
||||
`##` comment now changes the generated bindings, so `nimble check_bindings`
|
||||
flags them stale until regenerated; an undocumented proc still generates
|
||||
byte-identical output.
|
||||
- `{.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
|
||||
C export symbol. Pass a string literal only to override it.
|
||||
- FFI annotations (`{.ffi.}`, `{.ffiCtor.}`, `{.ffiDtor.}`, `{.ffiEvent.}`,
|
||||
`{.ffiHandle.}`, `{.ffiRaw.}`) that expand after `genBindings()` now produce a
|
||||
loud compile error instead of being silently dropped from the generated
|
||||
|
||||
72
README.md
72
README.md
@ -39,9 +39,9 @@ type Counter = object
|
||||
|
||||
declareLibrary("counter", Counter)
|
||||
|
||||
# 2. Request/response shapes. Any {.ffi.} object type becomes a first-class
|
||||
# struct/class in the generated bindings and rides the wire in the library's
|
||||
# ABI format (CBOR by default).
|
||||
# 2. Request/response shapes. Any {.ffi.} object or enum type becomes a
|
||||
# first-class struct/class in the generated bindings and rides the wire in
|
||||
# the library's ABI format (CBOR by default).
|
||||
type BumpRequest {.ffi.} = object
|
||||
by: int
|
||||
|
||||
@ -76,14 +76,76 @@ The generated C export names are the snake_case form of the proc names, e.g.
|
||||
| Pragma | Applies to | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `declareLibrary(name, LibType[, defaultABIFormat])` | call | Registers the library, its state type, and the default wire format. Must run before any annotation. |
|
||||
| `{.ffi.}` on a `type` | `object` | Registers the type for binding generation; it serializes via the library's ABI format (CBOR by default). |
|
||||
| `{.ffi.}` on a `type` | `object`, `enum` | Registers the type for binding generation; it serializes via the library's ABI format (CBOR by default). Enums are CBOR-only — see below. |
|
||||
| `{.ffi.}` on a `proc` | proc | Exposes a method. First param is the library value, then typed params; returns `Future[Result[T, string]]`. |
|
||||
| `{.ffiCtor.}` | proc | The constructor. Returns `Future[Result[LibType, string]]`; creates the FFI context. |
|
||||
| `{.ffiDtor.}` | proc | The destructor. Exactly one param `(x: LibType)`; tears the context down. |
|
||||
| `{.ffiEvent[: "wire_name"].}` | proc (empty body) | A library-initiated callback. Call the proc from any `{.ffi.}` handler to fire it. The wire name is optional — see below. |
|
||||
| `{.ffiHandle.}` | `ref object` | Marks a type as an opaque handle: it stays server-side and crosses the wire as a `uint64` id. |
|
||||
| `{.ffiConst.}` | `const` | Re-emits the value as a native constant in every generated binding — see below. |
|
||||
| `genBindings()` | call | Emits the bindings. Must be the **last** FFI call in the compilation root. |
|
||||
|
||||
### Enums
|
||||
|
||||
`{.ffi.}` on an `enum` gives each language its own native enum:
|
||||
|
||||
```nim
|
||||
type Level {.ffi.} = enum
|
||||
lLow = "low"
|
||||
lHigh = "high"
|
||||
```
|
||||
|
||||
```c
|
||||
typedef enum { LEVEL_L_LOW = 0, LEVEL_L_HIGH = 1 } Level; /* C */
|
||||
enum class Level { lLow = 0, lHigh = 1 }; // C++
|
||||
pub enum Level { #[serde(rename = "low")] LLow, ... } // Rust
|
||||
```
|
||||
|
||||
An enum crosses the wire as **text**, not as its ordinal — whatever `$value`
|
||||
yields, so the associated string if the enum declares one (`"low"` above) and
|
||||
the symbol name otherwise (`lLow`). That is what `cbor_serialization` writes on
|
||||
the Nim side, and the generated codecs map name ↔ value on the far side, so
|
||||
reordering or renumbering values doesn't break an already-deployed peer.
|
||||
Explicit ordinals are carried into the foreign enum so the two sides agree if
|
||||
you ever cast.
|
||||
|
||||
Enums are supported on the CBOR wire only. Reaching one from an `abi = c` type
|
||||
or proc is a compile error naming the type — the `abi = c` `_CWire` structs have
|
||||
no enum form yet.
|
||||
|
||||
### Constants
|
||||
|
||||
`{.ffiConst.}` copies a Nim `const` into the generated bindings, so callers
|
||||
don't hand-maintain a second copy of a limit, a default or a protocol string:
|
||||
|
||||
```nim
|
||||
const
|
||||
MaxPeers* {.ffiConst.} = 42
|
||||
DefaultTimeoutMs* {.ffiConst.}: uint32 = 3 * 1000
|
||||
Greeting* {.ffiConst.} = "hello"
|
||||
```
|
||||
|
||||
```c
|
||||
static const int64_t MAX_PEERS = 42LL; /* C */
|
||||
static const uint32_t DEFAULT_TIMEOUT_MS = 3000;
|
||||
static const char* const GREETING = "hello";
|
||||
```
|
||||
|
||||
```cpp
|
||||
constexpr int64_t MAX_PEERS = 42LL; // C++
|
||||
```
|
||||
|
||||
```rust
|
||||
pub const MAX_PEERS: i64 = 42; // Rust
|
||||
```
|
||||
|
||||
Integer, float, `bool` and `string` consts are supported; anything else is a
|
||||
compile error. The value is whatever the const evaluates to, so computed
|
||||
expressions arrive folded. Names are re-cased to `UPPER_SNAKE`, preserving
|
||||
acronyms (`httpTTL` → `HTTP_TTL`). A constant is a compile-time value in each
|
||||
language, not a symbol exported by the shared library — it never crosses the
|
||||
wire, so `{.ffiConst.}` is ABI-agnostic.
|
||||
|
||||
### Doc comments
|
||||
|
||||
A `##` doc comment on an annotated proc is carried through to every generated
|
||||
@ -182,6 +244,8 @@ header shape from the library's ABI format. It carries two honest limits today:
|
||||
- **Events are CBOR-only.** Applying `abi = c` to an `{.ffiEvent.}` proc is a
|
||||
hard compile error; declare events with `abi = cbor` (they ride CBOR
|
||||
internally regardless of the library default).
|
||||
- **Enums are CBOR-only.** A `{.ffi.}` enum in an `abi = c` library, or reached
|
||||
from an `abi = c` type or proc, is a hard compile error naming the type.
|
||||
- **All-scalar `abi = c` procs bind only in the `abi = c` C header.** A
|
||||
`{.ffi: "abi = c".}` method whose params and return are all scalars — ints,
|
||||
floats, bools; a `string` return is fine, a `string` param is not — takes a
|
||||
|
||||
@ -14,6 +14,12 @@
|
||||
terminal RET_OK/RET_ERR. Ignore it unless you want progress. */
|
||||
#define NIMFFI_RET_STALE_WARN 3
|
||||
|
||||
/* ============================================================ */
|
||||
/* Generated constants */
|
||||
/* ============================================================ */
|
||||
|
||||
static const int64_t MAX_SHOUT_LEN = 512;
|
||||
|
||||
/* `abi = c` wire structs — the C ABI. Strings are borrowed, NUL-terminated
|
||||
`const char*` valid only for the duration of the call they cross. */
|
||||
typedef struct {
|
||||
|
||||
@ -2,6 +2,12 @@
|
||||
#define NIM_FFI_LIB_ECHO_H_INCLUDED
|
||||
#include "nim_ffi_cbor.h"
|
||||
|
||||
/* ============================================================ */
|
||||
/* Generated constants */
|
||||
/* ============================================================ */
|
||||
|
||||
static const int64_t MAX_SHOUT_LEN = 512;
|
||||
|
||||
/* ============================================================ */
|
||||
/* Generated types (user-declared + per-proc request envelopes) */
|
||||
/* ============================================================ */
|
||||
|
||||
@ -292,6 +292,12 @@ inline Result<T> decodeCborFFI(const std::vector<std::uint8_t>& bytes) {
|
||||
|
||||
#endif // NIM_FFI_CBOR_HELPERS_HPP_INCLUDED
|
||||
|
||||
// ============================================================
|
||||
// Generated constants
|
||||
// ============================================================
|
||||
|
||||
constexpr int64_t MAX_SHOUT_LEN = 512;
|
||||
|
||||
// ============================================================
|
||||
// User-declared FFI types
|
||||
// ============================================================
|
||||
|
||||
@ -12,6 +12,9 @@ when defined(ffiEchoAbiC):
|
||||
else:
|
||||
declareLibrary("echo", Echo)
|
||||
|
||||
# Constants never cross the wire, so {.ffiConst.} lands in the abi = c header too.
|
||||
const MaxShoutLen* {.ffiConst.} = 512
|
||||
|
||||
type EchoConfig {.ffi.} = object
|
||||
prefix: string
|
||||
|
||||
@ -31,6 +34,8 @@ proc echoShout*(
|
||||
e: Echo, req: ShoutRequest
|
||||
): Future[Result[ShoutResponse, string]] {.ffi.} =
|
||||
## Upper-cases `req.text` and returns it behind the context's prefix.
|
||||
if req.text.len > MaxShoutLen:
|
||||
return err("text must not exceed " & $MaxShoutLen & " bytes")
|
||||
await sleepAsync(1.milliseconds)
|
||||
let upper = req.text.toUpperAscii
|
||||
return ok(ShoutResponse(shouted: e.prefix & ": " & upper, prefix: e.prefix))
|
||||
|
||||
@ -116,7 +116,8 @@ static void on_schedule(int ec, const ScheduleResult* reply, const char* em, voi
|
||||
w->err_code = ec;
|
||||
if (reply) {
|
||||
w->num_a = (long long)reply->willRunCount;
|
||||
w->num_b = (long long)reply->firstRunAtMs;
|
||||
w->num_b = (long long)reply->effectiveBackoffMs;
|
||||
w->flag = (int)reply->priority;
|
||||
if (reply->jobId.data)
|
||||
snprintf(w->text_a, sizeof(w->text_a), "%s", reply->jobId.data);
|
||||
}
|
||||
@ -160,6 +161,9 @@ int main(void) {
|
||||
RUN(my_timer_ctx_version(ctx, on_version, &w), w);
|
||||
printf("[2] Version: %s\n", w.text_a);
|
||||
|
||||
printf("[2b] Header consts: TIMER_VERSION=%s, MAX_DELAY_MS=%lld\n", TIMER_VERSION,
|
||||
(long long)MAX_DELAY_MS);
|
||||
|
||||
EchoRequest echo_req = {nimffi_str("hello from C"), 50};
|
||||
RUN(my_timer_ctx_echo(ctx, &echo_req, on_echo, &w), w);
|
||||
printf("[3] Echo: echoed=%s, timerName=%s\n", w.text_a, w.text_b);
|
||||
@ -188,7 +192,7 @@ int main(void) {
|
||||
job.name = nimffi_str("nightly-rollup");
|
||||
job.payload.data = job_payload;
|
||||
job.payload.len = 2;
|
||||
job.priority = 10;
|
||||
job.priority = JOB_PRIORITY_JP_HIGH;
|
||||
|
||||
NimFfiStr retry_on[2] = {nimffi_str("timeout"), nimffi_str("5xx")};
|
||||
RetryPolicy retry;
|
||||
@ -204,8 +208,9 @@ int main(void) {
|
||||
schedule.jitter.value = 250;
|
||||
|
||||
RUN(my_timer_ctx_schedule(ctx, &job, &retry, &schedule, on_schedule, &w), w);
|
||||
printf("[5] Schedule: jobId=%s, willRunCount=%lld, firstRunAtMs=%lld\n",
|
||||
w.text_a, w.num_a, w.num_b);
|
||||
printf("[5] Schedule: jobId=%s, willRunCount=%lld, effectiveBackoffMs=%lld, "
|
||||
"priority=%d\n",
|
||||
w.text_a, w.num_a, w.num_b, w.flag);
|
||||
|
||||
uint64_t handle =
|
||||
my_timer_ctx_add_on_echo_fired_listener(ctx, on_echo_fired, NULL);
|
||||
|
||||
@ -2,6 +2,14 @@
|
||||
#define NIM_FFI_LIB_MY_TIMER_H_INCLUDED
|
||||
#include "nim_ffi_cbor.h"
|
||||
|
||||
/* ============================================================ */
|
||||
/* Generated constants */
|
||||
/* ============================================================ */
|
||||
|
||||
static const int64_t MAX_DELAY_MS = 5000;
|
||||
static const uint32_t DEFAULT_BACKOFF_MS = 250;
|
||||
static const char* const TIMER_VERSION = "nim-timer v0.1.0";
|
||||
|
||||
/* ============================================================ */
|
||||
/* Generated types (user-declared + per-proc request envelopes) */
|
||||
/* ============================================================ */
|
||||
@ -48,10 +56,15 @@ typedef struct {
|
||||
NimFfiStr message;
|
||||
int64_t echoCount;
|
||||
} EchoEvent;
|
||||
typedef enum {
|
||||
JOB_PRIORITY_JP_LOW = 0,
|
||||
JOB_PRIORITY_JP_NORMAL = 1,
|
||||
JOB_PRIORITY_JP_HIGH = 2
|
||||
} JobPriority;
|
||||
typedef struct {
|
||||
NimFfiStr name;
|
||||
MyTimerSeq_Str payload;
|
||||
int64_t priority;
|
||||
JobPriority priority;
|
||||
} JobSpec;
|
||||
typedef struct {
|
||||
int64_t maxAttempts;
|
||||
@ -68,6 +81,7 @@ typedef struct {
|
||||
int64_t willRunCount;
|
||||
int64_t firstRunAtMs;
|
||||
int64_t effectiveBackoffMs;
|
||||
JobPriority priority;
|
||||
} ScheduleResult;
|
||||
typedef struct {
|
||||
TimerConfig config;
|
||||
@ -431,6 +445,32 @@ static inline void my_timer_free_EchoEvent(EchoEvent* v) {
|
||||
if (!v) return;
|
||||
nimffi_free_str(&v->message);
|
||||
}
|
||||
static inline CborError my_timer_enc_JobPriority(
|
||||
CborEncoder* e, const JobPriority* v) {
|
||||
switch (*v) {
|
||||
case JOB_PRIORITY_JP_LOW: return cbor_encode_text_stringz(e, "low");
|
||||
case JOB_PRIORITY_JP_NORMAL: return cbor_encode_text_stringz(e, "normal");
|
||||
case JOB_PRIORITY_JP_HIGH: return cbor_encode_text_stringz(e, "high");
|
||||
}
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
static inline CborError my_timer_dec_JobPriority(
|
||||
CborValue* it, JobPriority* out) {
|
||||
if (!cbor_value_is_text_string(it)) return CborErrorImproperValue;
|
||||
size_t len = 0;
|
||||
CborError err = cbor_value_get_string_length(it, &len);
|
||||
if (err) return err;
|
||||
char buf[7];
|
||||
if (len >= sizeof(buf)) return CborErrorImproperValue;
|
||||
size_t copied = sizeof(buf);
|
||||
err = cbor_value_copy_text_string(it, buf, &copied, NULL);
|
||||
if (err) return err;
|
||||
buf[len] = '\0';
|
||||
if (strcmp(buf, "low") == 0) { *out = JOB_PRIORITY_JP_LOW; return cbor_value_advance(it); }
|
||||
if (strcmp(buf, "normal") == 0) { *out = JOB_PRIORITY_JP_NORMAL; return cbor_value_advance(it); }
|
||||
if (strcmp(buf, "high") == 0) { *out = JOB_PRIORITY_JP_HIGH; return cbor_value_advance(it); }
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
static inline CborError my_timer_enc_JobSpec(
|
||||
CborEncoder* e, const JobSpec* v) {
|
||||
CborEncoder m;
|
||||
@ -446,7 +486,7 @@ static inline CborError my_timer_enc_JobSpec(
|
||||
if (err) return err;
|
||||
err = cbor_encode_text_stringz(&m, "priority");
|
||||
if (err) return err;
|
||||
err = nimffi_enc_i64(&m, &v->priority);
|
||||
err = my_timer_enc_JobPriority(&m, &v->priority);
|
||||
if (err) return err;
|
||||
return cbor_encoder_close_container(e, &m);
|
||||
}
|
||||
@ -468,7 +508,7 @@ static inline CborError my_timer_dec_JobSpec(
|
||||
err = cbor_value_map_find_value(it, "priority", &field);
|
||||
if (err) return err;
|
||||
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
|
||||
err = nimffi_dec_i64(&field, &out->priority);
|
||||
err = my_timer_dec_JobPriority(&field, &out->priority);
|
||||
if (err) return err;
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
@ -566,7 +606,7 @@ static inline CborError my_timer_dec_ScheduleConfig(
|
||||
static inline CborError my_timer_enc_ScheduleResult(
|
||||
CborEncoder* e, const ScheduleResult* v) {
|
||||
CborEncoder m;
|
||||
CborError err = cbor_encoder_create_map(e, &m, 4);
|
||||
CborError err = cbor_encoder_create_map(e, &m, 5);
|
||||
if (err) return err;
|
||||
err = cbor_encode_text_stringz(&m, "jobId");
|
||||
if (err) return err;
|
||||
@ -584,6 +624,10 @@ static inline CborError my_timer_enc_ScheduleResult(
|
||||
if (err) return err;
|
||||
err = nimffi_enc_i64(&m, &v->effectiveBackoffMs);
|
||||
if (err) return err;
|
||||
err = cbor_encode_text_stringz(&m, "priority");
|
||||
if (err) return err;
|
||||
err = my_timer_enc_JobPriority(&m, &v->priority);
|
||||
if (err) return err;
|
||||
return cbor_encoder_close_container(e, &m);
|
||||
}
|
||||
static inline CborError my_timer_dec_ScheduleResult(
|
||||
@ -611,6 +655,11 @@ static inline CborError my_timer_dec_ScheduleResult(
|
||||
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
|
||||
err = nimffi_dec_i64(&field, &out->effectiveBackoffMs);
|
||||
if (err) return err;
|
||||
err = cbor_value_map_find_value(it, "priority", &field);
|
||||
if (err) return err;
|
||||
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
|
||||
err = my_timer_dec_JobPriority(&field, &out->priority);
|
||||
if (err) return err;
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
static inline void my_timer_free_ScheduleResult(ScheduleResult* v) {
|
||||
|
||||
@ -9,10 +9,11 @@ EchoResponse = { echoed: tstr, timerName: tstr }
|
||||
ComplexRequest = { messages: [* EchoRequest], tags: [* tstr], note: tstr / nil, retries: int / nil }
|
||||
ComplexResponse = { summary: tstr, itemCount: int, hasNote: bool }
|
||||
EchoEvent = { message: tstr, echoCount: int }
|
||||
JobSpec = { name: tstr, payload: [* tstr], priority: int }
|
||||
JobPriority = "low" / "normal" / "high"
|
||||
JobSpec = { name: tstr, payload: [* tstr], priority: JobPriority }
|
||||
RetryPolicy = { maxAttempts: int, backoffMs: int, retryOn: [* tstr] }
|
||||
ScheduleConfig = { startAtMs: int, intervalMs: int, jitter: int / nil }
|
||||
ScheduleResult = { jobId: tstr, willRunCount: int, firstRunAtMs: int, effectiveBackoffMs: int }
|
||||
ScheduleResult = { jobId: tstr, willRunCount: int, firstRunAtMs: int, effectiveBackoffMs: int, priority: JobPriority }
|
||||
|
||||
; ─── Request envelopes (one CBOR blob per request) ────────────────
|
||||
MyTimerCreateCtorReq = { config: TimerConfig }
|
||||
|
||||
@ -25,7 +25,8 @@ int main() {
|
||||
std::cerr << "Error: " << version.error() << "\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "[2] Version: " << version.value() << "\n";
|
||||
std::cout << "[2] Version: " << version.value() << " (const TIMER_VERSION="
|
||||
<< TIMER_VERSION << ", MAX_DELAY_MS=" << MAX_DELAY_MS << ")\n";
|
||||
|
||||
auto echo = echo1Future.get();
|
||||
if (echo.isErr()) {
|
||||
@ -66,7 +67,7 @@ int main() {
|
||||
auto job = JobSpec{
|
||||
/*name*/ "nightly-rollup",
|
||||
/*payload*/ std::vector<std::string>{"rollup", "v2"},
|
||||
/*priority*/ 10,
|
||||
JobPriority::jpHigh,
|
||||
};
|
||||
auto retry = RetryPolicy{
|
||||
/*maxAttempts*/ 3,
|
||||
@ -88,7 +89,7 @@ int main() {
|
||||
<< ", willRunCount=" << scheduleRes->willRunCount
|
||||
<< ", firstRunAtMs=" << scheduleRes->firstRunAtMs
|
||||
<< ", effectiveBackoffMs=" << scheduleRes->effectiveBackoffMs
|
||||
<< "\n";
|
||||
<< ", priority=" << static_cast<int>(scheduleRes->priority) << "\n";
|
||||
|
||||
// Each `{.ffiEvent.}` declared on the Nim side gets a typed
|
||||
// registration method — `addOnEchoFiredListener(handler)` here.
|
||||
|
||||
@ -293,6 +293,37 @@ inline Result<T> decodeCborFFI(const std::vector<std::uint8_t>& bytes) {
|
||||
|
||||
#endif // NIM_FFI_CBOR_HELPERS_HPP_INCLUDED
|
||||
|
||||
// ============================================================
|
||||
// Generated constants
|
||||
// ============================================================
|
||||
|
||||
constexpr int64_t MAX_DELAY_MS = 5000;
|
||||
constexpr uint32_t DEFAULT_BACKOFF_MS = 250;
|
||||
constexpr const char* TIMER_VERSION = "nim-timer v0.1.0";
|
||||
|
||||
enum class JobPriority {
|
||||
jpLow = 0,
|
||||
jpNormal = 1,
|
||||
jpHigh = 2,
|
||||
};
|
||||
inline CborError encode_cbor(CborEncoder& e, const JobPriority& v) {
|
||||
switch (v) {
|
||||
case JobPriority::jpLow: return cbor_encode_text_stringz(&e, "low");
|
||||
case JobPriority::jpNormal: return cbor_encode_text_stringz(&e, "normal");
|
||||
case JobPriority::jpHigh: return cbor_encode_text_stringz(&e, "high");
|
||||
}
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
inline CborError decode_cbor(CborValue& it, JobPriority& v) {
|
||||
std::string name;
|
||||
CborError err = decode_cbor(it, name);
|
||||
if (err) return err;
|
||||
if (name == "low") { v = JobPriority::jpLow; return CborNoError; }
|
||||
if (name == "normal") { v = JobPriority::jpNormal; return CborNoError; }
|
||||
if (name == "high") { v = JobPriority::jpHigh; return CborNoError; }
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// User-declared FFI types
|
||||
// ============================================================
|
||||
@ -474,7 +505,7 @@ inline CborError decode_cbor(CborValue& it, EchoEvent& v) {
|
||||
struct JobSpec {
|
||||
std::string name;
|
||||
std::vector<std::string> payload;
|
||||
int64_t priority;
|
||||
JobPriority priority;
|
||||
};
|
||||
inline CborError encode_cbor(CborEncoder& e, const JobSpec& v) {
|
||||
CborEncoder m;
|
||||
@ -575,10 +606,11 @@ struct ScheduleResult {
|
||||
int64_t willRunCount;
|
||||
int64_t firstRunAtMs;
|
||||
int64_t effectiveBackoffMs;
|
||||
JobPriority priority;
|
||||
};
|
||||
inline CborError encode_cbor(CborEncoder& e, const ScheduleResult& v) {
|
||||
CborEncoder m;
|
||||
CborError err = cbor_encoder_create_map(&e, &m, 4);
|
||||
CborError err = cbor_encoder_create_map(&e, &m, 5);
|
||||
if (err) return err;
|
||||
err = cbor_encode_text_stringz(&m, "jobId"); if (err) return err;
|
||||
err = encode_cbor(m, v.jobId); if (err) return err;
|
||||
@ -588,6 +620,8 @@ inline CborError encode_cbor(CborEncoder& e, const ScheduleResult& v) {
|
||||
err = encode_cbor(m, v.firstRunAtMs); if (err) return err;
|
||||
err = cbor_encode_text_stringz(&m, "effectiveBackoffMs"); if (err) return err;
|
||||
err = encode_cbor(m, v.effectiveBackoffMs); if (err) return err;
|
||||
err = cbor_encode_text_stringz(&m, "priority"); if (err) return err;
|
||||
err = encode_cbor(m, v.priority); if (err) return err;
|
||||
return cbor_encoder_close_container(&e, &m);
|
||||
}
|
||||
inline CborError decode_cbor(CborValue& it, ScheduleResult& v) {
|
||||
@ -606,6 +640,9 @@ inline CborError decode_cbor(CborValue& it, ScheduleResult& v) {
|
||||
err = cbor_value_map_find_value(&it, "effectiveBackoffMs", &field); if (err) return err;
|
||||
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
|
||||
err = decode_cbor(field, v.effectiveBackoffMs); if (err) return err;
|
||||
err = cbor_value_map_find_value(&it, "priority", &field); if (err) return err;
|
||||
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
|
||||
err = decode_cbor(field, v.priority); if (err) return err;
|
||||
return cbor_value_advance(&it);
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,19 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const MAX_DELAY_MS: i64 = 5000;
|
||||
pub const DEFAULT_BACKOFF_MS: u32 = 250;
|
||||
pub const TIMER_VERSION: &str = "nim-timer v0.1.0";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum JobPriority {
|
||||
#[serde(rename = "low")]
|
||||
JpLow,
|
||||
#[serde(rename = "normal")]
|
||||
JpNormal,
|
||||
#[serde(rename = "high")]
|
||||
JpHigh,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TimerConfig {
|
||||
pub name: String,
|
||||
@ -47,7 +61,7 @@ pub struct EchoEvent {
|
||||
pub struct JobSpec {
|
||||
pub name: String,
|
||||
pub payload: Vec<String>,
|
||||
pub priority: i64,
|
||||
pub priority: JobPriority,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@ -79,6 +93,7 @@ pub struct ScheduleResult {
|
||||
pub first_run_at_ms: i64,
|
||||
#[serde(rename = "effectiveBackoffMs")]
|
||||
pub effective_backoff_ms: i64,
|
||||
pub priority: JobPriority,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
@ -7,7 +7,8 @@
|
||||
// nimble genbindings_rust
|
||||
use std::time::Duration;
|
||||
use my_timer::{
|
||||
EchoRequest, JobSpec, MyTimerCtx, RetryPolicy, ScheduleConfig, TimerConfig,
|
||||
EchoRequest, JobPriority, JobSpec, MyTimerCtx, RetryPolicy, ScheduleConfig,
|
||||
TimerConfig, MAX_DELAY_MS, TIMER_VERSION,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
@ -21,6 +22,8 @@ fn main() {
|
||||
// ── 2. Sync call: version ──────────────────────────────────────────────
|
||||
let version = ctx.version().expect("my_timer_version failed");
|
||||
println!("[2] Version (sync call, callback fired inline): {version}");
|
||||
assert_eq!(version, TIMER_VERSION);
|
||||
println!("[2b] Consts from the bindings: MAX_DELAY_MS={MAX_DELAY_MS}");
|
||||
|
||||
// ── 3. Async call: echo (200 ms delay) ────────────────────────────────
|
||||
let echo = ctx
|
||||
@ -52,7 +55,7 @@ fn main() {
|
||||
JobSpec {
|
||||
name: "nightly-rollup".into(),
|
||||
payload: vec!["rollup".into(), "v2".into()],
|
||||
priority: 10,
|
||||
priority: JobPriority::JpHigh,
|
||||
},
|
||||
RetryPolicy {
|
||||
max_attempts: 3,
|
||||
@ -67,11 +70,12 @@ fn main() {
|
||||
)
|
||||
.expect("my_timer_schedule failed");
|
||||
println!(
|
||||
"[5] Schedule (3 complex params): jobId={}, willRunCount={}, firstRunAtMs={}, effectiveBackoffMs={}",
|
||||
"[5] Schedule (3 complex params): jobId={}, willRunCount={}, firstRunAtMs={}, effectiveBackoffMs={}, priority={:?}",
|
||||
schedule.job_id,
|
||||
schedule.will_run_count,
|
||||
schedule.first_run_at_ms,
|
||||
schedule.effective_backoff_ms,
|
||||
schedule.priority,
|
||||
);
|
||||
|
||||
println!("\nDone. The Nim FFI thread and watchdog are still running.");
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use std::time::Duration;
|
||||
use my_timer::{
|
||||
EchoRequest, JobSpec, MyTimerCtx, RetryPolicy, ScheduleConfig, TimerConfig,
|
||||
EchoRequest, JobPriority, JobSpec, MyTimerCtx, RetryPolicy, ScheduleConfig,
|
||||
TimerConfig,
|
||||
};
|
||||
|
||||
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
|
||||
@ -40,7 +41,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
JobSpec {
|
||||
name: "hourly-sync".into(),
|
||||
payload: vec!["sync".into(), "users".into()],
|
||||
priority: 5,
|
||||
priority: JobPriority::JpNormal,
|
||||
},
|
||||
RetryPolicy {
|
||||
max_attempts: 5,
|
||||
@ -55,8 +56,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
)
|
||||
.await?;
|
||||
println!(
|
||||
"[5] Schedule (3 complex params, awaited): jobId={}, willRunCount={}, firstRunAtMs={}",
|
||||
schedule.job_id, schedule.will_run_count, schedule.first_run_at_ms,
|
||||
"[5] Schedule (3 complex params, awaited): jobId={}, willRunCount={}, firstRunAtMs={}, priority={:?}",
|
||||
schedule.job_id, schedule.will_run_count, schedule.first_run_at_ms, schedule.priority,
|
||||
);
|
||||
|
||||
println!("\nDone. Tokio runtime shut down.");
|
||||
|
||||
@ -9,6 +9,12 @@ type MyTimer = object
|
||||
# `defaultABIFormat` is the wire format every annotation inherits (override per-annotation with "abi = ...").
|
||||
declareLibrary("my_timer", MyTimer, defaultABIFormat = "cbor")
|
||||
|
||||
# {.ffiConst.}: re-emitted as a native constant in every binding.
|
||||
const
|
||||
MaxDelayMs* {.ffiConst.} = 5_000
|
||||
DefaultBackoffMs* {.ffiConst.}: uint32 = 250
|
||||
TimerVersion* {.ffiConst.} = "nim-timer v0.1.0"
|
||||
|
||||
type TimerConfig {.ffi.} = object
|
||||
name: string
|
||||
|
||||
@ -48,6 +54,8 @@ proc myTimerEcho*(
|
||||
timer: MyTimer, req: EchoRequest
|
||||
): Future[Result[EchoResponse, string]] {.ffi.} =
|
||||
## Sleeps `delayMs` then echoes the message back, firing `on_echo_fired`.
|
||||
if req.delayMs > MaxDelayMs:
|
||||
return err("delayMs must not exceed " & $MaxDelayMs)
|
||||
await sleepAsync(req.delayMs.milliseconds)
|
||||
onEchoFired(EchoEvent(message: req.message, echoCount: 1))
|
||||
return ok(EchoResponse(echoed: req.message, timerName: timer.name))
|
||||
@ -55,7 +63,7 @@ proc myTimerEcho*(
|
||||
# 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")
|
||||
return ok(TimerVersion)
|
||||
|
||||
proc myTimerComplex*(
|
||||
timer: MyTimer, req: ComplexRequest
|
||||
@ -68,11 +76,17 @@ proc myTimerComplex*(
|
||||
return
|
||||
ok(ComplexResponse(summary: summary, itemCount: count, hasNote: req.note.isSome))
|
||||
|
||||
# {.ffi.} on an enum: a native enum in every binding, crossing the wire as text.
|
||||
type JobPriority {.ffi.} = enum
|
||||
jpLow = "low"
|
||||
jpNormal = "normal"
|
||||
jpHigh = "high"
|
||||
|
||||
# Multiple object-typed params: each is its own {.ffi.} type, all carried in one per-proc Req envelope.
|
||||
type JobSpec {.ffi.} = object
|
||||
name: string
|
||||
payload: seq[string]
|
||||
priority: int # higher = runs sooner
|
||||
priority: JobPriority
|
||||
|
||||
type RetryPolicy {.ffi.} = object
|
||||
maxAttempts: int
|
||||
@ -89,6 +103,17 @@ type ScheduleResult {.ffi.} = object
|
||||
willRunCount: int
|
||||
firstRunAtMs: int
|
||||
effectiveBackoffMs: int
|
||||
priority: JobPriority
|
||||
|
||||
func effectiveBackoff(priority: JobPriority, backoffMs: int): int =
|
||||
let base = if backoffMs > 0: backoffMs else: DefaultBackoffMs.int
|
||||
case priority
|
||||
of jpHigh:
|
||||
return base div 2
|
||||
of jpNormal:
|
||||
return base
|
||||
of jpLow:
|
||||
return base * 2
|
||||
|
||||
proc myTimerSchedule*(
|
||||
timer: MyTimer, job: JobSpec, retry: RetryPolicy, schedule: ScheduleConfig
|
||||
@ -110,7 +135,8 @@ proc myTimerSchedule*(
|
||||
jobId: timer.name & ":" & job.name,
|
||||
willRunCount: willRunCount,
|
||||
firstRunAtMs: schedule.startAtMs + jitter,
|
||||
effectiveBackoffMs: retry.backoffMs,
|
||||
effectiveBackoffMs: effectiveBackoff(job.priority, retry.backoffMs),
|
||||
priority: job.priority,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
## generics, each distinct `seq[T]`/`Option[T]` is monomorphised per type.
|
||||
|
||||
import std/[os, strutils, tables, sets, options]
|
||||
import ./meta, ./string_helpers, ./c_cpp_common, ./types_ir
|
||||
import ./meta, ./string_helpers, ./c_cpp_common, ./types_ir, ./consts
|
||||
|
||||
## Fixed 64-bit wire type for any Nim `ptr T`/`pointer` (mirrors CppPtrType).
|
||||
const CPtrType* = "uint64_t"
|
||||
@ -174,6 +174,60 @@ proc emitOptType(reg: var CTypeReg, name, elemC: string, elemOwns: bool) =
|
||||
|
||||
proc ensureCType(reg: var CTypeReg, nimType: string): tuple[cType: string, owns: bool]
|
||||
|
||||
func enumConstName*(typeName, valueName: string): string =
|
||||
## C/CDDL-safe constant name for an enum value, e.g. ("Color", "cRed") → COLOR_C_RED.
|
||||
return identToUpperSnake(typeName) & "_" & identToUpperSnake(valueName)
|
||||
|
||||
proc emitEnumType(reg: var CTypeReg, t: FFITypeMeta) =
|
||||
## A `{.ffi.}` enum becomes a C enum whose codec maps to the CBOR text form
|
||||
## (the value's Nim symbol name, or its associated string) that
|
||||
## cbor_serialization writes.
|
||||
var members: seq[string] = @[]
|
||||
for v in t.enumValues:
|
||||
members.add(" " & enumConstName(t.name, v.name) & " = " & $v.ord & ",")
|
||||
members[^1].removeSuffix(',')
|
||||
reg.decls.add("typedef enum {\n" & members.join("\n") & "\n} " & t.name & ";")
|
||||
|
||||
var longest = 0
|
||||
for v in t.enumValues:
|
||||
longest = max(longest, v.wire.len)
|
||||
|
||||
var body: seq[string] = @[]
|
||||
body.add("static inline CborError " & reg.libName & "_enc_" & t.name & "(")
|
||||
body.add(" CborEncoder* e, const " & t.name & "* v) {")
|
||||
body.add(" switch (*v) {")
|
||||
for v in t.enumValues:
|
||||
body.add(
|
||||
" case " & enumConstName(t.name, v.name) &
|
||||
": return cbor_encode_text_stringz(e, \"" & v.wire & "\");"
|
||||
)
|
||||
body.add(" }")
|
||||
body.add(" return CborErrorImproperValue;")
|
||||
body.add("}")
|
||||
|
||||
body.add("static inline CborError " & reg.libName & "_dec_" & t.name & "(")
|
||||
body.add(" CborValue* it, " & t.name & "* out) {")
|
||||
body.add(" if (!cbor_value_is_text_string(it)) return CborErrorImproperValue;")
|
||||
body.add(" size_t len = 0;")
|
||||
body.add(" CborError err = cbor_value_get_string_length(it, &len);")
|
||||
body.add(" if (err) return err;")
|
||||
body.add(" char buf[" & $(longest + 1) & "];")
|
||||
body.add(" if (len >= sizeof(buf)) return CborErrorImproperValue;")
|
||||
body.add(" size_t copied = sizeof(buf);")
|
||||
body.add(" err = cbor_value_copy_text_string(it, buf, &copied, NULL);")
|
||||
body.add(" if (err) return err;")
|
||||
body.add(" buf[len] = '\\0';")
|
||||
for v in t.enumValues:
|
||||
body.add(
|
||||
" if (strcmp(buf, \"" & v.wire & "\") == 0) { *out = " &
|
||||
enumConstName(t.name, v.name) & "; return cbor_value_advance(it); }"
|
||||
)
|
||||
body.add(" return CborErrorImproperValue;")
|
||||
body.add("}")
|
||||
|
||||
reg.codecs.add(body.join("\n"))
|
||||
reg.owns[t.name] = false
|
||||
|
||||
proc emitStructType(reg: var CTypeReg, t: FFITypeMeta) =
|
||||
var fieldDecls: seq[string] = @[]
|
||||
var members: seq[tuple[name, cType: string, owns: bool]] = @[]
|
||||
@ -269,7 +323,11 @@ proc ensureCType(reg: var CTypeReg, t: FFIType): tuple[cType: string, owns: bool
|
||||
if name notin reg.emitted:
|
||||
reg.emitted.incl(name)
|
||||
if name in reg.typeTable:
|
||||
emitStructType(reg, reg.typeTable[name])
|
||||
let meta = reg.typeTable[name]
|
||||
if meta.isEnum():
|
||||
emitEnumType(reg, meta)
|
||||
else:
|
||||
emitStructType(reg, meta)
|
||||
else:
|
||||
reg.decls.add("/* unknown type referenced: " & name & " */")
|
||||
return (name, reg.owns.getOrDefault(name, false))
|
||||
@ -285,11 +343,14 @@ proc reqTypeMeta(p: FFIProcMeta): FFITypeMeta =
|
||||
fields.add(FFIFieldMeta(name: ep.name, typeName: typeName))
|
||||
return FFITypeMeta(name: reqStructName(p), fields: fields)
|
||||
|
||||
func paramByValue(nimType: string, ridesAsPtr: bool): bool =
|
||||
## Scalars/pointers/string views pass by value; aggregates by const pointer.
|
||||
func paramByValue(reg: CTypeReg, nimType: string, ridesAsPtr: bool): bool =
|
||||
## Scalars/pointers/string views and enums pass by value; aggregates by const pointer.
|
||||
if ridesAsPtr:
|
||||
return true
|
||||
return parseFFIType(nimType).kind in {ftScalar, ftStr, ftPtr}
|
||||
let t = parseFFIType(nimType)
|
||||
if t.kind == ftStruct and reg.typeTable.getOrDefault(t.name).isEnum():
|
||||
return true
|
||||
return t.kind in {ftScalar, ftStr, ftPtr}
|
||||
|
||||
proc cReturnType(reg: var CTypeReg, p: FFIProcMeta): string =
|
||||
if p.returnRidesAsPtr():
|
||||
@ -308,7 +369,7 @@ proc buildReqParams(
|
||||
CPtrType
|
||||
else:
|
||||
ensureCType(reg, ep.typeName).cType
|
||||
if paramByValue(ep.typeName, rides):
|
||||
if paramByValue(reg, ep.typeName, rides):
|
||||
params.add(cType & " " & ep.name)
|
||||
assigns.add(" ffi_req." & ep.name & " = " & ep.name & ";")
|
||||
else:
|
||||
@ -746,6 +807,33 @@ proc monomorphiseAll(
|
||||
discard ensureCType(reg, ev.payloadTypeName)
|
||||
return (reqTypes, respTypes)
|
||||
|
||||
func constDeclLines(consts: seq[FFIConstMeta]): seq[string] =
|
||||
## `{.ffiConst.}` values as typed `static const` definitions; shared by the
|
||||
## CBOR and `abi = c` headers.
|
||||
if consts.len == 0:
|
||||
return @[]
|
||||
var lines = @[
|
||||
"/* ============================================================ */",
|
||||
"/* Generated constants */",
|
||||
"/* ============================================================ */", "",
|
||||
]
|
||||
for c in consts:
|
||||
let t = parseFFIType(c.typeName)
|
||||
let name = identToUpperSnake(c.name)
|
||||
let value = cConstValue(t, c.value)
|
||||
case t.kind
|
||||
of ftStr:
|
||||
lines.add("static const char* const " & name & " = " & value & ";")
|
||||
of ftScalar:
|
||||
lines.add(
|
||||
"static const " & scalarCInfoTable[t.scalar].cType & " " & name & " = " & value &
|
||||
";"
|
||||
)
|
||||
else:
|
||||
discard
|
||||
lines.add("")
|
||||
return lines
|
||||
|
||||
func generateCPreludeHeader*(): string =
|
||||
## The library-agnostic `nim_ffi_prelude.h`, emitted verbatim.
|
||||
return HeaderPreludeTpl & "\n"
|
||||
@ -759,6 +847,7 @@ proc generateCLibHeader*(
|
||||
types: seq[FFITypeMeta],
|
||||
libName: string,
|
||||
events: seq[FFIEventMeta] = @[],
|
||||
consts: seq[FFIConstMeta] = @[],
|
||||
): string =
|
||||
## The `<lib>.h` header: library structs, monomorphised codecs and async API.
|
||||
let classified = classifyProcs(procs)
|
||||
@ -777,6 +866,8 @@ proc generateCLibHeader*(
|
||||
lines.add("#include \"" & CborHeaderName & "\"")
|
||||
lines.add("")
|
||||
|
||||
lines.add(constDeclLines(consts))
|
||||
|
||||
lines.add("/* ============================================================ */")
|
||||
lines.add("/* Generated types (user-declared + per-proc request envelopes) */")
|
||||
lines.add("/* ============================================================ */")
|
||||
@ -1362,6 +1453,7 @@ proc generateCAbiLibHeader*(
|
||||
types: seq[FFITypeMeta],
|
||||
libName: string,
|
||||
events: seq[FFIEventMeta] = @[],
|
||||
consts: seq[FFIConstMeta] = @[],
|
||||
): string =
|
||||
if events.len > 0:
|
||||
raise newException(
|
||||
@ -1398,6 +1490,7 @@ proc generateCAbiLibHeader*(
|
||||
lines.add(" terminal RET_OK/RET_ERR. Ignore it unless you want progress. */")
|
||||
lines.add("#define NIMFFI_RET_STALE_WARN 3")
|
||||
lines.add("")
|
||||
lines.add(constDeclLines(consts))
|
||||
lines.add(
|
||||
"/* `abi = c` wire structs — the C ABI. Strings are borrowed, NUL-terminated"
|
||||
)
|
||||
@ -1451,13 +1544,15 @@ proc generateCBindings*(
|
||||
outputDir: string,
|
||||
nimSrcRelPath: string,
|
||||
events: seq[FFIEventMeta] = @[],
|
||||
consts: seq[FFIConstMeta] = @[],
|
||||
) =
|
||||
## Emits the C binding for `libName`, picking the `abi = c` or CBOR shape.
|
||||
createDir(outputDir)
|
||||
case libWireFormat(procs, types)
|
||||
of ABIFormat.C:
|
||||
writeFile(
|
||||
outputDir / (libName & ".h"), generateCAbiLibHeader(procs, types, libName, events)
|
||||
outputDir / (libName & ".h"),
|
||||
generateCAbiLibHeader(procs, types, libName, events, consts),
|
||||
)
|
||||
writeFile(
|
||||
outputDir / "CMakeLists.txt", generateCAbiCMakeLists(libName, nimSrcRelPath)
|
||||
@ -1466,6 +1561,7 @@ proc generateCBindings*(
|
||||
writeFile(outputDir / PreludeHeaderName, generateCPreludeHeader())
|
||||
writeFile(outputDir / CborHeaderName, generateCCborHeader())
|
||||
writeFile(
|
||||
outputDir / (libName & ".h"), generateCLibHeader(procs, types, libName, events)
|
||||
outputDir / (libName & ".h"),
|
||||
generateCLibHeader(procs, types, libName, events, consts),
|
||||
)
|
||||
writeFile(outputDir / "CMakeLists.txt", generateCCMakeLists(libName, nimSrcRelPath))
|
||||
|
||||
@ -81,6 +81,14 @@ proc emitMap(
|
||||
parts.add(f.name & ": " & cddlType)
|
||||
"{ " & parts.join(", ") & " }"
|
||||
|
||||
proc emitEnumAlternatives(t: FFITypeMeta): string =
|
||||
## An enum rides as the CBOR text `$value` yields, so the rule is a choice of
|
||||
## string literals.
|
||||
var alts: seq[string] = @[]
|
||||
for v in t.enumValues:
|
||||
alts.add("\"" & v.wire & "\"")
|
||||
alts.join(" / ")
|
||||
|
||||
proc emitObjectFields(t: FFITypeMeta): string =
|
||||
var fields: seq[tuple[name: string, typeName: string, isPtr: bool]] = @[]
|
||||
for f in t.fields:
|
||||
@ -125,7 +133,12 @@ proc generateCddlSchema*(
|
||||
"; ─── User-declared FFI types ──────────────────────────────────────"
|
||||
)
|
||||
for t in types:
|
||||
L.add(t.name & " = " & emitObjectFields(t))
|
||||
let rule =
|
||||
if t.isEnum():
|
||||
emitEnumAlternatives(t)
|
||||
else:
|
||||
emitObjectFields(t)
|
||||
L.add(t.name & " = " & rule)
|
||||
L.add("")
|
||||
|
||||
# Per-proc request envelopes (one CBOR blob per request).
|
||||
|
||||
69
ffi/codegen/consts.nim
Normal file
69
ffi/codegen/consts.nim
Normal file
@ -0,0 +1,69 @@
|
||||
## Literal rendering for `{.ffiConst.}` values, shared by the C/C++/Rust generators.
|
||||
## The registry stores Nim's `$value`; each backend re-quotes it for its syntax.
|
||||
|
||||
import std/strutils
|
||||
import ./types_ir
|
||||
|
||||
func cByteEscape(ch: char): string =
|
||||
## 3-digit octal: C caps an octal escape at 3 digits, so a following digit
|
||||
## can't be swallowed into it the way it can with `\x`.
|
||||
return "\\" & toOct(ord(ch), 3)
|
||||
|
||||
func rustByteEscape(ch: char): string =
|
||||
return "\\x" & toHex(ord(ch), 2)
|
||||
|
||||
func escapeLit(
|
||||
s: string, byteEscape: proc(ch: char): string {.noSideEffect, nimcall.}
|
||||
): string =
|
||||
var escaped = ""
|
||||
for ch in s:
|
||||
case ch
|
||||
of '"':
|
||||
escaped.add("\\\"")
|
||||
of '\\':
|
||||
escaped.add("\\\\")
|
||||
of '\n':
|
||||
escaped.add("\\n")
|
||||
of '\r':
|
||||
escaped.add("\\r")
|
||||
of '\t':
|
||||
escaped.add("\\t")
|
||||
else:
|
||||
if ch < ' ' or ch == '\x7F':
|
||||
escaped.add(byteEscape(ch))
|
||||
else:
|
||||
escaped.add(ch)
|
||||
return escaped
|
||||
|
||||
func cEscapeStringLit*(s: string): string =
|
||||
return escapeLit(s, cByteEscape)
|
||||
|
||||
func rustEscapeStringLit*(s: string): string =
|
||||
return escapeLit(s, rustByteEscape)
|
||||
|
||||
func cConstValue*(t: FFIType, value: string): string =
|
||||
## C/C++ literal. Every emission site is a typed declaration, so the declared
|
||||
## type already fixes the width; only the two cases the type can't rescue get
|
||||
## a suffix — `ULL` because a decimal above `INT64_MAX` fits no signed type,
|
||||
## and `f` because a bare `1.5` is a double and narrowing it warns.
|
||||
case t.kind
|
||||
of ftStr:
|
||||
return "\"" & cEscapeStringLit(value) & "\""
|
||||
of ftScalar:
|
||||
case t.scalar
|
||||
of skU64:
|
||||
return value & "ULL"
|
||||
of skF32:
|
||||
return value & "f"
|
||||
else:
|
||||
return value
|
||||
else:
|
||||
return value
|
||||
|
||||
func rustConstValue*(t: FFIType, value: string): string =
|
||||
## Rust literal; the declared type annotation carries the width, so no suffix.
|
||||
case t.kind
|
||||
of ftStr:
|
||||
return "\"" & rustEscapeStringLit(value) & "\""
|
||||
else:
|
||||
return value
|
||||
@ -1,7 +1,7 @@
|
||||
## C++ binding generator: header-only binding + CMakeLists, CBOR over the wire.
|
||||
|
||||
import std/[os, strutils]
|
||||
import ./meta, ./string_helpers, ./c_cpp_common, ./types_ir
|
||||
import ./meta, ./string_helpers, ./c_cpp_common, ./types_ir, ./consts
|
||||
|
||||
## Fixed 64-bit wire type for any Nim `ptr T` / `pointer`.
|
||||
const CppPtrType* = "uint64_t"
|
||||
@ -46,6 +46,38 @@ const cppMap = NativeTypeMap(
|
||||
proc nimTypeToCpp*(typeName: string): string =
|
||||
renderNative(cppMap, parseFFIType(typeName))
|
||||
|
||||
proc emitEnumCborCodec(lines: var seq[string], t: FFITypeMeta) =
|
||||
## Appends the `enum class` plus its TinyCBOR codec pair. The wire form is the
|
||||
## CBOR text `$value` yields on the Nim side, so the codec maps name ↔ value.
|
||||
lines.add("enum class $1 {" % [t.name])
|
||||
for v in t.enumValues:
|
||||
lines.add(" $1 = $2," % [v.name, $v.ord])
|
||||
lines.add("};")
|
||||
|
||||
lines.add("inline CborError encode_cbor(CborEncoder& e, const $1& v) {" % [t.name])
|
||||
lines.add(" switch (v) {")
|
||||
for v in t.enumValues:
|
||||
lines.add(
|
||||
" case $1::$2: return cbor_encode_text_stringz(&e, \"$3\");" %
|
||||
[t.name, v.name, v.wire]
|
||||
)
|
||||
lines.add(" }")
|
||||
lines.add(" return CborErrorImproperValue;")
|
||||
lines.add("}")
|
||||
|
||||
lines.add("inline CborError decode_cbor(CborValue& it, $1& v) {" % [t.name])
|
||||
lines.add(" std::string name;")
|
||||
lines.add(" CborError err = decode_cbor(it, name);")
|
||||
lines.add(" if (err) return err;")
|
||||
for v in t.enumValues:
|
||||
lines.add(
|
||||
" if (name == \"$1\") { v = $2::$3; return CborNoError; }" %
|
||||
[v.wire, t.name, v.name]
|
||||
)
|
||||
lines.add(" return CborErrorImproperValue;")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
proc emitStructCborCodec(
|
||||
lines: var seq[string], structName: string, fields: seq[(string, string)]
|
||||
) =
|
||||
@ -186,6 +218,7 @@ proc generateCppHeader*(
|
||||
types: seq[FFITypeMeta],
|
||||
libName: string,
|
||||
events: seq[FFIEventMeta] = @[],
|
||||
consts: seq[FFIConstMeta] = @[],
|
||||
): string =
|
||||
var lines: seq[string] = @[]
|
||||
|
||||
@ -198,12 +231,39 @@ proc generateCppHeader*(
|
||||
# Generic CBOR overloads must precede the non-template struct codecs that call them (parse-time name lookup).
|
||||
lines.add(CborHelpersTpl)
|
||||
|
||||
if types.len > 0:
|
||||
if consts.len > 0:
|
||||
lines.add("// ============================================================")
|
||||
lines.add("// Generated constants")
|
||||
lines.add("// ============================================================")
|
||||
lines.add("")
|
||||
for c in consts:
|
||||
let t = parseFFIType(c.typeName)
|
||||
# A string const is a `const char*`, not std::string: constexpr can't own a heap value.
|
||||
let cppType =
|
||||
if t.kind == ftStr:
|
||||
"const char*"
|
||||
else:
|
||||
nimTypeToCpp(c.typeName)
|
||||
lines.add(
|
||||
"constexpr $1 $2 = $3;" %
|
||||
[cppType, identToUpperSnake(c.name), cConstValue(t, c.value)]
|
||||
)
|
||||
lines.add("")
|
||||
|
||||
# Enums first: a struct codec that takes one must see its overload already declared.
|
||||
var structTypes: seq[FFITypeMeta] = @[]
|
||||
for t in types:
|
||||
if t.isEnum():
|
||||
emitEnumCborCodec(lines, t)
|
||||
else:
|
||||
structTypes.add(t)
|
||||
|
||||
if structTypes.len > 0:
|
||||
lines.add("// ============================================================")
|
||||
lines.add("// User-declared FFI types")
|
||||
lines.add("// ============================================================")
|
||||
lines.add("")
|
||||
for t in types:
|
||||
for t in structTypes:
|
||||
lines.add("struct $1 {" % [t.name])
|
||||
for f in t.fields:
|
||||
lines.add(" $1 $2;" % [nimTypeToCpp(f.typeName), f.name])
|
||||
@ -478,9 +538,11 @@ proc generateCppBindings*(
|
||||
outputDir: string,
|
||||
nimSrcRelPath: string,
|
||||
events: seq[FFIEventMeta] = @[],
|
||||
consts: seq[FFIConstMeta] = @[],
|
||||
) =
|
||||
createDir(outputDir)
|
||||
writeFile(
|
||||
outputDir / (libName & ".hpp"), generateCppHeader(procs, types, libName, events)
|
||||
outputDir / (libName & ".hpp"),
|
||||
generateCppHeader(procs, types, libName, events, consts),
|
||||
)
|
||||
writeFile(outputDir / "CMakeLists.txt", generateCppCMakeLists(libName, nimSrcRelPath))
|
||||
|
||||
@ -40,10 +40,26 @@ type
|
||||
name*: string
|
||||
typeName*: string
|
||||
|
||||
FFIEnumValueMeta* = object
|
||||
## One `{.ffi.}` enum value. `wire` is what `$value` yields — the symbol name,
|
||||
## or the associated string if the enum declares one — which is exactly what
|
||||
## cbor_serialization puts on the wire.
|
||||
name*: string
|
||||
wire*: string
|
||||
ord*: int
|
||||
|
||||
FFITypeMeta* = object
|
||||
name*: string
|
||||
fields*: seq[FFIFieldMeta]
|
||||
abiFormat*: ABIFormat
|
||||
enumValues*: seq[FFIEnumValueMeta] ## non-empty iff the type is an enum
|
||||
|
||||
FFIConstMeta* = object
|
||||
## A `{.ffiConst.}` value. `value` is the compile-time-evaluated result of
|
||||
## `$theConst`, re-rendered as a literal by each backend.
|
||||
name*: string
|
||||
typeName*: string
|
||||
value*: string
|
||||
|
||||
FFIEventMeta* = object
|
||||
## Library-initiated event from `{.ffiEvent: "wire_name".}`; `wireName` is
|
||||
@ -58,6 +74,7 @@ type
|
||||
var ffiProcRegistry* {.compileTime.}: seq[FFIProcMeta]
|
||||
var ffiTypeRegistry* {.compileTime.}: seq[FFITypeMeta]
|
||||
var ffiEventRegistry* {.compileTime.}: seq[FFIEventMeta]
|
||||
var ffiConstRegistry* {.compileTime.}: seq[FFIConstMeta]
|
||||
var currentLibName* {.compileTime.}: string
|
||||
|
||||
# Set by `declareLibrary`; the FFI annotations require it.
|
||||
@ -120,6 +137,15 @@ var ffiHandleTypeNames* {.compileTime.}: seq[string]
|
||||
proc isFFIHandleTypeName*(name: string): bool {.compileTime.} =
|
||||
name in ffiHandleTypeNames
|
||||
|
||||
func isEnum*(t: FFITypeMeta): bool =
|
||||
return t.enumValues.len > 0
|
||||
|
||||
# Names of `{.ffi.}` enum types; the `abi = c` wire path has to reject them.
|
||||
var ffiEnumTypeNames* {.compileTime.}: seq[string]
|
||||
|
||||
proc isFFIEnumTypeName*(name: string): bool {.compileTime.} =
|
||||
name in ffiEnumTypeNames
|
||||
|
||||
proc ridesAsPtr*(ep: FFIParamMeta): bool =
|
||||
## True if the param crosses the wire as an opaque uint64 (raw ptr or handle).
|
||||
ep.isPtr or ep.isHandle
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
## Rust binding generator: emits a complete Rust crate using CBOR (ciborium).
|
||||
|
||||
import std/[os, strutils]
|
||||
import ./meta, ./string_helpers, ./types_ir
|
||||
import ./meta, ./string_helpers, ./types_ir, ./consts
|
||||
|
||||
## Wire-format Rust type for any Nim `ptr T`/`pointer`; fixed 64-bit for a
|
||||
## host-independent CBOR payload size (mirrors CppPtrType).
|
||||
@ -217,13 +217,49 @@ proc generateFFIRs*(procs: seq[FFIProcMeta]): string =
|
||||
lines.add("}")
|
||||
return lines.join("\n") & "\n"
|
||||
|
||||
proc generateTypesRs*(types: seq[FFITypeMeta], procs: seq[FFIProcMeta]): string =
|
||||
func rustConstType(typeName: string): string =
|
||||
## `&str` rather than `String`: a `pub const` can't own a heap value. The
|
||||
## 'static lifetime is implied, and spelling it out trips clippy.
|
||||
let t = parseFFIType(typeName)
|
||||
if t.kind == ftStr:
|
||||
return "&str"
|
||||
return renderNative(rustMap, t)
|
||||
|
||||
proc generateTypesRs*(
|
||||
types: seq[FFITypeMeta], procs: seq[FFIProcMeta], consts: seq[FFIConstMeta] = @[]
|
||||
): string =
|
||||
## Generates types.rs: Rust structs for user FFI types and each per-proc Req.
|
||||
var lines: seq[string] = @[]
|
||||
lines.add("use serde::{Deserialize, Serialize};")
|
||||
lines.add("")
|
||||
|
||||
for c in consts:
|
||||
let t = parseFFIType(c.typeName)
|
||||
lines.add(
|
||||
"pub const $1: $2 = $3;" % [
|
||||
identToUpperSnake(c.name), rustConstType(c.typeName), rustConstValue(t, c.value)
|
||||
]
|
||||
)
|
||||
if consts.len > 0:
|
||||
lines.add("")
|
||||
|
||||
for t in types:
|
||||
if not t.isEnum():
|
||||
continue
|
||||
lines.add("#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]")
|
||||
lines.add("pub enum $1 {" % [t.name])
|
||||
for v in t.enumValues:
|
||||
let variant = capitalizeFirstLetter(v.name)
|
||||
# serde carries the same text form Nim's cbor_serialization writes.
|
||||
if variant != v.wire:
|
||||
lines.add(" #[serde(rename = \"$1\")]" % [v.wire])
|
||||
lines.add(" $1," % [variant])
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
for t in types:
|
||||
if t.isEnum():
|
||||
continue
|
||||
lines.add("#[derive(Debug, Clone, Serialize, Deserialize)]")
|
||||
lines.add("pub struct $1 {" % [t.name])
|
||||
for f in t.fields:
|
||||
@ -742,6 +778,7 @@ proc generateRustCrate*(
|
||||
outputDir: string,
|
||||
nimSrcRelPath: string,
|
||||
events: seq[FFIEventMeta] = @[],
|
||||
consts: seq[FFIConstMeta] = @[],
|
||||
) =
|
||||
## Generates a complete Rust crate in outputDir.
|
||||
createDir(outputDir)
|
||||
@ -751,5 +788,5 @@ proc generateRustCrate*(
|
||||
writeFile(outputDir / "build.rs", generateBuildRs(libName, nimSrcRelPath))
|
||||
writeFile(outputDir / "src" / "lib.rs", generateLibRs())
|
||||
writeFile(outputDir / "src" / "ffi.rs", generateFFIRs(procs))
|
||||
writeFile(outputDir / "src" / "types.rs", generateTypesRs(types, procs))
|
||||
writeFile(outputDir / "src" / "types.rs", generateTypesRs(types, procs, consts))
|
||||
writeFile(outputDir / "src" / "api.rs", generateApiRs(procs, libName, events))
|
||||
|
||||
@ -69,6 +69,24 @@ func capitalizeFirstLetter*(s: string): string =
|
||||
runesSeq[0] = runesSeq[0].toUpper()
|
||||
return $runesSeq
|
||||
|
||||
func identToUpperSnake*(s: string): string =
|
||||
## Nim identifier → UPPER_SNAKE, keeping acronym runs intact: "maxPeers" and
|
||||
## "MAX_PEERS" both give "MAX_PEERS", "httpTTL" gives "HTTP_TTL".
|
||||
var upper = ""
|
||||
let rs = toRunes(s)
|
||||
for i, r in rs:
|
||||
if r == Rune('_'):
|
||||
if upper.len > 0 and upper[^1] != '_':
|
||||
upper.add('_')
|
||||
continue
|
||||
let startsWord =
|
||||
i > 0 and r.isUpper() and
|
||||
(not rs[i - 1].isUpper() or (i + 1 < rs.len and rs[i + 1].isLower()))
|
||||
if startsWord and upper.len > 0 and upper[^1] != '_':
|
||||
upper.add('_')
|
||||
upper.add($r.toUpper())
|
||||
return upper
|
||||
|
||||
proc snakeToPascalCase*(s: string): string =
|
||||
## snake_case → PascalCase, e.g. "hello_world" → "HelloWorld".
|
||||
let parts = s.split('_')
|
||||
|
||||
@ -62,13 +62,22 @@ proc tupleComponents(t: NimNode): seq[tuple[name: string, typ: NimNode]] =
|
||||
|
||||
proc isKnownFFIType(name: string): bool {.compileTime.} =
|
||||
for typeMeta in ffiTypeRegistry:
|
||||
if typeMeta.name == name:
|
||||
if typeMeta.name == name and not typeMeta.isEnum():
|
||||
return true
|
||||
false
|
||||
|
||||
proc isNestedFFIType(t: NimNode): bool =
|
||||
## Enums are excluded: they are registered types but have no `_CWire`
|
||||
## companion, and treating one as a nested struct would silently drop its value.
|
||||
t.kind == nnkIdent and isKnownFFIType($t)
|
||||
|
||||
proc rejectEnumOnCWire(t: NimNode) {.compileTime.} =
|
||||
if t.kind == nnkIdent and isFFIEnumTypeName($t):
|
||||
error(
|
||||
"cwire: `abi = c` does not support enum types yet, but " & $t &
|
||||
" crosses the boundary here; use the CBOR ABI for this proc or type"
|
||||
)
|
||||
|
||||
proc cwireNeedsFree(t: NimNode): bool =
|
||||
## Whether the wire form of `t` owns allocations `cwireFree` must release.
|
||||
if isStringType(t) or isNestedFFIType(t) or isOptionType(t) or isSeqType(t):
|
||||
@ -90,6 +99,7 @@ proc rejectNestedSeq(t: NimNode) =
|
||||
|
||||
proc wireValueType(t: NimNode): NimNode =
|
||||
## Single-field wire form of value type `t`; `seq` has none, so it errors here.
|
||||
rejectEnumOnCWire(t)
|
||||
if isStringType(t):
|
||||
return ident("cstring")
|
||||
if isNestedFFIType(t):
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import std/[macros, tables, strutils]
|
||||
import std/[macros, options, tables, strutils]
|
||||
from std/os import `/`, relativePath
|
||||
from std/compilesettings import querySetting, SingleValueSetting
|
||||
import chronos
|
||||
@ -107,6 +107,74 @@ proc rejectRawPtrType(typ: NimNode, where: string) =
|
||||
"(only the ctx handle, managed by the framework, may be a pointer)"
|
||||
)
|
||||
|
||||
proc enumWireName(rhs: NimNode, fieldName: string): string {.compileTime.} =
|
||||
## What `$value` yields: the associated string if the enum declares one
|
||||
## (`cRed = "red"` or `cRed = (3, "red")`), else the symbol name.
|
||||
case rhs.kind
|
||||
of nnkStrLit, nnkRStrLit, nnkTripleStrLit:
|
||||
$rhs
|
||||
of nnkTupleConstr, nnkPar:
|
||||
if rhs.len == 2 and rhs[1].kind in {nnkStrLit, nnkRStrLit, nnkTripleStrLit}:
|
||||
$rhs[1]
|
||||
else:
|
||||
fieldName
|
||||
else:
|
||||
fieldName
|
||||
|
||||
proc enumValueMetas(
|
||||
enumTy: NimNode, typeName: string
|
||||
): seq[FFIEnumValueMeta] {.compileTime.} =
|
||||
## Walks an `nnkEnumTy`, resolving each value's wire name and ordinal.
|
||||
var values: seq[FFIEnumValueMeta] = @[]
|
||||
var nextOrd = 0
|
||||
for child in enumTy:
|
||||
if child.kind == nnkEmpty:
|
||||
continue
|
||||
var name: string
|
||||
var wire: string
|
||||
var ordinal = nextOrd
|
||||
case child.kind
|
||||
of nnkIdent, nnkSym:
|
||||
name = $child
|
||||
wire = name
|
||||
of nnkEnumFieldDef:
|
||||
name = $child[0]
|
||||
wire = enumWireName(child[1], name)
|
||||
let explicitOrd =
|
||||
if child[1].kind == nnkIntLit:
|
||||
some(int(child[1].intVal))
|
||||
elif child[1].kind in {nnkTupleConstr, nnkPar} and child[1].len == 2 and
|
||||
child[1][0].kind == nnkIntLit:
|
||||
some(int(child[1][0].intVal))
|
||||
else:
|
||||
none(int)
|
||||
if explicitOrd.isSome():
|
||||
ordinal = explicitOrd.get()
|
||||
else:
|
||||
error("`.ffi.` enum " & typeName & ": unsupported enum value " & child.repr)
|
||||
values.add(FFIEnumValueMeta(name: name, wire: wire, ord: ordinal))
|
||||
nextOrd = ordinal + 1
|
||||
values
|
||||
|
||||
proc registerFFIEnumInfo(
|
||||
typeDef: NimNode, typeNameStr: string, abiFormat: ABIFormat
|
||||
) {.compileTime.} =
|
||||
## Registers an `{.ffi.}` enum. Only the CBOR wire carries enums; `abi = c`
|
||||
## has no representation for them yet, so reject it at the annotation.
|
||||
if abiFormat == ABIFormat.C:
|
||||
error(
|
||||
"`.ffi.` enum " & typeNameStr &
|
||||
": `abi = c` does not support enum types yet; use the CBOR ABI for this type"
|
||||
)
|
||||
ffiTypeRegistry.add(
|
||||
FFITypeMeta(
|
||||
name: typeNameStr,
|
||||
abiFormat: abiFormat,
|
||||
enumValues: enumValueMetas(typeDef[2], typeNameStr),
|
||||
)
|
||||
)
|
||||
ffiEnumTypeNames.add(typeNameStr)
|
||||
|
||||
proc registerFFITypeInfo(
|
||||
typeDef: NimNode, abiFormat: ABIFormat
|
||||
): NimNode {.compileTime.} =
|
||||
@ -118,6 +186,10 @@ proc registerFFITypeInfo(
|
||||
typeDef[0]
|
||||
let typeNameStr = $typeName
|
||||
|
||||
if typeDef[2].kind == nnkEnumTy:
|
||||
registerFFIEnumInfo(typeDef, typeNameStr, abiFormat)
|
||||
return typeDef
|
||||
|
||||
var fieldMetas: seq[FFIFieldMeta] = @[]
|
||||
let objTy = typeDef[2]
|
||||
if objTy.kind == nnkObjectTy and objTy.len >= 3:
|
||||
@ -643,6 +715,51 @@ macro ffiHandle*(args: varargs[untyped]): untyped =
|
||||
echo clean.repr
|
||||
return clean
|
||||
|
||||
proc registerFFIConst(nameNode: NimNode): NimNode {.compileTime.} =
|
||||
## Emits the type guard plus the `static:` block that records the const's
|
||||
## evaluated value; `$typeof` runs after the const is defined, so computed
|
||||
## expressions (`3 * 7`) land in the registry as their result.
|
||||
let nameStr = newLit($nameNode)
|
||||
let unsupported = newLit(
|
||||
"`.ffiConst.` " & $nameNode &
|
||||
": only integer, float, bool and string consts can cross the FFI boundary"
|
||||
)
|
||||
# bindSym: the emitted code lands in the user's module, which doesn't import meta.
|
||||
let registry = bindSym("ffiConstRegistry")
|
||||
let metaType = bindSym("FFIConstMeta")
|
||||
quote:
|
||||
when not (`nameNode` is (SomeInteger | SomeFloat | bool | string)):
|
||||
{.error: `unsupported`.}
|
||||
static:
|
||||
`registry`.add(
|
||||
`metaType`(name: `nameStr`, typeName: $typeof(`nameNode`), value: $(`nameNode`))
|
||||
)
|
||||
|
||||
macro ffiConst*(args: varargs[untyped]): untyped =
|
||||
## Exposes a Nim `const` to the generated bindings as a native constant
|
||||
## (`static const` in C/C++, `pub const` in Rust). An `"abi = ..."` spec is
|
||||
## accepted but only validated — a constant never rides the wire.
|
||||
requireBeforeGenBindings("`.ffiConst.`")
|
||||
requireLibraryDeclared("`.ffiConst.`")
|
||||
let section = args[^1]
|
||||
discard resolveABIFormat(args[0 ..^ 2])
|
||||
if section.kind != nnkConstSection:
|
||||
error("`.ffiConst.` must be applied to a `const` definition")
|
||||
|
||||
# Nim splits the section so only the annotated defs reach this macro.
|
||||
var stmts = newStmtList(section.copyNimTree())
|
||||
for def in section:
|
||||
let nameNode =
|
||||
if def[0].kind == nnkPostfix:
|
||||
def[0][1]
|
||||
else:
|
||||
def[0]
|
||||
stmts.add(registerFFIConst(nameNode))
|
||||
|
||||
when defined(ffiDumpMacros):
|
||||
echo stmts.repr
|
||||
return stmts
|
||||
|
||||
macro ffi*(args: varargs[untyped]): untyped =
|
||||
## Simplified FFI macro for procs or types: a type registers for binding gen; a
|
||||
## proc takes a library-type param plus optional Nim params, returns
|
||||
@ -1586,15 +1703,18 @@ when defined(ffiGenBindings):
|
||||
case lang
|
||||
of "rust":
|
||||
generateRustCrate(
|
||||
genProcs, ffiTypeRegistry, libName, outDir, srcRel, ffiEventRegistry
|
||||
genProcs, ffiTypeRegistry, libName, outDir, srcRel, ffiEventRegistry,
|
||||
ffiConstRegistry,
|
||||
)
|
||||
of "cpp", "c++":
|
||||
generateCppBindings(
|
||||
genProcs, ffiTypeRegistry, libName, outDir, srcRel, ffiEventRegistry
|
||||
genProcs, ffiTypeRegistry, libName, outDir, srcRel, ffiEventRegistry,
|
||||
ffiConstRegistry,
|
||||
)
|
||||
of "c":
|
||||
generateCBindings(
|
||||
genProcs, ffiTypeRegistry, libName, outDir, srcRel, ffiEventRegistry
|
||||
genProcs, ffiTypeRegistry, libName, outDir, srcRel, ffiEventRegistry,
|
||||
ffiConstRegistry,
|
||||
)
|
||||
of "cddl":
|
||||
generateCddlBindings(genProcs, ffiTypeRegistry, libName, outDir, srcRel)
|
||||
|
||||
@ -74,6 +74,7 @@ typedef struct {
|
||||
char text_a[256];
|
||||
char text_b[256];
|
||||
long long num_a;
|
||||
long long num_b;
|
||||
int flag;
|
||||
} ReplyWaiter;
|
||||
|
||||
@ -91,7 +92,7 @@ static void test_version(MyTimerCtx* ctx) {
|
||||
my_timer_ctx_version(ctx, on_version, &w);
|
||||
wait_done(&w.done);
|
||||
assert(w.err_code == 0);
|
||||
assert(strcmp(w.text_a, "nim-timer v0.1.0") == 0);
|
||||
assert(strcmp(w.text_a, TIMER_VERSION) == 0);
|
||||
}
|
||||
|
||||
static void on_echo(int ec, const EchoResponse* reply, const char* em, void* ud) {
|
||||
@ -158,6 +159,8 @@ static void on_schedule(int ec, const ScheduleResult* reply, const char* em, voi
|
||||
w->err_code = ec;
|
||||
if (reply) {
|
||||
w->num_a = (long long)reply->willRunCount;
|
||||
w->num_b = (long long)reply->effectiveBackoffMs;
|
||||
w->flag = (int)reply->priority;
|
||||
if (reply->jobId.data) snprintf(w->text_a, sizeof(w->text_a), "%s", reply->jobId.data);
|
||||
}
|
||||
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
|
||||
@ -170,7 +173,7 @@ static void test_schedule_ok(MyTimerCtx* ctx) {
|
||||
job.name = nimffi_str("rollup");
|
||||
job.payload.data = payload;
|
||||
job.payload.len = 1;
|
||||
job.priority = 1;
|
||||
job.priority = JOB_PRIORITY_JP_HIGH;
|
||||
|
||||
NimFfiStr retry_on[1] = {nimffi_str("timeout")};
|
||||
RetryPolicy retry;
|
||||
@ -192,6 +195,9 @@ static void test_schedule_ok(MyTimerCtx* ctx) {
|
||||
assert(w.err_code == 0);
|
||||
assert(strcmp(w.text_a, "c-e2e:rollup") == 0);
|
||||
assert(w.num_a == 1);
|
||||
/* jpHigh halves the requested 100ms backoff. */
|
||||
assert(w.num_b == 50);
|
||||
assert(w.flag == JOB_PRIORITY_JP_HIGH);
|
||||
}
|
||||
|
||||
static void test_schedule_error(MyTimerCtx* ctx) {
|
||||
@ -200,7 +206,7 @@ static void test_schedule_error(MyTimerCtx* ctx) {
|
||||
job.name = nimffi_str(""); /* empty name → handler returns err */
|
||||
job.payload.data = payload;
|
||||
job.payload.len = 1;
|
||||
job.priority = 1;
|
||||
job.priority = JOB_PRIORITY_JP_LOW;
|
||||
|
||||
NimFfiStr retry_on[1] = {nimffi_str("timeout")};
|
||||
RetryPolicy retry;
|
||||
@ -224,6 +230,16 @@ static void test_schedule_error(MyTimerCtx* ctx) {
|
||||
assert(strstr(w.err, "job name") != NULL);
|
||||
}
|
||||
|
||||
static void test_delay_limit(MyTimerCtx* ctx) {
|
||||
ReplyWaiter w;
|
||||
memset(&w, 0, sizeof(w));
|
||||
EchoRequest req = {nimffi_str("too-slow"), MAX_DELAY_MS + 1};
|
||||
my_timer_ctx_echo(ctx, &req, on_echo, &w);
|
||||
wait_done(&w.done);
|
||||
assert(w.err_code != 0);
|
||||
assert(strstr(w.err, "delayMs") != NULL);
|
||||
}
|
||||
|
||||
static void test_event(MyTimerCtx* ctx) {
|
||||
int hits = 0;
|
||||
uint64_t handle =
|
||||
@ -256,6 +272,7 @@ int main(void) {
|
||||
test_complex(ctx);
|
||||
test_schedule_ok(ctx);
|
||||
test_schedule_error(ctx);
|
||||
test_delay_limit(ctx);
|
||||
test_event(ctx);
|
||||
assert(my_timer_ctx_destroy(ctx) == NIMFFI_RET_OK);
|
||||
printf("all C e2e checks passed\n");
|
||||
|
||||
@ -86,6 +86,21 @@ static void test_shout(EchoCtx* ctx) {
|
||||
assert(strcmp(w.text_b, "c-abi") == 0);
|
||||
}
|
||||
|
||||
/* Constants are ABI-agnostic: the abi = c header carries the same limit. */
|
||||
static void test_shout_too_long(EchoCtx* ctx) {
|
||||
char text[MAX_SHOUT_LEN + 2];
|
||||
memset(text, 'a', sizeof(text) - 1);
|
||||
text[sizeof(text) - 1] = '\0';
|
||||
|
||||
ReplyWaiter w;
|
||||
memset(&w, 0, sizeof(w));
|
||||
ShoutRequest req = {text};
|
||||
echo_ctx_shout(ctx, &req, on_shout, &w);
|
||||
wait_done(&w.done);
|
||||
assert(w.err_code != 0);
|
||||
assert(strstr(w.err, "must not exceed") != NULL);
|
||||
}
|
||||
|
||||
static void on_version(int ec, const char* reply, const char* em, void* ud) {
|
||||
ReplyWaiter* w = (ReplyWaiter*)ud;
|
||||
w->err_code = ec;
|
||||
@ -106,6 +121,7 @@ static void test_version(EchoCtx* ctx) {
|
||||
int main(void) {
|
||||
EchoCtx* ctx = make_ctx();
|
||||
test_shout(ctx);
|
||||
test_shout_too_long(ctx);
|
||||
test_version(ctx);
|
||||
assert(echo_ctx_destroy(ctx) == NIMFFI_RET_OK);
|
||||
printf("all abi=c echo e2e checks passed\n");
|
||||
|
||||
@ -56,13 +56,13 @@ TEST(TimerE2E, CreateAndDestroy) {
|
||||
TEST(TimerE2E, VersionSync) {
|
||||
auto ctx = makeCtx("version-sync");
|
||||
const auto v = mustOk(ctx->version());
|
||||
EXPECT_EQ(v, "nim-timer v0.1.0");
|
||||
EXPECT_EQ(v, TIMER_VERSION);
|
||||
}
|
||||
|
||||
TEST(TimerE2E, VersionAsync) {
|
||||
auto ctx = makeCtx("version-async");
|
||||
auto fut = ctx->versionAsync();
|
||||
EXPECT_EQ(mustOk(fut.get()), "nim-timer v0.1.0");
|
||||
EXPECT_EQ(mustOk(fut.get()), TIMER_VERSION);
|
||||
}
|
||||
|
||||
TEST(TimerE2E, EchoRoundTripsMessageAndTimerName) {
|
||||
@ -152,6 +152,32 @@ TEST(TimerE2E, IndependentContextsKeepTheirOwnState) {
|
||||
EXPECT_EQ(rB.timerName, "beta");
|
||||
}
|
||||
|
||||
// jpHigh halves the requested backoff, and the enum comes back as itself.
|
||||
TEST(TimerE2E, SchedulePriorityRoundTrips) {
|
||||
auto ctx = makeCtx("prio");
|
||||
const auto job = JobSpec{"rollup", {"p"}, JobPriority::jpHigh};
|
||||
const auto res = mustOk(ctx->schedule(job, RetryPolicy{3, 100, {}},
|
||||
ScheduleConfig{0, 0, std::nullopt}));
|
||||
EXPECT_EQ(res.priority, JobPriority::jpHigh);
|
||||
EXPECT_EQ(res.effectiveBackoffMs, 50);
|
||||
}
|
||||
|
||||
// backoffMs 0 falls back to DEFAULT_BACKOFF_MS, doubled for jpLow.
|
||||
TEST(TimerE2E, ScheduleDefaultBackoffComesFromConst) {
|
||||
auto ctx = makeCtx("prio-default");
|
||||
const auto job = JobSpec{"rollup", {}, JobPriority::jpLow};
|
||||
const auto res = mustOk(ctx->schedule(job, RetryPolicy{1, 0, {}},
|
||||
ScheduleConfig{0, 0, std::nullopt}));
|
||||
EXPECT_EQ(res.effectiveBackoffMs, DEFAULT_BACKOFF_MS * 2);
|
||||
}
|
||||
|
||||
TEST(TimerE2E, EchoRejectsDelayAboveMaxDelayMs) {
|
||||
auto ctx = makeCtx("max-delay");
|
||||
const auto res = ctx->echo(EchoRequest{"too-slow", MAX_DELAY_MS + 1});
|
||||
ASSERT_TRUE(res.isErr());
|
||||
EXPECT_EQ(res.error(), "delayMs must not exceed " + std::to_string(MAX_DELAY_MS));
|
||||
}
|
||||
|
||||
// N contexts keep independent state; an error on one must not poison siblings.
|
||||
// Empty JobSpec.name is the chosen error trigger: schedule() returns
|
||||
// err("job name must not be empty"), which the bindings surface as an
|
||||
@ -170,7 +196,7 @@ TEST(TimerE2E, MultiContextIsolation) {
|
||||
EXPECT_EQ(resp.timerName, "iso-" + std::to_string(i));
|
||||
}
|
||||
|
||||
const auto bad = JobSpec{/*name*/ "", /*payload*/ {}, /*priority*/ 0};
|
||||
const auto bad = JobSpec{/*name*/ "", /*payload*/ {}, JobPriority::jpNormal};
|
||||
const auto retry = RetryPolicy{1, 10, {}};
|
||||
const auto sched = ScheduleConfig{0, 0, std::nullopt};
|
||||
const auto scheduleRes = ctxs[2]->schedule(bad, retry, sched);
|
||||
@ -194,7 +220,7 @@ TEST(TimerE2E, CrossLibrary) {
|
||||
auto timerCtx = mustOk(MyTimerCtx::create(TimerConfig{"x-timer"}));
|
||||
auto echoCtx = mustOk(EchoCtx::create(EchoConfig{"X-ECHO"}));
|
||||
|
||||
EXPECT_EQ(mustOk(timerCtx->version()), "nim-timer v0.1.0");
|
||||
EXPECT_EQ(mustOk(timerCtx->version()), TIMER_VERSION);
|
||||
EXPECT_EQ(mustOk(echoCtx->version()), "nim-echo v0.1.0");
|
||||
|
||||
const auto timerResp = mustOk(timerCtx->echo(EchoRequest{"hello", 0}));
|
||||
|
||||
48
tests/unit/fixture_gen.nim
Normal file
48
tests/unit/fixture_gen.nim
Normal file
@ -0,0 +1,48 @@
|
||||
## Drives the compile fixtures under `fixtures/` through a child compiler, so a
|
||||
## fixture's success or failure becomes an assertion instead of this suite's own
|
||||
## compile error.
|
||||
|
||||
import std/[os, osproc, strutils, compilesettings]
|
||||
|
||||
const
|
||||
nimExe = getCurrentCompilerExe()
|
||||
ffiSearchPaths = querySettingSeq(searchPaths)
|
||||
fixtureDir = currentSourcePath().parentDir() / "fixtures"
|
||||
|
||||
func compilerCmd(subCmd, fixture, cacheTag: string): string =
|
||||
var cmd = quoteShell(nimExe) & " " & subCmd & " --hints:off --warnings:off"
|
||||
for p in ffiSearchPaths:
|
||||
cmd.add(" --path:" & quoteShell(p))
|
||||
cmd.add(" --nimcache:" & quoteShell(getTempDir() / ("ffi_fixture_cache_" & cacheTag)))
|
||||
return cmd
|
||||
|
||||
proc genFixtureBindings*(
|
||||
fixture, lang: string, extraDefs: openArray[string] = []
|
||||
): tuple[outDir: string, output: string, exitCode: int] =
|
||||
## Generates `lang` bindings for `fixtures/<fixture>.nim`. The output dir is
|
||||
## wiped first, so a stale artifact from an earlier run can't satisfy an
|
||||
## assertion the current generator would have failed.
|
||||
let tag = fixture & "_" & lang
|
||||
let outDir = getTempDir() / ("ffi_fixture_out_" & tag)
|
||||
removeDir(outDir)
|
||||
createDir(outDir)
|
||||
var cmd = compilerCmd("c --compileOnly", fixture, tag)
|
||||
cmd.add(" -d:ffiGenBindings -d:targetLang=" & lang)
|
||||
cmd.add(" -d:ffiOutputDir=" & quoteShell(outDir))
|
||||
for d in extraDefs:
|
||||
cmd.add(" " & d)
|
||||
cmd.add(" " & quoteShell(fixtureDir / (fixture & ".nim")))
|
||||
let (output, code) = execCmdEx(cmd)
|
||||
if code != 0:
|
||||
echo output
|
||||
return (outDir, output, code)
|
||||
|
||||
proc checkFixture*(
|
||||
fixture: string, extraDefs: openArray[string] = []
|
||||
): tuple[output: string, exitCode: int] =
|
||||
## `nim check` of a fixture expected to fail, so its error is a test assertion.
|
||||
var cmd = compilerCmd("check", fixture, fixture & "_check")
|
||||
for d in extraDefs:
|
||||
cmd.add(" " & d)
|
||||
cmd.add(" " & quoteShell(fixtureDir / (fixture & ".nim")))
|
||||
return execCmdEx(cmd)
|
||||
32
tests/unit/fixtures/consts_fixture.nim
Normal file
32
tests/unit/fixtures/consts_fixture.nim
Normal file
@ -0,0 +1,32 @@
|
||||
## Compile fixture for `{.ffiConst.}` binding generation (see
|
||||
## tests/unit/test_ffi_const.nim): one const of every supported shape, including
|
||||
## a computed value and a string needing escapes.
|
||||
|
||||
import ffi, chronos
|
||||
|
||||
type ConstLib = object
|
||||
base: int
|
||||
|
||||
declareLibrary("constlib", ConstLib)
|
||||
|
||||
const
|
||||
MaxPeers* {.ffiConst.} = 42
|
||||
DefaultTimeoutMs* {.ffiConst.}: uint32 = 3 * 1000
|
||||
Ratio* {.ffiConst.} = 1.5
|
||||
DebugEnabled* {.ffiConst.} = false
|
||||
Greeting* {.ffiConst.} = "he\"llo\n"
|
||||
HTTPPort* {.ffiConst.} = 8080
|
||||
|
||||
type ConstConfig {.ffi.} = object
|
||||
base: int
|
||||
|
||||
proc constlib_create*(cfg: ConstConfig): Future[Result[ConstLib, string]] {.ffiCtor.} =
|
||||
return ok(ConstLib(base: cfg.base))
|
||||
|
||||
proc constlib_base*(lib: ConstLib): Future[Result[int, string]] {.ffi.} =
|
||||
return ok(lib.base)
|
||||
|
||||
proc constlib_destroy*(lib: ConstLib) {.ffiDtor.} =
|
||||
discard
|
||||
|
||||
genBindings()
|
||||
18
tests/unit/fixtures/enum_abi_c_field_fixture.nim
Normal file
18
tests/unit/fixtures/enum_abi_c_field_fixture.nim
Normal file
@ -0,0 +1,18 @@
|
||||
## Compile fixture: a CBOR enum reached from an `abi = c` type has no `_CWire`
|
||||
## form, so the cwire builder must reject it (see tests/unit/test_ffi_enum.nim).
|
||||
|
||||
import ffi, chronos
|
||||
|
||||
type AbiFieldLib = object
|
||||
n: int
|
||||
|
||||
declareLibrary("abifieldlib", AbiFieldLib)
|
||||
|
||||
type Color {.ffi.} = enum
|
||||
cRed
|
||||
cGreen
|
||||
|
||||
type Wrapper {.ffi: "abi = c".} = object
|
||||
color: Color
|
||||
|
||||
genBindings()
|
||||
15
tests/unit/fixtures/enum_abi_c_type_fixture.nim
Normal file
15
tests/unit/fixtures/enum_abi_c_type_fixture.nim
Normal file
@ -0,0 +1,15 @@
|
||||
## Compile fixture: an enum in an `abi = c` library must be rejected, not
|
||||
## silently mis-wired (see tests/unit/test_ffi_enum.nim).
|
||||
|
||||
import ffi, chronos
|
||||
|
||||
type AbiEnumLib = object
|
||||
n: int
|
||||
|
||||
declareLibrary("abienumlib", AbiEnumLib, "c")
|
||||
|
||||
type Color {.ffi.} = enum
|
||||
cRed
|
||||
cGreen
|
||||
|
||||
genBindings()
|
||||
48
tests/unit/fixtures/enum_fixture.nim
Normal file
48
tests/unit/fixtures/enum_fixture.nim
Normal file
@ -0,0 +1,48 @@
|
||||
## Compile fixture for `{.ffi.}` enum binding generation (see
|
||||
## tests/unit/test_ffi_enum.nim): a plain enum, one with associated strings and
|
||||
## one with explicit ordinals, used as a field and as a param/return type.
|
||||
|
||||
import ffi, chronos
|
||||
|
||||
type EnumLib = object
|
||||
calls: int
|
||||
|
||||
declareLibrary("enumlib", EnumLib)
|
||||
|
||||
type
|
||||
Color {.ffi.} = enum
|
||||
cRed
|
||||
cGreen
|
||||
cBlue
|
||||
|
||||
Level {.ffi.} = enum
|
||||
lLow = "low"
|
||||
lHigh = "high"
|
||||
|
||||
Status {.ffi.} = enum
|
||||
stIdle = 2
|
||||
stBusy = 7
|
||||
|
||||
type PaintRequest {.ffi.} = object
|
||||
color: Color
|
||||
level: Level
|
||||
|
||||
type PaintResponse {.ffi.} = object
|
||||
status: Status
|
||||
applied: Color
|
||||
|
||||
proc enumlib_create*(): Future[Result[EnumLib, string]] {.ffiCtor.} =
|
||||
return ok(EnumLib(calls: 0))
|
||||
|
||||
proc enumlib_paint*(
|
||||
lib: EnumLib, req: PaintRequest
|
||||
): Future[Result[PaintResponse, string]] {.ffi.} =
|
||||
return ok(PaintResponse(status: stBusy, applied: req.color))
|
||||
|
||||
proc enumlib_pick*(lib: EnumLib, level: Level): Future[Result[Color, string]] {.ffi.} =
|
||||
return ok(if level == lHigh: cBlue else: cRed)
|
||||
|
||||
proc enumlib_destroy*(lib: EnumLib) {.ffiDtor.} =
|
||||
discard
|
||||
|
||||
genBindings()
|
||||
105
tests/unit/test_ffi_const.nim
Normal file
105
tests/unit/test_ffi_const.nim
Normal file
@ -0,0 +1,105 @@
|
||||
## Unit tests for `{.ffiConst.}`: the per-backend rendering of a synthetic const
|
||||
## registry, plus an end-to-end generation run over a fixture so the macro's own
|
||||
## registration path is covered.
|
||||
|
||||
import std/[os, strutils]
|
||||
import unittest2
|
||||
import ./fixture_gen
|
||||
import ffi/codegen/[meta, c, cpp, rust]
|
||||
|
||||
proc constMeta(name, typeName, value: string): FFIConstMeta =
|
||||
FFIConstMeta(name: name, typeName: typeName, value: value)
|
||||
|
||||
let consts = @[
|
||||
constMeta("maxPeers", "int", "42"),
|
||||
constMeta("DefaultTimeoutMs", "uint32", "3000"),
|
||||
constMeta("Ratio", "float64", "1.5"),
|
||||
constMeta("DebugEnabled", "bool", "false"),
|
||||
constMeta("Greeting", "string", "he\"llo\n"),
|
||||
]
|
||||
|
||||
let ctor = FFIProcMeta(
|
||||
procName: "constlib_create",
|
||||
libName: "constlib",
|
||||
kind: FFIKind.CTOR,
|
||||
libTypeName: "ConstLib",
|
||||
returnTypeName: "ConstLib",
|
||||
)
|
||||
|
||||
suite "constants in the C header":
|
||||
setup:
|
||||
let header = generateCLibHeader(@[ctor], @[], "constlib", @[], consts)
|
||||
|
||||
test "scalars become typed static consts under an UPPER_SNAKE name":
|
||||
check "static const int64_t MAX_PEERS = 42;" in header
|
||||
check "static const uint32_t DEFAULT_TIMEOUT_MS = 3000;" in header
|
||||
check "static const double RATIO = 1.5;" in header
|
||||
check "static const bool DEBUG_ENABLED = false;" in header
|
||||
|
||||
test "strings become const char* with escapes applied":
|
||||
check "static const char* const GREETING = \"he\\\"llo\\n\";" in header
|
||||
|
||||
test "no constants means no constants section":
|
||||
let bare = generateCLibHeader(@[ctor], @[], "constlib")
|
||||
check "Generated constants" notin bare
|
||||
|
||||
suite "constants in the abi = c header":
|
||||
test "the CBOR-free header carries the same declarations":
|
||||
let cabiCtor = FFIProcMeta(
|
||||
procName: "constlib_create",
|
||||
libName: "constlib",
|
||||
kind: FFIKind.CTOR,
|
||||
libTypeName: "ConstLib",
|
||||
returnTypeName: "ConstLib",
|
||||
abiFormat: ABIFormat.C,
|
||||
)
|
||||
let header = generateCAbiLibHeader(@[cabiCtor], @[], "constlib", @[], consts)
|
||||
check "static const int64_t MAX_PEERS = 42;" in header
|
||||
check "static const char* const GREETING = \"he\\\"llo\\n\";" in header
|
||||
|
||||
suite "constants in the C++ header":
|
||||
setup:
|
||||
let header = generateCppHeader(@[ctor], @[], "constlib", @[], consts)
|
||||
|
||||
test "constexpr, not static const":
|
||||
check "constexpr int64_t MAX_PEERS = 42;" in header
|
||||
check "constexpr bool DEBUG_ENABLED = false;" in header
|
||||
|
||||
test "a string const is a const char*, not std::string":
|
||||
check "constexpr const char* GREETING = \"he\\\"llo\\n\";" in header
|
||||
|
||||
suite "constants in the Rust crate":
|
||||
setup:
|
||||
let typesRs = generateTypesRs(@[], @[ctor], consts)
|
||||
|
||||
test "pub const with the mapped Rust type":
|
||||
check "pub const MAX_PEERS: i64 = 42;" in typesRs
|
||||
check "pub const DEFAULT_TIMEOUT_MS: u32 = 3000;" in typesRs
|
||||
check "pub const RATIO: f64 = 1.5;" in typesRs
|
||||
|
||||
test "a string const is a &str, without the redundant 'static":
|
||||
check "pub const GREETING: &str = \"he\\\"llo\\n\";" in typesRs
|
||||
|
||||
suite "{.ffiConst.} end-to-end generation":
|
||||
test "the C header carries every annotated const, computed values included":
|
||||
let (outDir, _, code) = genFixtureBindings("consts_fixture", "c")
|
||||
require code == 0
|
||||
let header = readFile(outDir / "constlib.h")
|
||||
check "static const int64_t MAX_PEERS = 42;" in header
|
||||
# `3 * 1000` is evaluated before it reaches the registry.
|
||||
check "static const uint32_t DEFAULT_TIMEOUT_MS = 3000;" in header
|
||||
check "static const double RATIO = 1.5;" in header
|
||||
check "static const bool DEBUG_ENABLED = false;" in header
|
||||
check "static const char* const GREETING = \"he\\\"llo\\n\";" in header
|
||||
check "static const int64_t HTTP_PORT = 8080;" in header
|
||||
|
||||
test "the Rust crate carries them too":
|
||||
let (outDir, _, code) = genFixtureBindings("consts_fixture", "rust")
|
||||
require code == 0
|
||||
let typesRs = readFile(outDir / "src" / "types.rs")
|
||||
check "pub const MAX_PEERS: i64 = 42;" in typesRs
|
||||
check "pub const DEFAULT_TIMEOUT_MS: u32 = 3000;" in typesRs
|
||||
check "pub const RATIO: f64 = 1.5;" in typesRs
|
||||
check "pub const DEBUG_ENABLED: bool = false;" in typesRs
|
||||
check "pub const GREETING: &str = \"he\\\"llo\\n\";" in typesRs
|
||||
check "pub const HTTP_PORT: i64 = 8080;" in typesRs
|
||||
153
tests/unit/test_ffi_enum.nim
Normal file
153
tests/unit/test_ffi_enum.nim
Normal file
@ -0,0 +1,153 @@
|
||||
## Unit tests for `{.ffi.}` enums: per-backend rendering of a synthetic registry,
|
||||
## a Nim-side CBOR round-trip, and an end-to-end generation run over a fixture.
|
||||
|
||||
import std/[os, strutils]
|
||||
import unittest2
|
||||
import ./fixture_gen
|
||||
import ffi
|
||||
import ffi/codegen/[meta, c, cpp, rust, cddl]
|
||||
|
||||
proc enumValue(name, wire: string, ord: int): FFIEnumValueMeta =
|
||||
FFIEnumValueMeta(name: name, wire: wire, ord: ord)
|
||||
|
||||
let colorMeta = FFITypeMeta(
|
||||
name: "Color",
|
||||
enumValues: @[enumValue("cRed", "cRed", 0), enumValue("cGreen", "green", 1)],
|
||||
)
|
||||
|
||||
let paintMeta = FFITypeMeta(
|
||||
name: "PaintRequest", fields: @[FFIFieldMeta(name: "color", typeName: "Color")]
|
||||
)
|
||||
|
||||
let ctor = FFIProcMeta(
|
||||
procName: "enumlib_create",
|
||||
libName: "enumlib",
|
||||
kind: FFIKind.CTOR,
|
||||
libTypeName: "EnumLib",
|
||||
returnTypeName: "EnumLib",
|
||||
)
|
||||
|
||||
let paint = FFIProcMeta(
|
||||
procName: "enumlib_paint",
|
||||
libName: "enumlib",
|
||||
kind: FFIKind.FFI,
|
||||
libTypeName: "EnumLib",
|
||||
extraParams: @[FFIParamMeta(name: "color", typeName: "Color")],
|
||||
returnTypeName: "PaintRequest",
|
||||
)
|
||||
|
||||
suite "enums in the C header":
|
||||
setup:
|
||||
let header = generateCLibHeader(@[ctor, paint], @[colorMeta, paintMeta], "enumlib")
|
||||
|
||||
test "the enum becomes a C enum with explicit ordinals":
|
||||
check "typedef enum {" in header
|
||||
check "COLOR_C_RED = 0," in header
|
||||
check "COLOR_C_GREEN = 1" in header
|
||||
check "} Color;" in header
|
||||
|
||||
test "the codec maps to the CBOR text form, not the ordinal":
|
||||
check "case COLOR_C_RED: return cbor_encode_text_stringz(e, \"cRed\");" in header
|
||||
check "case COLOR_C_GREEN: return cbor_encode_text_stringz(e, \"green\");" in header
|
||||
check "if (strcmp(buf, \"green\") == 0) { *out = COLOR_C_GREEN;" in header
|
||||
|
||||
test "the decode buffer is sized to the longest wire name":
|
||||
# Wire forms are "cRed" (4) and "green" (5); the longest plus a NUL is 6.
|
||||
check "char buf[6];" in header
|
||||
|
||||
test "an enum field is a plain member, not a nested struct":
|
||||
check " Color color;" in header
|
||||
|
||||
test "an enum param passes by value":
|
||||
check "int enumlib_ctx_paint(const EnumLibCtx* ctx, Color color," in header
|
||||
check "const Color* color" notin header
|
||||
|
||||
suite "enums in the C++ header":
|
||||
setup:
|
||||
let header = generateCppHeader(@[ctor, paint], @[colorMeta, paintMeta], "enumlib")
|
||||
|
||||
test "the enum becomes a scoped enum class":
|
||||
check "enum class Color {" in header
|
||||
check " cRed = 0," in header
|
||||
|
||||
test "the codec is declared before the struct codec that calls it":
|
||||
check header.find("enum class Color") < header.find("struct PaintRequest")
|
||||
|
||||
test "decode goes through the std::string overload":
|
||||
check "if (name == \"green\") { v = Color::cGreen; return CborNoError; }" in header
|
||||
|
||||
suite "enums in the Rust crate":
|
||||
setup:
|
||||
let typesRs = generateTypesRs(@[colorMeta, paintMeta], @[ctor, paint])
|
||||
|
||||
test "the enum becomes a fieldless Rust enum":
|
||||
check "pub enum Color {" in typesRs
|
||||
check " CRed," in typesRs
|
||||
|
||||
test "serde renames each variant to its wire form":
|
||||
check "#[serde(rename = \"green\")]" in typesRs
|
||||
check " CGreen," in typesRs
|
||||
|
||||
test "a variant whose name already matches the wire form needs no rename":
|
||||
check "#[serde(rename = \"CRed\")]" notin typesRs
|
||||
|
||||
suite "enums in the CDDL schema":
|
||||
test "the rule is a choice of the wire strings":
|
||||
let schema =
|
||||
generateCddlSchema(@[ctor, paint], @[colorMeta, paintMeta], "enumlib", "x.nim")
|
||||
check "Color = \"cRed\" / \"green\"" in schema
|
||||
|
||||
type
|
||||
RoundTripColor {.ffi.} = enum
|
||||
rtRed
|
||||
rtBlue
|
||||
|
||||
RoundTripLevel {.ffi.} = enum
|
||||
rtLow = "low"
|
||||
rtHigh = "high"
|
||||
|
||||
type RoundTripPaint {.ffi.} = object
|
||||
color: RoundTripColor
|
||||
level: RoundTripLevel
|
||||
|
||||
suite "enum CBOR round-trip on the Nim side":
|
||||
test "an enum field survives encode/decode":
|
||||
let sent = RoundTripPaint(color: rtBlue, level: rtHigh)
|
||||
let back = cborDecode(cborEncode(sent), RoundTripPaint)
|
||||
check back.isOk()
|
||||
check back.get() == sent
|
||||
|
||||
test "the wire form is the associated string when the enum declares one":
|
||||
var wire = ""
|
||||
for b in cborEncode(RoundTripPaint(color: rtRed, level: rtLow)):
|
||||
wire.add(char(b))
|
||||
check "low" in wire
|
||||
check "rtRed" in wire
|
||||
|
||||
let genned = genFixtureBindings("enum_fixture", "c")
|
||||
|
||||
suite "{.ffi.} enum end-to-end generation":
|
||||
setup:
|
||||
require genned.exitCode == 0
|
||||
let header = readFile(genned.outDir / "enumlib.h")
|
||||
|
||||
test "explicit ordinals and associated strings both survive to the C header":
|
||||
check "STATUS_ST_IDLE = 2," in header
|
||||
check "STATUS_ST_BUSY = 7" in header
|
||||
check "case LEVEL_L_LOW: return cbor_encode_text_stringz(e, \"low\");" in header
|
||||
|
||||
test "an enum return reaches the host through the reply callback":
|
||||
check "(*EnumLibPickReplyFn)(int err_code, const Color* reply," in header
|
||||
check "int enumlib_ctx_pick(const EnumLibCtx* ctx, Level level," in header
|
||||
|
||||
suite "enums on the abi = c path are rejected, not silently mis-wired":
|
||||
test "an enum in an abi = c library fails to compile":
|
||||
let (output, code) = checkFixture("enum_abi_c_type_fixture")
|
||||
check code != 0
|
||||
check "`abi = c` does not support enum types yet" in output
|
||||
|
||||
test "an enum reached from an abi = c type fails to compile":
|
||||
let (output, code) = checkFixture("enum_abi_c_field_fixture")
|
||||
check code != 0
|
||||
check "cwire: `abi = c` does not support enum types yet" in output
|
||||
check "Color" in output
|
||||
@ -34,6 +34,34 @@ suite "camelToSnakeCase":
|
||||
test "already snake_case passes through":
|
||||
check camelToSnakeCase("already_snake") == "already_snake"
|
||||
|
||||
suite "identToUpperSnake":
|
||||
test "empty string":
|
||||
check identToUpperSnake("") == ""
|
||||
|
||||
test "camelCase":
|
||||
check identToUpperSnake("maxPeers") == "MAX_PEERS"
|
||||
|
||||
test "PascalCase":
|
||||
check identToUpperSnake("MaxPeers") == "MAX_PEERS"
|
||||
|
||||
test "already UPPER_SNAKE passes through":
|
||||
check identToUpperSnake("MAX_PEERS") == "MAX_PEERS"
|
||||
|
||||
test "acronym run stays one word":
|
||||
check identToUpperSnake("HTTPPort") == "HTTP_PORT"
|
||||
|
||||
test "acronym after a word":
|
||||
check identToUpperSnake("httpTTL") == "HTTP_TTL"
|
||||
|
||||
test "digits don't split a word":
|
||||
check identToUpperSnake("v2Enabled") == "V2_ENABLED"
|
||||
|
||||
test "single word":
|
||||
check identToUpperSnake("timeout") == "TIMEOUT"
|
||||
|
||||
test "runs of underscores collapse":
|
||||
check identToUpperSnake("a__b") == "A_B"
|
||||
|
||||
suite "capitalizeFirstLetter":
|
||||
test "empty string":
|
||||
check capitalizeFirstLetter("") == ""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user