feat(codegen): C binding generator (-d:targetLang=c) (#102)

This commit is contained in:
Gabriel Cruz 2026-07-01 12:00:47 -03:00 committed by GitHub
parent f1cb110e52
commit 7e3fd96e74
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 5078 additions and 300 deletions

View File

@ -20,6 +20,20 @@ All notable changes to this project are documented in this file.
where `-install_name` requires `-dynamiclib`.
### Added
- **C binding generator** (`-d:targetLang=c`): emits a header-only C binding
(`<lib>.h`) plus a `CMakeLists.txt`, alongside the existing Rust / C++ / CDDL
backends. Requests/responses travel as CBOR using the same vendored TinyCBOR
the C++ backend uses. C has no generics or overloading, so each `seq[T]` /
`Option[T]` is monomorphised into its own struct + encode/decode/free triple.
The high-level `<lib>_ctx_*` API is asynchronous: each method/constructor
takes a typed result callback and the binding owns and reclaims all reply
data and error strings (valid only for the duration of the callback), so the
caller never frees anything — there is no blocking wait and no manual-free
contract. Shared codegen helpers were extracted
into `ffi/codegen/common.nim` (used by both the C and C++ backends). New
`nimble genbindings_c` / `genbindings_c_echo` / `check_bindings_c` /
`test_c_e2e` tasks, a `tests/e2e/c` ctest harness, and a
`tests/unit/test_c_codegen.nim` unit suite.
- Per-interaction ABI-format annotations: `declareLibrary` now takes an
optional `defaultABIFormat` (`"cbor"` default, or `"c"`) that every
`{.ffi.}` / `{.ffiCtor.}` / `{.ffiDtor.}` / `{.ffiRaw.}` / `{.ffiEvent.}`

View File

@ -0,0 +1,47 @@
cmake_minimum_required(VERSION 3.14)
project(echo_c_bindings C)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
# Locate the repository root (contains ffi.nimble)
set(_search_dir "${CMAKE_CURRENT_SOURCE_DIR}")
set(REPO_ROOT "")
foreach(_i RANGE 10)
if(EXISTS "${_search_dir}/ffi.nimble")
set(REPO_ROOT "${_search_dir}")
break()
endif()
get_filename_component(_search_dir "${_search_dir}" DIRECTORY)
endforeach()
if("${REPO_ROOT}" STREQUAL "")
message(FATAL_ERROR "Cannot find repo root (no ffi.nimble in any ancestor)")
endif()
# Build the Nim dylib + vendored TinyCBOR (shared with the C++ backend).
set(NIM_FFI_LIB echo)
set(NIM_FFI_SRC ../echo.nim)
include("${REPO_ROOT}/ffi/codegen/templates/nim_ffi_lib.cmake")
find_package(Threads REQUIRED)
add_library(echo_headers INTERFACE)
target_include_directories(echo_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")
target_link_libraries(echo_headers INTERFACE echo tinycbor Threads::Threads)
# The generated header is async (no blocking helper), but consumer code that
# waits on a result callback typically uses nanosleep / pthreads, which need a
# POSIX feature level that strict `-std=c11` hides. Define it for consumers.
target_compile_definitions(echo_headers INTERFACE _POSIX_C_SOURCE=200809L)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.c")
add_executable(echo_example main.c)
target_link_libraries(echo_example PRIVATE echo_headers)
add_dependencies(echo_example echo_nim_lib)
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
add_custom_command(TARGET echo_example POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${echo_RUNTIME_LIB}"
"$<TARGET_FILE_DIR:echo_example>"
COMMENT "Staging echo.dll next to echo_example.exe")
endif()
endif()

View File

@ -0,0 +1,415 @@
#ifndef NIM_FFI_LIB_ECHO_H_INCLUDED
#define NIM_FFI_LIB_ECHO_H_INCLUDED
#include "nim_ffi_cbor.h"
/* ============================================================ */
/* Generated types (user-declared + per-proc request envelopes) */
/* ============================================================ */
typedef struct {
NimFfiStr prefix;
} EchoConfig;
typedef struct {
NimFfiStr text;
} ShoutRequest;
typedef struct {
NimFfiStr shouted;
NimFfiStr prefix;
} ShoutResponse;
typedef struct {
EchoConfig config;
} EchoCreateCtorReq;
typedef struct {
ShoutRequest req;
} EchoShoutReq;
typedef struct {
char _nimffi_empty; /* C forbids empty structs */
} EchoVersionReq;
static inline CborError echo_enc_EchoConfig(
CborEncoder* e, const EchoConfig* v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(e, &m, 1);
if (err) return err;
err = cbor_encode_text_stringz(&m, "prefix");
if (err) return err;
err = nimffi_enc_str(&m, &v->prefix);
if (err) return err;
return cbor_encoder_close_container(e, &m);
}
static inline CborError echo_dec_EchoConfig(
CborValue* it, EchoConfig* out) {
if (!cbor_value_is_map(it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(it, "prefix", &field);
if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = nimffi_dec_str(&field, &out->prefix);
if (err) return err;
return cbor_value_advance(it);
}
static inline void echo_free_EchoConfig(EchoConfig* v) {
if (!v) return;
nimffi_free_str(&v->prefix);
}
static inline CborError echo_enc_ShoutRequest(
CborEncoder* e, const ShoutRequest* v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(e, &m, 1);
if (err) return err;
err = cbor_encode_text_stringz(&m, "text");
if (err) return err;
err = nimffi_enc_str(&m, &v->text);
if (err) return err;
return cbor_encoder_close_container(e, &m);
}
static inline CborError echo_dec_ShoutRequest(
CborValue* it, ShoutRequest* out) {
if (!cbor_value_is_map(it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(it, "text", &field);
if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = nimffi_dec_str(&field, &out->text);
if (err) return err;
return cbor_value_advance(it);
}
static inline void echo_free_ShoutRequest(ShoutRequest* v) {
if (!v) return;
nimffi_free_str(&v->text);
}
static inline CborError echo_enc_ShoutResponse(
CborEncoder* e, const ShoutResponse* v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(e, &m, 2);
if (err) return err;
err = cbor_encode_text_stringz(&m, "shouted");
if (err) return err;
err = nimffi_enc_str(&m, &v->shouted);
if (err) return err;
err = cbor_encode_text_stringz(&m, "prefix");
if (err) return err;
err = nimffi_enc_str(&m, &v->prefix);
if (err) return err;
return cbor_encoder_close_container(e, &m);
}
static inline CborError echo_dec_ShoutResponse(
CborValue* it, ShoutResponse* out) {
if (!cbor_value_is_map(it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(it, "shouted", &field);
if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = nimffi_dec_str(&field, &out->shouted);
if (err) return err;
err = cbor_value_map_find_value(it, "prefix", &field);
if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = nimffi_dec_str(&field, &out->prefix);
if (err) return err;
return cbor_value_advance(it);
}
static inline void echo_free_ShoutResponse(ShoutResponse* v) {
if (!v) return;
nimffi_free_str(&v->shouted);
nimffi_free_str(&v->prefix);
}
static inline CborError echo_enc_EchoCreateCtorReq(
CborEncoder* e, const EchoCreateCtorReq* v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(e, &m, 1);
if (err) return err;
err = cbor_encode_text_stringz(&m, "config");
if (err) return err;
err = echo_enc_EchoConfig(&m, &v->config);
if (err) return err;
return cbor_encoder_close_container(e, &m);
}
static inline CborError echo_dec_EchoCreateCtorReq(
CborValue* it, EchoCreateCtorReq* out) {
if (!cbor_value_is_map(it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(it, "config", &field);
if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = echo_dec_EchoConfig(&field, &out->config);
if (err) return err;
return cbor_value_advance(it);
}
static inline void echo_free_EchoCreateCtorReq(EchoCreateCtorReq* v) {
if (!v) return;
echo_free_EchoConfig(&v->config);
}
static inline CborError echo_enc_EchoShoutReq(
CborEncoder* e, const EchoShoutReq* v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(e, &m, 1);
if (err) return err;
err = cbor_encode_text_stringz(&m, "req");
if (err) return err;
err = echo_enc_ShoutRequest(&m, &v->req);
if (err) return err;
return cbor_encoder_close_container(e, &m);
}
static inline CborError echo_dec_EchoShoutReq(
CborValue* it, EchoShoutReq* out) {
if (!cbor_value_is_map(it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(it, "req", &field);
if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = echo_dec_ShoutRequest(&field, &out->req);
if (err) return err;
return cbor_value_advance(it);
}
static inline void echo_free_EchoShoutReq(EchoShoutReq* v) {
if (!v) return;
echo_free_ShoutRequest(&v->req);
}
static inline CborError echo_enc_EchoVersionReq(
CborEncoder* e, const EchoVersionReq* v) {
(void)v;
CborEncoder m;
CborError err = cbor_encoder_create_map(e, &m, 0);
if (err) return err;
return cbor_encoder_close_container(e, &m);
}
static inline CborError echo_dec_EchoVersionReq(
CborValue* it, EchoVersionReq* out) {
if (!cbor_value_is_map(it)) return CborErrorImproperValue;
(void)out;
return cbor_value_advance(it);
}
/* ============================================================ */
/* C ABI declarations (symbols exported by the Nim dylib) */
/* ============================================================ */
#ifdef __cplusplus
extern "C" {
#endif
void* echo_create(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback callback, void* user_data);
int echo_shout(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
int echo_version(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
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);
#ifdef __cplusplus
} /* extern "C" */
#endif
/* CBOR buffer adapters (typed codec → void* driver signature) */
static inline CborError echo_encv_EchoCreateCtorReq(CborEncoder* e, const void* v) { return echo_enc_EchoCreateCtorReq(e, (const EchoCreateCtorReq*)v); }
static inline CborError echo_encv_EchoShoutReq(CborEncoder* e, const void* v) { return echo_enc_EchoShoutReq(e, (const EchoShoutReq*)v); }
static inline CborError echo_encv_EchoVersionReq(CborEncoder* e, const void* v) { return echo_enc_EchoVersionReq(e, (const EchoVersionReq*)v); }
static inline CborError echo_decv_ShoutResponse(CborValue* it, void* v) { return echo_dec_ShoutResponse(it, (ShoutResponse*)v); }
static inline CborError echo_decv_Str(CborValue* it, void* v) { return nimffi_dec_str(it, (NimFfiStr*)v); }
/* ============================================================ */
/* High-level context wrapper */
/* ============================================================ */
typedef struct {
void* ptr;
} EchoCtx;
typedef void (*EchoCreateFn)(int err_code, EchoCtx* ctx, const char* err_msg, void* user_data);
typedef struct { EchoCreateFn fn; void* user_data; } EchoCreateBox;
static void echo_create_trampoline(int ret, const char* msg, size_t len, void* ud) {
EchoCreateBox* box = (EchoCreateBox*)ud;
if (!box->fn) {
free(box);
return;
}
if (ret != 0) {
char* em = nimffi_dup_cstr_n(msg ? msg : "", msg ? len : 0);
box->fn(ret, NULL, em ? em : "FFI create failed", box->user_data);
free(em);
free(box);
return;
}
char* err = NULL;
NimFfiStr addr;
memset(&addr, 0, sizeof(addr));
if (nimffi_decode_from_buf(echo_decv_Str, (const uint8_t*)msg, len, &addr, &err) != 0) {
box->fn(-1, NULL, err ? err : "decode failed", box->user_data);
free(err);
free(box);
return;
}
char* endp = NULL;
unsigned long long a = addr.data ? strtoull(addr.data, &endp, 10) : 0;
bool ok = addr.data && addr.len > 0 && endp && *endp == '\0';
nimffi_free_str(&addr);
if (!ok) {
box->fn(-1, NULL, "FFI create returned non-numeric address", box->user_data);
free(box);
return;
}
EchoCtx* ctx = (EchoCtx*)calloc(1, sizeof(EchoCtx));
if (!ctx) {
box->fn(-1, NULL, "out of memory", box->user_data);
free(box);
return;
}
ctx->ptr = (void*)(uintptr_t)a;
box->fn(NIMFFI_RET_OK, ctx, NULL, box->user_data);
free(box);
}
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));
ffi_req.config = *config;
uint8_t* req_buf = NULL;
size_t req_len = 0;
char* err = NULL;
if (nimffi_encode_to_buf(echo_encv_EchoCreateCtorReq, &ffi_req, &req_buf, &req_len, &err) != 0) {
if (on_created) on_created(-1, NULL, err ? err : "encode failed", user_data);
free(err);
return -1;
}
EchoCreateBox* box = (EchoCreateBox*)malloc(sizeof(EchoCreateBox));
if (!box) {
free(req_buf);
if (on_created) on_created(-1, NULL, "out of memory", user_data);
return -1;
}
box->fn = on_created;
box->user_data = user_data;
(void)echo_create(req_buf, req_len, echo_create_trampoline, box);
free(req_buf);
return 0;
}
static inline void echo_ctx_destroy(EchoCtx* ctx) {
if (!ctx) return;
if (ctx->ptr) { echo_destroy(ctx->ptr); ctx->ptr = NULL; }
free(ctx);
}
typedef void (*EchoShoutReplyFn)(int err_code, const ShoutResponse* reply, const char* err_msg, void* user_data);
typedef struct { EchoShoutReplyFn fn; void* user_data; } EchoShoutCallBox;
static void echo_shout_reply_trampoline(int ret, const char* msg, size_t len, void* ud) {
EchoShoutCallBox* box = (EchoShoutCallBox*)ud;
if (!box->fn) {
free(box);
return;
}
if (ret != 0) {
char* em = nimffi_dup_cstr_n(msg ? msg : "", msg ? len : 0);
box->fn(ret, NULL, em ? em : "FFI call failed", box->user_data);
free(em);
free(box);
return;
}
char* err = NULL;
ShoutResponse out;
memset(&out, 0, sizeof(out));
int dec = nimffi_decode_from_buf(echo_decv_ShoutResponse, (const uint8_t*)msg, len, &out, &err);
if (dec != 0) {
box->fn(-1, NULL, err ? err : "decode failed", box->user_data);
free(err);
echo_free_ShoutResponse(&out);
free(box);
return;
}
box->fn(NIMFFI_RET_OK, &out, NULL, box->user_data);
echo_free_ShoutResponse(&out);
free(box);
}
static inline int echo_ctx_shout(const EchoCtx* ctx, const ShoutRequest* req, EchoShoutReplyFn on_reply, void* user_data) {
EchoShoutReq ffi_req;
memset(&ffi_req, 0, sizeof(ffi_req));
ffi_req.req = *req;
uint8_t* req_buf = NULL;
size_t req_len = 0;
char* err = NULL;
if (nimffi_encode_to_buf(echo_encv_EchoShoutReq, &ffi_req, &req_buf, &req_len, &err) != 0) {
if (on_reply) on_reply(-1, NULL, err ? err : "encode failed", user_data);
free(err);
return -1;
}
EchoShoutCallBox* box = (EchoShoutCallBox*)malloc(sizeof(EchoShoutCallBox));
if (!box) {
free(req_buf);
if (on_reply) on_reply(-1, NULL, "out of memory", user_data);
return -1;
}
box->fn = on_reply;
box->user_data = user_data;
int ret = echo_shout(ctx->ptr, echo_shout_reply_trampoline, box, req_buf, req_len);
free(req_buf);
if (ret == NIMFFI_RET_MISSING_CALLBACK) {
if (on_reply) on_reply(-1, NULL, "RET_MISSING_CALLBACK (internal error)", user_data);
free(box);
return -1;
}
return 0;
}
typedef void (*EchoVersionReplyFn)(int err_code, const NimFfiStr* reply, const char* err_msg, void* user_data);
typedef struct { EchoVersionReplyFn fn; void* user_data; } EchoVersionCallBox;
static void echo_version_reply_trampoline(int ret, const char* msg, size_t len, void* ud) {
EchoVersionCallBox* box = (EchoVersionCallBox*)ud;
if (!box->fn) {
free(box);
return;
}
if (ret != 0) {
char* em = nimffi_dup_cstr_n(msg ? msg : "", msg ? len : 0);
box->fn(ret, NULL, em ? em : "FFI call failed", box->user_data);
free(em);
free(box);
return;
}
char* err = NULL;
NimFfiStr out;
memset(&out, 0, sizeof(out));
int dec = nimffi_decode_from_buf(echo_decv_Str, (const uint8_t*)msg, len, &out, &err);
if (dec != 0) {
box->fn(-1, NULL, err ? err : "decode failed", box->user_data);
free(err);
nimffi_free_str(&out);
free(box);
return;
}
box->fn(NIMFFI_RET_OK, &out, NULL, box->user_data);
nimffi_free_str(&out);
free(box);
}
static inline int echo_ctx_version(const EchoCtx* ctx, EchoVersionReplyFn on_reply, void* user_data) {
EchoVersionReq ffi_req;
memset(&ffi_req, 0, sizeof(ffi_req));
uint8_t* req_buf = NULL;
size_t req_len = 0;
char* err = NULL;
if (nimffi_encode_to_buf(echo_encv_EchoVersionReq, &ffi_req, &req_buf, &req_len, &err) != 0) {
if (on_reply) on_reply(-1, NULL, err ? err : "encode failed", user_data);
free(err);
return -1;
}
EchoVersionCallBox* box = (EchoVersionCallBox*)malloc(sizeof(EchoVersionCallBox));
if (!box) {
free(req_buf);
if (on_reply) on_reply(-1, NULL, "out of memory", user_data);
return -1;
}
box->fn = on_reply;
box->user_data = user_data;
int ret = echo_version(ctx->ptr, echo_version_reply_trampoline, box, req_buf, req_len);
free(req_buf);
if (ret == NIMFFI_RET_MISSING_CALLBACK) {
if (on_reply) on_reply(-1, NULL, "RET_MISSING_CALLBACK (internal error)", user_data);
free(box);
return -1;
}
return 0;
}
#endif /* NIM_FFI_LIB_ECHO_H_INCLUDED */

View File

@ -0,0 +1,342 @@
#ifndef NIM_FFI_CBOR_HELPERS_H_INCLUDED
#define NIM_FFI_CBOR_HELPERS_H_INCLUDED
/* Leaf CBOR codecs (scalars, text strings, byte strings) plus the buffer
* drivers. The per-struct / per-container codecs in the library header call
* into these by name (C has no overloading, so each leaf gets a distinct
* nimffi_enc_* / nimffi_dec_* symbol). Guarded so two nim-ffi headers can
* share a translation unit. */
#include "nim_ffi_prelude.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Result delivery callback exported by the Nim dylib: `ret` is 0 on success
* (then `msg`/`len` carry the CBOR response) or non-zero on failure (then
* `msg`/`len` carry the error text, which is NOT NUL-terminated). */
typedef void (*FFICallback)(int ret, const char* msg, size_t len, void* user_data);
/* Return / callback status codes. NIMFFI_RET_OK (0) is success; any non-zero
* value handed to a result callback's `err_code` (or returned by a submit call)
* is a failure. NIMFFI_RET_MISSING_CALLBACK is a special case from the Nim
* dispatcher: the callback will never fire, so the request path must report the
* failure itself. */
#define NIMFFI_RET_OK 0
#define NIMFFI_RET_ERROR 1
#define NIMFFI_RET_MISSING_CALLBACK 2
/* ── leaf encoders ─────────────────────────────────────────────────────── */
static inline CborError nimffi_enc_bool(CborEncoder* e, const bool* v) {
return cbor_encode_boolean(e, *v);
}
static inline CborError nimffi_enc_i64(CborEncoder* e, const int64_t* v) {
return cbor_encode_int(e, *v);
}
static inline CborError nimffi_enc_i32(CborEncoder* e, const int32_t* v) {
return cbor_encode_int(e, (int64_t)*v);
}
static inline CborError nimffi_enc_i16(CborEncoder* e, const int16_t* v) {
return cbor_encode_int(e, (int64_t)*v);
}
static inline CborError nimffi_enc_i8(CborEncoder* e, const int8_t* v) {
return cbor_encode_int(e, (int64_t)*v);
}
static inline CborError nimffi_enc_u64(CborEncoder* e, const uint64_t* v) {
return cbor_encode_uint(e, *v);
}
static inline CborError nimffi_enc_u32(CborEncoder* e, const uint32_t* v) {
return cbor_encode_uint(e, (uint64_t)*v);
}
static inline CborError nimffi_enc_u16(CborEncoder* e, const uint16_t* v) {
return cbor_encode_uint(e, (uint64_t)*v);
}
static inline CborError nimffi_enc_u8(CborEncoder* e, const uint8_t* v) {
return cbor_encode_uint(e, (uint64_t)*v);
}
static inline CborError nimffi_enc_f64(CborEncoder* e, const double* v) {
return cbor_encode_double(e, *v);
}
static inline CborError nimffi_enc_f32(CborEncoder* e, const float* v) {
return cbor_encode_float(e, *v);
}
static inline CborError nimffi_enc_str(CborEncoder* e, const NimFfiStr* v) {
return cbor_encode_text_string(e, v->data ? v->data : "", v->len);
}
static inline CborError nimffi_enc_bytes(CborEncoder* e, const NimFfiBytes* v) {
return cbor_encode_byte_string(e, v->data, v->len);
}
/* ── leaf decoders ─────────────────────────────────────────────────────── */
/* After reading a leaf, the parser must advance past it; both steps
* short-circuit on the same CborError, so they travel together. */
static inline CborError nimffi_advance_if_ok(CborValue* it, CborError err) {
if (err) {
return err;
}
return cbor_value_advance(it);
}
static inline CborError nimffi_dec_bool(CborValue* it, bool* out) {
if (!cbor_value_is_boolean(it)) {
return CborErrorImproperValue;
}
return nimffi_advance_if_ok(it, cbor_value_get_boolean(it, out));
}
static inline CborError nimffi_dec_i64(CborValue* it, int64_t* out) {
if (!cbor_value_is_integer(it)) {
return CborErrorImproperValue;
}
return nimffi_advance_if_ok(it, cbor_value_get_int64_checked(it, out));
}
static inline CborError nimffi_dec_i32(CborValue* it, int32_t* out) {
int64_t tmp = 0;
CborError err = nimffi_dec_i64(it, &tmp);
if (err) {
return err;
}
if (tmp < INT32_MIN || tmp > INT32_MAX) {
return CborErrorDataTooLarge;
}
*out = (int32_t)tmp;
return CborNoError;
}
static inline CborError nimffi_dec_i16(CborValue* it, int16_t* out) {
int64_t tmp = 0;
CborError err = nimffi_dec_i64(it, &tmp);
if (err) {
return err;
}
if (tmp < INT16_MIN || tmp > INT16_MAX) {
return CborErrorDataTooLarge;
}
*out = (int16_t)tmp;
return CborNoError;
}
static inline CborError nimffi_dec_i8(CborValue* it, int8_t* out) {
int64_t tmp = 0;
CborError err = nimffi_dec_i64(it, &tmp);
if (err) {
return err;
}
if (tmp < INT8_MIN || tmp > INT8_MAX) {
return CborErrorDataTooLarge;
}
*out = (int8_t)tmp;
return CborNoError;
}
static inline CborError nimffi_dec_u64(CborValue* it, uint64_t* out) {
if (!cbor_value_is_unsigned_integer(it)) {
return CborErrorImproperValue;
}
return nimffi_advance_if_ok(it, cbor_value_get_uint64(it, out));
}
static inline CborError nimffi_dec_u32(CborValue* it, uint32_t* out) {
uint64_t tmp = 0;
CborError err = nimffi_dec_u64(it, &tmp);
if (err) {
return err;
}
if (tmp > UINT32_MAX) {
return CborErrorDataTooLarge;
}
*out = (uint32_t)tmp;
return CborNoError;
}
static inline CborError nimffi_dec_u16(CborValue* it, uint16_t* out) {
uint64_t tmp = 0;
CborError err = nimffi_dec_u64(it, &tmp);
if (err) {
return err;
}
if (tmp > UINT16_MAX) {
return CborErrorDataTooLarge;
}
*out = (uint16_t)tmp;
return CborNoError;
}
static inline CborError nimffi_dec_u8(CborValue* it, uint8_t* out) {
uint64_t tmp = 0;
CborError err = nimffi_dec_u64(it, &tmp);
if (err) {
return err;
}
if (tmp > UINT8_MAX) {
return CborErrorDataTooLarge;
}
*out = (uint8_t)tmp;
return CborNoError;
}
static inline CborError nimffi_dec_f64(CborValue* it, double* out) {
if (cbor_value_is_double(it)) {
return nimffi_advance_if_ok(it, cbor_value_get_double(it, out));
}
if (cbor_value_is_float(it)) {
float f = 0.0f;
CborError err = cbor_value_get_float(it, &f);
if (err) {
return err;
}
*out = (double)f;
return cbor_value_advance(it);
}
return CborErrorImproperValue;
}
static inline CborError nimffi_dec_f32(CborValue* it, float* out) {
if (cbor_value_is_float(it)) {
return nimffi_advance_if_ok(it, cbor_value_get_float(it, out));
}
if (cbor_value_is_double(it)) {
double d = 0.0;
CborError err = cbor_value_get_double(it, &d);
if (err) {
return err;
}
*out = (float)d;
return cbor_value_advance(it);
}
return CborErrorImproperValue;
}
static inline CborError nimffi_dec_str(CborValue* it, NimFfiStr* 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;
}
if (len == SIZE_MAX) { /* len + 1 would wrap to a 0-byte allocation */
return CborErrorDataTooLarge;
}
/* one extra byte so a NUL-free payload is a valid C string */
out->data = (char*)malloc(len + 1);
if (!out->data) {
return CborErrorOutOfMemory;
}
out->len = len;
size_t copied = len;
err = cbor_value_copy_text_string(it, out->data, &copied, NULL);
if (err) {
free(out->data);
out->data = NULL;
out->len = 0;
return err;
}
out->data[len] = '\0';
return cbor_value_advance(it);
}
static inline CborError nimffi_dec_bytes(CborValue* it, NimFfiBytes* out) {
if (!cbor_value_is_byte_string(it)) {
return CborErrorImproperValue;
}
size_t len = 0;
CborError err = cbor_value_get_string_length(it, &len);
if (err) {
return err;
}
out->data = (uint8_t*)malloc(len ? len : 1);
if (!out->data) {
return CborErrorOutOfMemory;
}
out->len = len;
size_t copied = len;
err = cbor_value_copy_byte_string(it, out->data, &copied, NULL);
if (err) {
free(out->data);
out->data = NULL;
out->len = 0;
return err;
}
return cbor_value_advance(it);
}
/* ── buffer drivers ────────────────────────────────────────────────────── */
typedef CborError (*nimffi_enc_fn)(CborEncoder*, const void*);
typedef CborError (*nimffi_dec_fn)(CborValue*, void*);
static inline char* nimffi_dup_cstr(const char* s) {
size_t n = strlen(s) + 1;
char* p = (char*)malloc(n);
if (p) {
memcpy(p, s, n);
}
return p;
}
/* NUL-terminated copy of a length-delimited (not NUL-terminated) byte run,
* for turning the FFICallback's raw error `msg`/`len` into a C string. */
static inline char* nimffi_dup_cstr_n(const char* s, size_t n) {
char* p = (char*)malloc(n + 1);
if (p) {
if (n > 0) {
memcpy(p, s, n);
}
p[n] = '\0';
}
return p;
}
/* Encode `val` with `fn` into a freshly malloc'd buffer, doubling on overflow.
* Returns 0 and sets out/outlen on success; -1 and *err (heap) on failure. */
static inline int nimffi_encode_to_buf(
nimffi_enc_fn fn, const void* val,
uint8_t** out, size_t* outlen, char** err) {
size_t cap = 4096;
uint8_t* buf = (uint8_t*)malloc(cap);
if (!buf) {
if (err) *err = nimffi_dup_cstr("out of memory");
return -1;
}
for (;;) {
CborEncoder enc;
cbor_encoder_init(&enc, buf, cap, 0);
CborError e = fn(&enc, val);
if (e == CborNoError) {
*outlen = cbor_encoder_get_buffer_size(&enc, buf);
*out = buf;
return 0;
}
if (e == CborErrorOutOfMemory) {
size_t extra = cbor_encoder_get_extra_bytes_needed(&enc);
cap += extra > 0 ? extra : cap;
uint8_t* grown = (uint8_t*)realloc(buf, cap);
if (!grown) {
free(buf);
if (err) *err = nimffi_dup_cstr("out of memory");
return -1;
}
buf = grown;
continue;
}
free(buf);
if (err) *err = nimffi_dup_cstr(cbor_error_string(e));
return -1;
}
}
/* Decode a CBOR buffer into `out` with `fn`. Returns 0 on success; -1 and
* *err (heap) on failure. */
static inline int nimffi_decode_from_buf(
nimffi_dec_fn fn, const uint8_t* buf, size_t len,
void* out, char** err) {
CborParser parser;
CborValue it;
CborError e = cbor_parser_init(buf, len, 0, &parser, &it);
if (e != CborNoError) {
if (err) *err = nimffi_dup_cstr(cbor_error_string(e));
return -1;
}
e = fn(&it, out);
if (e != CborNoError) {
if (err) *err = nimffi_dup_cstr(cbor_error_string(e));
return -1;
}
return 0;
}
#ifdef __cplusplus
}
#endif
#endif /* NIM_FFI_CBOR_HELPERS_H_INCLUDED */

View File

@ -0,0 +1,89 @@
#ifndef NIM_FFI_PRELUDE_H_INCLUDED
#define NIM_FFI_PRELUDE_H_INCLUDED
/* Generated C binding for a nim-ffi library. Requests/responses travel as
* CBOR (encoded with vendored TinyCBOR on this side, matching the Nim-side
* cbor_serial codec on the wire both ends speak RFC 8949).
*
* The API is asynchronous: every method/constructor takes a result callback
* and returns immediately. The callback fires exactly once synchronously on
* a submit-time failure, otherwise from the Nim dispatch thread when the reply
* arrives.
*
* Memory ownership contract:
* - Request-side strings/sequences are *borrowed*: the binding only reads
* them while encoding, so a string literal wrapped with nimffi_str() is
* fine and is never freed by the binding.
* - Response values and error strings passed into a result callback are
* *owned by the binding* and valid only for the duration of that callback;
* the binding reclaims them once the callback returns. The caller never
* frees them. (The generated <lib>_free_<Type>() helpers are internal the
* trampolines use them to reclaim decoded payloads.)
* - A context handle delivered to a constructor callback is the exception:
* ownership transfers to the caller, who releases it with
* <lib>_ctx_destroy(). It is a lifecycle handle, not returned data.
*
* Trust boundary: the decoders assume the CBOR they parse was produced by the
* paired Nim library. They reject malformed input rather than trusting it, but
* they are not hardened against a hostile peer feeding crafted payloads through
* the raw nimffi_decode_from_buf entry point.
*/
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <tinycbor/cbor.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Owned, length-delimited UTF-8 text (Nim `string`/`cstring`). On the request
* side `data` may point at borrowed storage (see nimffi_str); on the response
* side it is heap-allocated and freed by nimffi_free_str. Always NUL-padded by
* one byte after decode so `data` is usable as a C string when it has no
* embedded NULs. */
typedef struct {
char* data;
size_t len;
} NimFfiStr;
/* Owned, length-delimited byte buffer (Nim `seq[byte]`). */
typedef struct {
uint8_t* data;
size_t len;
} NimFfiBytes;
/* Wrap a borrowed C string for use as a request field. The returned view is
* not owned by the binding and must outlive the call that encodes it. */
static inline NimFfiStr nimffi_str(const char* s) {
NimFfiStr v;
v.data = (char*)s;
v.len = s ? strlen(s) : 0;
return v;
}
static inline void nimffi_free_str(NimFfiStr* v) {
if (!v || !v->data) {
return;
}
free(v->data);
v->data = NULL;
v->len = 0;
}
static inline void nimffi_free_bytes(NimFfiBytes* v) {
if (!v || !v->data) {
return;
}
free(v->data);
v->data = NULL;
v->len = 0;
}
#ifdef __cplusplus
}
#endif
#endif /* NIM_FFI_PRELUDE_H_INCLUDED */

View File

@ -27,92 +27,10 @@ if("${REPO_ROOT}" STREQUAL "")
message(FATAL_ERROR "Cannot find repo root (no ffi.nimble in any ancestor)")
endif()
get_filename_component(NIM_SRC
"${CMAKE_CURRENT_SOURCE_DIR}/../echo.nim"
ABSOLUTE)
find_program(NIM_EXECUTABLE nim REQUIRED)
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
set(NIM_LIB_FILE "${REPO_ROOT}/libecho.dylib")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
set(NIM_LIB_FILE "${REPO_ROOT}/echo.dll")
# MSVC consumers link against the `.lib` import library, not the DLL.
# MinGW's ld emits one when asked via `--out-implib`; the resulting COFF
# archive is readable by MSVC's link.exe.
set(NIM_IMPLIB_FILE "${REPO_ROOT}/echo.lib")
else()
set(NIM_LIB_FILE "${REPO_ROOT}/libecho.so")
endif()
# On Windows the default Nim toolchain (mingw gcc) doesn't emit an import
# library unless told to. Without it, MSVC consumers can't resolve any
# symbol exported by the DLL at link time.
set(NIM_IMPLIB_PASSL "")
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
set(NIM_IMPLIB_PASSL "--passL:-Wl,--out-implib,${NIM_IMPLIB_FILE}")
endif()
add_custom_command(
OUTPUT "${NIM_LIB_FILE}"
COMMAND "${NIM_EXECUTABLE}" c
--mm:orc
-d:chronicles_log_level=WARN
--app:lib
--noMain
"--nimMainPrefix:libecho"
${NIM_IMPLIB_PASSL}
"-o:${NIM_LIB_FILE}"
"${NIM_SRC}"
WORKING_DIRECTORY "${REPO_ROOT}"
DEPENDS "${NIM_SRC}"
BYPRODUCTS "${NIM_IMPLIB_FILE}"
COMMENT "Compiling Nim library libecho"
VERBATIM
)
add_custom_target(echo_nim_lib ALL DEPENDS "${NIM_LIB_FILE}")
# On Windows, an `IMPORTED SHARED` target needs IMPORTED_IMPLIB pointing at
# the `.lib` import library so MSVC's `link.exe` can resolve symbols. The
# Visual Studio multi-config generator did not pick up `IMPORTED_IMPLIB`
# nor per-config `IMPORTED_IMPLIB_<CONFIG>` variants and emitted
# `echo-NOTFOUND.obj` into every link line. Side-step the IMPORTED
# machinery on Windows by exposing the import library through a plain
# INTERFACE library that links the `.lib` by path.
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
add_library(echo INTERFACE)
target_link_libraries(echo INTERFACE "${NIM_IMPLIB_FILE}")
else()
add_library(echo SHARED IMPORTED GLOBAL)
set_target_properties(echo PROPERTIES IMPORTED_LOCATION "${NIM_LIB_FILE}")
endif()
add_dependencies(echo echo_nim_lib)
# Absolute path to the runtime library (DLL/dylib/so). Exposed via the cache
# so consumers in other directories (e.g. tests/e2e/cpp) can stage the DLL
# next to their executable on Windows.
set(echo_RUNTIME_LIB "${NIM_LIB_FILE}" CACHE INTERNAL
"Absolute path to the echo runtime library")
# TinyCBOR (vendored at ffi/codegen/templates/cpp/vendor/tinycbor)
# Guarded so two sibling cpp_bindings dirs in one parent project don't redefine
# the `tinycbor` target.
set(TINYCBOR_SRC_DIR "${REPO_ROOT}/ffi/codegen/templates/cpp/vendor")
if(NOT TARGET tinycbor)
add_library(tinycbor STATIC
"${TINYCBOR_SRC_DIR}/tinycbor/cborencoder.c"
"${TINYCBOR_SRC_DIR}/tinycbor/cborencoder_close_container_checked.c"
"${TINYCBOR_SRC_DIR}/tinycbor/cborparser.c"
"${TINYCBOR_SRC_DIR}/tinycbor/cborparser_dup_string.c"
"${TINYCBOR_SRC_DIR}/tinycbor/cborerrorstrings.c"
)
target_include_directories(tinycbor PUBLIC
"${TINYCBOR_SRC_DIR}" # consumer uses #include <tinycbor/cbor.h>
"${TINYCBOR_SRC_DIR}/tinycbor" # internal _p.h includes resolve here
)
set_property(TARGET tinycbor PROPERTY C_STANDARD 99)
set_property(TARGET tinycbor PROPERTY POSITION_INDEPENDENT_CODE ON)
endif()
# Build the Nim dylib + vendored TinyCBOR (shared with the C backend).
set(NIM_FFI_LIB echo)
set(NIM_FFI_SRC ../echo.nim)
include("${REPO_ROOT}/ffi/codegen/templates/nim_ffi_lib.cmake")
add_library(echo_headers INTERFACE)
target_include_directories(echo_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")

View File

@ -0,0 +1,47 @@
cmake_minimum_required(VERSION 3.14)
project(my_timer_c_bindings C)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
# Locate the repository root (contains ffi.nimble)
set(_search_dir "${CMAKE_CURRENT_SOURCE_DIR}")
set(REPO_ROOT "")
foreach(_i RANGE 10)
if(EXISTS "${_search_dir}/ffi.nimble")
set(REPO_ROOT "${_search_dir}")
break()
endif()
get_filename_component(_search_dir "${_search_dir}" DIRECTORY)
endforeach()
if("${REPO_ROOT}" STREQUAL "")
message(FATAL_ERROR "Cannot find repo root (no ffi.nimble in any ancestor)")
endif()
# Build the Nim dylib + vendored TinyCBOR (shared with the C++ backend).
set(NIM_FFI_LIB my_timer)
set(NIM_FFI_SRC ../timer.nim)
include("${REPO_ROOT}/ffi/codegen/templates/nim_ffi_lib.cmake")
find_package(Threads REQUIRED)
add_library(my_timer_headers INTERFACE)
target_include_directories(my_timer_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")
target_link_libraries(my_timer_headers INTERFACE my_timer tinycbor Threads::Threads)
# The generated header is async (no blocking helper), but consumer code that
# waits on a result callback typically uses nanosleep / pthreads, which need a
# POSIX feature level that strict `-std=c11` hides. Define it for consumers.
target_compile_definitions(my_timer_headers INTERFACE _POSIX_C_SOURCE=200809L)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.c")
add_executable(my_timer_example main.c)
target_link_libraries(my_timer_example PRIVATE my_timer_headers)
add_dependencies(my_timer_example my_timer_nim_lib)
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
add_custom_command(TARGET my_timer_example POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${my_timer_RUNTIME_LIB}"
"$<TARGET_FILE_DIR:my_timer_example>"
COMMENT "Staging my_timer.dll next to my_timer_example.exe")
endif()
endif()

View File

@ -0,0 +1,72 @@
# C Bindings for nim-timer
## Purpose
This folder contains **auto-generated C bindings** for the `my_timer` Nim
library. It is generated from `../timer.nim` and provides:
- `my_timer.h`: header-only C binding (`MyTimerCtx` + `my_timer_ctx_*` API)
- `main.c`: example executable demonstrating how to use the bindings
- `CMakeLists.txt`: build configuration that compiles the Nim library, the
vendored TinyCBOR, and the C example
The bindings speak CBOR on the wire (the same format as the Rust and C++
backends) using the TinyCBOR copy vendored at
`ffi/codegen/templates/cpp/vendor/tinycbor`.
## How It's Generated
Regenerate these bindings by running from the parent directory:
```sh
cd examples/timer
nimble genbindings_c
```
This invokes the Nim compiler with `-d:targetLang=c`, triggering
`genBindings(...)` in `timer.nim`, which reads the compile-time FFI registries
and emits the binding files.
## Building the Example
```sh
cd examples/timer/c_bindings
cmake -S . -B build
cmake --build build
./build/my_timer_example
```
## Asynchronous API
Every method and the constructor take a typed **result callback** and return
immediately. The callback fires exactly once — synchronously if the request
fails to even submit, otherwise from the Nim dispatch thread when the reply
arrives:
```c
static void on_echo(int err_code, const EchoResponse* reply,
const char* err_msg, void* user_data) {
if (err_code != 0) { /* err_msg is set, reply is NULL */ return; }
printf("echoed: %s\n", reply->echoed.data);
}
...
my_timer_ctx_echo(ctx, &req, on_echo, /*user_data=*/NULL);
```
See `main.c` for the full pattern, including a small `wait_done()` poll helper
that turns each async call back into a sequential step.
## Memory Ownership
- Request-side strings/sequences are *borrowed* — wrap C strings with
`nimffi_str(...)`; the binding never frees them.
- Reply values and error strings passed into a result callback are **owned by
the binding** and valid only for the duration of that callback. The caller
never frees them — copy out anything you need to keep before returning.
- A `MyTimerCtx*` delivered to the constructor callback is the exception:
ownership transfers to you, and you release it with `my_timer_ctx_destroy()`.
## Do Not Edit
The generated files in this folder are overwritten each time
`nimble genbindings_c` runs. Any manual changes will be lost.

View File

@ -0,0 +1,226 @@
#include "my_timer.h"
#include <stdio.h>
#include <string.h>
#if defined(__STDC_NO_ATOMICS__)
# error "C11 atomics required (or provide a mutex/condvar fallback)"
#endif
#include <stdatomic.h>
/* The `done` flags below are written from the library's dispatch thread and
* polled from main, so they cross a thread boundary atomics, not `volatile`,
* give the visibility guarantee. sleep_ms wraps the platform nap so the demo
* builds on Windows too. */
#if defined(_WIN32)
# include <windows.h>
static void sleep_ms(unsigned ms) { Sleep(ms); }
#else
# include <time.h>
static void sleep_ms(unsigned ms) {
struct timespec t = {(time_t)(ms / 1000), (long)(ms % 1000) * 1000 * 1000};
nanosleep(&t, NULL);
}
#endif
/* The generated bindings are asynchronous: each call takes a result callback
* and returns immediately. The reply and any error string handed to that
* callback are owned by the binding and valid only while the callback runs
* the caller never frees them; it copies out whatever it wants to keep. This
* demo turns each async call back into a sequential step by polling a `done`
* flag (the same pattern the typed event listener already uses). */
/* Poll up to ~5s for a callback to fire. Returns false if it never did, so the
* caller can report a stuck call instead of treating it as an empty success. */
static bool wait_done(atomic_int* done) {
for (int i = 0; i < 500 && !atomic_load(done); i++) {
sleep_ms(10);
}
return atomic_load(done) != 0;
}
static atomic_int g_echo_count = 0;
static char g_echo_message[256];
static void on_echo_fired(const EchoEvent* evt, void* user_data) {
(void)user_data;
atomic_store(&g_echo_count, (int)evt->echoCount);
snprintf(g_echo_message, sizeof(g_echo_message), "%s",
evt->message.data ? evt->message.data : "");
}
typedef struct {
atomic_int done;
int err_code;
MyTimerCtx* ctx;
char err[256];
} CreateWaiter;
static void on_created(int ec, MyTimerCtx* ctx, const char* em, void* ud) {
CreateWaiter* w = (CreateWaiter*)ud;
w->err_code = ec;
w->ctx = ctx;
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
atomic_store(&w->done, 1);
}
/* Generic reply sink: each step copies the fields it cares about out of its
* typed reply into these slots (text_a/text_b for strings, num_a/num_b for
* integers, flag for a boolean) before the binding reclaims the reply. */
typedef struct {
atomic_int done;
int err_code;
char err[256];
char text_a[256];
char text_b[256];
long long num_a;
long long num_b;
int flag;
} ReplyWaiter;
static void on_version(int ec, const NimFfiStr* reply, const char* em, void* ud) {
ReplyWaiter* w = (ReplyWaiter*)ud;
w->err_code = ec;
if (reply && reply->data) snprintf(w->text_a, sizeof(w->text_a), "%s", reply->data);
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
atomic_store(&w->done, 1);
}
static void on_echo(int ec, const EchoResponse* reply, const char* em, void* ud) {
ReplyWaiter* w = (ReplyWaiter*)ud;
w->err_code = ec;
if (reply) {
if (reply->echoed.data)
snprintf(w->text_a, sizeof(w->text_a), "%s", reply->echoed.data);
if (reply->timerName.data)
snprintf(w->text_b, sizeof(w->text_b), "%s", reply->timerName.data);
}
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
atomic_store(&w->done, 1);
}
static void on_complex(int ec, const ComplexResponse* reply, const char* em, void* ud) {
ReplyWaiter* w = (ReplyWaiter*)ud;
w->err_code = ec;
if (reply) {
w->num_a = (long long)reply->itemCount;
w->flag = (int)reply->hasNote;
if (reply->summary.data)
snprintf(w->text_a, sizeof(w->text_a), "%s", reply->summary.data);
}
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
atomic_store(&w->done, 1);
}
static void on_schedule(int ec, const ScheduleResult* reply, const char* em, void* ud) {
ReplyWaiter* w = (ReplyWaiter*)ud;
w->err_code = ec;
if (reply) {
w->num_a = (long long)reply->willRunCount;
w->num_b = (long long)reply->firstRunAtMs;
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);
atomic_store(&w->done, 1);
}
/* Fire an async call, block until its callback lands, and bail to cleanup on a
* timeout or error. Relies on `ctx` being in scope for that cleanup these
* steps all run against the one context created in main(). */
#define RUN(call, w) \
do { \
memset(&(w), 0, sizeof(w)); \
call; \
const char* run_err = NULL; \
if (!wait_done(&(w).done)) \
run_err = "FFI call did not complete"; \
else if ((w).err_code != 0) \
run_err = (w).err[0] ? (w).err : "unknown"; \
if (run_err) { \
fprintf(stderr, "Error: %s\n", run_err); \
my_timer_ctx_destroy(ctx); \
return 1; \
} \
} while (0)
int main(void) {
CreateWaiter cw;
memset(&cw, 0, sizeof(cw));
TimerConfig config = {nimffi_str("c-demo")};
my_timer_ctx_create(&config, on_created, &cw);
if (!wait_done(&cw.done) || cw.err_code != 0 || !cw.ctx) {
fprintf(stderr, "Error: %s\n",
cw.err[0] ? cw.err : "create did not complete");
return 1;
}
MyTimerCtx* ctx = cw.ctx;
printf("[1] Context created\n");
ReplyWaiter w;
RUN(my_timer_ctx_version(ctx, on_version, &w), w);
printf("[2] Version: %s\n", w.text_a);
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);
EchoRequest items[2] = {
{nimffi_str("one"), 10},
{nimffi_str("two"), 20},
};
NimFfiStr tags[2] = {nimffi_str("fast"), nimffi_str("c")};
ComplexRequest complex_req;
complex_req.messages.data = items;
complex_req.messages.len = 2;
complex_req.tags.data = tags;
complex_req.tags.len = 2;
complex_req.note.has_value = true;
complex_req.note.value = nimffi_str("extra note");
complex_req.retries.has_value = true;
complex_req.retries.value = 3;
RUN(my_timer_ctx_complex(ctx, &complex_req, on_complex, &w), w);
printf("[4] Complex: summary=%s, itemCount=%lld, hasNote=%d\n", w.text_a, w.num_a,
w.flag);
NimFfiStr job_payload[2] = {nimffi_str("rollup"), nimffi_str("v2")};
JobSpec job;
job.name = nimffi_str("nightly-rollup");
job.payload.data = job_payload;
job.payload.len = 2;
job.priority = 10;
NimFfiStr retry_on[2] = {nimffi_str("timeout"), nimffi_str("5xx")};
RetryPolicy retry;
retry.maxAttempts = 3;
retry.backoffMs = 500;
retry.retryOn.data = retry_on;
retry.retryOn.len = 2;
ScheduleConfig schedule;
schedule.startAtMs = 1000;
schedule.intervalMs = 15000;
schedule.jitter.has_value = true;
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);
uint64_t handle =
my_timer_ctx_add_on_echo_fired_listener(ctx, on_echo_fired, NULL);
EchoRequest evt_req = {nimffi_str("event-demo"), 1};
memset(&w, 0, sizeof(w));
my_timer_ctx_echo(ctx, &evt_req, on_echo, &w);
wait_done(&w.done);
/* The event fires from the library's dispatch thread; give it a moment. */
sleep_ms(500);
printf("[6] typed event onEchoFired: message=%s, echoCount=%d\n",
g_echo_message, atomic_load(&g_echo_count));
my_timer_ctx_remove_event_listener(ctx, handle);
my_timer_ctx_destroy(ctx);
printf("\nDone.\n");
return 0;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,342 @@
#ifndef NIM_FFI_CBOR_HELPERS_H_INCLUDED
#define NIM_FFI_CBOR_HELPERS_H_INCLUDED
/* Leaf CBOR codecs (scalars, text strings, byte strings) plus the buffer
* drivers. The per-struct / per-container codecs in the library header call
* into these by name (C has no overloading, so each leaf gets a distinct
* nimffi_enc_* / nimffi_dec_* symbol). Guarded so two nim-ffi headers can
* share a translation unit. */
#include "nim_ffi_prelude.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Result delivery callback exported by the Nim dylib: `ret` is 0 on success
* (then `msg`/`len` carry the CBOR response) or non-zero on failure (then
* `msg`/`len` carry the error text, which is NOT NUL-terminated). */
typedef void (*FFICallback)(int ret, const char* msg, size_t len, void* user_data);
/* Return / callback status codes. NIMFFI_RET_OK (0) is success; any non-zero
* value handed to a result callback's `err_code` (or returned by a submit call)
* is a failure. NIMFFI_RET_MISSING_CALLBACK is a special case from the Nim
* dispatcher: the callback will never fire, so the request path must report the
* failure itself. */
#define NIMFFI_RET_OK 0
#define NIMFFI_RET_ERROR 1
#define NIMFFI_RET_MISSING_CALLBACK 2
/* ── leaf encoders ─────────────────────────────────────────────────────── */
static inline CborError nimffi_enc_bool(CborEncoder* e, const bool* v) {
return cbor_encode_boolean(e, *v);
}
static inline CborError nimffi_enc_i64(CborEncoder* e, const int64_t* v) {
return cbor_encode_int(e, *v);
}
static inline CborError nimffi_enc_i32(CborEncoder* e, const int32_t* v) {
return cbor_encode_int(e, (int64_t)*v);
}
static inline CborError nimffi_enc_i16(CborEncoder* e, const int16_t* v) {
return cbor_encode_int(e, (int64_t)*v);
}
static inline CborError nimffi_enc_i8(CborEncoder* e, const int8_t* v) {
return cbor_encode_int(e, (int64_t)*v);
}
static inline CborError nimffi_enc_u64(CborEncoder* e, const uint64_t* v) {
return cbor_encode_uint(e, *v);
}
static inline CborError nimffi_enc_u32(CborEncoder* e, const uint32_t* v) {
return cbor_encode_uint(e, (uint64_t)*v);
}
static inline CborError nimffi_enc_u16(CborEncoder* e, const uint16_t* v) {
return cbor_encode_uint(e, (uint64_t)*v);
}
static inline CborError nimffi_enc_u8(CborEncoder* e, const uint8_t* v) {
return cbor_encode_uint(e, (uint64_t)*v);
}
static inline CborError nimffi_enc_f64(CborEncoder* e, const double* v) {
return cbor_encode_double(e, *v);
}
static inline CborError nimffi_enc_f32(CborEncoder* e, const float* v) {
return cbor_encode_float(e, *v);
}
static inline CborError nimffi_enc_str(CborEncoder* e, const NimFfiStr* v) {
return cbor_encode_text_string(e, v->data ? v->data : "", v->len);
}
static inline CborError nimffi_enc_bytes(CborEncoder* e, const NimFfiBytes* v) {
return cbor_encode_byte_string(e, v->data, v->len);
}
/* ── leaf decoders ─────────────────────────────────────────────────────── */
/* After reading a leaf, the parser must advance past it; both steps
* short-circuit on the same CborError, so they travel together. */
static inline CborError nimffi_advance_if_ok(CborValue* it, CborError err) {
if (err) {
return err;
}
return cbor_value_advance(it);
}
static inline CborError nimffi_dec_bool(CborValue* it, bool* out) {
if (!cbor_value_is_boolean(it)) {
return CborErrorImproperValue;
}
return nimffi_advance_if_ok(it, cbor_value_get_boolean(it, out));
}
static inline CborError nimffi_dec_i64(CborValue* it, int64_t* out) {
if (!cbor_value_is_integer(it)) {
return CborErrorImproperValue;
}
return nimffi_advance_if_ok(it, cbor_value_get_int64_checked(it, out));
}
static inline CborError nimffi_dec_i32(CborValue* it, int32_t* out) {
int64_t tmp = 0;
CborError err = nimffi_dec_i64(it, &tmp);
if (err) {
return err;
}
if (tmp < INT32_MIN || tmp > INT32_MAX) {
return CborErrorDataTooLarge;
}
*out = (int32_t)tmp;
return CborNoError;
}
static inline CborError nimffi_dec_i16(CborValue* it, int16_t* out) {
int64_t tmp = 0;
CborError err = nimffi_dec_i64(it, &tmp);
if (err) {
return err;
}
if (tmp < INT16_MIN || tmp > INT16_MAX) {
return CborErrorDataTooLarge;
}
*out = (int16_t)tmp;
return CborNoError;
}
static inline CborError nimffi_dec_i8(CborValue* it, int8_t* out) {
int64_t tmp = 0;
CborError err = nimffi_dec_i64(it, &tmp);
if (err) {
return err;
}
if (tmp < INT8_MIN || tmp > INT8_MAX) {
return CborErrorDataTooLarge;
}
*out = (int8_t)tmp;
return CborNoError;
}
static inline CborError nimffi_dec_u64(CborValue* it, uint64_t* out) {
if (!cbor_value_is_unsigned_integer(it)) {
return CborErrorImproperValue;
}
return nimffi_advance_if_ok(it, cbor_value_get_uint64(it, out));
}
static inline CborError nimffi_dec_u32(CborValue* it, uint32_t* out) {
uint64_t tmp = 0;
CborError err = nimffi_dec_u64(it, &tmp);
if (err) {
return err;
}
if (tmp > UINT32_MAX) {
return CborErrorDataTooLarge;
}
*out = (uint32_t)tmp;
return CborNoError;
}
static inline CborError nimffi_dec_u16(CborValue* it, uint16_t* out) {
uint64_t tmp = 0;
CborError err = nimffi_dec_u64(it, &tmp);
if (err) {
return err;
}
if (tmp > UINT16_MAX) {
return CborErrorDataTooLarge;
}
*out = (uint16_t)tmp;
return CborNoError;
}
static inline CborError nimffi_dec_u8(CborValue* it, uint8_t* out) {
uint64_t tmp = 0;
CborError err = nimffi_dec_u64(it, &tmp);
if (err) {
return err;
}
if (tmp > UINT8_MAX) {
return CborErrorDataTooLarge;
}
*out = (uint8_t)tmp;
return CborNoError;
}
static inline CborError nimffi_dec_f64(CborValue* it, double* out) {
if (cbor_value_is_double(it)) {
return nimffi_advance_if_ok(it, cbor_value_get_double(it, out));
}
if (cbor_value_is_float(it)) {
float f = 0.0f;
CborError err = cbor_value_get_float(it, &f);
if (err) {
return err;
}
*out = (double)f;
return cbor_value_advance(it);
}
return CborErrorImproperValue;
}
static inline CborError nimffi_dec_f32(CborValue* it, float* out) {
if (cbor_value_is_float(it)) {
return nimffi_advance_if_ok(it, cbor_value_get_float(it, out));
}
if (cbor_value_is_double(it)) {
double d = 0.0;
CborError err = cbor_value_get_double(it, &d);
if (err) {
return err;
}
*out = (float)d;
return cbor_value_advance(it);
}
return CborErrorImproperValue;
}
static inline CborError nimffi_dec_str(CborValue* it, NimFfiStr* 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;
}
if (len == SIZE_MAX) { /* len + 1 would wrap to a 0-byte allocation */
return CborErrorDataTooLarge;
}
/* one extra byte so a NUL-free payload is a valid C string */
out->data = (char*)malloc(len + 1);
if (!out->data) {
return CborErrorOutOfMemory;
}
out->len = len;
size_t copied = len;
err = cbor_value_copy_text_string(it, out->data, &copied, NULL);
if (err) {
free(out->data);
out->data = NULL;
out->len = 0;
return err;
}
out->data[len] = '\0';
return cbor_value_advance(it);
}
static inline CborError nimffi_dec_bytes(CborValue* it, NimFfiBytes* out) {
if (!cbor_value_is_byte_string(it)) {
return CborErrorImproperValue;
}
size_t len = 0;
CborError err = cbor_value_get_string_length(it, &len);
if (err) {
return err;
}
out->data = (uint8_t*)malloc(len ? len : 1);
if (!out->data) {
return CborErrorOutOfMemory;
}
out->len = len;
size_t copied = len;
err = cbor_value_copy_byte_string(it, out->data, &copied, NULL);
if (err) {
free(out->data);
out->data = NULL;
out->len = 0;
return err;
}
return cbor_value_advance(it);
}
/* ── buffer drivers ────────────────────────────────────────────────────── */
typedef CborError (*nimffi_enc_fn)(CborEncoder*, const void*);
typedef CborError (*nimffi_dec_fn)(CborValue*, void*);
static inline char* nimffi_dup_cstr(const char* s) {
size_t n = strlen(s) + 1;
char* p = (char*)malloc(n);
if (p) {
memcpy(p, s, n);
}
return p;
}
/* NUL-terminated copy of a length-delimited (not NUL-terminated) byte run,
* for turning the FFICallback's raw error `msg`/`len` into a C string. */
static inline char* nimffi_dup_cstr_n(const char* s, size_t n) {
char* p = (char*)malloc(n + 1);
if (p) {
if (n > 0) {
memcpy(p, s, n);
}
p[n] = '\0';
}
return p;
}
/* Encode `val` with `fn` into a freshly malloc'd buffer, doubling on overflow.
* Returns 0 and sets out/outlen on success; -1 and *err (heap) on failure. */
static inline int nimffi_encode_to_buf(
nimffi_enc_fn fn, const void* val,
uint8_t** out, size_t* outlen, char** err) {
size_t cap = 4096;
uint8_t* buf = (uint8_t*)malloc(cap);
if (!buf) {
if (err) *err = nimffi_dup_cstr("out of memory");
return -1;
}
for (;;) {
CborEncoder enc;
cbor_encoder_init(&enc, buf, cap, 0);
CborError e = fn(&enc, val);
if (e == CborNoError) {
*outlen = cbor_encoder_get_buffer_size(&enc, buf);
*out = buf;
return 0;
}
if (e == CborErrorOutOfMemory) {
size_t extra = cbor_encoder_get_extra_bytes_needed(&enc);
cap += extra > 0 ? extra : cap;
uint8_t* grown = (uint8_t*)realloc(buf, cap);
if (!grown) {
free(buf);
if (err) *err = nimffi_dup_cstr("out of memory");
return -1;
}
buf = grown;
continue;
}
free(buf);
if (err) *err = nimffi_dup_cstr(cbor_error_string(e));
return -1;
}
}
/* Decode a CBOR buffer into `out` with `fn`. Returns 0 on success; -1 and
* *err (heap) on failure. */
static inline int nimffi_decode_from_buf(
nimffi_dec_fn fn, const uint8_t* buf, size_t len,
void* out, char** err) {
CborParser parser;
CborValue it;
CborError e = cbor_parser_init(buf, len, 0, &parser, &it);
if (e != CborNoError) {
if (err) *err = nimffi_dup_cstr(cbor_error_string(e));
return -1;
}
e = fn(&it, out);
if (e != CborNoError) {
if (err) *err = nimffi_dup_cstr(cbor_error_string(e));
return -1;
}
return 0;
}
#ifdef __cplusplus
}
#endif
#endif /* NIM_FFI_CBOR_HELPERS_H_INCLUDED */

View File

@ -0,0 +1,89 @@
#ifndef NIM_FFI_PRELUDE_H_INCLUDED
#define NIM_FFI_PRELUDE_H_INCLUDED
/* Generated C binding for a nim-ffi library. Requests/responses travel as
* CBOR (encoded with vendored TinyCBOR on this side, matching the Nim-side
* cbor_serial codec on the wire both ends speak RFC 8949).
*
* The API is asynchronous: every method/constructor takes a result callback
* and returns immediately. The callback fires exactly once synchronously on
* a submit-time failure, otherwise from the Nim dispatch thread when the reply
* arrives.
*
* Memory ownership contract:
* - Request-side strings/sequences are *borrowed*: the binding only reads
* them while encoding, so a string literal wrapped with nimffi_str() is
* fine and is never freed by the binding.
* - Response values and error strings passed into a result callback are
* *owned by the binding* and valid only for the duration of that callback;
* the binding reclaims them once the callback returns. The caller never
* frees them. (The generated <lib>_free_<Type>() helpers are internal the
* trampolines use them to reclaim decoded payloads.)
* - A context handle delivered to a constructor callback is the exception:
* ownership transfers to the caller, who releases it with
* <lib>_ctx_destroy(). It is a lifecycle handle, not returned data.
*
* Trust boundary: the decoders assume the CBOR they parse was produced by the
* paired Nim library. They reject malformed input rather than trusting it, but
* they are not hardened against a hostile peer feeding crafted payloads through
* the raw nimffi_decode_from_buf entry point.
*/
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <tinycbor/cbor.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Owned, length-delimited UTF-8 text (Nim `string`/`cstring`). On the request
* side `data` may point at borrowed storage (see nimffi_str); on the response
* side it is heap-allocated and freed by nimffi_free_str. Always NUL-padded by
* one byte after decode so `data` is usable as a C string when it has no
* embedded NULs. */
typedef struct {
char* data;
size_t len;
} NimFfiStr;
/* Owned, length-delimited byte buffer (Nim `seq[byte]`). */
typedef struct {
uint8_t* data;
size_t len;
} NimFfiBytes;
/* Wrap a borrowed C string for use as a request field. The returned view is
* not owned by the binding and must outlive the call that encodes it. */
static inline NimFfiStr nimffi_str(const char* s) {
NimFfiStr v;
v.data = (char*)s;
v.len = s ? strlen(s) : 0;
return v;
}
static inline void nimffi_free_str(NimFfiStr* v) {
if (!v || !v->data) {
return;
}
free(v->data);
v->data = NULL;
v->len = 0;
}
static inline void nimffi_free_bytes(NimFfiBytes* v) {
if (!v || !v->data) {
return;
}
free(v->data);
v->data = NULL;
v->len = 0;
}
#ifdef __cplusplus
}
#endif
#endif /* NIM_FFI_PRELUDE_H_INCLUDED */

View File

@ -27,92 +27,10 @@ if("${REPO_ROOT}" STREQUAL "")
message(FATAL_ERROR "Cannot find repo root (no ffi.nimble in any ancestor)")
endif()
get_filename_component(NIM_SRC
"${CMAKE_CURRENT_SOURCE_DIR}/../timer.nim"
ABSOLUTE)
find_program(NIM_EXECUTABLE nim REQUIRED)
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
set(NIM_LIB_FILE "${REPO_ROOT}/libmy_timer.dylib")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
set(NIM_LIB_FILE "${REPO_ROOT}/my_timer.dll")
# MSVC consumers link against the `.lib` import library, not the DLL.
# MinGW's ld emits one when asked via `--out-implib`; the resulting COFF
# archive is readable by MSVC's link.exe.
set(NIM_IMPLIB_FILE "${REPO_ROOT}/my_timer.lib")
else()
set(NIM_LIB_FILE "${REPO_ROOT}/libmy_timer.so")
endif()
# On Windows the default Nim toolchain (mingw gcc) doesn't emit an import
# library unless told to. Without it, MSVC consumers can't resolve any
# symbol exported by the DLL at link time.
set(NIM_IMPLIB_PASSL "")
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
set(NIM_IMPLIB_PASSL "--passL:-Wl,--out-implib,${NIM_IMPLIB_FILE}")
endif()
add_custom_command(
OUTPUT "${NIM_LIB_FILE}"
COMMAND "${NIM_EXECUTABLE}" c
--mm:orc
-d:chronicles_log_level=WARN
--app:lib
--noMain
"--nimMainPrefix:libmy_timer"
${NIM_IMPLIB_PASSL}
"-o:${NIM_LIB_FILE}"
"${NIM_SRC}"
WORKING_DIRECTORY "${REPO_ROOT}"
DEPENDS "${NIM_SRC}"
BYPRODUCTS "${NIM_IMPLIB_FILE}"
COMMENT "Compiling Nim library libmy_timer"
VERBATIM
)
add_custom_target(my_timer_nim_lib ALL DEPENDS "${NIM_LIB_FILE}")
# On Windows, an `IMPORTED SHARED` target needs IMPORTED_IMPLIB pointing at
# the `.lib` import library so MSVC's `link.exe` can resolve symbols. The
# Visual Studio multi-config generator did not pick up `IMPORTED_IMPLIB`
# nor per-config `IMPORTED_IMPLIB_<CONFIG>` variants and emitted
# `my_timer-NOTFOUND.obj` into every link line. Side-step the IMPORTED
# machinery on Windows by exposing the import library through a plain
# INTERFACE library that links the `.lib` by path.
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
add_library(my_timer INTERFACE)
target_link_libraries(my_timer INTERFACE "${NIM_IMPLIB_FILE}")
else()
add_library(my_timer SHARED IMPORTED GLOBAL)
set_target_properties(my_timer PROPERTIES IMPORTED_LOCATION "${NIM_LIB_FILE}")
endif()
add_dependencies(my_timer my_timer_nim_lib)
# Absolute path to the runtime library (DLL/dylib/so). Exposed via the cache
# so consumers in other directories (e.g. tests/e2e/cpp) can stage the DLL
# next to their executable on Windows.
set(my_timer_RUNTIME_LIB "${NIM_LIB_FILE}" CACHE INTERNAL
"Absolute path to the my_timer runtime library")
# TinyCBOR (vendored at ffi/codegen/templates/cpp/vendor/tinycbor)
# Guarded so two sibling cpp_bindings dirs in one parent project don't redefine
# the `tinycbor` target.
set(TINYCBOR_SRC_DIR "${REPO_ROOT}/ffi/codegen/templates/cpp/vendor")
if(NOT TARGET tinycbor)
add_library(tinycbor STATIC
"${TINYCBOR_SRC_DIR}/tinycbor/cborencoder.c"
"${TINYCBOR_SRC_DIR}/tinycbor/cborencoder_close_container_checked.c"
"${TINYCBOR_SRC_DIR}/tinycbor/cborparser.c"
"${TINYCBOR_SRC_DIR}/tinycbor/cborparser_dup_string.c"
"${TINYCBOR_SRC_DIR}/tinycbor/cborerrorstrings.c"
)
target_include_directories(tinycbor PUBLIC
"${TINYCBOR_SRC_DIR}" # consumer uses #include <tinycbor/cbor.h>
"${TINYCBOR_SRC_DIR}/tinycbor" # internal _p.h includes resolve here
)
set_property(TARGET tinycbor PROPERTY C_STANDARD 99)
set_property(TARGET tinycbor PROPERTY POSITION_INDEPENDENT_CODE ON)
endif()
# Build the Nim dylib + vendored TinyCBOR (shared with the C backend).
set(NIM_FFI_LIB my_timer)
set(NIM_FFI_SRC ../timer.nim)
include("${REPO_ROOT}/ffi/codegen/templates/nim_ffi_lib.cmake")
add_library(my_timer_headers INTERFACE)
target_include_directories(my_timer_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")

View File

@ -24,3 +24,8 @@ task genbindings_cpp, "Generate C++ bindings for the timer example":
exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libmy_timer" &
" -d:ffiGenBindings -d:targetLang=cpp" & " -d:ffiOutputDir=cpp_bindings" &
" -d:ffiSrcPath=timer.nim" & " -o:/dev/null timer.nim"
task genbindings_c, "Generate C bindings for the timer example":
exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libmy_timer" &
" -d:ffiGenBindings -d:targetLang=c" & " -d:ffiOutputDir=c_bindings" &
" -d:ffiSrcPath=timer.nim" & " -o:/dev/null timer.nim"

View File

@ -131,6 +131,13 @@ task test_cpp_e2e, "Build and run the C++ end-to-end tests for the timer example
# single-config generators (Make/Ninja) on Linux/macOS.
runOrQuit "ctest --test-dir tests/e2e/cpp/build --output-on-failure -C Debug"
task test_c_e2e, "Build and run the C end-to-end tests for the timer example":
# Regenerate the C bindings so the suite always runs against fresh codegen.
runOrQuit "nimble genbindings_c"
runOrQuit "cmake -S tests/e2e/c -B tests/e2e/c/build"
runOrQuit "cmake --build tests/e2e/c/build --config Debug"
runOrQuit "ctest --test-dir tests/e2e/c/build --output-on-failure -C Debug"
task test_sanitized,
"Run all unit tests under a sanitizer (NIM_FFI_SAN) and mm (NIM_FFI_MM)":
let san = getEnv("NIM_FFI_SAN", "none")
@ -152,6 +159,16 @@ task test_cpp_e2e_sanitized,
runOrQuit "cmake --build tests/e2e/cpp/build --config Debug -j"
runOrQuit "ctest --test-dir tests/e2e/cpp/build --output-on-failure -C Debug"
task test_c_e2e_sanitized,
"Build and run the C e2e tests with a sanitizer (NIM_FFI_SAN) and mm (NIM_FFI_MM)":
let mm = getEnv("NIM_FFI_MM", "orc")
let san = getEnv("NIM_FFI_SAN", "none")
runOrQuit "nimble genbindings_c"
runOrQuit "cmake -S tests/e2e/c -B tests/e2e/c/build" & " -DNIM_FFI_MM=" & mm &
" -DNIM_FFI_SANITIZER=" & san
runOrQuit "cmake --build tests/e2e/c/build --config Debug -j"
runOrQuit "ctest --test-dir tests/e2e/c/build --output-on-failure -C Debug"
task genbindings_example, "Generate Rust bindings for the timer example":
exec "nim c " & nimFlagsOrc &
" --app:lib --noMain --nimMainPrefix:libmy_timer -d:ffiGenBindings -o:/dev/null examples/timer/timer.nim"
@ -194,6 +211,22 @@ task genbindings_cpp_echo, "Generate C++ bindings for the echo example":
" -d:ffiOutputDir=examples/echo/cpp_bindings" & " -d:ffiSrcPath=../echo.nim" &
" -o:/dev/null examples/echo/echo.nim"
task genbindings_c, "Generate C bindings for the timer example":
exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libmy_timer" &
" -d:ffiGenBindings -d:targetLang=c" & " -d:ffiOutputDir=examples/timer/c_bindings" &
" -d:ffiSrcPath=../timer.nim" & " -o:/dev/null examples/timer/timer.nim"
exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libmy_timer" &
" -d:ffiGenBindings -d:targetLang=c" & " -d:ffiOutputDir=examples/timer/c_bindings" &
" -d:ffiSrcPath=../timer.nim" & " -o:/dev/null examples/timer/timer.nim"
task genbindings_c_echo, "Generate C bindings for the echo example":
exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libecho" &
" -d:ffiGenBindings -d:targetLang=c" & " -d:ffiOutputDir=examples/echo/c_bindings" &
" -d:ffiSrcPath=../echo.nim" & " -o:/dev/null examples/echo/echo.nim"
exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libecho" &
" -d:ffiGenBindings -d:targetLang=c" & " -d:ffiOutputDir=examples/echo/c_bindings" &
" -d:ffiSrcPath=../echo.nim" & " -o:/dev/null examples/echo/echo.nim"
task check_bindings_rust, "Verify checked-in Rust bindings match Nim source":
exec "nimble genbindings_rust"
exec "git diff --exit-code --" & " examples/timer/rust_bindings/Cargo.toml" &
@ -206,6 +239,18 @@ task check_bindings_cpp, "Verify checked-in C++ bindings match Nim source":
" examples/timer/cpp_bindings/CMakeLists.txt" &
" examples/echo/cpp_bindings/echo.hpp" & " examples/echo/cpp_bindings/CMakeLists.txt"
task check_bindings_c, "Verify checked-in C bindings match Nim source":
exec "nimble genbindings_c"
exec "nimble genbindings_c_echo"
exec "git diff --exit-code --" & " examples/timer/c_bindings/my_timer.h" &
" examples/timer/c_bindings/nim_ffi_prelude.h" &
" examples/timer/c_bindings/nim_ffi_cbor.h" &
" examples/timer/c_bindings/CMakeLists.txt" & " examples/echo/c_bindings/echo.h" &
" examples/echo/c_bindings/nim_ffi_prelude.h" &
" examples/echo/c_bindings/nim_ffi_cbor.h" &
" examples/echo/c_bindings/CMakeLists.txt"
task check_bindings, "Verify all checked-in example bindings match Nim source":
exec "nimble check_bindings_rust"
exec "nimble check_bindings_cpp"
exec "nimble check_bindings_c"

926
ffi/codegen/c.nim Normal file
View File

@ -0,0 +1,926 @@
## C99 binding generator for the nim-ffi framework.
## Emits a header-only C binding plus a CMakeLists.txt. The binding is split
## into three headers so the example reads cleanly: `nim_ffi_prelude.h` (owned
## string/byte types + libc includes), `nim_ffi_cbor.h` (leaf CBOR codecs and
## buffer drivers, includes the prelude), and `<lib>.h` (the library-specific
## structs, codecs and async API, includes the cbor header). Requests/responses
## travel as CBOR (encoded with the same vendored TinyCBOR the C++ backend
## uses, matching the Nim-side cbor_serial codec — both ends speak RFC 8949).
##
## C has neither generics nor overloading, so the codecs the C++ backend gets
## from templates are monomorphised here: every distinct `seq[T]` / `Option[T]`
## becomes its own struct + encode/decode/free triple, and each leaf type has a
## distinctly-named codec emitted by the cbor_helpers template.
import std/[os, strutils, tables, sets]
import ./meta, ./string_helpers, ./c_cpp_common
## Wire-format C type for any Nim `ptr T` / `pointer`. Fixed 64-bit so the CBOR
## payload size is stable regardless of host architecture (mirrors CppPtrType).
const CPtrType* = "uint64_t"
const
HeaderPreludeTpl = staticRead("templates/c/header_prelude.h.tpl")
CborHelpersTpl = staticRead("templates/c/cbor_helpers.h.tpl")
CMakeListsTpl = staticRead("templates/c/CMakeLists.txt.tpl")
# Shared headers written alongside the library header. Their names match the
# include guards baked into the templates and the `#include` the cbor header
# emits for the prelude.
PreludeHeaderName* = "nim_ffi_prelude.h"
CborHeaderName* = "nim_ffi_cbor.h"
type LeafInfo = tuple[ok: bool, cType: string, suffix: string, owns: bool]
func leafCType(t: string): LeafInfo =
## Maps a Nim leaf type to its C type, codec suffix and whether a decoded
## value owns heap memory. `ok` is false for composite types (seq/Option/
## user structs), which are monomorphised separately.
case t
of "int", "int64":
(true, "int64_t", "i64", false)
of "int32":
(true, "int32_t", "i32", false)
of "int16":
(true, "int16_t", "i16", false)
of "int8":
(true, "int8_t", "i8", false)
of "uint", "uint64":
(true, "uint64_t", "u64", false)
of "uint32":
(true, "uint32_t", "u32", false)
of "uint16":
(true, "uint16_t", "u16", false)
of "uint8", "byte":
(true, "uint8_t", "u8", false)
of "bool":
(true, "bool", "bool", false)
of "float", "float64":
(true, "double", "f64", false)
of "float32":
(true, "float", "f32", false)
of "pointer":
(true, CPtrType, "u64", false)
of "string", "cstring":
(true, "NimFfiStr", "str", true)
else:
(false, "", "", false)
func cToken(cType: string): string =
## Short PascalCase token used to build monomorphised container names and
## codec-adapter symbols. Composite C type names are already unique C
## identifiers, so they pass through verbatim.
case cType
of "int64_t": "I64"
of "int32_t": "I32"
of "int16_t": "I16"
of "int8_t": "I8"
of "uint64_t": "U64"
of "uint32_t": "U32"
of "uint16_t": "U16"
of "uint8_t": "U8"
of "bool": "Bool"
of "double": "F64"
of "float": "F32"
of "NimFfiStr": "Str"
of "NimFfiBytes": "Bytes"
else: cType
func leafSuffix(cType: string): string =
## Inverse of leafCType's cType→suffix for the leaf codecs the template
## provides; empty string for composite types.
case cType
of "int64_t": "i64"
of "int32_t": "i32"
of "int16_t": "i16"
of "int8_t": "i8"
of "uint64_t": "u64"
of "uint32_t": "u32"
of "uint16_t": "u16"
of "uint8_t": "u8"
of "bool": "bool"
of "double": "f64"
of "float": "f32"
of "NimFfiStr": "str"
of "NimFfiBytes": "bytes"
else: ""
type CTypeReg = object
libName: string ## snake_case symbol prefix, e.g. "my_timer"
libType: string ## PascalCase container-name prefix, e.g. "MyTimer"
typeTable: Table[string, FFITypeMeta] ## user structs + synthetic Req structs
emitted: HashSet[string] ## composite C type names already emitted
owns: Table[string, bool] ## C type name → owns-heap-memory
decls: seq[string] ## struct typedefs, dependency order
codecs: seq[string] ## enc/dec/free defs, dependency order
func encFn(reg: CTypeReg, cType: string): string =
let suffix = leafSuffix(cType)
if suffix.len > 0:
return "nimffi_enc_" & suffix
reg.libName & "_enc_" & cType
func decFn(reg: CTypeReg, cType: string): string =
let suffix = leafSuffix(cType)
if suffix.len > 0:
return "nimffi_dec_" & suffix
reg.libName & "_dec_" & cType
func freeFn(reg: CTypeReg, cType: string): string =
## Free-function name for `cType`, or "" when the type owns no heap memory.
case cType
of "NimFfiStr":
"nimffi_free_str"
of "NimFfiBytes":
"nimffi_free_bytes"
else:
if leafSuffix(cType).len > 0:
""
elif reg.owns.getOrDefault(cType, false):
reg.libName & "_free_" & cType
else:
""
proc emitSeqType(reg: var CTypeReg, name, elemC: string) =
let eEnc = encFn(reg, elemC)
let eDec = decFn(reg, elemC)
let eFree = freeFn(reg, elemC)
reg.decls.add(
"typedef struct {\n " & elemC & "* data;\n size_t len;\n} " & name & ";"
)
var body: seq[string] = @[]
body.add("static inline CborError " & reg.libName & "_enc_" & name & "(")
body.add(" CborEncoder* e, const " & name & "* v) {")
body.add(" CborEncoder arr;")
body.add(" CborError err = cbor_encoder_create_array(e, &arr, v->len);")
body.add(" if (err) return err;")
body.add(" for (size_t i = 0; i < v->len; i++) {")
body.add(" err = " & eEnc & "(&arr, &v->data[i]);")
body.add(" if (err) return err;")
body.add(" }")
body.add(" return cbor_encoder_close_container(e, &arr);")
body.add("}")
body.add("static inline CborError " & reg.libName & "_dec_" & name & "(")
body.add(" CborValue* it, " & name & "* out) {")
body.add(" if (!cbor_value_is_array(it)) return CborErrorImproperValue;")
body.add(" size_t len = 0;")
body.add(" CborError err = cbor_value_get_array_length(it, &len);")
body.add(" if (err) return err;")
body.add(
" out->data = (" & elemC & "*)calloc(len ? len : 1, sizeof(" & elemC & "));"
)
body.add(" if (!out->data) return CborErrorOutOfMemory;")
body.add(" out->len = len;")
body.add(" CborValue inner;")
body.add(" err = cbor_value_enter_container(it, &inner);")
body.add(" if (err) return err;")
body.add(" for (size_t i = 0; i < len; i++) {")
body.add(" err = " & eDec & "(&inner, &out->data[i]);")
body.add(" if (err) return err;")
body.add(" }")
body.add(" return cbor_value_leave_container(it, &inner);")
body.add("}")
body.add(
"static inline void " & reg.libName & "_free_" & name & "(" & name & "* v) {"
)
body.add(" if (!v || !v->data) return;")
if eFree.len > 0:
body.add(" for (size_t i = 0; i < v->len; i++) " & eFree & "(&v->data[i]);")
body.add(" free(v->data);")
body.add(" v->data = NULL;")
body.add(" v->len = 0;")
body.add("}")
reg.codecs.add(body.join("\n"))
reg.owns[name] = true
proc emitOptType(reg: var CTypeReg, name, elemC: string, elemOwns: bool) =
let eEnc = encFn(reg, elemC)
let eDec = decFn(reg, elemC)
let eFree = freeFn(reg, elemC)
reg.decls.add(
"typedef struct {\n bool has_value;\n " & elemC & " value;\n} " & name & ";"
)
var body: seq[string] = @[]
body.add("static inline CborError " & reg.libName & "_enc_" & name & "(")
body.add(" CborEncoder* e, const " & name & "* v) {")
body.add(" if (!v->has_value) return cbor_encode_null(e);")
body.add(" return " & eEnc & "(e, &v->value);")
body.add("}")
body.add("static inline CborError " & reg.libName & "_dec_" & name & "(")
body.add(" CborValue* it, " & name & "* out) {")
body.add(" if (cbor_value_is_null(it)) {")
body.add(" out->has_value = false;")
body.add(" memset(&out->value, 0, sizeof(out->value));")
body.add(" return cbor_value_advance(it);")
body.add(" }")
body.add(" out->has_value = true;")
body.add(" return " & eDec & "(it, &out->value);")
body.add("}")
if elemOwns and eFree.len > 0:
body.add(
"static inline void " & reg.libName & "_free_" & name & "(" & name & "* v) {"
)
body.add(" if (!v || !v->has_value) return;")
body.add(" " & eFree & "(&v->value);")
body.add(" v->has_value = false;")
body.add("}")
reg.codecs.add(body.join("\n"))
reg.owns[name] = elemOwns
proc ensureCType(reg: var CTypeReg, nimType: string): tuple[cType: string, owns: bool]
proc emitStructType(reg: var CTypeReg, t: FFITypeMeta) =
var fieldDecls: seq[string] = @[]
var members: seq[tuple[name, cType: string, owns: bool]] = @[]
for f in t.fields:
let (cType, owns) = ensureCType(reg, f.typeName)
fieldDecls.add(" " & cType & " " & f.name & ";")
members.add((f.name, cType, owns))
if members.len == 0:
fieldDecls.add(" char _nimffi_empty; /* C forbids empty structs */")
reg.decls.add("typedef struct {\n" & fieldDecls.join("\n") & "\n} " & t.name & ";")
var body: seq[string] = @[]
body.add("static inline CborError " & reg.libName & "_enc_" & t.name & "(")
body.add(" CborEncoder* e, const " & t.name & "* v) {")
if members.len == 0:
body.add(" (void)v;")
body.add(" CborEncoder m;")
body.add(" CborError err = cbor_encoder_create_map(e, &m, " & $members.len & ");")
body.add(" if (err) return err;")
for mem in members:
body.add(" err = cbor_encode_text_stringz(&m, \"" & mem.name & "\");")
body.add(" if (err) return err;")
body.add(" err = " & encFn(reg, mem.cType) & "(&m, &v->" & mem.name & ");")
body.add(" if (err) return err;")
body.add(" return cbor_encoder_close_container(e, &m);")
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_map(it)) return CborErrorImproperValue;")
if members.len == 0:
body.add(" (void)out;")
body.add(" return cbor_value_advance(it);")
else:
body.add(" CborValue field;")
body.add(" CborError err;")
for mem in members:
body.add(" err = cbor_value_map_find_value(it, \"" & mem.name & "\", &field);")
body.add(" if (err) return err;")
body.add(" if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;")
body.add(
" err = " & decFn(reg, mem.cType) & "(&field, &out->" & mem.name & ");"
)
body.add(" if (err) return err;")
body.add(" return cbor_value_advance(it);")
body.add("}")
var owns = false
for mem in members:
if mem.owns:
owns = true
if owns:
body.add(
"static inline void " & reg.libName & "_free_" & t.name & "(" & t.name & "* v) {"
)
body.add(" if (!v) return;")
for mem in members:
let ff = freeFn(reg, mem.cType)
if mem.owns and ff.len > 0:
body.add(" " & ff & "(&v->" & mem.name & ");")
body.add("}")
reg.codecs.add(body.join("\n"))
reg.owns[t.name] = owns
proc ensureCType(reg: var CTypeReg, nimType: string): tuple[cType: string, owns: bool] =
let t = nimType.strip()
if t.startsWith("ptr ") or t == "pointer":
return (CPtrType, false)
let leaf = leafCType(t)
if leaf.ok:
return (leaf.cType, leaf.owns)
let seqInner = genericInnerType(t, "seq[")
if seqInner.len > 0:
let inner = seqInner.strip()
if inner == "byte" or inner == "uint8":
return ("NimFfiBytes", true)
let (elemC, _) = ensureCType(reg, inner)
let name = reg.libType & "Seq_" & cToken(elemC)
if name notin reg.emitted:
reg.emitted.incl(name)
emitSeqType(reg, name, elemC)
return (name, true)
var optInner = genericInnerType(t, "Option[")
if optInner.len == 0:
optInner = genericInnerType(t, "Maybe[")
if optInner.len > 0:
let (elemC, elemOwns) = ensureCType(reg, optInner.strip())
let name = reg.libType & "Opt_" & cToken(elemC)
if name notin reg.emitted:
reg.emitted.incl(name)
emitOptType(reg, name, elemC, elemOwns)
return (name, reg.owns.getOrDefault(name, false))
if t notin reg.emitted:
reg.emitted.incl(t)
if t in reg.typeTable:
emitStructType(reg, reg.typeTable[t])
else:
reg.decls.add("/* unknown type referenced: " & t & " */")
(t, reg.owns.getOrDefault(t, false))
proc reqTypeMeta(p: FFIProcMeta): FFITypeMeta =
## Synthesises the per-proc Req struct as an FFITypeMeta so it flows through
## the same monomorphisation path as user-declared types. Pointer/handle
## params ride the wire as the opaque uint64 pointer type.
var fields: seq[FFIFieldMeta] = @[]
for ep in p.extraParams:
let typeName = if ep.ridesAsPtr(): "pointer" else: ep.typeName
fields.add(FFIFieldMeta(name: ep.name, typeName: typeName))
FFITypeMeta(name: reqStructName(p), fields: fields)
func paramByValue(nimType: string, ridesAsPtr: bool): bool =
## Scalars / opaque pointers / string views pass by value; composite
## aggregates (seq, Option, user structs) pass by const pointer.
if ridesAsPtr:
return true
leafCType(nimType.strip()).ok
proc cReturnType(reg: var CTypeReg, p: FFIProcMeta): string =
if p.returnRidesAsPtr():
return CPtrType
ensureCType(reg, p.returnTypeName).cType
proc buildReqParams(
reg: var CTypeReg, eps: seq[FFIParamMeta]
): tuple[params, assigns: seq[string]] =
var params: seq[string] = @[]
var assigns: seq[string] = @[]
for ep in eps:
let rides = ep.ridesAsPtr()
let cType =
if rides:
CPtrType
else:
ensureCType(reg, ep.typeName).cType
if paramByValue(ep.typeName, rides):
params.add(cType & " " & ep.name)
assigns.add(" ffi_req." & ep.name & " = " & ep.name & ";")
else:
params.add("const " & cType & "* " & ep.name)
assigns.add(" ffi_req." & ep.name & " = *" & ep.name & ";")
(params, assigns)
proc evNames(
libType, libName: string, ev: FFIEventMeta
): tuple[fnType, boxType, tramp, regName: string] =
let pascal = capitalizeFirstLetter(ev.nimProcName)
let snake = camelToSnakeCase(ev.nimProcName)
(
libType & pascal & "Fn",
libType & pascal & "Box",
libName & "_" & snake & "_trampoline",
libName & "_ctx_add_" & snake & "_listener",
)
proc emitEventMachinery(
lines: var seq[string],
reg: CTypeReg,
libType, libName: string,
events: seq[FFIEventMeta],
) =
if events.len == 0:
return
lines.add("/* Event listener machinery */")
for ev in events:
let n = evNames(libType, libName, ev)
let payC = ev.payloadTypeName
let payFree = freeFn(reg, payC)
lines.add(
"typedef void (*" & n.fnType & ")(const " & payC & "* evt, void* user_data);"
)
lines.add(
"typedef struct { " & n.fnType & " fn; void* user_data; } " & n.boxType & ";"
)
lines.add(
"static void " & n.tramp & "(int ret, const char* msg, size_t len, void* ud) {"
)
lines.add(" if (!ud || ret != 0 || !msg || len == 0) return;")
lines.add(" " & n.boxType & "* box = (" & n.boxType & "*)ud;")
lines.add(" if (!box->fn) return;")
lines.add(" CborParser parser;")
lines.add(" CborValue it;")
lines.add(
" if (cbor_parser_init((const uint8_t*)msg, len, 0, &parser, &it) != CborNoError) return;"
)
lines.add(" if (!cbor_value_is_map(&it)) return;")
lines.add(" CborValue payloadField;")
lines.add(
" if (cbor_value_map_find_value(&it, \"payload\", &payloadField) != CborNoError) return;"
)
lines.add(" " & payC & " payload;")
lines.add(" memset(&payload, 0, sizeof(payload));")
lines.add(
" if (" & decFn(reg, payC) & "(&payloadField, &payload) != CborNoError) return;"
)
lines.add(" box->fn(&payload, box->user_data);")
if payFree.len > 0:
lines.add(" " & payFree & "(&payload);")
lines.add("}")
lines.add("")
proc emitContextStruct(
lines: var seq[string], ctxType: string, events: seq[FFIEventMeta]
) =
lines.add("/* ============================================================ */")
lines.add("/* High-level context wrapper */")
lines.add("/* ============================================================ */")
if events.len > 0:
lines.add("typedef struct {")
lines.add(" uint64_t id;")
lines.add(" void* box;")
lines.add("} " & ctxType & "Listener;")
lines.add("")
lines.add("typedef struct {")
lines.add(" void* ptr;")
if events.len > 0:
lines.add(" " & ctxType & "Listener* listeners;")
lines.add(" size_t listeners_len;")
lines.add(" size_t listeners_cap;")
lines.add("} " & ctxType & ";")
lines.add("")
proc emitCallBox(lines: var seq[string], fnType, boxType: string) =
lines.add("typedef struct { " & fnType & " fn; void* user_data; } " & boxType & ";")
proc emitReplyTrampolineHead(lines: var seq[string], tramp, boxType, fallback: string) =
## Opens a reply trampoline: cast the user-data back to the call box, bail if
## the caller passed no callback (nothing to deliver to, and leaving early
## avoids allocating a result nobody receives), then deliver a non-zero `ret`
## as an error. The error text in msg/len is not NUL-terminated, so copy it.
lines.add(
"static void " & tramp & "(int ret, const char* msg, size_t len, void* ud) {"
)
lines.add(" " & boxType & "* box = (" & boxType & "*)ud;")
lines.add(" if (!box->fn) {")
lines.add(" free(box);")
lines.add(" return;")
lines.add(" }")
lines.add(" if (ret != 0) {")
lines.add(" char* em = nimffi_dup_cstr_n(msg ? msg : \"\", msg ? len : 0);")
lines.add(
" box->fn(ret, NULL, em ? em : \"" & fallback & "\", box->user_data);"
)
lines.add(" free(em);")
lines.add(" free(box);")
lines.add(" return;")
lines.add(" }")
proc emitConstructors(
lines: var seq[string],
reg: var CTypeReg,
ctxType, libType, libName: string,
ctors: seq[FFIProcMeta],
) =
if ctors.len == 0:
return
let fnType = libType & "CreateFn"
let boxType = libType & "CreateBox"
let tramp = libName & "_create_trampoline"
lines.add(
"typedef void (*" & fnType & ")(int err_code, " & ctxType &
"* ctx, const char* err_msg, void* user_data);"
)
emitCallBox(lines, fnType, boxType)
emitReplyTrampolineHead(lines, tramp, boxType, "FFI create failed")
lines.add(" char* err = NULL;")
lines.add(" NimFfiStr addr;")
lines.add(" memset(&addr, 0, sizeof(addr));")
lines.add(
" if (nimffi_decode_from_buf(" & libName &
"_decv_Str, (const uint8_t*)msg, len, &addr, &err) != 0) {"
)
lines.add(" box->fn(-1, NULL, err ? err : \"decode failed\", box->user_data);")
lines.add(" free(err);")
lines.add(" free(box);")
lines.add(" return;")
lines.add(" }")
lines.add(" char* endp = NULL;")
lines.add(
" unsigned long long a = addr.data ? strtoull(addr.data, &endp, 10) : 0;"
)
lines.add(" bool ok = addr.data && addr.len > 0 && endp && *endp == '\\0';")
lines.add(" nimffi_free_str(&addr);")
lines.add(" if (!ok) {")
lines.add(
" box->fn(-1, NULL, \"FFI create returned non-numeric address\", box->user_data);"
)
lines.add(" free(box);")
lines.add(" return;")
lines.add(" }")
lines.add(
" " & ctxType & "* ctx = (" & ctxType & "*)calloc(1, sizeof(" & ctxType & "));"
)
lines.add(" if (!ctx) {")
lines.add(" box->fn(-1, NULL, \"out of memory\", box->user_data);")
lines.add(" free(box);")
lines.add(" return;")
lines.add(" }")
lines.add(" ctx->ptr = (void*)(uintptr_t)a;")
lines.add(" box->fn(NIMFFI_RET_OK, ctx, NULL, box->user_data);")
lines.add(" free(box);")
lines.add("}")
lines.add("")
for ctor in ctors:
let reqName = reqStructName(ctor)
let (params, assigns) = buildReqParams(reg, ctor.extraParams)
let head = "static inline int " & libName & "_ctx_create("
let sig =
if params.len > 0:
head & params.join(", ") & ", " & fnType & " on_created, void* user_data) {"
else:
head & fnType & " on_created, void* user_data) {"
lines.add(sig)
lines.add(" " & reqName & " ffi_req;")
lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));")
for a in assigns:
lines.add(a)
lines.add(" uint8_t* req_buf = NULL;")
lines.add(" size_t req_len = 0;")
lines.add(" char* err = NULL;")
lines.add(
" if (nimffi_encode_to_buf(" & libName & "_encv_" & cToken(reqName) &
", &ffi_req, &req_buf, &req_len, &err) != 0) {"
)
lines.add(
" if (on_created) on_created(-1, NULL, err ? err : \"encode failed\", user_data);"
)
lines.add(" free(err);")
lines.add(" return -1;")
lines.add(" }")
lines.add(
" " & boxType & "* box = (" & boxType & "*)malloc(sizeof(" & boxType & "));"
)
lines.add(" if (!box) {")
lines.add(" free(req_buf);")
lines.add(
" if (on_created) on_created(-1, NULL, \"out of memory\", user_data);"
)
lines.add(" return -1;")
lines.add(" }")
lines.add(" box->fn = on_created;")
lines.add(" box->user_data = user_data;")
lines.add(" (void)" & ctor.procName & "(req_buf, req_len, " & tramp & ", box);")
lines.add(" free(req_buf);")
lines.add(" return 0;")
lines.add("}")
lines.add("")
proc emitDestructor(
lines: var seq[string],
ctxType, libName, dtorProcName: string,
events: seq[FFIEventMeta],
) =
lines.add("static inline void " & libName & "_ctx_destroy(" & ctxType & "* ctx) {")
lines.add(" if (!ctx) return;")
if dtorProcName.len > 0:
lines.add(" if (ctx->ptr) { " & dtorProcName & "(ctx->ptr); ctx->ptr = NULL; }")
if events.len > 0:
lines.add(
" for (size_t i = 0; i < ctx->listeners_len; i++) free(ctx->listeners[i].box);"
)
lines.add(" free(ctx->listeners);")
lines.add(" free(ctx);")
lines.add("}")
lines.add("")
proc emitListenerApi(
lines: var seq[string], ctxType, libType, libName: string, events: seq[FFIEventMeta]
) =
if events.len == 0:
return
for ev in events:
let n = evNames(libType, libName, ev)
lines.add(
"static inline uint64_t " & n.regName & "(" & ctxType & "* ctx, " & n.fnType &
" fn, void* user_data) {"
)
lines.add(
" " & n.boxType & "* box = (" & n.boxType & "*)malloc(sizeof(" & n.boxType &
"));"
)
lines.add(" if (!box) return 0;")
lines.add(" box->fn = fn;")
lines.add(" box->user_data = user_data;")
lines.add(
" uint64_t id = " & libName & "_add_event_listener(ctx->ptr, \"" & ev.wireName &
"\", " & n.tramp & ", box);"
)
lines.add(" if (id == 0) { free(box); return 0; }")
lines.add(" if (ctx->listeners_len == ctx->listeners_cap) {")
lines.add(" size_t ncap = ctx->listeners_cap ? ctx->listeners_cap * 2 : 4;")
lines.add(
" " & ctxType & "Listener* grown = (" & ctxType &
"Listener*)realloc(ctx->listeners, ncap * sizeof(" & ctxType & "Listener));"
)
lines.add(
" if (!grown) { " & libName &
"_remove_event_listener(ctx->ptr, id); free(box); return 0; }"
)
lines.add(" ctx->listeners = grown;")
lines.add(" ctx->listeners_cap = ncap;")
lines.add(" }")
lines.add(" ctx->listeners[ctx->listeners_len].id = id;")
lines.add(" ctx->listeners[ctx->listeners_len].box = box;")
lines.add(" ctx->listeners_len++;")
lines.add(" return id;")
lines.add("}")
lines.add("")
lines.add(
"static inline bool " & libName & "_ctx_remove_event_listener(" & ctxType &
"* ctx, uint64_t id) {"
)
lines.add(" if (id == 0) return false;")
lines.add(" int rc = " & libName & "_remove_event_listener(ctx->ptr, id);")
lines.add(" for (size_t i = 0; i < ctx->listeners_len; i++) {")
lines.add(" if (ctx->listeners[i].id == id) {")
lines.add(" free(ctx->listeners[i].box);")
lines.add(" ctx->listeners[i] = ctx->listeners[ctx->listeners_len - 1];")
lines.add(" ctx->listeners_len--;")
lines.add(" break;")
lines.add(" }")
lines.add(" }")
lines.add(" return rc == 0;")
lines.add("}")
lines.add("")
proc emitMethod(
lines: var seq[string],
reg: var CTypeReg,
ctxType, libType, libName: string,
m: FFIProcMeta,
) =
let stripped = stripLibPrefix(m.procName, libName)
let reqName = reqStructName(m)
let retC = cReturnType(reg, m)
let retFree = freeFn(reg, retC)
let (params, assigns) = buildReqParams(reg, m.extraParams)
let methodPascal = snakeToPascalCase(stripped)
let fnType = libType & methodPascal & "ReplyFn"
let boxType = libType & methodPascal & "CallBox"
let tramp = libName & "_" & stripped & "_reply_trampoline"
lines.add(
"typedef void (*" & fnType & ")(int err_code, const " & retC &
"* reply, const char* err_msg, void* user_data);"
)
emitCallBox(lines, fnType, boxType)
emitReplyTrampolineHead(lines, tramp, boxType, "FFI call failed")
lines.add(" char* err = NULL;")
lines.add(" " & retC & " out;")
lines.add(" memset(&out, 0, sizeof(out));")
lines.add(
" int dec = nimffi_decode_from_buf(" & libName & "_decv_" & cToken(retC) &
", (const uint8_t*)msg, len, &out, &err);"
)
lines.add(" if (dec != 0) {")
lines.add(" box->fn(-1, NULL, err ? err : \"decode failed\", box->user_data);")
lines.add(" free(err);")
# A partial decode may have allocated some fields; reclaim them (out is
# zeroed, so the typed free skips what was never written).
if retFree.len > 0:
lines.add(" " & retFree & "(&out);")
lines.add(" free(box);")
lines.add(" return;")
lines.add(" }")
lines.add(" box->fn(NIMFFI_RET_OK, &out, NULL, box->user_data);")
if retFree.len > 0:
lines.add(" " & retFree & "(&out);")
lines.add(" free(box);")
lines.add("}")
let head =
"static inline int " & libName & "_ctx_" & stripped & "(const " & ctxType & "* ctx, "
let sig =
if params.len > 0:
head & params.join(", ") & ", " & fnType & " on_reply, void* user_data) {"
else:
head & fnType & " on_reply, void* user_data) {"
lines.add(sig)
lines.add(" " & reqName & " ffi_req;")
lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));")
for a in assigns:
lines.add(a)
lines.add(" uint8_t* req_buf = NULL;")
lines.add(" size_t req_len = 0;")
lines.add(" char* err = NULL;")
lines.add(
" if (nimffi_encode_to_buf(" & libName & "_encv_" & cToken(reqName) &
", &ffi_req, &req_buf, &req_len, &err) != 0) {"
)
lines.add(
" if (on_reply) on_reply(-1, NULL, err ? err : \"encode failed\", user_data);"
)
lines.add(" free(err);")
lines.add(" return -1;")
lines.add(" }")
lines.add(
" " & boxType & "* box = (" & boxType & "*)malloc(sizeof(" & boxType & "));"
)
lines.add(" if (!box) {")
lines.add(" free(req_buf);")
lines.add(" if (on_reply) on_reply(-1, NULL, \"out of memory\", user_data);")
lines.add(" return -1;")
lines.add(" }")
lines.add(" box->fn = on_reply;")
lines.add(" box->user_data = user_data;")
lines.add(
" int ret = " & m.procName & "(ctx->ptr, " & tramp & ", box, req_buf, req_len);"
)
lines.add(" free(req_buf);")
lines.add(" if (ret == NIMFFI_RET_MISSING_CALLBACK) {")
lines.add(
" if (on_reply) on_reply(-1, NULL, \"RET_MISSING_CALLBACK (internal error)\", user_data);"
)
lines.add(" free(box);")
lines.add(" return -1;")
lines.add(" }")
lines.add(" return 0;")
lines.add("}")
lines.add("")
proc newCTypeReg(
libName, libType: string, types: seq[FFITypeMeta], procs: seq[FFIProcMeta]
): CTypeReg =
var reg = CTypeReg(libName: libName, libType: libType)
for t in types:
reg.typeTable[t.name] = t
for p in procs:
if p.kind != FFIKind.DTOR:
let rt = reqTypeMeta(p)
reg.typeTable[rt.name] = rt
reg
proc monomorphiseAll(
reg: var CTypeReg,
types: seq[FFITypeMeta],
procs, methods: seq[FFIProcMeta],
events: seq[FFIEventMeta],
): tuple[reqTypes, respTypes: seq[string]] =
## Walks every user type, per-proc Req envelope, return type and event
## payload through ensureCType, emitting their structs/codecs into `reg` in
## dependency order. Returns the Req and response C type names the buffer
## adapters need.
for t in types:
discard ensureCType(reg, t.name)
var reqTypes: seq[string] = @[]
for p in procs:
if p.kind != FFIKind.DTOR:
let n = reqStructName(p)
discard ensureCType(reg, n)
reqTypes.add(n)
var respTypes: seq[string] = @[]
for m in methods:
respTypes.add(cReturnType(reg, m))
for ev in events:
discard ensureCType(reg, ev.payloadTypeName)
(reqTypes, respTypes)
func generateCPreludeHeader*(): string =
## The `nim_ffi_prelude.h` shared header: owned string/byte types plus the
## libc/TinyCBOR includes every nim-ffi C binding needs. Identical across
## libraries, so it is emitted verbatim from the template.
HeaderPreludeTpl & "\n"
func generateCCborHeader*(): string =
## The `nim_ffi_cbor.h` shared header: leaf CBOR codecs and buffer drivers.
## Includes the prelude (its guard is inside the template) and is library-
## agnostic, so it too is emitted verbatim.
CborHelpersTpl & "\n"
proc generateCLibHeader*(
procs: seq[FFIProcMeta],
types: seq[FFITypeMeta],
libName: string,
events: seq[FFIEventMeta] = @[],
): string =
## The `<lib>.h` header: library-specific structs, monomorphised codecs and
## the async API. Pulls the two shared headers in via the cbor header.
let classified = classifyProcs(procs)
let ctors = classified.ctors
let methods = classified.methods
let libType = libTypeName(ctors, libName)
let ctxType = libType & "Ctx"
var reg = newCTypeReg(libName, libType, types, procs)
let (reqTypes, respTypes) = monomorphiseAll(reg, types, procs, methods, events)
let guard = "NIM_FFI_LIB_" & libName.toUpperAscii() & "_H_INCLUDED"
var lines: seq[string] = @[]
lines.add("#ifndef " & guard)
lines.add("#define " & guard)
lines.add("#include \"" & CborHeaderName & "\"")
lines.add("")
lines.add("/* ============================================================ */")
lines.add("/* Generated types (user-declared + per-proc request envelopes) */")
lines.add("/* ============================================================ */")
lines.add("")
for decl in reg.decls:
lines.add(decl)
lines.add("")
for codec in reg.codecs:
lines.add(codec)
lines.add("")
lines.add("/* ============================================================ */")
lines.add("/* C ABI declarations (symbols exported by the Nim dylib) */")
lines.add("/* ============================================================ */")
lines.add("#ifdef __cplusplus")
lines.add("extern \"C\" {")
lines.add("#endif")
lines.add("")
for p in procs:
case p.kind
of FFIKind.FFI:
lines.add(
"int " & p.procName & "(void* ctx, FFICallback callback, void* user_data, " &
"const uint8_t* req_cbor, size_t req_cbor_len);"
)
of FFIKind.CTOR:
lines.add(
"void* " & p.procName & "(const uint8_t* req_cbor, size_t req_cbor_len, " &
"FFICallback callback, void* user_data);"
)
of FFIKind.DTOR:
lines.add("int " & p.procName & "(void* ctx);")
lines.add(
"uint64_t " & libName & "_add_event_listener(void* ctx, const char* event_name, " &
"FFICallback callback, void* user_data);"
)
lines.add(
"int " & libName & "_remove_event_listener(void* ctx, uint64_t listener_id);"
)
lines.add("")
lines.add("#ifdef __cplusplus")
lines.add("} /* extern \"C\" */")
lines.add("#endif")
lines.add("")
# Per-Req encode / per-response decode void* adapters for the buffer drivers.
var adaptersDone = initHashSet[string]()
lines.add("/* CBOR buffer adapters (typed codec → void* driver signature) */")
for n in reqTypes:
let tok = cToken(n)
if ("enc" & tok) notin adaptersDone:
adaptersDone.incl("enc" & tok)
lines.add(
"static inline CborError " & libName & "_encv_" & tok &
"(CborEncoder* e, const void* v) { return " & reg.libName & "_enc_" & n &
"(e, (const " & n & "*)v); }"
)
var respSet = respTypes
respSet.add("NimFfiStr") # ctor address payload
for n in respSet:
let tok = cToken(n)
if ("dec" & tok) notin adaptersDone:
adaptersDone.incl("dec" & tok)
lines.add(
"static inline CborError " & libName & "_decv_" & tok &
"(CborValue* it, void* v) { return " & decFn(reg, n) & "(it, (" & n & "*)v); }"
)
lines.add("")
emitEventMachinery(lines, reg, libType, libName, events)
emitContextStruct(lines, ctxType, events)
emitConstructors(lines, reg, ctxType, libType, libName, ctors)
emitDestructor(lines, ctxType, libName, classified.dtorProcName, events)
emitListenerApi(lines, ctxType, libType, libName, events)
for m in methods:
emitMethod(lines, reg, ctxType, libType, libName, m)
lines.add("#endif /* " & guard & " */")
lines.join("\n") & "\n"
proc generateCCMakeLists*(libName, nimSrcRelPath: string): string =
let src = nimSrcRelPath.replace("\\", "/")
CMakeListsTpl.multiReplace(("{{LIB}}", libName), ("{{SRC}}", src))
proc generateCBindings*(
procs: seq[FFIProcMeta],
types: seq[FFITypeMeta],
libName: string,
outputDir: string,
nimSrcRelPath: string,
events: seq[FFIEventMeta] = @[],
) =
createDir(outputDir)
writeFile(outputDir / PreludeHeaderName, generateCPreludeHeader())
writeFile(outputDir / CborHeaderName, generateCCborHeader())
writeFile(
outputDir / (libName & ".h"), generateCLibHeader(procs, types, libName, events)
)
writeFile(outputDir / "CMakeLists.txt", generateCCMakeLists(libName, nimSrcRelPath))

View File

@ -0,0 +1,62 @@
## Helpers shared by the language-specific binding generators (cpp.nim, c.nim).
## Kept here so the per-proc envelope naming, lib-prefix stripping and
## proc-classification logic live in one place rather than being copy-pasted
## into each backend.
import std/strutils
import ./meta, ./string_helpers
proc genericInnerType*(typeName, prefix: string): string =
## Inner type of a single-parameter generic written `Prefix[Inner]`, e.g.
## `genericInnerType("seq[int]", "seq[")` → `"int"`. Empty string when
## `typeName` is not of that shape.
if typeName.startsWith(prefix) and typeName.endsWith("]"):
let start = prefix.len
let lastIndex = typeName.len - 2
return typeName[start .. lastIndex]
return ""
proc stripLibPrefix*(procName, libName: string): string =
## Drops the `<lib>_` prefix from an exported C symbol, e.g.
## `stripLibPrefix("timer_echo", "timer")` → `"echo"`.
let prefix = libName & "_"
if procName.startsWith(prefix):
return procName[prefix.len .. ^1]
return procName
proc reqStructName*(p: FFIProcMeta): string =
## Mirrors the Nim macro: `<PascalCase(procName)>Req`, or `...CtorReq` for a
## constructor. The per-proc envelope every backend encodes onto the wire.
let camel = snakeToPascalCase(p.procName)
if p.kind == FFIKind.CTOR:
camel & "CtorReq"
else:
camel & "Req"
type ClassifiedProcs* = object
ctors*: seq[FFIProcMeta]
methods*: seq[FFIProcMeta]
dtorProcName*: string
proc classifyProcs*(procs: seq[FFIProcMeta]): ClassifiedProcs =
## Splits the registry into constructors, instance methods and (the first)
## destructor symbol — the split every backend needs before emitting a
## high-level context wrapper.
var c: ClassifiedProcs
for p in procs:
case p.kind
of FFIKind.CTOR:
c.ctors.add(p)
of FFIKind.FFI:
c.methods.add(p)
of FFIKind.DTOR:
if c.dtorProcName.len == 0:
c.dtorProcName = p.procName
c
proc libTypeName*(ctors: seq[FFIProcMeta], libName: string): string =
## The user's library type name (e.g. `MyTimer`), taken from the first ctor
## or derived from `libName` when the library declares none.
if ctors.len > 0:
return ctors[0].libTypeName
capitalizeFirstLetter(libName)

View File

@ -4,7 +4,7 @@
## the Nim-side cbor_serial codec on the wire — both ends speak RFC 8949).
import std/[os, strutils]
import ./meta, ./string_helpers
import ./meta, ./string_helpers, ./c_cpp_common
## Wire-format C++ type used for any Nim `ptr T` / `pointer`. Fixed 64-bit so
## the CBOR payload size is stable regardless of host architecture.
@ -21,13 +21,6 @@ const
ContextRuleOf5Tpl = staticRead("templates/cpp/context_rule_of_5.hpp.tpl")
CMakeListsTpl = staticRead("templates/cpp/CMakeLists.txt.tpl")
proc genericInnerType(typeName, prefix: string): string =
if typeName.startsWith(prefix) and typeName.endsWith("]"):
let start = prefix.len
let lastIndex = typeName.len - 2
return typeName[start .. lastIndex]
return ""
proc nimTypeToCpp*(typeName: string): string =
let trimmed = typeName.strip()
if trimmed.startsWith("ptr "):
@ -58,19 +51,6 @@ proc nimTypeToCpp*(typeName: string): string =
of "pointer": CppPtrType
else: trimmed
proc stripLibPrefixCpp(procName, libName: string): string =
let prefix = libName & "_"
if procName.startsWith(prefix):
return procName[prefix.len .. ^1]
return procName
proc reqStructName(p: FFIProcMeta): string =
let camel = snakeToPascalCase(p.procName)
if p.kind == FFIKind.CTOR:
camel & "CtorReq"
else:
camel & "Req"
proc emitStructCborCodec(
lines: var seq[string], structName: string, fields: seq[(string, string)]
) =
@ -361,24 +341,10 @@ proc generateCppHeader*(
lines.add(SyncCallHelperTpl)
# ── High-level C++ context class ──────────────────────────────────────────
var ctors: seq[FFIProcMeta] = @[]
var methods: seq[FFIProcMeta] = @[]
for p in procs:
case p.kind
of FFIKind.CTOR:
ctors.add(p)
of FFIKind.FFI:
methods.add(p)
of FFIKind.DTOR:
discard
let libTypeName =
if ctors.len > 0:
ctors[0].libTypeName
else:
capitalizeFirstLetter(libName)
let ctxTypeName = libTypeName & "Ctx"
let classified = classifyProcs(procs)
let ctors = classified.ctors
let methods = classified.methods
let ctxTypeName = libTypeName(ctors, libName) & "Ctx"
lines.add("// ============================================================")
lines.add("// High-level C++ context class")
@ -496,7 +462,7 @@ proc generateCppHeader*(
# ── Instance methods ────────────────────────────────────────────────────
for m in methods:
let methodName = stripLibPrefixCpp(m.procName, libName)
let methodName = stripLibPrefix(m.procName, libName)
let retCppType =
if m.returnRidesAsPtr():
CppPtrType

View File

@ -0,0 +1,47 @@
cmake_minimum_required(VERSION 3.14)
project({{LIB}}_c_bindings C)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
# ── Locate the repository root (contains ffi.nimble) ─────────────────────────
set(_search_dir "${CMAKE_CURRENT_SOURCE_DIR}")
set(REPO_ROOT "")
foreach(_i RANGE 10)
if(EXISTS "${_search_dir}/ffi.nimble")
set(REPO_ROOT "${_search_dir}")
break()
endif()
get_filename_component(_search_dir "${_search_dir}" DIRECTORY)
endforeach()
if("${REPO_ROOT}" STREQUAL "")
message(FATAL_ERROR "Cannot find repo root (no ffi.nimble in any ancestor)")
endif()
# Build the Nim dylib + vendored TinyCBOR (shared with the C++ backend).
set(NIM_FFI_LIB {{LIB}})
set(NIM_FFI_SRC {{SRC}})
include("${REPO_ROOT}/ffi/codegen/templates/nim_ffi_lib.cmake")
find_package(Threads REQUIRED)
add_library({{LIB}}_headers INTERFACE)
target_include_directories({{LIB}}_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")
target_link_libraries({{LIB}}_headers INTERFACE {{LIB}} tinycbor Threads::Threads)
# The generated header is async (no blocking helper), but consumer code that
# waits on a result callback typically uses nanosleep / pthreads, which need a
# POSIX feature level that strict `-std=c11` hides. Define it for consumers.
target_compile_definitions({{LIB}}_headers INTERFACE _POSIX_C_SOURCE=200809L)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.c")
add_executable({{LIB}}_example main.c)
target_link_libraries({{LIB}}_example PRIVATE {{LIB}}_headers)
add_dependencies({{LIB}}_example {{LIB}}_nim_lib)
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
add_custom_command(TARGET {{LIB}}_example POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${{{LIB}}_RUNTIME_LIB}"
"$<TARGET_FILE_DIR:{{LIB}}_example>"
COMMENT "Staging {{LIB}}.dll next to {{LIB}}_example.exe")
endif()
endif()

View File

@ -0,0 +1,341 @@
#ifndef NIM_FFI_CBOR_HELPERS_H_INCLUDED
#define NIM_FFI_CBOR_HELPERS_H_INCLUDED
/* Leaf CBOR codecs (scalars, text strings, byte strings) plus the buffer
* drivers. The per-struct / per-container codecs in the library header call
* into these by name (C has no overloading, so each leaf gets a distinct
* nimffi_enc_* / nimffi_dec_* symbol). Guarded so two nim-ffi headers can
* share a translation unit. */
#include "nim_ffi_prelude.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Result delivery callback exported by the Nim dylib: `ret` is 0 on success
* (then `msg`/`len` carry the CBOR response) or non-zero on failure (then
* `msg`/`len` carry the error text, which is NOT NUL-terminated). */
typedef void (*FFICallback)(int ret, const char* msg, size_t len, void* user_data);
/* Return / callback status codes. NIMFFI_RET_OK (0) is success; any non-zero
* value handed to a result callback's `err_code` (or returned by a submit call)
* is a failure. NIMFFI_RET_MISSING_CALLBACK is a special case from the Nim
* dispatcher: the callback will never fire, so the request path must report the
* failure itself. */
#define NIMFFI_RET_OK 0
#define NIMFFI_RET_ERROR 1
#define NIMFFI_RET_MISSING_CALLBACK 2
/* ── leaf encoders ─────────────────────────────────────────────────────── */
static inline CborError nimffi_enc_bool(CborEncoder* e, const bool* v) {
return cbor_encode_boolean(e, *v);
}
static inline CborError nimffi_enc_i64(CborEncoder* e, const int64_t* v) {
return cbor_encode_int(e, *v);
}
static inline CborError nimffi_enc_i32(CborEncoder* e, const int32_t* v) {
return cbor_encode_int(e, (int64_t)*v);
}
static inline CborError nimffi_enc_i16(CborEncoder* e, const int16_t* v) {
return cbor_encode_int(e, (int64_t)*v);
}
static inline CborError nimffi_enc_i8(CborEncoder* e, const int8_t* v) {
return cbor_encode_int(e, (int64_t)*v);
}
static inline CborError nimffi_enc_u64(CborEncoder* e, const uint64_t* v) {
return cbor_encode_uint(e, *v);
}
static inline CborError nimffi_enc_u32(CborEncoder* e, const uint32_t* v) {
return cbor_encode_uint(e, (uint64_t)*v);
}
static inline CborError nimffi_enc_u16(CborEncoder* e, const uint16_t* v) {
return cbor_encode_uint(e, (uint64_t)*v);
}
static inline CborError nimffi_enc_u8(CborEncoder* e, const uint8_t* v) {
return cbor_encode_uint(e, (uint64_t)*v);
}
static inline CborError nimffi_enc_f64(CborEncoder* e, const double* v) {
return cbor_encode_double(e, *v);
}
static inline CborError nimffi_enc_f32(CborEncoder* e, const float* v) {
return cbor_encode_float(e, *v);
}
static inline CborError nimffi_enc_str(CborEncoder* e, const NimFfiStr* v) {
return cbor_encode_text_string(e, v->data ? v->data : "", v->len);
}
static inline CborError nimffi_enc_bytes(CborEncoder* e, const NimFfiBytes* v) {
return cbor_encode_byte_string(e, v->data, v->len);
}
/* ── leaf decoders ─────────────────────────────────────────────────────── */
/* After reading a leaf, the parser must advance past it; both steps
* short-circuit on the same CborError, so they travel together. */
static inline CborError nimffi_advance_if_ok(CborValue* it, CborError err) {
if (err) {
return err;
}
return cbor_value_advance(it);
}
static inline CborError nimffi_dec_bool(CborValue* it, bool* out) {
if (!cbor_value_is_boolean(it)) {
return CborErrorImproperValue;
}
return nimffi_advance_if_ok(it, cbor_value_get_boolean(it, out));
}
static inline CborError nimffi_dec_i64(CborValue* it, int64_t* out) {
if (!cbor_value_is_integer(it)) {
return CborErrorImproperValue;
}
return nimffi_advance_if_ok(it, cbor_value_get_int64_checked(it, out));
}
static inline CborError nimffi_dec_i32(CborValue* it, int32_t* out) {
int64_t tmp = 0;
CborError err = nimffi_dec_i64(it, &tmp);
if (err) {
return err;
}
if (tmp < INT32_MIN || tmp > INT32_MAX) {
return CborErrorDataTooLarge;
}
*out = (int32_t)tmp;
return CborNoError;
}
static inline CborError nimffi_dec_i16(CborValue* it, int16_t* out) {
int64_t tmp = 0;
CborError err = nimffi_dec_i64(it, &tmp);
if (err) {
return err;
}
if (tmp < INT16_MIN || tmp > INT16_MAX) {
return CborErrorDataTooLarge;
}
*out = (int16_t)tmp;
return CborNoError;
}
static inline CborError nimffi_dec_i8(CborValue* it, int8_t* out) {
int64_t tmp = 0;
CborError err = nimffi_dec_i64(it, &tmp);
if (err) {
return err;
}
if (tmp < INT8_MIN || tmp > INT8_MAX) {
return CborErrorDataTooLarge;
}
*out = (int8_t)tmp;
return CborNoError;
}
static inline CborError nimffi_dec_u64(CborValue* it, uint64_t* out) {
if (!cbor_value_is_unsigned_integer(it)) {
return CborErrorImproperValue;
}
return nimffi_advance_if_ok(it, cbor_value_get_uint64(it, out));
}
static inline CborError nimffi_dec_u32(CborValue* it, uint32_t* out) {
uint64_t tmp = 0;
CborError err = nimffi_dec_u64(it, &tmp);
if (err) {
return err;
}
if (tmp > UINT32_MAX) {
return CborErrorDataTooLarge;
}
*out = (uint32_t)tmp;
return CborNoError;
}
static inline CborError nimffi_dec_u16(CborValue* it, uint16_t* out) {
uint64_t tmp = 0;
CborError err = nimffi_dec_u64(it, &tmp);
if (err) {
return err;
}
if (tmp > UINT16_MAX) {
return CborErrorDataTooLarge;
}
*out = (uint16_t)tmp;
return CborNoError;
}
static inline CborError nimffi_dec_u8(CborValue* it, uint8_t* out) {
uint64_t tmp = 0;
CborError err = nimffi_dec_u64(it, &tmp);
if (err) {
return err;
}
if (tmp > UINT8_MAX) {
return CborErrorDataTooLarge;
}
*out = (uint8_t)tmp;
return CborNoError;
}
static inline CborError nimffi_dec_f64(CborValue* it, double* out) {
if (cbor_value_is_double(it)) {
return nimffi_advance_if_ok(it, cbor_value_get_double(it, out));
}
if (cbor_value_is_float(it)) {
float f = 0.0f;
CborError err = cbor_value_get_float(it, &f);
if (err) {
return err;
}
*out = (double)f;
return cbor_value_advance(it);
}
return CborErrorImproperValue;
}
static inline CborError nimffi_dec_f32(CborValue* it, float* out) {
if (cbor_value_is_float(it)) {
return nimffi_advance_if_ok(it, cbor_value_get_float(it, out));
}
if (cbor_value_is_double(it)) {
double d = 0.0;
CborError err = cbor_value_get_double(it, &d);
if (err) {
return err;
}
*out = (float)d;
return cbor_value_advance(it);
}
return CborErrorImproperValue;
}
static inline CborError nimffi_dec_str(CborValue* it, NimFfiStr* 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;
}
if (len == SIZE_MAX) { /* len + 1 would wrap to a 0-byte allocation */
return CborErrorDataTooLarge;
}
/* one extra byte so a NUL-free payload is a valid C string */
out->data = (char*)malloc(len + 1);
if (!out->data) {
return CborErrorOutOfMemory;
}
out->len = len;
size_t copied = len;
err = cbor_value_copy_text_string(it, out->data, &copied, NULL);
if (err) {
free(out->data);
out->data = NULL;
out->len = 0;
return err;
}
out->data[len] = '\0';
return cbor_value_advance(it);
}
static inline CborError nimffi_dec_bytes(CborValue* it, NimFfiBytes* out) {
if (!cbor_value_is_byte_string(it)) {
return CborErrorImproperValue;
}
size_t len = 0;
CborError err = cbor_value_get_string_length(it, &len);
if (err) {
return err;
}
out->data = (uint8_t*)malloc(len ? len : 1);
if (!out->data) {
return CborErrorOutOfMemory;
}
out->len = len;
size_t copied = len;
err = cbor_value_copy_byte_string(it, out->data, &copied, NULL);
if (err) {
free(out->data);
out->data = NULL;
out->len = 0;
return err;
}
return cbor_value_advance(it);
}
/* ── buffer drivers ────────────────────────────────────────────────────── */
typedef CborError (*nimffi_enc_fn)(CborEncoder*, const void*);
typedef CborError (*nimffi_dec_fn)(CborValue*, void*);
static inline char* nimffi_dup_cstr(const char* s) {
size_t n = strlen(s) + 1;
char* p = (char*)malloc(n);
if (p) {
memcpy(p, s, n);
}
return p;
}
/* NUL-terminated copy of a length-delimited (not NUL-terminated) byte run,
* for turning the FFICallback's raw error `msg`/`len` into a C string. */
static inline char* nimffi_dup_cstr_n(const char* s, size_t n) {
char* p = (char*)malloc(n + 1);
if (p) {
if (n > 0) {
memcpy(p, s, n);
}
p[n] = '\0';
}
return p;
}
/* Encode `val` with `fn` into a freshly malloc'd buffer, doubling on overflow.
* Returns 0 and sets out/outlen on success; -1 and *err (heap) on failure. */
static inline int nimffi_encode_to_buf(
nimffi_enc_fn fn, const void* val,
uint8_t** out, size_t* outlen, char** err) {
size_t cap = 4096;
uint8_t* buf = (uint8_t*)malloc(cap);
if (!buf) {
if (err) *err = nimffi_dup_cstr("out of memory");
return -1;
}
for (;;) {
CborEncoder enc;
cbor_encoder_init(&enc, buf, cap, 0);
CborError e = fn(&enc, val);
if (e == CborNoError) {
*outlen = cbor_encoder_get_buffer_size(&enc, buf);
*out = buf;
return 0;
}
if (e == CborErrorOutOfMemory) {
size_t extra = cbor_encoder_get_extra_bytes_needed(&enc);
cap += extra > 0 ? extra : cap;
uint8_t* grown = (uint8_t*)realloc(buf, cap);
if (!grown) {
free(buf);
if (err) *err = nimffi_dup_cstr("out of memory");
return -1;
}
buf = grown;
continue;
}
free(buf);
if (err) *err = nimffi_dup_cstr(cbor_error_string(e));
return -1;
}
}
/* Decode a CBOR buffer into `out` with `fn`. Returns 0 on success; -1 and
* *err (heap) on failure. */
static inline int nimffi_decode_from_buf(
nimffi_dec_fn fn, const uint8_t* buf, size_t len,
void* out, char** err) {
CborParser parser;
CborValue it;
CborError e = cbor_parser_init(buf, len, 0, &parser, &it);
if (e != CborNoError) {
if (err) *err = nimffi_dup_cstr(cbor_error_string(e));
return -1;
}
e = fn(&it, out);
if (e != CborNoError) {
if (err) *err = nimffi_dup_cstr(cbor_error_string(e));
return -1;
}
return 0;
}
#ifdef __cplusplus
}
#endif
#endif /* NIM_FFI_CBOR_HELPERS_H_INCLUDED */

View File

@ -0,0 +1,88 @@
#ifndef NIM_FFI_PRELUDE_H_INCLUDED
#define NIM_FFI_PRELUDE_H_INCLUDED
/* Generated C binding for a nim-ffi library. Requests/responses travel as
* CBOR (encoded with vendored TinyCBOR on this side, matching the Nim-side
* cbor_serial codec on the wire — both ends speak RFC 8949).
*
* The API is asynchronous: every method/constructor takes a result callback
* and returns immediately. The callback fires exactly once — synchronously on
* a submit-time failure, otherwise from the Nim dispatch thread when the reply
* arrives.
*
* Memory ownership contract:
* - Request-side strings/sequences are *borrowed*: the binding only reads
* them while encoding, so a string literal wrapped with nimffi_str() is
* fine and is never freed by the binding.
* - Response values and error strings passed into a result callback are
* *owned by the binding* and valid only for the duration of that callback;
* the binding reclaims them once the callback returns. The caller never
* frees them. (The generated <lib>_free_<Type>() helpers are internal — the
* trampolines use them to reclaim decoded payloads.)
* - A context handle delivered to a constructor callback is the exception:
* ownership transfers to the caller, who releases it with
* <lib>_ctx_destroy(). It is a lifecycle handle, not returned data.
*
* Trust boundary: the decoders assume the CBOR they parse was produced by the
* paired Nim library. They reject malformed input rather than trusting it, but
* they are not hardened against a hostile peer feeding crafted payloads through
* the raw nimffi_decode_from_buf entry point.
*/
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <tinycbor/cbor.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Owned, length-delimited UTF-8 text (Nim `string`/`cstring`). On the request
* side `data` may point at borrowed storage (see nimffi_str); on the response
* side it is heap-allocated and freed by nimffi_free_str. Always NUL-padded by
* one byte after decode so `data` is usable as a C string when it has no
* embedded NULs. */
typedef struct {
char* data;
size_t len;
} NimFfiStr;
/* Owned, length-delimited byte buffer (Nim `seq[byte]`). */
typedef struct {
uint8_t* data;
size_t len;
} NimFfiBytes;
/* Wrap a borrowed C string for use as a request field. The returned view is
* not owned by the binding and must outlive the call that encodes it. */
static inline NimFfiStr nimffi_str(const char* s) {
NimFfiStr v;
v.data = (char*)s;
v.len = s ? strlen(s) : 0;
return v;
}
static inline void nimffi_free_str(NimFfiStr* v) {
if (!v || !v->data) {
return;
}
free(v->data);
v->data = NULL;
v->len = 0;
}
static inline void nimffi_free_bytes(NimFfiBytes* v) {
if (!v || !v->data) {
return;
}
free(v->data);
v->data = NULL;
v->len = 0;
}
#ifdef __cplusplus
}
#endif
#endif /* NIM_FFI_PRELUDE_H_INCLUDED */

View File

@ -27,92 +27,10 @@ if("${REPO_ROOT}" STREQUAL "")
message(FATAL_ERROR "Cannot find repo root (no ffi.nimble in any ancestor)")
endif()
get_filename_component(NIM_SRC
"${CMAKE_CURRENT_SOURCE_DIR}/{{SRC}}"
ABSOLUTE)
find_program(NIM_EXECUTABLE nim REQUIRED)
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
set(NIM_LIB_FILE "${REPO_ROOT}/lib{{LIB}}.dylib")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
set(NIM_LIB_FILE "${REPO_ROOT}/{{LIB}}.dll")
# MSVC consumers link against the `.lib` import library, not the DLL.
# MinGW's ld emits one when asked via `--out-implib`; the resulting COFF
# archive is readable by MSVC's link.exe.
set(NIM_IMPLIB_FILE "${REPO_ROOT}/{{LIB}}.lib")
else()
set(NIM_LIB_FILE "${REPO_ROOT}/lib{{LIB}}.so")
endif()
# On Windows the default Nim toolchain (mingw gcc) doesn't emit an import
# library unless told to. Without it, MSVC consumers can't resolve any
# symbol exported by the DLL at link time.
set(NIM_IMPLIB_PASSL "")
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
set(NIM_IMPLIB_PASSL "--passL:-Wl,--out-implib,${NIM_IMPLIB_FILE}")
endif()
add_custom_command(
OUTPUT "${NIM_LIB_FILE}"
COMMAND "${NIM_EXECUTABLE}" c
--mm:orc
-d:chronicles_log_level=WARN
--app:lib
--noMain
"--nimMainPrefix:lib{{LIB}}"
${NIM_IMPLIB_PASSL}
"-o:${NIM_LIB_FILE}"
"${NIM_SRC}"
WORKING_DIRECTORY "${REPO_ROOT}"
DEPENDS "${NIM_SRC}"
BYPRODUCTS "${NIM_IMPLIB_FILE}"
COMMENT "Compiling Nim library lib{{LIB}}"
VERBATIM
)
add_custom_target({{LIB}}_nim_lib ALL DEPENDS "${NIM_LIB_FILE}")
# On Windows, an `IMPORTED SHARED` target needs IMPORTED_IMPLIB pointing at
# the `.lib` import library so MSVC's `link.exe` can resolve symbols. The
# Visual Studio multi-config generator did not pick up `IMPORTED_IMPLIB` —
# nor per-config `IMPORTED_IMPLIB_<CONFIG>` variants — and emitted
# `{{LIB}}-NOTFOUND.obj` into every link line. Side-step the IMPORTED
# machinery on Windows by exposing the import library through a plain
# INTERFACE library that links the `.lib` by path.
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
add_library({{LIB}} INTERFACE)
target_link_libraries({{LIB}} INTERFACE "${NIM_IMPLIB_FILE}")
else()
add_library({{LIB}} SHARED IMPORTED GLOBAL)
set_target_properties({{LIB}} PROPERTIES IMPORTED_LOCATION "${NIM_LIB_FILE}")
endif()
add_dependencies({{LIB}} {{LIB}}_nim_lib)
# Absolute path to the runtime library (DLL/dylib/so). Exposed via the cache
# so consumers in other directories (e.g. tests/e2e/cpp) can stage the DLL
# next to their executable on Windows.
set({{LIB}}_RUNTIME_LIB "${NIM_LIB_FILE}" CACHE INTERNAL
"Absolute path to the {{LIB}} runtime library")
# ── TinyCBOR (vendored at ffi/codegen/templates/cpp/vendor/tinycbor) ─────────
# Guarded so two sibling cpp_bindings dirs in one parent project don't redefine
# the `tinycbor` target.
set(TINYCBOR_SRC_DIR "${REPO_ROOT}/ffi/codegen/templates/cpp/vendor")
if(NOT TARGET tinycbor)
add_library(tinycbor STATIC
"${TINYCBOR_SRC_DIR}/tinycbor/cborencoder.c"
"${TINYCBOR_SRC_DIR}/tinycbor/cborencoder_close_container_checked.c"
"${TINYCBOR_SRC_DIR}/tinycbor/cborparser.c"
"${TINYCBOR_SRC_DIR}/tinycbor/cborparser_dup_string.c"
"${TINYCBOR_SRC_DIR}/tinycbor/cborerrorstrings.c"
)
target_include_directories(tinycbor PUBLIC
"${TINYCBOR_SRC_DIR}" # consumer uses #include <tinycbor/cbor.h>
"${TINYCBOR_SRC_DIR}/tinycbor" # internal _p.h includes resolve here
)
set_property(TARGET tinycbor PROPERTY C_STANDARD 99)
set_property(TARGET tinycbor PROPERTY POSITION_INDEPENDENT_CODE ON)
endif()
# Build the Nim dylib + vendored TinyCBOR (shared with the C backend).
set(NIM_FFI_LIB {{LIB}})
set(NIM_FFI_SRC {{SRC}})
include("${REPO_ROOT}/ffi/codegen/templates/nim_ffi_lib.cmake")
add_library({{LIB}}_headers INTERFACE)
target_include_directories({{LIB}}_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")

View File

@ -0,0 +1,87 @@
# Shared CMake logic for nim-ffi generated bindings. Builds the Nim library as
# a shared object and the vendored TinyCBOR as a static library, and exposes
# them as the imported target `${NIM_FFI_LIB}` (+ `${NIM_FFI_LIB}_nim_lib`) and
# the `tinycbor` target. Included by the per-language generated CMakeLists,
# which set REPO_ROOT, NIM_FFI_LIB (library name) and NIM_FFI_SRC (path to the
# .nim root, relative to the including CMakeLists) before including this file.
get_filename_component(NIM_SRC
"${CMAKE_CURRENT_SOURCE_DIR}/${NIM_FFI_SRC}"
ABSOLUTE)
find_program(NIM_EXECUTABLE nim REQUIRED)
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
set(NIM_LIB_FILE "${REPO_ROOT}/lib${NIM_FFI_LIB}.dylib")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
set(NIM_LIB_FILE "${REPO_ROOT}/${NIM_FFI_LIB}.dll")
set(NIM_IMPLIB_FILE "${REPO_ROOT}/${NIM_FFI_LIB}.lib")
else()
set(NIM_LIB_FILE "${REPO_ROOT}/lib${NIM_FFI_LIB}.so")
endif()
# On Windows the default Nim toolchain (mingw gcc) doesn't emit an import
# library unless told to; without it MSVC consumers can't resolve any exported
# symbol at link time.
set(NIM_IMPLIB_PASSL "")
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
set(NIM_IMPLIB_PASSL "--passL:-Wl,--out-implib,${NIM_IMPLIB_FILE}")
endif()
add_custom_command(
OUTPUT "${NIM_LIB_FILE}"
COMMAND "${NIM_EXECUTABLE}" c
--mm:orc
-d:chronicles_log_level=WARN
--app:lib
--noMain
"--nimMainPrefix:lib${NIM_FFI_LIB}"
${NIM_IMPLIB_PASSL}
"-o:${NIM_LIB_FILE}"
"${NIM_SRC}"
WORKING_DIRECTORY "${REPO_ROOT}"
DEPENDS "${NIM_SRC}"
BYPRODUCTS "${NIM_IMPLIB_FILE}"
COMMENT "Compiling Nim library lib${NIM_FFI_LIB}"
VERBATIM
)
add_custom_target(${NIM_FFI_LIB}_nim_lib ALL DEPENDS "${NIM_LIB_FILE}")
# On Windows an IMPORTED SHARED target needs IMPORTED_IMPLIB, but the Visual
# Studio multi-config generator did not pick it up and emitted
# `${NIM_FFI_LIB}-NOTFOUND.obj`. Side-step the IMPORTED machinery there by
# exposing the import library through a plain INTERFACE library.
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
add_library(${NIM_FFI_LIB} INTERFACE)
target_link_libraries(${NIM_FFI_LIB} INTERFACE "${NIM_IMPLIB_FILE}")
else()
add_library(${NIM_FFI_LIB} SHARED IMPORTED GLOBAL)
set_target_properties(${NIM_FFI_LIB} PROPERTIES IMPORTED_LOCATION "${NIM_LIB_FILE}")
endif()
add_dependencies(${NIM_FFI_LIB} ${NIM_FFI_LIB}_nim_lib)
# Absolute path to the runtime library (DLL/dylib/so). Exposed via the cache so
# consumers in other directories can stage the DLL next to their executable on
# Windows.
set(${NIM_FFI_LIB}_RUNTIME_LIB "${NIM_LIB_FILE}" CACHE INTERNAL
"Absolute path to the ${NIM_FFI_LIB} runtime library")
# TinyCBOR (vendored at ffi/codegen/templates/cpp/vendor/tinycbor)
# The C and C++ backends share one vendored TinyCBOR copy. Guarded so two
# sibling bindings dirs in one parent project don't redefine the target.
set(TINYCBOR_SRC_DIR "${REPO_ROOT}/ffi/codegen/templates/cpp/vendor")
if(NOT TARGET tinycbor)
add_library(tinycbor STATIC
"${TINYCBOR_SRC_DIR}/tinycbor/cborencoder.c"
"${TINYCBOR_SRC_DIR}/tinycbor/cborencoder_close_container_checked.c"
"${TINYCBOR_SRC_DIR}/tinycbor/cborparser.c"
"${TINYCBOR_SRC_DIR}/tinycbor/cborparser_dup_string.c"
"${TINYCBOR_SRC_DIR}/tinycbor/cborerrorstrings.c"
)
target_include_directories(tinycbor PUBLIC
"${TINYCBOR_SRC_DIR}" # consumer uses #include <tinycbor/cbor.h>
"${TINYCBOR_SRC_DIR}/tinycbor" # internal _p.h includes resolve here
)
set_property(TARGET tinycbor PROPERTY C_STANDARD 99)
set_property(TARGET tinycbor PROPERTY POSITION_INDEPENDENT_CODE ON)
endif()

View File

@ -6,6 +6,7 @@ import ./c_macro_helpers
when defined(ffiGenBindings):
import ../codegen/rust
import ../codegen/cpp
import ../codegen/c
import ../codegen/cddl
proc requireLibraryDeclared(where: string) {.compileTime.} =
@ -1655,7 +1656,7 @@ macro genBindings*(
## In a multi-file library, import all sub-modules first and call
## genBindings() once at the bottom of the top-level compilation-root file.
##
## Supported languages (-d:targetLang): "rust" (default), "cpp".
## Supported languages (-d:targetLang): "rust" (default), "cpp", "c", "cddl".
## Output path and nim source path default to -d:ffiOutputDir and
## -d:ffiSrcPath, or can be passed as explicit arguments.
## Foreign-binding file emission is a no-op unless -d:ffiGenBindings is set;
@ -1687,13 +1688,19 @@ macro genBindings*(
ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath,
ffiEventRegistry,
)
of "c":
generateCBindings(
ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath,
ffiEventRegistry,
)
of "cddl":
generateCddlBindings(
ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath
)
else:
error(
"genBindings: unknown targetLang '" & lang & "'. Use 'rust', 'cpp', or 'cddl'."
"genBindings: unknown targetLang '" & lang &
"'. Use 'rust', 'cpp', 'c', or 'cddl'."
)
let cwireCompanions = flushCWireCompanions()

View File

@ -0,0 +1,55 @@
cmake_minimum_required(VERSION 3.14)
project(nim_ffi_c_e2e C)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
# Reuse the timer c_bindings (exposes my_timer_headers / my_timer_nim_lib)
get_filename_component(_timer_c_bindings_dir
"${CMAKE_CURRENT_SOURCE_DIR}/../../../examples/timer/c_bindings"
ABSOLUTE)
add_subdirectory("${_timer_c_bindings_dir}" timer_c_bindings_build)
enable_testing()
add_executable(timer_e2e_c test_timer_e2e.c)
target_link_libraries(timer_e2e_c PRIVATE my_timer_headers)
add_dependencies(timer_e2e_c my_timer_nim_lib)
if(NIM_FFI_SAN_CFLAGS)
target_compile_options(timer_e2e_c PRIVATE ${NIM_FFI_SAN_CFLAGS})
target_link_options(timer_e2e_c PRIVATE ${NIM_FFI_SAN_LFLAGS})
endif()
# Nim-built dylibs use `@rpath/lib*.so|dylib`, so embed the IMPORTED target's
# build-tree dir as an rpath. Windows has no rpath stage the DLL next to the
# exe instead.
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Windows")
get_target_property(_my_timer_loc my_timer IMPORTED_LOCATION)
get_filename_component(_my_timer_dir "${_my_timer_loc}" DIRECTORY)
set_target_properties(timer_e2e_c PROPERTIES
BUILD_RPATH "${_my_timer_dir}"
INSTALL_RPATH "${_my_timer_dir}")
else()
add_custom_command(TARGET timer_e2e_c POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${my_timer_RUNTIME_LIB}"
"$<TARGET_FILE_DIR:timer_e2e_c>"
COMMENT "Staging my_timer.dll next to timer_e2e_c.exe")
endif()
set(_san_test_env "")
if(NIM_FFI_SANITIZER STREQUAL "asan-ubsan")
list(APPEND _san_test_env
"ASAN_OPTIONS=halt_on_error=1:abort_on_error=1:detect_leaks=1:strict_string_checks=1"
"UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1"
"LSAN_OPTIONS=suppressions=${CMAKE_CURRENT_SOURCE_DIR}/lsan.supp:print_suppressions=0")
elseif(NIM_FFI_SANITIZER STREQUAL "tsan")
list(APPEND _san_test_env
"TSAN_OPTIONS=halt_on_error=1:second_deadlock_stack=1:history_size=7")
endif()
add_test(NAME timer_e2e_c COMMAND timer_e2e_c)
if(_san_test_env)
set_tests_properties(timer_e2e_c PROPERTIES ENVIRONMENT "${_san_test_env}")
endif()

18
tests/e2e/c/README.md Normal file
View File

@ -0,0 +1,18 @@
# C end-to-end test
Builds the generated C bindings for the timer example (`examples/timer/c_bindings`)
and drives them against the real Nim dylib, asserting on every response. Run it
with:
```sh
nimble test_c_e2e
```
which regenerates the bindings, configures CMake, builds, and runs the test via
`ctest`. The test program (`test_timer_e2e.c`) exercises the constructor, the
sync and async methods, nested `seq`/`Option` payloads, multi-parameter
requests, the error channel, and the typed event listener.
`test_timer_e2e.c` is hand-written (it is the consumer of the bindings, not a
generated artifact). The bindings under `examples/timer/c_bindings` are
generated and must not be edited by hand.

22
tests/e2e/c/lsan.supp Normal file
View File

@ -0,0 +1,22 @@
# LeakSanitizer suppressions for the Nim runtime.
#
# These are process-lifetime allocations freed implicitly at exit —
# not real leaks. Add new entries here only with a comment justifying
# why the leak is unavoidable, and only for symbols inside the Nim
# standard library, chronos, or chronicles. Anything in our own code
# (ffi/*) or the generated bindings must be fixed, not suppressed.
# Nim runtime initialisation — allocates global state freed at exit.
leak:NimMain
leak:PreMain
leak:systemDatInit
# GC bootstrap — registers stack bottom / TLS slots once per thread.
leak:nimGC_setStackBottom
leak:initStackBottomWith
leak:setupForeignThreadGc
# Async / logging library globals (event loop singletons, logger
# registries) — owned for the process lifetime.
leak:chronos
leak:chronicles

View File

@ -0,0 +1,263 @@
/* End-to-end test for the generated C timer bindings. Exercises the same
* surface as the C++ suite constructor, methods, nested seq/Option payloads,
* multi-parameter requests, the error channel and the typed event listener
* and aborts (non-zero exit) on the first failure so ctest reports it.
*
* The binding is asynchronous: every call takes a result callback and the
* reply/error are owned by the binding and valid only for the duration of that
* callback. So each callback *copies out* what it needs into a waiter struct,
* and the test polls a `done` flag (the same volatile-flag pattern the event
* listener uses) to turn each async call back into a sequential check. The
* caller never frees reply data or error strings that is the whole point. */
#include "my_timer.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
static void wait_done(volatile int* done) {
for (int i = 0; i < 500 && !*done; i++) {
struct timespec t = {0, 10 * 1000 * 1000}; /* 10ms */
nanosleep(&t, NULL);
}
assert(*done);
}
static int g_event_count = 0;
static char g_event_message[256];
static void on_echo_fired(const EchoEvent* evt, void* user_data) {
int* hits = (int*)user_data;
if (hits) {
(*hits)++;
}
g_event_count = (int)evt->echoCount;
snprintf(g_event_message, sizeof(g_event_message), "%s",
evt->message.data ? evt->message.data : "");
}
typedef struct {
volatile int done;
int err_code;
MyTimerCtx* ctx;
char err[256];
} CreateWaiter;
static void on_created(int ec, MyTimerCtx* ctx, const char* em, void* ud) {
CreateWaiter* w = (CreateWaiter*)ud;
w->err_code = ec;
w->ctx = ctx;
if (em) {
snprintf(w->err, sizeof(w->err), "%s", em);
}
w->done = 1;
}
static MyTimerCtx* make_ctx(void) {
CreateWaiter w;
memset(&w, 0, sizeof(w));
TimerConfig config = {nimffi_str("c-e2e")};
my_timer_ctx_create(&config, on_created, &w);
wait_done(&w.done);
if (w.err_code != 0) {
fprintf(stderr, "create failed: %s\n", w.err[0] ? w.err : "?");
}
assert(w.err_code == 0);
assert(w.ctx != NULL);
return w.ctx;
}
typedef struct {
volatile int done;
int err_code;
char err[256];
char text_a[256];
char text_b[256];
long long num_a;
int flag;
} ReplyWaiter;
static void on_version(int ec, const NimFfiStr* reply, const char* em, void* ud) {
ReplyWaiter* w = (ReplyWaiter*)ud;
w->err_code = ec;
if (reply && reply->data) snprintf(w->text_a, sizeof(w->text_a), "%s", reply->data);
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
w->done = 1;
}
static void test_version(MyTimerCtx* ctx) {
ReplyWaiter w;
memset(&w, 0, sizeof(w));
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);
}
static void on_echo(int ec, const EchoResponse* reply, const char* em, void* ud) {
ReplyWaiter* w = (ReplyWaiter*)ud;
w->err_code = ec;
if (reply) {
if (reply->echoed.data) snprintf(w->text_a, sizeof(w->text_a), "%s", reply->echoed.data);
if (reply->timerName.data)
snprintf(w->text_b, sizeof(w->text_b), "%s", reply->timerName.data);
}
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
w->done = 1;
}
static void test_echo(MyTimerCtx* ctx) {
ReplyWaiter w;
memset(&w, 0, sizeof(w));
EchoRequest req = {nimffi_str("hello"), 10};
my_timer_ctx_echo(ctx, &req, on_echo, &w);
wait_done(&w.done);
assert(w.err_code == 0);
assert(strcmp(w.text_a, "hello") == 0);
assert(strcmp(w.text_b, "c-e2e") == 0);
}
static void on_complex(int ec, const ComplexResponse* reply, const char* em, void* ud) {
ReplyWaiter* w = (ReplyWaiter*)ud;
w->err_code = ec;
if (reply) {
w->num_a = (long long)reply->itemCount;
w->flag = (int)reply->hasNote;
if (reply->summary.data)
snprintf(w->text_a, sizeof(w->text_a), "%s", reply->summary.data);
}
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
w->done = 1;
}
static void test_complex(MyTimerCtx* ctx) {
EchoRequest items[2] = {{nimffi_str("one"), 1}, {nimffi_str("two"), 2}};
NimFfiStr tags[2] = {nimffi_str("a"), nimffi_str("b")};
ComplexRequest req;
req.messages.data = items;
req.messages.len = 2;
req.tags.data = tags;
req.tags.len = 2;
req.note.has_value = true;
req.note.value = nimffi_str("note");
req.retries.has_value = false;
req.retries.value = 0;
ReplyWaiter w;
memset(&w, 0, sizeof(w));
my_timer_ctx_complex(ctx, &req, on_complex, &w);
wait_done(&w.done);
assert(w.err_code == 0);
assert(w.num_a == 2);
assert(w.flag == true);
assert(strstr(w.text_a, "note=note") != NULL);
}
static void on_schedule(int ec, const ScheduleResult* reply, const char* em, void* ud) {
ReplyWaiter* w = (ReplyWaiter*)ud;
w->err_code = ec;
if (reply) {
w->num_a = (long long)reply->willRunCount;
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);
w->done = 1;
}
static void test_schedule_ok(MyTimerCtx* ctx) {
NimFfiStr payload[1] = {nimffi_str("p")};
JobSpec job;
job.name = nimffi_str("rollup");
job.payload.data = payload;
job.payload.len = 1;
job.priority = 1;
NimFfiStr retry_on[1] = {nimffi_str("timeout")};
RetryPolicy retry;
retry.maxAttempts = 3;
retry.backoffMs = 100;
retry.retryOn.data = retry_on;
retry.retryOn.len = 1;
ScheduleConfig sched;
sched.startAtMs = 1000;
sched.intervalMs = 0;
sched.jitter.has_value = false;
sched.jitter.value = 0;
ReplyWaiter w;
memset(&w, 0, sizeof(w));
my_timer_ctx_schedule(ctx, &job, &retry, &sched, on_schedule, &w);
wait_done(&w.done);
assert(w.err_code == 0);
assert(strcmp(w.text_a, "c-e2e:rollup") == 0);
assert(w.num_a == 1);
}
static void test_schedule_error(MyTimerCtx* ctx) {
NimFfiStr payload[1] = {nimffi_str("p")};
JobSpec job;
job.name = nimffi_str(""); /* empty name → handler returns err */
job.payload.data = payload;
job.payload.len = 1;
job.priority = 1;
NimFfiStr retry_on[1] = {nimffi_str("timeout")};
RetryPolicy retry;
retry.maxAttempts = 3;
retry.backoffMs = 100;
retry.retryOn.data = retry_on;
retry.retryOn.len = 1;
ScheduleConfig sched;
sched.startAtMs = 0;
sched.intervalMs = 0;
sched.jitter.has_value = false;
sched.jitter.value = 0;
ReplyWaiter w;
memset(&w, 0, sizeof(w));
my_timer_ctx_schedule(ctx, &job, &retry, &sched, on_schedule, &w);
wait_done(&w.done);
assert(w.err_code != 0);
assert(w.err[0] != '\0');
assert(strstr(w.err, "job name") != NULL);
}
static void test_event(MyTimerCtx* ctx) {
int hits = 0;
uint64_t handle =
my_timer_ctx_add_on_echo_fired_listener(ctx, on_echo_fired, &hits);
assert(handle != 0);
ReplyWaiter w;
memset(&w, 0, sizeof(w));
EchoRequest req = {nimffi_str("evt"), 1};
my_timer_ctx_echo(ctx, &req, on_echo, &w);
wait_done(&w.done);
assert(w.err_code == 0);
/* The event fires from the dispatch thread; poll briefly for delivery. */
for (int i = 0; i < 100 && hits == 0; i++) {
struct timespec t = {0, 10 * 1000 * 1000};
nanosleep(&t, NULL);
}
assert(hits >= 1);
assert(strcmp(g_event_message, "evt") == 0);
assert(g_event_count == 1);
assert(my_timer_ctx_remove_event_listener(ctx, handle) == true);
}
int main(void) {
MyTimerCtx* ctx = make_ctx();
test_version(ctx);
test_echo(ctx);
test_complex(ctx);
test_schedule_ok(ctx);
test_schedule_error(ctx);
test_event(ctx);
my_timer_ctx_destroy(ctx);
printf("all C e2e checks passed\n");
return 0;
}

View File

@ -0,0 +1,229 @@
## Unit-tests for the C binding generator. Drives generateCLibHeader (and the
## shared-header generators) directly against a synthetic registry (no macro
## pipeline, no files written) and asserts on the emitted text — the same
## approach as test_cddl_codegen.
import std/strutils
import unittest2
import ffi/codegen/[meta, c]
proc field(n, t: string): FFIFieldMeta =
FFIFieldMeta(name: n, typeName: t)
proc param(n, t: string, isPtr = false): FFIParamMeta =
FFIParamMeta(name: n, typeName: t, isPtr: isPtr)
suite "generateCLibHeader: types and codecs":
setup:
let types = @[
FFITypeMeta(
name: "EchoRequest",
fields: @[field("message", "string"), field("delayMs", "int")],
),
FFITypeMeta(name: "EchoResponse", fields: @[field("echoed", "string")]),
FFITypeMeta(
name: "ComplexRequest",
fields:
@[field("messages", "seq[EchoRequest]"), field("note", "Option[string]")],
),
]
let procs = @[
FFIProcMeta(
procName: "timer_create",
libName: "timer",
kind: FFIKind.CTOR,
libTypeName: "Timer",
extraParams: @[param("config", "EchoRequest")],
returnTypeName: "Timer",
),
FFIProcMeta(
procName: "timer_echo",
libName: "timer",
kind: FFIKind.FFI,
libTypeName: "Timer",
extraParams: @[param("req", "EchoRequest")],
returnTypeName: "EchoResponse",
),
FFIProcMeta(
procName: "timer_destroy",
libName: "timer",
kind: FFIKind.DTOR,
libTypeName: "Timer",
extraParams: @[],
returnTypeName: "",
),
]
let header = generateCLibHeader(procs, types, "timer")
test "the lib header pulls in the shared cbor header and uses its codecs":
check "#include \"nim_ffi_cbor.h\"" in header
check "NimFfiStr" in header
check "nimffi_enc_str" in header
test "user structs become C structs with mapped field types":
check "} EchoRequest;" in header
check "int64_t delayMs;" in header
check "NimFfiStr message;" in header
test "per-struct encode/decode/free are emitted":
check "timer_enc_EchoRequest(" in header
check "timer_dec_EchoRequest(" in header
check "timer_free_EchoRequest(" in header
test "seq[T] is monomorphised into a sized struct":
check "} TimerSeq_EchoRequest;" in header
check "EchoRequest* data;" in header
check "timer_enc_TimerSeq_EchoRequest(" in header
test "Option[T] is monomorphised with a has_value flag":
check "} TimerOpt_Str;" in header
check "bool has_value;" in header
test "a struct whose fields own no heap memory gets no free helper":
# EchoResponse has only a string field, so it does get a free; assert the
# inverse with the per-proc Version-less example via the int-only check:
check "timer_free_EchoResponse(" in header
suite "generateCLibHeader: ABI declarations and context API":
setup:
let procs = @[
FFIProcMeta(
procName: "timer_create",
libName: "timer",
kind: FFIKind.CTOR,
libTypeName: "Timer",
extraParams: @[param("config", "EchoRequest")],
returnTypeName: "Timer",
),
FFIProcMeta(
procName: "timer_version",
libName: "timer",
kind: FFIKind.FFI,
libTypeName: "Timer",
extraParams: @[],
returnTypeName: "string",
),
FFIProcMeta(
procName: "timer_destroy",
libName: "timer",
kind: FFIKind.DTOR,
libTypeName: "Timer",
extraParams: @[],
returnTypeName: "",
),
]
let types = @[FFITypeMeta(name: "EchoRequest", fields: @[field("m", "string")])]
let header = generateCLibHeader(procs, types, "timer")
test "raw dylib symbols are declared with the C ABI shape":
check "void* timer_create(const uint8_t* req_cbor, size_t req_cbor_len," in header
check "int timer_version(void* ctx, FFICallback callback" in header
check "int timer_destroy(void* ctx);" in header
check "uint64_t timer_add_event_listener(" in header
test "high-level wrappers are namespaced to avoid the raw symbols":
check "timer_ctx_create(" in header
check "timer_ctx_version(" in header
check "timer_ctx_destroy(" in header
test "the async API is callback-driven, not blocking":
# methods take a typed reply callback + user_data; no out-param, no char** err
check "typedef void (*TimerVersionReplyFn)(int err_code, const NimFfiStr* reply, const char* err_msg, void* user_data);" in
header
check "TimerVersionCallBox" in header
check "timer_version_reply_trampoline(" in header
check "timer_ctx_version(const TimerCtx* ctx, TimerVersionReplyFn on_reply, void* user_data)" in
header
test "the constructor is async and hands the context to a callback":
check "typedef void (*TimerCreateFn)(int err_code, TimerCtx* ctx, const char* err_msg, void* user_data);" in
header
check "timer_create_trampoline(" in header
check "timer_ctx_create(const EchoRequest* config, TimerCreateFn on_created, void* user_data)" in
header
test "no blocking sync-call machinery or per-call timeout survives":
check "nimffi_wait_result" notin header
check "NimFfiCallState" notin header
check "timeout_ms" notin header
test "an empty request envelope still encodes a (zero-length) map":
check "_nimffi_empty" in header
suite "generateCLibHeader: events":
setup:
let procs = @[
FFIProcMeta(
procName: "timer_create",
libName: "timer",
kind: FFIKind.CTOR,
libTypeName: "Timer",
extraParams: @[],
returnTypeName: "Timer",
),
FFIProcMeta(
procName: "timer_destroy",
libName: "timer",
kind: FFIKind.DTOR,
libTypeName: "Timer",
extraParams: @[],
returnTypeName: "",
),
]
let types = @[FFITypeMeta(name: "TickEvent", fields: @[field("count", "int")])]
let events = @[
FFIEventMeta(
wireName: "on_tick",
nimProcName: "onTick",
libName: "timer",
payloadTypeName: "TickEvent",
)
]
let header = generateCLibHeader(procs, types, "timer", events)
test "a typed handler, box and trampoline are emitted per event":
check "TimerOnTickFn" in header
check "TimerOnTickBox" in header
check "timer_on_tick_trampoline(" in header
test "the registration API uses the wire name and snake-cased proc name":
check "timer_ctx_add_on_tick_listener(" in header
check "\"on_tick\"" in header
check "timer_ctx_remove_event_listener(" in header
test "the context tracks listeners only when events exist":
check "TimerCtxListener* listeners;" in header
suite "generateCLibHeader: no-event libraries stay lean":
test "a library without events has no listener bookkeeping":
let procs = @[
FFIProcMeta(
procName: "timer_create",
libName: "timer",
kind: FFIKind.CTOR,
libTypeName: "Timer",
extraParams: @[],
returnTypeName: "Timer",
)
]
let header = generateCLibHeader(procs, @[], "timer")
check "listeners_len" notin header
check "_add_event_listener" in header # raw ABI symbol is always declared
suite "shared headers: prelude and cbor split":
test "the prelude owns the leaf types and libc/TinyCBOR includes":
let prelude = generateCPreludeHeader()
check "#include <tinycbor/cbor.h>" in prelude
check "} NimFfiStr;" in prelude
check "nimffi_free_str" in prelude
test "the cbor header carries the leaf codecs and pulls in the prelude":
let cbor = generateCCborHeader()
check "#include \"nim_ffi_prelude.h\"" in cbor
check "nimffi_enc_str" in cbor
check "nimffi_decode_from_buf" in cbor
test "each generated file is independently include-guarded":
check "NIM_FFI_PRELUDE_H_INCLUDED" in generateCPreludeHeader()
check "NIM_FFI_CBOR_HELPERS_H_INCLUDED" in generateCCborHeader()
check "NIM_FFI_LIB_TIMER_H_INCLUDED" in generateCLibHeader(@[], @[], "timer")