diff --git a/CHANGELOG.md b/CHANGELOG.md index f6e3654..04e811d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 + (`.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 `_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.}` diff --git a/examples/echo/c_bindings/CMakeLists.txt b/examples/echo/c_bindings/CMakeLists.txt new file mode 100644 index 0000000..bdc4246 --- /dev/null +++ b/examples/echo/c_bindings/CMakeLists.txt @@ -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}" + "$" + COMMENT "Staging echo.dll next to echo_example.exe") + endif() +endif() diff --git a/examples/echo/c_bindings/echo.h b/examples/echo/c_bindings/echo.h new file mode 100644 index 0000000..d68f69d --- /dev/null +++ b/examples/echo/c_bindings/echo.h @@ -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 */ diff --git a/examples/echo/c_bindings/nim_ffi_cbor.h b/examples/echo/c_bindings/nim_ffi_cbor.h new file mode 100644 index 0000000..fd38ffd --- /dev/null +++ b/examples/echo/c_bindings/nim_ffi_cbor.h @@ -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 */ + diff --git a/examples/echo/c_bindings/nim_ffi_prelude.h b/examples/echo/c_bindings/nim_ffi_prelude.h new file mode 100644 index 0000000..2316dd8 --- /dev/null +++ b/examples/echo/c_bindings/nim_ffi_prelude.h @@ -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 _free_() 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 + * _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 +#include +#include +#include +#include +#include + +#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 */ + diff --git a/examples/echo/cpp_bindings/CMakeLists.txt b/examples/echo/cpp_bindings/CMakeLists.txt index 77dd7aa..c13bc14 100644 --- a/examples/echo/cpp_bindings/CMakeLists.txt +++ b/examples/echo/cpp_bindings/CMakeLists.txt @@ -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_` 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_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}") diff --git a/examples/timer/c_bindings/CMakeLists.txt b/examples/timer/c_bindings/CMakeLists.txt new file mode 100644 index 0000000..016a04c --- /dev/null +++ b/examples/timer/c_bindings/CMakeLists.txt @@ -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}" + "$" + COMMENT "Staging my_timer.dll next to my_timer_example.exe") + endif() +endif() diff --git a/examples/timer/c_bindings/README.md b/examples/timer/c_bindings/README.md new file mode 100644 index 0000000..9b42d2e --- /dev/null +++ b/examples/timer/c_bindings/README.md @@ -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. diff --git a/examples/timer/c_bindings/main.c b/examples/timer/c_bindings/main.c new file mode 100644 index 0000000..0c73bbd --- /dev/null +++ b/examples/timer/c_bindings/main.c @@ -0,0 +1,226 @@ +#include "my_timer.h" +#include +#include + +#if defined(__STDC_NO_ATOMICS__) +# error "C11 atomics required (or provide a mutex/condvar fallback)" +#endif +#include + +/* 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 +static void sleep_ms(unsigned ms) { Sleep(ms); } +#else +# include +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; +} diff --git a/examples/timer/c_bindings/my_timer.h b/examples/timer/c_bindings/my_timer.h new file mode 100644 index 0000000..119f614 --- /dev/null +++ b/examples/timer/c_bindings/my_timer.h @@ -0,0 +1,1180 @@ +#ifndef NIM_FFI_LIB_MY_TIMER_H_INCLUDED +#define NIM_FFI_LIB_MY_TIMER_H_INCLUDED +#include "nim_ffi_cbor.h" + +/* ============================================================ */ +/* Generated types (user-declared + per-proc request envelopes) */ +/* ============================================================ */ + +typedef struct { + NimFfiStr name; +} TimerConfig; +typedef struct { + NimFfiStr message; + int64_t delayMs; +} EchoRequest; +typedef struct { + NimFfiStr echoed; + NimFfiStr timerName; +} EchoResponse; +typedef struct { + EchoRequest* data; + size_t len; +} MyTimerSeq_EchoRequest; +typedef struct { + NimFfiStr* data; + size_t len; +} MyTimerSeq_Str; +typedef struct { + bool has_value; + NimFfiStr value; +} MyTimerOpt_Str; +typedef struct { + bool has_value; + int64_t value; +} MyTimerOpt_I64; +typedef struct { + MyTimerSeq_EchoRequest messages; + MyTimerSeq_Str tags; + MyTimerOpt_Str note; + MyTimerOpt_I64 retries; +} ComplexRequest; +typedef struct { + NimFfiStr summary; + int64_t itemCount; + bool hasNote; +} ComplexResponse; +typedef struct { + NimFfiStr message; + int64_t echoCount; +} EchoEvent; +typedef struct { + NimFfiStr name; + MyTimerSeq_Str payload; + int64_t priority; +} JobSpec; +typedef struct { + int64_t maxAttempts; + int64_t backoffMs; + MyTimerSeq_Str retryOn; +} RetryPolicy; +typedef struct { + int64_t startAtMs; + int64_t intervalMs; + MyTimerOpt_I64 jitter; +} ScheduleConfig; +typedef struct { + NimFfiStr jobId; + int64_t willRunCount; + int64_t firstRunAtMs; + int64_t effectiveBackoffMs; +} ScheduleResult; +typedef struct { + TimerConfig config; +} MyTimerCreateCtorReq; +typedef struct { + EchoRequest req; +} MyTimerEchoReq; +typedef struct { + char _nimffi_empty; /* C forbids empty structs */ +} MyTimerVersionReq; +typedef struct { + ComplexRequest req; +} MyTimerComplexReq; +typedef struct { + JobSpec job; + RetryPolicy retry; + ScheduleConfig schedule; +} MyTimerScheduleReq; + +static inline CborError my_timer_enc_TimerConfig( + CborEncoder* e, const TimerConfig* v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(e, &m, 1); + if (err) return err; + err = cbor_encode_text_stringz(&m, "name"); + if (err) return err; + err = nimffi_enc_str(&m, &v->name); + if (err) return err; + return cbor_encoder_close_container(e, &m); +} +static inline CborError my_timer_dec_TimerConfig( + CborValue* it, TimerConfig* out) { + if (!cbor_value_is_map(it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(it, "name", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = nimffi_dec_str(&field, &out->name); + if (err) return err; + return cbor_value_advance(it); +} +static inline void my_timer_free_TimerConfig(TimerConfig* v) { + if (!v) return; + nimffi_free_str(&v->name); +} +static inline CborError my_timer_enc_EchoRequest( + CborEncoder* e, const EchoRequest* v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(e, &m, 2); + if (err) return err; + err = cbor_encode_text_stringz(&m, "message"); + if (err) return err; + err = nimffi_enc_str(&m, &v->message); + if (err) return err; + err = cbor_encode_text_stringz(&m, "delayMs"); + if (err) return err; + err = nimffi_enc_i64(&m, &v->delayMs); + if (err) return err; + return cbor_encoder_close_container(e, &m); +} +static inline CborError my_timer_dec_EchoRequest( + CborValue* it, EchoRequest* out) { + if (!cbor_value_is_map(it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(it, "message", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = nimffi_dec_str(&field, &out->message); + if (err) return err; + err = cbor_value_map_find_value(it, "delayMs", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = nimffi_dec_i64(&field, &out->delayMs); + if (err) return err; + return cbor_value_advance(it); +} +static inline void my_timer_free_EchoRequest(EchoRequest* v) { + if (!v) return; + nimffi_free_str(&v->message); +} +static inline CborError my_timer_enc_EchoResponse( + CborEncoder* e, const EchoResponse* v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(e, &m, 2); + if (err) return err; + err = cbor_encode_text_stringz(&m, "echoed"); + if (err) return err; + err = nimffi_enc_str(&m, &v->echoed); + if (err) return err; + err = cbor_encode_text_stringz(&m, "timerName"); + if (err) return err; + err = nimffi_enc_str(&m, &v->timerName); + if (err) return err; + return cbor_encoder_close_container(e, &m); +} +static inline CborError my_timer_dec_EchoResponse( + CborValue* it, EchoResponse* out) { + if (!cbor_value_is_map(it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(it, "echoed", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = nimffi_dec_str(&field, &out->echoed); + if (err) return err; + err = cbor_value_map_find_value(it, "timerName", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = nimffi_dec_str(&field, &out->timerName); + if (err) return err; + return cbor_value_advance(it); +} +static inline void my_timer_free_EchoResponse(EchoResponse* v) { + if (!v) return; + nimffi_free_str(&v->echoed); + nimffi_free_str(&v->timerName); +} +static inline CborError my_timer_enc_MyTimerSeq_EchoRequest( + CborEncoder* e, const MyTimerSeq_EchoRequest* v) { + CborEncoder arr; + CborError err = cbor_encoder_create_array(e, &arr, v->len); + if (err) return err; + for (size_t i = 0; i < v->len; i++) { + err = my_timer_enc_EchoRequest(&arr, &v->data[i]); + if (err) return err; + } + return cbor_encoder_close_container(e, &arr); +} +static inline CborError my_timer_dec_MyTimerSeq_EchoRequest( + CborValue* it, MyTimerSeq_EchoRequest* out) { + if (!cbor_value_is_array(it)) return CborErrorImproperValue; + size_t len = 0; + CborError err = cbor_value_get_array_length(it, &len); + if (err) return err; + out->data = (EchoRequest*)calloc(len ? len : 1, sizeof(EchoRequest)); + if (!out->data) return CborErrorOutOfMemory; + out->len = len; + CborValue inner; + err = cbor_value_enter_container(it, &inner); + if (err) return err; + for (size_t i = 0; i < len; i++) { + err = my_timer_dec_EchoRequest(&inner, &out->data[i]); + if (err) return err; + } + return cbor_value_leave_container(it, &inner); +} +static inline void my_timer_free_MyTimerSeq_EchoRequest(MyTimerSeq_EchoRequest* v) { + if (!v || !v->data) return; + for (size_t i = 0; i < v->len; i++) my_timer_free_EchoRequest(&v->data[i]); + free(v->data); + v->data = NULL; + v->len = 0; +} +static inline CborError my_timer_enc_MyTimerSeq_Str( + CborEncoder* e, const MyTimerSeq_Str* v) { + CborEncoder arr; + CborError err = cbor_encoder_create_array(e, &arr, v->len); + if (err) return err; + for (size_t i = 0; i < v->len; i++) { + err = nimffi_enc_str(&arr, &v->data[i]); + if (err) return err; + } + return cbor_encoder_close_container(e, &arr); +} +static inline CborError my_timer_dec_MyTimerSeq_Str( + CborValue* it, MyTimerSeq_Str* out) { + if (!cbor_value_is_array(it)) return CborErrorImproperValue; + size_t len = 0; + CborError err = cbor_value_get_array_length(it, &len); + if (err) return err; + out->data = (NimFfiStr*)calloc(len ? len : 1, sizeof(NimFfiStr)); + if (!out->data) return CborErrorOutOfMemory; + out->len = len; + CborValue inner; + err = cbor_value_enter_container(it, &inner); + if (err) return err; + for (size_t i = 0; i < len; i++) { + err = nimffi_dec_str(&inner, &out->data[i]); + if (err) return err; + } + return cbor_value_leave_container(it, &inner); +} +static inline void my_timer_free_MyTimerSeq_Str(MyTimerSeq_Str* v) { + if (!v || !v->data) return; + for (size_t i = 0; i < v->len; i++) nimffi_free_str(&v->data[i]); + free(v->data); + v->data = NULL; + v->len = 0; +} +static inline CborError my_timer_enc_MyTimerOpt_Str( + CborEncoder* e, const MyTimerOpt_Str* v) { + if (!v->has_value) return cbor_encode_null(e); + return nimffi_enc_str(e, &v->value); +} +static inline CborError my_timer_dec_MyTimerOpt_Str( + CborValue* it, MyTimerOpt_Str* out) { + if (cbor_value_is_null(it)) { + out->has_value = false; + memset(&out->value, 0, sizeof(out->value)); + return cbor_value_advance(it); + } + out->has_value = true; + return nimffi_dec_str(it, &out->value); +} +static inline void my_timer_free_MyTimerOpt_Str(MyTimerOpt_Str* v) { + if (!v || !v->has_value) return; + nimffi_free_str(&v->value); + v->has_value = false; +} +static inline CborError my_timer_enc_MyTimerOpt_I64( + CborEncoder* e, const MyTimerOpt_I64* v) { + if (!v->has_value) return cbor_encode_null(e); + return nimffi_enc_i64(e, &v->value); +} +static inline CborError my_timer_dec_MyTimerOpt_I64( + CborValue* it, MyTimerOpt_I64* out) { + if (cbor_value_is_null(it)) { + out->has_value = false; + memset(&out->value, 0, sizeof(out->value)); + return cbor_value_advance(it); + } + out->has_value = true; + return nimffi_dec_i64(it, &out->value); +} +static inline CborError my_timer_enc_ComplexRequest( + CborEncoder* e, const ComplexRequest* v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(e, &m, 4); + if (err) return err; + err = cbor_encode_text_stringz(&m, "messages"); + if (err) return err; + err = my_timer_enc_MyTimerSeq_EchoRequest(&m, &v->messages); + if (err) return err; + err = cbor_encode_text_stringz(&m, "tags"); + if (err) return err; + err = my_timer_enc_MyTimerSeq_Str(&m, &v->tags); + if (err) return err; + err = cbor_encode_text_stringz(&m, "note"); + if (err) return err; + err = my_timer_enc_MyTimerOpt_Str(&m, &v->note); + if (err) return err; + err = cbor_encode_text_stringz(&m, "retries"); + if (err) return err; + err = my_timer_enc_MyTimerOpt_I64(&m, &v->retries); + if (err) return err; + return cbor_encoder_close_container(e, &m); +} +static inline CborError my_timer_dec_ComplexRequest( + CborValue* it, ComplexRequest* out) { + if (!cbor_value_is_map(it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(it, "messages", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = my_timer_dec_MyTimerSeq_EchoRequest(&field, &out->messages); + if (err) return err; + err = cbor_value_map_find_value(it, "tags", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = my_timer_dec_MyTimerSeq_Str(&field, &out->tags); + if (err) return err; + err = cbor_value_map_find_value(it, "note", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = my_timer_dec_MyTimerOpt_Str(&field, &out->note); + if (err) return err; + err = cbor_value_map_find_value(it, "retries", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = my_timer_dec_MyTimerOpt_I64(&field, &out->retries); + if (err) return err; + return cbor_value_advance(it); +} +static inline void my_timer_free_ComplexRequest(ComplexRequest* v) { + if (!v) return; + my_timer_free_MyTimerSeq_EchoRequest(&v->messages); + my_timer_free_MyTimerSeq_Str(&v->tags); + my_timer_free_MyTimerOpt_Str(&v->note); +} +static inline CborError my_timer_enc_ComplexResponse( + CborEncoder* e, const ComplexResponse* v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(e, &m, 3); + if (err) return err; + err = cbor_encode_text_stringz(&m, "summary"); + if (err) return err; + err = nimffi_enc_str(&m, &v->summary); + if (err) return err; + err = cbor_encode_text_stringz(&m, "itemCount"); + if (err) return err; + err = nimffi_enc_i64(&m, &v->itemCount); + if (err) return err; + err = cbor_encode_text_stringz(&m, "hasNote"); + if (err) return err; + err = nimffi_enc_bool(&m, &v->hasNote); + if (err) return err; + return cbor_encoder_close_container(e, &m); +} +static inline CborError my_timer_dec_ComplexResponse( + CborValue* it, ComplexResponse* out) { + if (!cbor_value_is_map(it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(it, "summary", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = nimffi_dec_str(&field, &out->summary); + if (err) return err; + err = cbor_value_map_find_value(it, "itemCount", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = nimffi_dec_i64(&field, &out->itemCount); + if (err) return err; + err = cbor_value_map_find_value(it, "hasNote", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = nimffi_dec_bool(&field, &out->hasNote); + if (err) return err; + return cbor_value_advance(it); +} +static inline void my_timer_free_ComplexResponse(ComplexResponse* v) { + if (!v) return; + nimffi_free_str(&v->summary); +} +static inline CborError my_timer_enc_EchoEvent( + CborEncoder* e, const EchoEvent* v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(e, &m, 2); + if (err) return err; + err = cbor_encode_text_stringz(&m, "message"); + if (err) return err; + err = nimffi_enc_str(&m, &v->message); + if (err) return err; + err = cbor_encode_text_stringz(&m, "echoCount"); + if (err) return err; + err = nimffi_enc_i64(&m, &v->echoCount); + if (err) return err; + return cbor_encoder_close_container(e, &m); +} +static inline CborError my_timer_dec_EchoEvent( + CborValue* it, EchoEvent* out) { + if (!cbor_value_is_map(it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(it, "message", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = nimffi_dec_str(&field, &out->message); + if (err) return err; + err = cbor_value_map_find_value(it, "echoCount", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = nimffi_dec_i64(&field, &out->echoCount); + if (err) return err; + return cbor_value_advance(it); +} +static inline void my_timer_free_EchoEvent(EchoEvent* v) { + if (!v) return; + nimffi_free_str(&v->message); +} +static inline CborError my_timer_enc_JobSpec( + CborEncoder* e, const JobSpec* v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(e, &m, 3); + if (err) return err; + err = cbor_encode_text_stringz(&m, "name"); + if (err) return err; + err = nimffi_enc_str(&m, &v->name); + if (err) return err; + err = cbor_encode_text_stringz(&m, "payload"); + if (err) return err; + err = my_timer_enc_MyTimerSeq_Str(&m, &v->payload); + if (err) return err; + err = cbor_encode_text_stringz(&m, "priority"); + if (err) return err; + err = nimffi_enc_i64(&m, &v->priority); + if (err) return err; + return cbor_encoder_close_container(e, &m); +} +static inline CborError my_timer_dec_JobSpec( + CborValue* it, JobSpec* out) { + if (!cbor_value_is_map(it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(it, "name", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = nimffi_dec_str(&field, &out->name); + if (err) return err; + err = cbor_value_map_find_value(it, "payload", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = my_timer_dec_MyTimerSeq_Str(&field, &out->payload); + if (err) return err; + err = cbor_value_map_find_value(it, "priority", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = nimffi_dec_i64(&field, &out->priority); + if (err) return err; + return cbor_value_advance(it); +} +static inline void my_timer_free_JobSpec(JobSpec* v) { + if (!v) return; + nimffi_free_str(&v->name); + my_timer_free_MyTimerSeq_Str(&v->payload); +} +static inline CborError my_timer_enc_RetryPolicy( + CborEncoder* e, const RetryPolicy* v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(e, &m, 3); + if (err) return err; + err = cbor_encode_text_stringz(&m, "maxAttempts"); + if (err) return err; + err = nimffi_enc_i64(&m, &v->maxAttempts); + if (err) return err; + err = cbor_encode_text_stringz(&m, "backoffMs"); + if (err) return err; + err = nimffi_enc_i64(&m, &v->backoffMs); + if (err) return err; + err = cbor_encode_text_stringz(&m, "retryOn"); + if (err) return err; + err = my_timer_enc_MyTimerSeq_Str(&m, &v->retryOn); + if (err) return err; + return cbor_encoder_close_container(e, &m); +} +static inline CborError my_timer_dec_RetryPolicy( + CborValue* it, RetryPolicy* out) { + if (!cbor_value_is_map(it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(it, "maxAttempts", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = nimffi_dec_i64(&field, &out->maxAttempts); + if (err) return err; + err = cbor_value_map_find_value(it, "backoffMs", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = nimffi_dec_i64(&field, &out->backoffMs); + if (err) return err; + err = cbor_value_map_find_value(it, "retryOn", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = my_timer_dec_MyTimerSeq_Str(&field, &out->retryOn); + if (err) return err; + return cbor_value_advance(it); +} +static inline void my_timer_free_RetryPolicy(RetryPolicy* v) { + if (!v) return; + my_timer_free_MyTimerSeq_Str(&v->retryOn); +} +static inline CborError my_timer_enc_ScheduleConfig( + CborEncoder* e, const ScheduleConfig* v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(e, &m, 3); + if (err) return err; + err = cbor_encode_text_stringz(&m, "startAtMs"); + if (err) return err; + err = nimffi_enc_i64(&m, &v->startAtMs); + if (err) return err; + err = cbor_encode_text_stringz(&m, "intervalMs"); + if (err) return err; + err = nimffi_enc_i64(&m, &v->intervalMs); + if (err) return err; + err = cbor_encode_text_stringz(&m, "jitter"); + if (err) return err; + err = my_timer_enc_MyTimerOpt_I64(&m, &v->jitter); + if (err) return err; + return cbor_encoder_close_container(e, &m); +} +static inline CborError my_timer_dec_ScheduleConfig( + CborValue* it, ScheduleConfig* out) { + if (!cbor_value_is_map(it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(it, "startAtMs", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = nimffi_dec_i64(&field, &out->startAtMs); + if (err) return err; + err = cbor_value_map_find_value(it, "intervalMs", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = nimffi_dec_i64(&field, &out->intervalMs); + if (err) return err; + err = cbor_value_map_find_value(it, "jitter", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = my_timer_dec_MyTimerOpt_I64(&field, &out->jitter); + if (err) return err; + return cbor_value_advance(it); +} +static inline CborError my_timer_enc_ScheduleResult( + CborEncoder* e, const ScheduleResult* v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(e, &m, 4); + if (err) return err; + err = cbor_encode_text_stringz(&m, "jobId"); + if (err) return err; + err = nimffi_enc_str(&m, &v->jobId); + if (err) return err; + err = cbor_encode_text_stringz(&m, "willRunCount"); + if (err) return err; + err = nimffi_enc_i64(&m, &v->willRunCount); + if (err) return err; + err = cbor_encode_text_stringz(&m, "firstRunAtMs"); + if (err) return err; + err = nimffi_enc_i64(&m, &v->firstRunAtMs); + if (err) return err; + err = cbor_encode_text_stringz(&m, "effectiveBackoffMs"); + if (err) return err; + err = nimffi_enc_i64(&m, &v->effectiveBackoffMs); + if (err) return err; + return cbor_encoder_close_container(e, &m); +} +static inline CborError my_timer_dec_ScheduleResult( + CborValue* it, ScheduleResult* out) { + if (!cbor_value_is_map(it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(it, "jobId", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = nimffi_dec_str(&field, &out->jobId); + if (err) return err; + err = cbor_value_map_find_value(it, "willRunCount", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = nimffi_dec_i64(&field, &out->willRunCount); + if (err) return err; + err = cbor_value_map_find_value(it, "firstRunAtMs", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = nimffi_dec_i64(&field, &out->firstRunAtMs); + if (err) return err; + err = cbor_value_map_find_value(it, "effectiveBackoffMs", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = nimffi_dec_i64(&field, &out->effectiveBackoffMs); + if (err) return err; + return cbor_value_advance(it); +} +static inline void my_timer_free_ScheduleResult(ScheduleResult* v) { + if (!v) return; + nimffi_free_str(&v->jobId); +} +static inline CborError my_timer_enc_MyTimerCreateCtorReq( + CborEncoder* e, const MyTimerCreateCtorReq* 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 = my_timer_enc_TimerConfig(&m, &v->config); + if (err) return err; + return cbor_encoder_close_container(e, &m); +} +static inline CborError my_timer_dec_MyTimerCreateCtorReq( + CborValue* it, MyTimerCreateCtorReq* 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 = my_timer_dec_TimerConfig(&field, &out->config); + if (err) return err; + return cbor_value_advance(it); +} +static inline void my_timer_free_MyTimerCreateCtorReq(MyTimerCreateCtorReq* v) { + if (!v) return; + my_timer_free_TimerConfig(&v->config); +} +static inline CborError my_timer_enc_MyTimerEchoReq( + CborEncoder* e, const MyTimerEchoReq* 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 = my_timer_enc_EchoRequest(&m, &v->req); + if (err) return err; + return cbor_encoder_close_container(e, &m); +} +static inline CborError my_timer_dec_MyTimerEchoReq( + CborValue* it, MyTimerEchoReq* 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 = my_timer_dec_EchoRequest(&field, &out->req); + if (err) return err; + return cbor_value_advance(it); +} +static inline void my_timer_free_MyTimerEchoReq(MyTimerEchoReq* v) { + if (!v) return; + my_timer_free_EchoRequest(&v->req); +} +static inline CborError my_timer_enc_MyTimerVersionReq( + CborEncoder* e, const MyTimerVersionReq* v) { + (void)v; + CborEncoder m; + CborError err = cbor_encoder_create_map(e, &m, 0); + if (err) return err; + return cbor_encoder_close_container(e, &m); +} +static inline CborError my_timer_dec_MyTimerVersionReq( + CborValue* it, MyTimerVersionReq* out) { + if (!cbor_value_is_map(it)) return CborErrorImproperValue; + (void)out; + return cbor_value_advance(it); +} +static inline CborError my_timer_enc_MyTimerComplexReq( + CborEncoder* e, const MyTimerComplexReq* v) { + CborEncoder m; + 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 = my_timer_enc_ComplexRequest(&m, &v->req); + if (err) return err; + return cbor_encoder_close_container(e, &m); +} +static inline CborError my_timer_dec_MyTimerComplexReq( + CborValue* it, MyTimerComplexReq* 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 = my_timer_dec_ComplexRequest(&field, &out->req); + if (err) return err; + return cbor_value_advance(it); +} +static inline void my_timer_free_MyTimerComplexReq(MyTimerComplexReq* v) { + if (!v) return; + my_timer_free_ComplexRequest(&v->req); +} +static inline CborError my_timer_enc_MyTimerScheduleReq( + CborEncoder* e, const MyTimerScheduleReq* v) { + CborEncoder m; + CborError err = cbor_encoder_create_map(e, &m, 3); + if (err) return err; + err = cbor_encode_text_stringz(&m, "job"); + if (err) return err; + err = my_timer_enc_JobSpec(&m, &v->job); + if (err) return err; + err = cbor_encode_text_stringz(&m, "retry"); + if (err) return err; + err = my_timer_enc_RetryPolicy(&m, &v->retry); + if (err) return err; + err = cbor_encode_text_stringz(&m, "schedule"); + if (err) return err; + err = my_timer_enc_ScheduleConfig(&m, &v->schedule); + if (err) return err; + return cbor_encoder_close_container(e, &m); +} +static inline CborError my_timer_dec_MyTimerScheduleReq( + CborValue* it, MyTimerScheduleReq* out) { + if (!cbor_value_is_map(it)) return CborErrorImproperValue; + CborValue field; + CborError err; + err = cbor_value_map_find_value(it, "job", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = my_timer_dec_JobSpec(&field, &out->job); + if (err) return err; + err = cbor_value_map_find_value(it, "retry", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = my_timer_dec_RetryPolicy(&field, &out->retry); + if (err) return err; + err = cbor_value_map_find_value(it, "schedule", &field); + if (err) return err; + if (!cbor_value_is_valid(&field)) return CborErrorImproperValue; + err = my_timer_dec_ScheduleConfig(&field, &out->schedule); + if (err) return err; + return cbor_value_advance(it); +} +static inline void my_timer_free_MyTimerScheduleReq(MyTimerScheduleReq* v) { + if (!v) return; + my_timer_free_JobSpec(&v->job); + my_timer_free_RetryPolicy(&v->retry); +} + +/* ============================================================ */ +/* C ABI declarations (symbols exported by the Nim dylib) */ +/* ============================================================ */ +#ifdef __cplusplus +extern "C" { +#endif + +void* my_timer_create(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback callback, void* user_data); +int my_timer_echo(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); +int my_timer_version(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); +int my_timer_complex(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); +int my_timer_schedule(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); +int my_timer_destroy(void* ctx); +uint64_t my_timer_add_event_listener(void* ctx, const char* event_name, FFICallback callback, void* user_data); +int my_timer_remove_event_listener(void* ctx, uint64_t listener_id); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +/* CBOR buffer adapters (typed codec → void* driver signature) */ +static inline CborError my_timer_encv_MyTimerCreateCtorReq(CborEncoder* e, const void* v) { return my_timer_enc_MyTimerCreateCtorReq(e, (const MyTimerCreateCtorReq*)v); } +static inline CborError my_timer_encv_MyTimerEchoReq(CborEncoder* e, const void* v) { return my_timer_enc_MyTimerEchoReq(e, (const MyTimerEchoReq*)v); } +static inline CborError my_timer_encv_MyTimerVersionReq(CborEncoder* e, const void* v) { return my_timer_enc_MyTimerVersionReq(e, (const MyTimerVersionReq*)v); } +static inline CborError my_timer_encv_MyTimerComplexReq(CborEncoder* e, const void* v) { return my_timer_enc_MyTimerComplexReq(e, (const MyTimerComplexReq*)v); } +static inline CborError my_timer_encv_MyTimerScheduleReq(CborEncoder* e, const void* v) { return my_timer_enc_MyTimerScheduleReq(e, (const MyTimerScheduleReq*)v); } +static inline CborError my_timer_decv_EchoResponse(CborValue* it, void* v) { return my_timer_dec_EchoResponse(it, (EchoResponse*)v); } +static inline CborError my_timer_decv_Str(CborValue* it, void* v) { return nimffi_dec_str(it, (NimFfiStr*)v); } +static inline CborError my_timer_decv_ComplexResponse(CborValue* it, void* v) { return my_timer_dec_ComplexResponse(it, (ComplexResponse*)v); } +static inline CborError my_timer_decv_ScheduleResult(CborValue* it, void* v) { return my_timer_dec_ScheduleResult(it, (ScheduleResult*)v); } + +/* Event listener machinery */ +typedef void (*MyTimerOnEchoFiredFn)(const EchoEvent* evt, void* user_data); +typedef struct { MyTimerOnEchoFiredFn fn; void* user_data; } MyTimerOnEchoFiredBox; +static void my_timer_on_echo_fired_trampoline(int ret, const char* msg, size_t len, void* ud) { + if (!ud || ret != 0 || !msg || len == 0) return; + MyTimerOnEchoFiredBox* box = (MyTimerOnEchoFiredBox*)ud; + if (!box->fn) return; + CborParser parser; + CborValue it; + if (cbor_parser_init((const uint8_t*)msg, len, 0, &parser, &it) != CborNoError) return; + if (!cbor_value_is_map(&it)) return; + CborValue payloadField; + if (cbor_value_map_find_value(&it, "payload", &payloadField) != CborNoError) return; + EchoEvent payload; + memset(&payload, 0, sizeof(payload)); + if (my_timer_dec_EchoEvent(&payloadField, &payload) != CborNoError) return; + box->fn(&payload, box->user_data); + my_timer_free_EchoEvent(&payload); +} + +/* ============================================================ */ +/* High-level context wrapper */ +/* ============================================================ */ +typedef struct { + uint64_t id; + void* box; +} MyTimerCtxListener; + +typedef struct { + void* ptr; + MyTimerCtxListener* listeners; + size_t listeners_len; + size_t listeners_cap; +} MyTimerCtx; + +typedef void (*MyTimerCreateFn)(int err_code, MyTimerCtx* ctx, const char* err_msg, void* user_data); +typedef struct { MyTimerCreateFn fn; void* user_data; } MyTimerCreateBox; +static void my_timer_create_trampoline(int ret, const char* msg, size_t len, void* ud) { + MyTimerCreateBox* box = (MyTimerCreateBox*)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(my_timer_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; + } + MyTimerCtx* ctx = (MyTimerCtx*)calloc(1, sizeof(MyTimerCtx)); + 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 my_timer_ctx_create(const TimerConfig* config, MyTimerCreateFn on_created, void* user_data) { + MyTimerCreateCtorReq 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(my_timer_encv_MyTimerCreateCtorReq, &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; + } + MyTimerCreateBox* box = (MyTimerCreateBox*)malloc(sizeof(MyTimerCreateBox)); + 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)my_timer_create(req_buf, req_len, my_timer_create_trampoline, box); + free(req_buf); + return 0; +} + +static inline void my_timer_ctx_destroy(MyTimerCtx* ctx) { + if (!ctx) return; + if (ctx->ptr) { my_timer_destroy(ctx->ptr); ctx->ptr = NULL; } + for (size_t i = 0; i < ctx->listeners_len; i++) free(ctx->listeners[i].box); + free(ctx->listeners); + free(ctx); +} + +static inline uint64_t my_timer_ctx_add_on_echo_fired_listener(MyTimerCtx* ctx, MyTimerOnEchoFiredFn fn, void* user_data) { + MyTimerOnEchoFiredBox* box = (MyTimerOnEchoFiredBox*)malloc(sizeof(MyTimerOnEchoFiredBox)); + if (!box) return 0; + box->fn = fn; + box->user_data = user_data; + uint64_t id = my_timer_add_event_listener(ctx->ptr, "on_echo_fired", my_timer_on_echo_fired_trampoline, box); + if (id == 0) { free(box); return 0; } + if (ctx->listeners_len == ctx->listeners_cap) { + size_t ncap = ctx->listeners_cap ? ctx->listeners_cap * 2 : 4; + MyTimerCtxListener* grown = (MyTimerCtxListener*)realloc(ctx->listeners, ncap * sizeof(MyTimerCtxListener)); + if (!grown) { my_timer_remove_event_listener(ctx->ptr, id); free(box); return 0; } + ctx->listeners = grown; + ctx->listeners_cap = ncap; + } + ctx->listeners[ctx->listeners_len].id = id; + ctx->listeners[ctx->listeners_len].box = box; + ctx->listeners_len++; + return id; +} + +static inline bool my_timer_ctx_remove_event_listener(MyTimerCtx* ctx, uint64_t id) { + if (id == 0) return false; + int rc = my_timer_remove_event_listener(ctx->ptr, id); + for (size_t i = 0; i < ctx->listeners_len; i++) { + if (ctx->listeners[i].id == id) { + free(ctx->listeners[i].box); + ctx->listeners[i] = ctx->listeners[ctx->listeners_len - 1]; + ctx->listeners_len--; + break; + } + } + return rc == 0; +} + +typedef void (*MyTimerEchoReplyFn)(int err_code, const EchoResponse* reply, const char* err_msg, void* user_data); +typedef struct { MyTimerEchoReplyFn fn; void* user_data; } MyTimerEchoCallBox; +static void my_timer_echo_reply_trampoline(int ret, const char* msg, size_t len, void* ud) { + MyTimerEchoCallBox* box = (MyTimerEchoCallBox*)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; + EchoResponse out; + memset(&out, 0, sizeof(out)); + int dec = nimffi_decode_from_buf(my_timer_decv_EchoResponse, (const uint8_t*)msg, len, &out, &err); + if (dec != 0) { + box->fn(-1, NULL, err ? err : "decode failed", box->user_data); + free(err); + my_timer_free_EchoResponse(&out); + free(box); + return; + } + box->fn(NIMFFI_RET_OK, &out, NULL, box->user_data); + my_timer_free_EchoResponse(&out); + free(box); +} +static inline int my_timer_ctx_echo(const MyTimerCtx* ctx, const EchoRequest* req, MyTimerEchoReplyFn on_reply, void* user_data) { + MyTimerEchoReq ffi_req; + memset(&ffi_req, 0, sizeof(ffi_req)); + ffi_req.req = *req; + uint8_t* req_buf = NULL; + size_t req_len = 0; + char* err = NULL; + if (nimffi_encode_to_buf(my_timer_encv_MyTimerEchoReq, &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; + } + MyTimerEchoCallBox* box = (MyTimerEchoCallBox*)malloc(sizeof(MyTimerEchoCallBox)); + if (!box) { + free(req_buf); + if (on_reply) on_reply(-1, NULL, "out of memory", user_data); + return -1; + } + box->fn = on_reply; + box->user_data = user_data; + int ret = my_timer_echo(ctx->ptr, my_timer_echo_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 (*MyTimerVersionReplyFn)(int err_code, const NimFfiStr* reply, const char* err_msg, void* user_data); +typedef struct { MyTimerVersionReplyFn fn; void* user_data; } MyTimerVersionCallBox; +static void my_timer_version_reply_trampoline(int ret, const char* msg, size_t len, void* ud) { + MyTimerVersionCallBox* box = (MyTimerVersionCallBox*)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(my_timer_decv_Str, (const uint8_t*)msg, len, &out, &err); + if (dec != 0) { + box->fn(-1, NULL, err ? err : "decode failed", box->user_data); + free(err); + nimffi_free_str(&out); + free(box); + return; + } + box->fn(NIMFFI_RET_OK, &out, NULL, box->user_data); + nimffi_free_str(&out); + free(box); +} +static inline int my_timer_ctx_version(const MyTimerCtx* ctx, MyTimerVersionReplyFn on_reply, void* user_data) { + MyTimerVersionReq ffi_req; + memset(&ffi_req, 0, sizeof(ffi_req)); + uint8_t* req_buf = NULL; + size_t req_len = 0; + char* err = NULL; + if (nimffi_encode_to_buf(my_timer_encv_MyTimerVersionReq, &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; + } + MyTimerVersionCallBox* box = (MyTimerVersionCallBox*)malloc(sizeof(MyTimerVersionCallBox)); + if (!box) { + free(req_buf); + if (on_reply) on_reply(-1, NULL, "out of memory", user_data); + return -1; + } + box->fn = on_reply; + box->user_data = user_data; + int ret = my_timer_version(ctx->ptr, my_timer_version_reply_trampoline, box, req_buf, req_len); + free(req_buf); + if (ret == NIMFFI_RET_MISSING_CALLBACK) { + if (on_reply) on_reply(-1, NULL, "RET_MISSING_CALLBACK (internal error)", user_data); + free(box); + return -1; + } + return 0; +} + +typedef void (*MyTimerComplexReplyFn)(int err_code, const ComplexResponse* reply, const char* err_msg, void* user_data); +typedef struct { MyTimerComplexReplyFn fn; void* user_data; } MyTimerComplexCallBox; +static void my_timer_complex_reply_trampoline(int ret, const char* msg, size_t len, void* ud) { + MyTimerComplexCallBox* box = (MyTimerComplexCallBox*)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; + ComplexResponse out; + memset(&out, 0, sizeof(out)); + int dec = nimffi_decode_from_buf(my_timer_decv_ComplexResponse, (const uint8_t*)msg, len, &out, &err); + if (dec != 0) { + box->fn(-1, NULL, err ? err : "decode failed", box->user_data); + free(err); + my_timer_free_ComplexResponse(&out); + free(box); + return; + } + box->fn(NIMFFI_RET_OK, &out, NULL, box->user_data); + my_timer_free_ComplexResponse(&out); + free(box); +} +static inline int my_timer_ctx_complex(const MyTimerCtx* ctx, const ComplexRequest* req, MyTimerComplexReplyFn on_reply, void* user_data) { + MyTimerComplexReq 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(my_timer_encv_MyTimerComplexReq, &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; + } + MyTimerComplexCallBox* box = (MyTimerComplexCallBox*)malloc(sizeof(MyTimerComplexCallBox)); + if (!box) { + free(req_buf); + if (on_reply) on_reply(-1, NULL, "out of memory", user_data); + return -1; + } + box->fn = on_reply; + box->user_data = user_data; + int ret = my_timer_complex(ctx->ptr, my_timer_complex_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 (*MyTimerScheduleReplyFn)(int err_code, const ScheduleResult* reply, const char* err_msg, void* user_data); +typedef struct { MyTimerScheduleReplyFn fn; void* user_data; } MyTimerScheduleCallBox; +static void my_timer_schedule_reply_trampoline(int ret, const char* msg, size_t len, void* ud) { + MyTimerScheduleCallBox* box = (MyTimerScheduleCallBox*)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; + ScheduleResult out; + memset(&out, 0, sizeof(out)); + int dec = nimffi_decode_from_buf(my_timer_decv_ScheduleResult, (const uint8_t*)msg, len, &out, &err); + if (dec != 0) { + box->fn(-1, NULL, err ? err : "decode failed", box->user_data); + free(err); + my_timer_free_ScheduleResult(&out); + free(box); + return; + } + box->fn(NIMFFI_RET_OK, &out, NULL, box->user_data); + my_timer_free_ScheduleResult(&out); + free(box); +} +static inline int my_timer_ctx_schedule(const MyTimerCtx* ctx, const JobSpec* job, const RetryPolicy* retry, const ScheduleConfig* schedule, MyTimerScheduleReplyFn on_reply, void* user_data) { + MyTimerScheduleReq ffi_req; + memset(&ffi_req, 0, sizeof(ffi_req)); + ffi_req.job = *job; + ffi_req.retry = *retry; + ffi_req.schedule = *schedule; + uint8_t* req_buf = NULL; + size_t req_len = 0; + char* err = NULL; + if (nimffi_encode_to_buf(my_timer_encv_MyTimerScheduleReq, &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; + } + MyTimerScheduleCallBox* box = (MyTimerScheduleCallBox*)malloc(sizeof(MyTimerScheduleCallBox)); + if (!box) { + free(req_buf); + if (on_reply) on_reply(-1, NULL, "out of memory", user_data); + return -1; + } + box->fn = on_reply; + box->user_data = user_data; + int ret = my_timer_schedule(ctx->ptr, my_timer_schedule_reply_trampoline, box, req_buf, req_len); + free(req_buf); + if (ret == NIMFFI_RET_MISSING_CALLBACK) { + if (on_reply) on_reply(-1, NULL, "RET_MISSING_CALLBACK (internal error)", user_data); + free(box); + return -1; + } + return 0; +} + +#endif /* NIM_FFI_LIB_MY_TIMER_H_INCLUDED */ diff --git a/examples/timer/c_bindings/nim_ffi_cbor.h b/examples/timer/c_bindings/nim_ffi_cbor.h new file mode 100644 index 0000000..fd38ffd --- /dev/null +++ b/examples/timer/c_bindings/nim_ffi_cbor.h @@ -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 */ + diff --git a/examples/timer/c_bindings/nim_ffi_prelude.h b/examples/timer/c_bindings/nim_ffi_prelude.h new file mode 100644 index 0000000..2316dd8 --- /dev/null +++ b/examples/timer/c_bindings/nim_ffi_prelude.h @@ -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 _free_() 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 + * _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 +#include +#include +#include +#include +#include + +#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 */ + diff --git a/examples/timer/cpp_bindings/CMakeLists.txt b/examples/timer/cpp_bindings/CMakeLists.txt index 162d844..5d0f904 100644 --- a/examples/timer/cpp_bindings/CMakeLists.txt +++ b/examples/timer/cpp_bindings/CMakeLists.txt @@ -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_` 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_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}") diff --git a/examples/timer/timer.nimble b/examples/timer/timer.nimble index 9743423..9b78c90 100644 --- a/examples/timer/timer.nimble +++ b/examples/timer/timer.nimble @@ -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" diff --git a/ffi.nimble b/ffi.nimble index da5f40f..88ab85b 100644 --- a/ffi.nimble +++ b/ffi.nimble @@ -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" diff --git a/ffi/codegen/c.nim b/ffi/codegen/c.nim new file mode 100644 index 0000000..57ef9ca --- /dev/null +++ b/ffi/codegen/c.nim @@ -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 `.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 `.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)) diff --git a/ffi/codegen/c_cpp_common.nim b/ffi/codegen/c_cpp_common.nim new file mode 100644 index 0000000..f6357f5 --- /dev/null +++ b/ffi/codegen/c_cpp_common.nim @@ -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 `_` 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: `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) diff --git a/ffi/codegen/cpp.nim b/ffi/codegen/cpp.nim index 65bb325..8f0b382 100644 --- a/ffi/codegen/cpp.nim +++ b/ffi/codegen/cpp.nim @@ -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 diff --git a/ffi/codegen/templates/c/CMakeLists.txt.tpl b/ffi/codegen/templates/c/CMakeLists.txt.tpl new file mode 100644 index 0000000..091a575 --- /dev/null +++ b/ffi/codegen/templates/c/CMakeLists.txt.tpl @@ -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}" + "$" + COMMENT "Staging {{LIB}}.dll next to {{LIB}}_example.exe") + endif() +endif() diff --git a/ffi/codegen/templates/c/cbor_helpers.h.tpl b/ffi/codegen/templates/c/cbor_helpers.h.tpl new file mode 100644 index 0000000..a0c463a --- /dev/null +++ b/ffi/codegen/templates/c/cbor_helpers.h.tpl @@ -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 */ diff --git a/ffi/codegen/templates/c/header_prelude.h.tpl b/ffi/codegen/templates/c/header_prelude.h.tpl new file mode 100644 index 0000000..cc04549 --- /dev/null +++ b/ffi/codegen/templates/c/header_prelude.h.tpl @@ -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 _free_() 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 + * _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 +#include +#include +#include +#include +#include + +#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 */ diff --git a/ffi/codegen/templates/cpp/CMakeLists.txt.tpl b/ffi/codegen/templates/cpp/CMakeLists.txt.tpl index b204d4e..e8416d6 100644 --- a/ffi/codegen/templates/cpp/CMakeLists.txt.tpl +++ b/ffi/codegen/templates/cpp/CMakeLists.txt.tpl @@ -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_` 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_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}") diff --git a/ffi/codegen/templates/nim_ffi_lib.cmake b/ffi/codegen/templates/nim_ffi_lib.cmake new file mode 100644 index 0000000..a9037c7 --- /dev/null +++ b/ffi/codegen/templates/nim_ffi_lib.cmake @@ -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_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() diff --git a/ffi/internal/ffi_macro.nim b/ffi/internal/ffi_macro.nim index 1e33c59..0d3b133 100644 --- a/ffi/internal/ffi_macro.nim +++ b/ffi/internal/ffi_macro.nim @@ -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() diff --git a/tests/e2e/c/CMakeLists.txt b/tests/e2e/c/CMakeLists.txt new file mode 100644 index 0000000..eaf5fb6 --- /dev/null +++ b/tests/e2e/c/CMakeLists.txt @@ -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}" + "$" + 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() diff --git a/tests/e2e/c/README.md b/tests/e2e/c/README.md new file mode 100644 index 0000000..fde5007 --- /dev/null +++ b/tests/e2e/c/README.md @@ -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. diff --git a/tests/e2e/c/lsan.supp b/tests/e2e/c/lsan.supp new file mode 100644 index 0000000..94add2f --- /dev/null +++ b/tests/e2e/c/lsan.supp @@ -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 diff --git a/tests/e2e/c/test_timer_e2e.c b/tests/e2e/c/test_timer_e2e.c new file mode 100644 index 0000000..e6939e8 --- /dev/null +++ b/tests/e2e/c/test_timer_e2e.c @@ -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 +#include +#include +#include + +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; +} diff --git a/tests/unit/test_c_codegen.nim b/tests/unit/test_c_codegen.nim new file mode 100644 index 0000000..f77917d --- /dev/null +++ b/tests/unit/test_c_codegen.nim @@ -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 " 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")