mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-07-02 14:19:31 +00:00
feat(codegen): C binding generator (-d:targetLang=c) (#102)
This commit is contained in:
parent
f1cb110e52
commit
7e3fd96e74
14
CHANGELOG.md
14
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
|
||||
(`<lib>.h`) plus a `CMakeLists.txt`, alongside the existing Rust / C++ / CDDL
|
||||
backends. Requests/responses travel as CBOR using the same vendored TinyCBOR
|
||||
the C++ backend uses. C has no generics or overloading, so each `seq[T]` /
|
||||
`Option[T]` is monomorphised into its own struct + encode/decode/free triple.
|
||||
The high-level `<lib>_ctx_*` API is asynchronous: each method/constructor
|
||||
takes a typed result callback and the binding owns and reclaims all reply
|
||||
data and error strings (valid only for the duration of the callback), so the
|
||||
caller never frees anything — there is no blocking wait and no manual-free
|
||||
contract. Shared codegen helpers were extracted
|
||||
into `ffi/codegen/common.nim` (used by both the C and C++ backends). New
|
||||
`nimble genbindings_c` / `genbindings_c_echo` / `check_bindings_c` /
|
||||
`test_c_e2e` tasks, a `tests/e2e/c` ctest harness, and a
|
||||
`tests/unit/test_c_codegen.nim` unit suite.
|
||||
- Per-interaction ABI-format annotations: `declareLibrary` now takes an
|
||||
optional `defaultABIFormat` (`"cbor"` default, or `"c"`) that every
|
||||
`{.ffi.}` / `{.ffiCtor.}` / `{.ffiDtor.}` / `{.ffiRaw.}` / `{.ffiEvent.}`
|
||||
|
||||
47
examples/echo/c_bindings/CMakeLists.txt
Normal file
47
examples/echo/c_bindings/CMakeLists.txt
Normal file
@ -0,0 +1,47 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(echo_c_bindings C)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
||||
# ── Locate the repository root (contains ffi.nimble) ─────────────────────────
|
||||
set(_search_dir "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
set(REPO_ROOT "")
|
||||
foreach(_i RANGE 10)
|
||||
if(EXISTS "${_search_dir}/ffi.nimble")
|
||||
set(REPO_ROOT "${_search_dir}")
|
||||
break()
|
||||
endif()
|
||||
get_filename_component(_search_dir "${_search_dir}" DIRECTORY)
|
||||
endforeach()
|
||||
if("${REPO_ROOT}" STREQUAL "")
|
||||
message(FATAL_ERROR "Cannot find repo root (no ffi.nimble in any ancestor)")
|
||||
endif()
|
||||
|
||||
# Build the Nim dylib + vendored TinyCBOR (shared with the C++ backend).
|
||||
set(NIM_FFI_LIB echo)
|
||||
set(NIM_FFI_SRC ../echo.nim)
|
||||
include("${REPO_ROOT}/ffi/codegen/templates/nim_ffi_lib.cmake")
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
add_library(echo_headers INTERFACE)
|
||||
target_include_directories(echo_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
target_link_libraries(echo_headers INTERFACE echo tinycbor Threads::Threads)
|
||||
# The generated header is async (no blocking helper), but consumer code that
|
||||
# waits on a result callback typically uses nanosleep / pthreads, which need a
|
||||
# POSIX feature level that strict `-std=c11` hides. Define it for consumers.
|
||||
target_compile_definitions(echo_headers INTERFACE _POSIX_C_SOURCE=200809L)
|
||||
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.c")
|
||||
add_executable(echo_example main.c)
|
||||
target_link_libraries(echo_example PRIVATE echo_headers)
|
||||
add_dependencies(echo_example echo_nim_lib)
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
add_custom_command(TARGET echo_example POST_BUILD
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${echo_RUNTIME_LIB}"
|
||||
"$<TARGET_FILE_DIR:echo_example>"
|
||||
COMMENT "Staging echo.dll next to echo_example.exe")
|
||||
endif()
|
||||
endif()
|
||||
415
examples/echo/c_bindings/echo.h
Normal file
415
examples/echo/c_bindings/echo.h
Normal file
@ -0,0 +1,415 @@
|
||||
#ifndef NIM_FFI_LIB_ECHO_H_INCLUDED
|
||||
#define NIM_FFI_LIB_ECHO_H_INCLUDED
|
||||
#include "nim_ffi_cbor.h"
|
||||
|
||||
/* ============================================================ */
|
||||
/* Generated types (user-declared + per-proc request envelopes) */
|
||||
/* ============================================================ */
|
||||
|
||||
typedef struct {
|
||||
NimFfiStr prefix;
|
||||
} EchoConfig;
|
||||
typedef struct {
|
||||
NimFfiStr text;
|
||||
} ShoutRequest;
|
||||
typedef struct {
|
||||
NimFfiStr shouted;
|
||||
NimFfiStr prefix;
|
||||
} ShoutResponse;
|
||||
typedef struct {
|
||||
EchoConfig config;
|
||||
} EchoCreateCtorReq;
|
||||
typedef struct {
|
||||
ShoutRequest req;
|
||||
} EchoShoutReq;
|
||||
typedef struct {
|
||||
char _nimffi_empty; /* C forbids empty structs */
|
||||
} EchoVersionReq;
|
||||
|
||||
static inline CborError echo_enc_EchoConfig(
|
||||
CborEncoder* e, const EchoConfig* v) {
|
||||
CborEncoder m;
|
||||
CborError err = cbor_encoder_create_map(e, &m, 1);
|
||||
if (err) return err;
|
||||
err = cbor_encode_text_stringz(&m, "prefix");
|
||||
if (err) return err;
|
||||
err = nimffi_enc_str(&m, &v->prefix);
|
||||
if (err) return err;
|
||||
return cbor_encoder_close_container(e, &m);
|
||||
}
|
||||
static inline CborError echo_dec_EchoConfig(
|
||||
CborValue* it, EchoConfig* out) {
|
||||
if (!cbor_value_is_map(it)) return CborErrorImproperValue;
|
||||
CborValue field;
|
||||
CborError err;
|
||||
err = cbor_value_map_find_value(it, "prefix", &field);
|
||||
if (err) return err;
|
||||
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
|
||||
err = nimffi_dec_str(&field, &out->prefix);
|
||||
if (err) return err;
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
static inline void echo_free_EchoConfig(EchoConfig* v) {
|
||||
if (!v) return;
|
||||
nimffi_free_str(&v->prefix);
|
||||
}
|
||||
static inline CborError echo_enc_ShoutRequest(
|
||||
CborEncoder* e, const ShoutRequest* v) {
|
||||
CborEncoder m;
|
||||
CborError err = cbor_encoder_create_map(e, &m, 1);
|
||||
if (err) return err;
|
||||
err = cbor_encode_text_stringz(&m, "text");
|
||||
if (err) return err;
|
||||
err = nimffi_enc_str(&m, &v->text);
|
||||
if (err) return err;
|
||||
return cbor_encoder_close_container(e, &m);
|
||||
}
|
||||
static inline CborError echo_dec_ShoutRequest(
|
||||
CborValue* it, ShoutRequest* out) {
|
||||
if (!cbor_value_is_map(it)) return CborErrorImproperValue;
|
||||
CborValue field;
|
||||
CborError err;
|
||||
err = cbor_value_map_find_value(it, "text", &field);
|
||||
if (err) return err;
|
||||
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
|
||||
err = nimffi_dec_str(&field, &out->text);
|
||||
if (err) return err;
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
static inline void echo_free_ShoutRequest(ShoutRequest* v) {
|
||||
if (!v) return;
|
||||
nimffi_free_str(&v->text);
|
||||
}
|
||||
static inline CborError echo_enc_ShoutResponse(
|
||||
CborEncoder* e, const ShoutResponse* v) {
|
||||
CborEncoder m;
|
||||
CborError err = cbor_encoder_create_map(e, &m, 2);
|
||||
if (err) return err;
|
||||
err = cbor_encode_text_stringz(&m, "shouted");
|
||||
if (err) return err;
|
||||
err = nimffi_enc_str(&m, &v->shouted);
|
||||
if (err) return err;
|
||||
err = cbor_encode_text_stringz(&m, "prefix");
|
||||
if (err) return err;
|
||||
err = nimffi_enc_str(&m, &v->prefix);
|
||||
if (err) return err;
|
||||
return cbor_encoder_close_container(e, &m);
|
||||
}
|
||||
static inline CborError echo_dec_ShoutResponse(
|
||||
CborValue* it, ShoutResponse* out) {
|
||||
if (!cbor_value_is_map(it)) return CborErrorImproperValue;
|
||||
CborValue field;
|
||||
CborError err;
|
||||
err = cbor_value_map_find_value(it, "shouted", &field);
|
||||
if (err) return err;
|
||||
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
|
||||
err = nimffi_dec_str(&field, &out->shouted);
|
||||
if (err) return err;
|
||||
err = cbor_value_map_find_value(it, "prefix", &field);
|
||||
if (err) return err;
|
||||
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
|
||||
err = nimffi_dec_str(&field, &out->prefix);
|
||||
if (err) return err;
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
static inline void echo_free_ShoutResponse(ShoutResponse* v) {
|
||||
if (!v) return;
|
||||
nimffi_free_str(&v->shouted);
|
||||
nimffi_free_str(&v->prefix);
|
||||
}
|
||||
static inline CborError echo_enc_EchoCreateCtorReq(
|
||||
CborEncoder* e, const EchoCreateCtorReq* v) {
|
||||
CborEncoder m;
|
||||
CborError err = cbor_encoder_create_map(e, &m, 1);
|
||||
if (err) return err;
|
||||
err = cbor_encode_text_stringz(&m, "config");
|
||||
if (err) return err;
|
||||
err = echo_enc_EchoConfig(&m, &v->config);
|
||||
if (err) return err;
|
||||
return cbor_encoder_close_container(e, &m);
|
||||
}
|
||||
static inline CborError echo_dec_EchoCreateCtorReq(
|
||||
CborValue* it, EchoCreateCtorReq* out) {
|
||||
if (!cbor_value_is_map(it)) return CborErrorImproperValue;
|
||||
CborValue field;
|
||||
CborError err;
|
||||
err = cbor_value_map_find_value(it, "config", &field);
|
||||
if (err) return err;
|
||||
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
|
||||
err = echo_dec_EchoConfig(&field, &out->config);
|
||||
if (err) return err;
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
static inline void echo_free_EchoCreateCtorReq(EchoCreateCtorReq* v) {
|
||||
if (!v) return;
|
||||
echo_free_EchoConfig(&v->config);
|
||||
}
|
||||
static inline CborError echo_enc_EchoShoutReq(
|
||||
CborEncoder* e, const EchoShoutReq* v) {
|
||||
CborEncoder m;
|
||||
CborError err = cbor_encoder_create_map(e, &m, 1);
|
||||
if (err) return err;
|
||||
err = cbor_encode_text_stringz(&m, "req");
|
||||
if (err) return err;
|
||||
err = echo_enc_ShoutRequest(&m, &v->req);
|
||||
if (err) return err;
|
||||
return cbor_encoder_close_container(e, &m);
|
||||
}
|
||||
static inline CborError echo_dec_EchoShoutReq(
|
||||
CborValue* it, EchoShoutReq* out) {
|
||||
if (!cbor_value_is_map(it)) return CborErrorImproperValue;
|
||||
CborValue field;
|
||||
CborError err;
|
||||
err = cbor_value_map_find_value(it, "req", &field);
|
||||
if (err) return err;
|
||||
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
|
||||
err = echo_dec_ShoutRequest(&field, &out->req);
|
||||
if (err) return err;
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
static inline void echo_free_EchoShoutReq(EchoShoutReq* v) {
|
||||
if (!v) return;
|
||||
echo_free_ShoutRequest(&v->req);
|
||||
}
|
||||
static inline CborError echo_enc_EchoVersionReq(
|
||||
CborEncoder* e, const EchoVersionReq* v) {
|
||||
(void)v;
|
||||
CborEncoder m;
|
||||
CborError err = cbor_encoder_create_map(e, &m, 0);
|
||||
if (err) return err;
|
||||
return cbor_encoder_close_container(e, &m);
|
||||
}
|
||||
static inline CborError echo_dec_EchoVersionReq(
|
||||
CborValue* it, EchoVersionReq* out) {
|
||||
if (!cbor_value_is_map(it)) return CborErrorImproperValue;
|
||||
(void)out;
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* C ABI declarations (symbols exported by the Nim dylib) */
|
||||
/* ============================================================ */
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void* echo_create(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback callback, void* user_data);
|
||||
int echo_shout(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
|
||||
int echo_version(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);
|
||||
int echo_destroy(void* ctx);
|
||||
uint64_t echo_add_event_listener(void* ctx, const char* event_name, FFICallback callback, void* user_data);
|
||||
int echo_remove_event_listener(void* ctx, uint64_t listener_id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
|
||||
/* CBOR buffer adapters (typed codec → void* driver signature) */
|
||||
static inline CborError echo_encv_EchoCreateCtorReq(CborEncoder* e, const void* v) { return echo_enc_EchoCreateCtorReq(e, (const EchoCreateCtorReq*)v); }
|
||||
static inline CborError echo_encv_EchoShoutReq(CborEncoder* e, const void* v) { return echo_enc_EchoShoutReq(e, (const EchoShoutReq*)v); }
|
||||
static inline CborError echo_encv_EchoVersionReq(CborEncoder* e, const void* v) { return echo_enc_EchoVersionReq(e, (const EchoVersionReq*)v); }
|
||||
static inline CborError echo_decv_ShoutResponse(CborValue* it, void* v) { return echo_dec_ShoutResponse(it, (ShoutResponse*)v); }
|
||||
static inline CborError echo_decv_Str(CborValue* it, void* v) { return nimffi_dec_str(it, (NimFfiStr*)v); }
|
||||
|
||||
/* ============================================================ */
|
||||
/* High-level context wrapper */
|
||||
/* ============================================================ */
|
||||
typedef struct {
|
||||
void* ptr;
|
||||
} EchoCtx;
|
||||
|
||||
typedef void (*EchoCreateFn)(int err_code, EchoCtx* ctx, const char* err_msg, void* user_data);
|
||||
typedef struct { EchoCreateFn fn; void* user_data; } EchoCreateBox;
|
||||
static void echo_create_trampoline(int ret, const char* msg, size_t len, void* ud) {
|
||||
EchoCreateBox* box = (EchoCreateBox*)ud;
|
||||
if (!box->fn) {
|
||||
free(box);
|
||||
return;
|
||||
}
|
||||
if (ret != 0) {
|
||||
char* em = nimffi_dup_cstr_n(msg ? msg : "", msg ? len : 0);
|
||||
box->fn(ret, NULL, em ? em : "FFI create failed", box->user_data);
|
||||
free(em);
|
||||
free(box);
|
||||
return;
|
||||
}
|
||||
char* err = NULL;
|
||||
NimFfiStr addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
if (nimffi_decode_from_buf(echo_decv_Str, (const uint8_t*)msg, len, &addr, &err) != 0) {
|
||||
box->fn(-1, NULL, err ? err : "decode failed", box->user_data);
|
||||
free(err);
|
||||
free(box);
|
||||
return;
|
||||
}
|
||||
char* endp = NULL;
|
||||
unsigned long long a = addr.data ? strtoull(addr.data, &endp, 10) : 0;
|
||||
bool ok = addr.data && addr.len > 0 && endp && *endp == '\0';
|
||||
nimffi_free_str(&addr);
|
||||
if (!ok) {
|
||||
box->fn(-1, NULL, "FFI create returned non-numeric address", box->user_data);
|
||||
free(box);
|
||||
return;
|
||||
}
|
||||
EchoCtx* ctx = (EchoCtx*)calloc(1, sizeof(EchoCtx));
|
||||
if (!ctx) {
|
||||
box->fn(-1, NULL, "out of memory", box->user_data);
|
||||
free(box);
|
||||
return;
|
||||
}
|
||||
ctx->ptr = (void*)(uintptr_t)a;
|
||||
box->fn(NIMFFI_RET_OK, ctx, NULL, box->user_data);
|
||||
free(box);
|
||||
}
|
||||
|
||||
static inline int echo_ctx_create(const EchoConfig* config, EchoCreateFn on_created, void* user_data) {
|
||||
EchoCreateCtorReq ffi_req;
|
||||
memset(&ffi_req, 0, sizeof(ffi_req));
|
||||
ffi_req.config = *config;
|
||||
uint8_t* req_buf = NULL;
|
||||
size_t req_len = 0;
|
||||
char* err = NULL;
|
||||
if (nimffi_encode_to_buf(echo_encv_EchoCreateCtorReq, &ffi_req, &req_buf, &req_len, &err) != 0) {
|
||||
if (on_created) on_created(-1, NULL, err ? err : "encode failed", user_data);
|
||||
free(err);
|
||||
return -1;
|
||||
}
|
||||
EchoCreateBox* box = (EchoCreateBox*)malloc(sizeof(EchoCreateBox));
|
||||
if (!box) {
|
||||
free(req_buf);
|
||||
if (on_created) on_created(-1, NULL, "out of memory", user_data);
|
||||
return -1;
|
||||
}
|
||||
box->fn = on_created;
|
||||
box->user_data = user_data;
|
||||
(void)echo_create(req_buf, req_len, echo_create_trampoline, box);
|
||||
free(req_buf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline void echo_ctx_destroy(EchoCtx* ctx) {
|
||||
if (!ctx) return;
|
||||
if (ctx->ptr) { echo_destroy(ctx->ptr); ctx->ptr = NULL; }
|
||||
free(ctx);
|
||||
}
|
||||
|
||||
typedef void (*EchoShoutReplyFn)(int err_code, const ShoutResponse* reply, const char* err_msg, void* user_data);
|
||||
typedef struct { EchoShoutReplyFn fn; void* user_data; } EchoShoutCallBox;
|
||||
static void echo_shout_reply_trampoline(int ret, const char* msg, size_t len, void* ud) {
|
||||
EchoShoutCallBox* box = (EchoShoutCallBox*)ud;
|
||||
if (!box->fn) {
|
||||
free(box);
|
||||
return;
|
||||
}
|
||||
if (ret != 0) {
|
||||
char* em = nimffi_dup_cstr_n(msg ? msg : "", msg ? len : 0);
|
||||
box->fn(ret, NULL, em ? em : "FFI call failed", box->user_data);
|
||||
free(em);
|
||||
free(box);
|
||||
return;
|
||||
}
|
||||
char* err = NULL;
|
||||
ShoutResponse out;
|
||||
memset(&out, 0, sizeof(out));
|
||||
int dec = nimffi_decode_from_buf(echo_decv_ShoutResponse, (const uint8_t*)msg, len, &out, &err);
|
||||
if (dec != 0) {
|
||||
box->fn(-1, NULL, err ? err : "decode failed", box->user_data);
|
||||
free(err);
|
||||
echo_free_ShoutResponse(&out);
|
||||
free(box);
|
||||
return;
|
||||
}
|
||||
box->fn(NIMFFI_RET_OK, &out, NULL, box->user_data);
|
||||
echo_free_ShoutResponse(&out);
|
||||
free(box);
|
||||
}
|
||||
static inline int echo_ctx_shout(const EchoCtx* ctx, const ShoutRequest* req, EchoShoutReplyFn on_reply, void* user_data) {
|
||||
EchoShoutReq ffi_req;
|
||||
memset(&ffi_req, 0, sizeof(ffi_req));
|
||||
ffi_req.req = *req;
|
||||
uint8_t* req_buf = NULL;
|
||||
size_t req_len = 0;
|
||||
char* err = NULL;
|
||||
if (nimffi_encode_to_buf(echo_encv_EchoShoutReq, &ffi_req, &req_buf, &req_len, &err) != 0) {
|
||||
if (on_reply) on_reply(-1, NULL, err ? err : "encode failed", user_data);
|
||||
free(err);
|
||||
return -1;
|
||||
}
|
||||
EchoShoutCallBox* box = (EchoShoutCallBox*)malloc(sizeof(EchoShoutCallBox));
|
||||
if (!box) {
|
||||
free(req_buf);
|
||||
if (on_reply) on_reply(-1, NULL, "out of memory", user_data);
|
||||
return -1;
|
||||
}
|
||||
box->fn = on_reply;
|
||||
box->user_data = user_data;
|
||||
int ret = echo_shout(ctx->ptr, echo_shout_reply_trampoline, box, req_buf, req_len);
|
||||
free(req_buf);
|
||||
if (ret == NIMFFI_RET_MISSING_CALLBACK) {
|
||||
if (on_reply) on_reply(-1, NULL, "RET_MISSING_CALLBACK (internal error)", user_data);
|
||||
free(box);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
typedef void (*EchoVersionReplyFn)(int err_code, const NimFfiStr* reply, const char* err_msg, void* user_data);
|
||||
typedef struct { EchoVersionReplyFn fn; void* user_data; } EchoVersionCallBox;
|
||||
static void echo_version_reply_trampoline(int ret, const char* msg, size_t len, void* ud) {
|
||||
EchoVersionCallBox* box = (EchoVersionCallBox*)ud;
|
||||
if (!box->fn) {
|
||||
free(box);
|
||||
return;
|
||||
}
|
||||
if (ret != 0) {
|
||||
char* em = nimffi_dup_cstr_n(msg ? msg : "", msg ? len : 0);
|
||||
box->fn(ret, NULL, em ? em : "FFI call failed", box->user_data);
|
||||
free(em);
|
||||
free(box);
|
||||
return;
|
||||
}
|
||||
char* err = NULL;
|
||||
NimFfiStr out;
|
||||
memset(&out, 0, sizeof(out));
|
||||
int dec = nimffi_decode_from_buf(echo_decv_Str, (const uint8_t*)msg, len, &out, &err);
|
||||
if (dec != 0) {
|
||||
box->fn(-1, NULL, err ? err : "decode failed", box->user_data);
|
||||
free(err);
|
||||
nimffi_free_str(&out);
|
||||
free(box);
|
||||
return;
|
||||
}
|
||||
box->fn(NIMFFI_RET_OK, &out, NULL, box->user_data);
|
||||
nimffi_free_str(&out);
|
||||
free(box);
|
||||
}
|
||||
static inline int echo_ctx_version(const EchoCtx* ctx, EchoVersionReplyFn on_reply, void* user_data) {
|
||||
EchoVersionReq ffi_req;
|
||||
memset(&ffi_req, 0, sizeof(ffi_req));
|
||||
uint8_t* req_buf = NULL;
|
||||
size_t req_len = 0;
|
||||
char* err = NULL;
|
||||
if (nimffi_encode_to_buf(echo_encv_EchoVersionReq, &ffi_req, &req_buf, &req_len, &err) != 0) {
|
||||
if (on_reply) on_reply(-1, NULL, err ? err : "encode failed", user_data);
|
||||
free(err);
|
||||
return -1;
|
||||
}
|
||||
EchoVersionCallBox* box = (EchoVersionCallBox*)malloc(sizeof(EchoVersionCallBox));
|
||||
if (!box) {
|
||||
free(req_buf);
|
||||
if (on_reply) on_reply(-1, NULL, "out of memory", user_data);
|
||||
return -1;
|
||||
}
|
||||
box->fn = on_reply;
|
||||
box->user_data = user_data;
|
||||
int ret = echo_version(ctx->ptr, echo_version_reply_trampoline, box, req_buf, req_len);
|
||||
free(req_buf);
|
||||
if (ret == NIMFFI_RET_MISSING_CALLBACK) {
|
||||
if (on_reply) on_reply(-1, NULL, "RET_MISSING_CALLBACK (internal error)", user_data);
|
||||
free(box);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif /* NIM_FFI_LIB_ECHO_H_INCLUDED */
|
||||
342
examples/echo/c_bindings/nim_ffi_cbor.h
Normal file
342
examples/echo/c_bindings/nim_ffi_cbor.h
Normal file
@ -0,0 +1,342 @@
|
||||
#ifndef NIM_FFI_CBOR_HELPERS_H_INCLUDED
|
||||
#define NIM_FFI_CBOR_HELPERS_H_INCLUDED
|
||||
/* Leaf CBOR codecs (scalars, text strings, byte strings) plus the buffer
|
||||
* drivers. The per-struct / per-container codecs in the library header call
|
||||
* into these by name (C has no overloading, so each leaf gets a distinct
|
||||
* nimffi_enc_* / nimffi_dec_* symbol). Guarded so two nim-ffi headers can
|
||||
* share a translation unit. */
|
||||
#include "nim_ffi_prelude.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Result delivery callback exported by the Nim dylib: `ret` is 0 on success
|
||||
* (then `msg`/`len` carry the CBOR response) or non-zero on failure (then
|
||||
* `msg`/`len` carry the error text, which is NOT NUL-terminated). */
|
||||
typedef void (*FFICallback)(int ret, const char* msg, size_t len, void* user_data);
|
||||
|
||||
/* Return / callback status codes. NIMFFI_RET_OK (0) is success; any non-zero
|
||||
* value handed to a result callback's `err_code` (or returned by a submit call)
|
||||
* is a failure. NIMFFI_RET_MISSING_CALLBACK is a special case from the Nim
|
||||
* dispatcher: the callback will never fire, so the request path must report the
|
||||
* failure itself. */
|
||||
#define NIMFFI_RET_OK 0
|
||||
#define NIMFFI_RET_ERROR 1
|
||||
#define NIMFFI_RET_MISSING_CALLBACK 2
|
||||
|
||||
/* ── leaf encoders ─────────────────────────────────────────────────────── */
|
||||
static inline CborError nimffi_enc_bool(CborEncoder* e, const bool* v) {
|
||||
return cbor_encode_boolean(e, *v);
|
||||
}
|
||||
static inline CborError nimffi_enc_i64(CborEncoder* e, const int64_t* v) {
|
||||
return cbor_encode_int(e, *v);
|
||||
}
|
||||
static inline CborError nimffi_enc_i32(CborEncoder* e, const int32_t* v) {
|
||||
return cbor_encode_int(e, (int64_t)*v);
|
||||
}
|
||||
static inline CborError nimffi_enc_i16(CborEncoder* e, const int16_t* v) {
|
||||
return cbor_encode_int(e, (int64_t)*v);
|
||||
}
|
||||
static inline CborError nimffi_enc_i8(CborEncoder* e, const int8_t* v) {
|
||||
return cbor_encode_int(e, (int64_t)*v);
|
||||
}
|
||||
static inline CborError nimffi_enc_u64(CborEncoder* e, const uint64_t* v) {
|
||||
return cbor_encode_uint(e, *v);
|
||||
}
|
||||
static inline CborError nimffi_enc_u32(CborEncoder* e, const uint32_t* v) {
|
||||
return cbor_encode_uint(e, (uint64_t)*v);
|
||||
}
|
||||
static inline CborError nimffi_enc_u16(CborEncoder* e, const uint16_t* v) {
|
||||
return cbor_encode_uint(e, (uint64_t)*v);
|
||||
}
|
||||
static inline CborError nimffi_enc_u8(CborEncoder* e, const uint8_t* v) {
|
||||
return cbor_encode_uint(e, (uint64_t)*v);
|
||||
}
|
||||
static inline CborError nimffi_enc_f64(CborEncoder* e, const double* v) {
|
||||
return cbor_encode_double(e, *v);
|
||||
}
|
||||
static inline CborError nimffi_enc_f32(CborEncoder* e, const float* v) {
|
||||
return cbor_encode_float(e, *v);
|
||||
}
|
||||
static inline CborError nimffi_enc_str(CborEncoder* e, const NimFfiStr* v) {
|
||||
return cbor_encode_text_string(e, v->data ? v->data : "", v->len);
|
||||
}
|
||||
static inline CborError nimffi_enc_bytes(CborEncoder* e, const NimFfiBytes* v) {
|
||||
return cbor_encode_byte_string(e, v->data, v->len);
|
||||
}
|
||||
|
||||
/* ── leaf decoders ─────────────────────────────────────────────────────── */
|
||||
/* After reading a leaf, the parser must advance past it; both steps
|
||||
* short-circuit on the same CborError, so they travel together. */
|
||||
static inline CborError nimffi_advance_if_ok(CborValue* it, CborError err) {
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
|
||||
static inline CborError nimffi_dec_bool(CborValue* it, bool* out) {
|
||||
if (!cbor_value_is_boolean(it)) {
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
return nimffi_advance_if_ok(it, cbor_value_get_boolean(it, out));
|
||||
}
|
||||
static inline CborError nimffi_dec_i64(CborValue* it, int64_t* out) {
|
||||
if (!cbor_value_is_integer(it)) {
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
return nimffi_advance_if_ok(it, cbor_value_get_int64_checked(it, out));
|
||||
}
|
||||
static inline CborError nimffi_dec_i32(CborValue* it, int32_t* out) {
|
||||
int64_t tmp = 0;
|
||||
CborError err = nimffi_dec_i64(it, &tmp);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (tmp < INT32_MIN || tmp > INT32_MAX) {
|
||||
return CborErrorDataTooLarge;
|
||||
}
|
||||
*out = (int32_t)tmp;
|
||||
return CborNoError;
|
||||
}
|
||||
static inline CborError nimffi_dec_i16(CborValue* it, int16_t* out) {
|
||||
int64_t tmp = 0;
|
||||
CborError err = nimffi_dec_i64(it, &tmp);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (tmp < INT16_MIN || tmp > INT16_MAX) {
|
||||
return CborErrorDataTooLarge;
|
||||
}
|
||||
*out = (int16_t)tmp;
|
||||
return CborNoError;
|
||||
}
|
||||
static inline CborError nimffi_dec_i8(CborValue* it, int8_t* out) {
|
||||
int64_t tmp = 0;
|
||||
CborError err = nimffi_dec_i64(it, &tmp);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (tmp < INT8_MIN || tmp > INT8_MAX) {
|
||||
return CborErrorDataTooLarge;
|
||||
}
|
||||
*out = (int8_t)tmp;
|
||||
return CborNoError;
|
||||
}
|
||||
static inline CborError nimffi_dec_u64(CborValue* it, uint64_t* out) {
|
||||
if (!cbor_value_is_unsigned_integer(it)) {
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
return nimffi_advance_if_ok(it, cbor_value_get_uint64(it, out));
|
||||
}
|
||||
static inline CborError nimffi_dec_u32(CborValue* it, uint32_t* out) {
|
||||
uint64_t tmp = 0;
|
||||
CborError err = nimffi_dec_u64(it, &tmp);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (tmp > UINT32_MAX) {
|
||||
return CborErrorDataTooLarge;
|
||||
}
|
||||
*out = (uint32_t)tmp;
|
||||
return CborNoError;
|
||||
}
|
||||
static inline CborError nimffi_dec_u16(CborValue* it, uint16_t* out) {
|
||||
uint64_t tmp = 0;
|
||||
CborError err = nimffi_dec_u64(it, &tmp);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (tmp > UINT16_MAX) {
|
||||
return CborErrorDataTooLarge;
|
||||
}
|
||||
*out = (uint16_t)tmp;
|
||||
return CborNoError;
|
||||
}
|
||||
static inline CborError nimffi_dec_u8(CborValue* it, uint8_t* out) {
|
||||
uint64_t tmp = 0;
|
||||
CborError err = nimffi_dec_u64(it, &tmp);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (tmp > UINT8_MAX) {
|
||||
return CborErrorDataTooLarge;
|
||||
}
|
||||
*out = (uint8_t)tmp;
|
||||
return CborNoError;
|
||||
}
|
||||
static inline CborError nimffi_dec_f64(CborValue* it, double* out) {
|
||||
if (cbor_value_is_double(it)) {
|
||||
return nimffi_advance_if_ok(it, cbor_value_get_double(it, out));
|
||||
}
|
||||
if (cbor_value_is_float(it)) {
|
||||
float f = 0.0f;
|
||||
CborError err = cbor_value_get_float(it, &f);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
*out = (double)f;
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
static inline CborError nimffi_dec_f32(CborValue* it, float* out) {
|
||||
if (cbor_value_is_float(it)) {
|
||||
return nimffi_advance_if_ok(it, cbor_value_get_float(it, out));
|
||||
}
|
||||
if (cbor_value_is_double(it)) {
|
||||
double d = 0.0;
|
||||
CborError err = cbor_value_get_double(it, &d);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
*out = (float)d;
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
static inline CborError nimffi_dec_str(CborValue* it, NimFfiStr* out) {
|
||||
if (!cbor_value_is_text_string(it)) {
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
size_t len = 0;
|
||||
CborError err = cbor_value_get_string_length(it, &len);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (len == SIZE_MAX) { /* len + 1 would wrap to a 0-byte allocation */
|
||||
return CborErrorDataTooLarge;
|
||||
}
|
||||
/* one extra byte so a NUL-free payload is a valid C string */
|
||||
out->data = (char*)malloc(len + 1);
|
||||
if (!out->data) {
|
||||
return CborErrorOutOfMemory;
|
||||
}
|
||||
out->len = len;
|
||||
size_t copied = len;
|
||||
err = cbor_value_copy_text_string(it, out->data, &copied, NULL);
|
||||
if (err) {
|
||||
free(out->data);
|
||||
out->data = NULL;
|
||||
out->len = 0;
|
||||
return err;
|
||||
}
|
||||
out->data[len] = '\0';
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
static inline CborError nimffi_dec_bytes(CborValue* it, NimFfiBytes* out) {
|
||||
if (!cbor_value_is_byte_string(it)) {
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
size_t len = 0;
|
||||
CborError err = cbor_value_get_string_length(it, &len);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
out->data = (uint8_t*)malloc(len ? len : 1);
|
||||
if (!out->data) {
|
||||
return CborErrorOutOfMemory;
|
||||
}
|
||||
out->len = len;
|
||||
size_t copied = len;
|
||||
err = cbor_value_copy_byte_string(it, out->data, &copied, NULL);
|
||||
if (err) {
|
||||
free(out->data);
|
||||
out->data = NULL;
|
||||
out->len = 0;
|
||||
return err;
|
||||
}
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
|
||||
/* ── buffer drivers ────────────────────────────────────────────────────── */
|
||||
typedef CborError (*nimffi_enc_fn)(CborEncoder*, const void*);
|
||||
typedef CborError (*nimffi_dec_fn)(CborValue*, void*);
|
||||
|
||||
static inline char* nimffi_dup_cstr(const char* s) {
|
||||
size_t n = strlen(s) + 1;
|
||||
char* p = (char*)malloc(n);
|
||||
if (p) {
|
||||
memcpy(p, s, n);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/* NUL-terminated copy of a length-delimited (not NUL-terminated) byte run,
|
||||
* for turning the FFICallback's raw error `msg`/`len` into a C string. */
|
||||
static inline char* nimffi_dup_cstr_n(const char* s, size_t n) {
|
||||
char* p = (char*)malloc(n + 1);
|
||||
if (p) {
|
||||
if (n > 0) {
|
||||
memcpy(p, s, n);
|
||||
}
|
||||
p[n] = '\0';
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/* Encode `val` with `fn` into a freshly malloc'd buffer, doubling on overflow.
|
||||
* Returns 0 and sets out/outlen on success; -1 and *err (heap) on failure. */
|
||||
static inline int nimffi_encode_to_buf(
|
||||
nimffi_enc_fn fn, const void* val,
|
||||
uint8_t** out, size_t* outlen, char** err) {
|
||||
size_t cap = 4096;
|
||||
uint8_t* buf = (uint8_t*)malloc(cap);
|
||||
if (!buf) {
|
||||
if (err) *err = nimffi_dup_cstr("out of memory");
|
||||
return -1;
|
||||
}
|
||||
for (;;) {
|
||||
CborEncoder enc;
|
||||
cbor_encoder_init(&enc, buf, cap, 0);
|
||||
CborError e = fn(&enc, val);
|
||||
if (e == CborNoError) {
|
||||
*outlen = cbor_encoder_get_buffer_size(&enc, buf);
|
||||
*out = buf;
|
||||
return 0;
|
||||
}
|
||||
if (e == CborErrorOutOfMemory) {
|
||||
size_t extra = cbor_encoder_get_extra_bytes_needed(&enc);
|
||||
cap += extra > 0 ? extra : cap;
|
||||
uint8_t* grown = (uint8_t*)realloc(buf, cap);
|
||||
if (!grown) {
|
||||
free(buf);
|
||||
if (err) *err = nimffi_dup_cstr("out of memory");
|
||||
return -1;
|
||||
}
|
||||
buf = grown;
|
||||
continue;
|
||||
}
|
||||
free(buf);
|
||||
if (err) *err = nimffi_dup_cstr(cbor_error_string(e));
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Decode a CBOR buffer into `out` with `fn`. Returns 0 on success; -1 and
|
||||
* *err (heap) on failure. */
|
||||
static inline int nimffi_decode_from_buf(
|
||||
nimffi_dec_fn fn, const uint8_t* buf, size_t len,
|
||||
void* out, char** err) {
|
||||
CborParser parser;
|
||||
CborValue it;
|
||||
CborError e = cbor_parser_init(buf, len, 0, &parser, &it);
|
||||
if (e != CborNoError) {
|
||||
if (err) *err = nimffi_dup_cstr(cbor_error_string(e));
|
||||
return -1;
|
||||
}
|
||||
e = fn(&it, out);
|
||||
if (e != CborNoError) {
|
||||
if (err) *err = nimffi_dup_cstr(cbor_error_string(e));
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NIM_FFI_CBOR_HELPERS_H_INCLUDED */
|
||||
|
||||
89
examples/echo/c_bindings/nim_ffi_prelude.h
Normal file
89
examples/echo/c_bindings/nim_ffi_prelude.h
Normal file
@ -0,0 +1,89 @@
|
||||
#ifndef NIM_FFI_PRELUDE_H_INCLUDED
|
||||
#define NIM_FFI_PRELUDE_H_INCLUDED
|
||||
/* Generated C binding for a nim-ffi library. Requests/responses travel as
|
||||
* CBOR (encoded with vendored TinyCBOR on this side, matching the Nim-side
|
||||
* cbor_serial codec on the wire — both ends speak RFC 8949).
|
||||
*
|
||||
* The API is asynchronous: every method/constructor takes a result callback
|
||||
* and returns immediately. The callback fires exactly once — synchronously on
|
||||
* a submit-time failure, otherwise from the Nim dispatch thread when the reply
|
||||
* arrives.
|
||||
*
|
||||
* Memory ownership contract:
|
||||
* - Request-side strings/sequences are *borrowed*: the binding only reads
|
||||
* them while encoding, so a string literal wrapped with nimffi_str() is
|
||||
* fine and is never freed by the binding.
|
||||
* - Response values and error strings passed into a result callback are
|
||||
* *owned by the binding* and valid only for the duration of that callback;
|
||||
* the binding reclaims them once the callback returns. The caller never
|
||||
* frees them. (The generated <lib>_free_<Type>() helpers are internal — the
|
||||
* trampolines use them to reclaim decoded payloads.)
|
||||
* - A context handle delivered to a constructor callback is the exception:
|
||||
* ownership transfers to the caller, who releases it with
|
||||
* <lib>_ctx_destroy(). It is a lifecycle handle, not returned data.
|
||||
*
|
||||
* Trust boundary: the decoders assume the CBOR they parse was produced by the
|
||||
* paired Nim library. They reject malformed input rather than trusting it, but
|
||||
* they are not hardened against a hostile peer feeding crafted payloads through
|
||||
* the raw nimffi_decode_from_buf entry point.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <tinycbor/cbor.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Owned, length-delimited UTF-8 text (Nim `string`/`cstring`). On the request
|
||||
* side `data` may point at borrowed storage (see nimffi_str); on the response
|
||||
* side it is heap-allocated and freed by nimffi_free_str. Always NUL-padded by
|
||||
* one byte after decode so `data` is usable as a C string when it has no
|
||||
* embedded NULs. */
|
||||
typedef struct {
|
||||
char* data;
|
||||
size_t len;
|
||||
} NimFfiStr;
|
||||
|
||||
/* Owned, length-delimited byte buffer (Nim `seq[byte]`). */
|
||||
typedef struct {
|
||||
uint8_t* data;
|
||||
size_t len;
|
||||
} NimFfiBytes;
|
||||
|
||||
/* Wrap a borrowed C string for use as a request field. The returned view is
|
||||
* not owned by the binding and must outlive the call that encodes it. */
|
||||
static inline NimFfiStr nimffi_str(const char* s) {
|
||||
NimFfiStr v;
|
||||
v.data = (char*)s;
|
||||
v.len = s ? strlen(s) : 0;
|
||||
return v;
|
||||
}
|
||||
|
||||
static inline void nimffi_free_str(NimFfiStr* v) {
|
||||
if (!v || !v->data) {
|
||||
return;
|
||||
}
|
||||
free(v->data);
|
||||
v->data = NULL;
|
||||
v->len = 0;
|
||||
}
|
||||
|
||||
static inline void nimffi_free_bytes(NimFfiBytes* v) {
|
||||
if (!v || !v->data) {
|
||||
return;
|
||||
}
|
||||
free(v->data);
|
||||
v->data = NULL;
|
||||
v->len = 0;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NIM_FFI_PRELUDE_H_INCLUDED */
|
||||
|
||||
@ -27,92 +27,10 @@ if("${REPO_ROOT}" STREQUAL "")
|
||||
message(FATAL_ERROR "Cannot find repo root (no ffi.nimble in any ancestor)")
|
||||
endif()
|
||||
|
||||
get_filename_component(NIM_SRC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../echo.nim"
|
||||
ABSOLUTE)
|
||||
|
||||
find_program(NIM_EXECUTABLE nim REQUIRED)
|
||||
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
set(NIM_LIB_FILE "${REPO_ROOT}/libecho.dylib")
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
set(NIM_LIB_FILE "${REPO_ROOT}/echo.dll")
|
||||
# MSVC consumers link against the `.lib` import library, not the DLL.
|
||||
# MinGW's ld emits one when asked via `--out-implib`; the resulting COFF
|
||||
# archive is readable by MSVC's link.exe.
|
||||
set(NIM_IMPLIB_FILE "${REPO_ROOT}/echo.lib")
|
||||
else()
|
||||
set(NIM_LIB_FILE "${REPO_ROOT}/libecho.so")
|
||||
endif()
|
||||
|
||||
# On Windows the default Nim toolchain (mingw gcc) doesn't emit an import
|
||||
# library unless told to. Without it, MSVC consumers can't resolve any
|
||||
# symbol exported by the DLL at link time.
|
||||
set(NIM_IMPLIB_PASSL "")
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
set(NIM_IMPLIB_PASSL "--passL:-Wl,--out-implib,${NIM_IMPLIB_FILE}")
|
||||
endif()
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${NIM_LIB_FILE}"
|
||||
COMMAND "${NIM_EXECUTABLE}" c
|
||||
--mm:orc
|
||||
-d:chronicles_log_level=WARN
|
||||
--app:lib
|
||||
--noMain
|
||||
"--nimMainPrefix:libecho"
|
||||
${NIM_IMPLIB_PASSL}
|
||||
"-o:${NIM_LIB_FILE}"
|
||||
"${NIM_SRC}"
|
||||
WORKING_DIRECTORY "${REPO_ROOT}"
|
||||
DEPENDS "${NIM_SRC}"
|
||||
BYPRODUCTS "${NIM_IMPLIB_FILE}"
|
||||
COMMENT "Compiling Nim library libecho"
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(echo_nim_lib ALL DEPENDS "${NIM_LIB_FILE}")
|
||||
|
||||
# On Windows, an `IMPORTED SHARED` target needs IMPORTED_IMPLIB pointing at
|
||||
# the `.lib` import library so MSVC's `link.exe` can resolve symbols. The
|
||||
# Visual Studio multi-config generator did not pick up `IMPORTED_IMPLIB` —
|
||||
# nor per-config `IMPORTED_IMPLIB_<CONFIG>` variants — and emitted
|
||||
# `echo-NOTFOUND.obj` into every link line. Side-step the IMPORTED
|
||||
# machinery on Windows by exposing the import library through a plain
|
||||
# INTERFACE library that links the `.lib` by path.
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
add_library(echo INTERFACE)
|
||||
target_link_libraries(echo INTERFACE "${NIM_IMPLIB_FILE}")
|
||||
else()
|
||||
add_library(echo SHARED IMPORTED GLOBAL)
|
||||
set_target_properties(echo PROPERTIES IMPORTED_LOCATION "${NIM_LIB_FILE}")
|
||||
endif()
|
||||
add_dependencies(echo echo_nim_lib)
|
||||
|
||||
# Absolute path to the runtime library (DLL/dylib/so). Exposed via the cache
|
||||
# so consumers in other directories (e.g. tests/e2e/cpp) can stage the DLL
|
||||
# next to their executable on Windows.
|
||||
set(echo_RUNTIME_LIB "${NIM_LIB_FILE}" CACHE INTERNAL
|
||||
"Absolute path to the echo runtime library")
|
||||
|
||||
# ── TinyCBOR (vendored at ffi/codegen/templates/cpp/vendor/tinycbor) ─────────
|
||||
# Guarded so two sibling cpp_bindings dirs in one parent project don't redefine
|
||||
# the `tinycbor` target.
|
||||
set(TINYCBOR_SRC_DIR "${REPO_ROOT}/ffi/codegen/templates/cpp/vendor")
|
||||
if(NOT TARGET tinycbor)
|
||||
add_library(tinycbor STATIC
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor/cborencoder.c"
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor/cborencoder_close_container_checked.c"
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor/cborparser.c"
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor/cborparser_dup_string.c"
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor/cborerrorstrings.c"
|
||||
)
|
||||
target_include_directories(tinycbor PUBLIC
|
||||
"${TINYCBOR_SRC_DIR}" # consumer uses #include <tinycbor/cbor.h>
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor" # internal _p.h includes resolve here
|
||||
)
|
||||
set_property(TARGET tinycbor PROPERTY C_STANDARD 99)
|
||||
set_property(TARGET tinycbor PROPERTY POSITION_INDEPENDENT_CODE ON)
|
||||
endif()
|
||||
# Build the Nim dylib + vendored TinyCBOR (shared with the C backend).
|
||||
set(NIM_FFI_LIB echo)
|
||||
set(NIM_FFI_SRC ../echo.nim)
|
||||
include("${REPO_ROOT}/ffi/codegen/templates/nim_ffi_lib.cmake")
|
||||
|
||||
add_library(echo_headers INTERFACE)
|
||||
target_include_directories(echo_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
|
||||
47
examples/timer/c_bindings/CMakeLists.txt
Normal file
47
examples/timer/c_bindings/CMakeLists.txt
Normal file
@ -0,0 +1,47 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(my_timer_c_bindings C)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
||||
# ── Locate the repository root (contains ffi.nimble) ─────────────────────────
|
||||
set(_search_dir "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
set(REPO_ROOT "")
|
||||
foreach(_i RANGE 10)
|
||||
if(EXISTS "${_search_dir}/ffi.nimble")
|
||||
set(REPO_ROOT "${_search_dir}")
|
||||
break()
|
||||
endif()
|
||||
get_filename_component(_search_dir "${_search_dir}" DIRECTORY)
|
||||
endforeach()
|
||||
if("${REPO_ROOT}" STREQUAL "")
|
||||
message(FATAL_ERROR "Cannot find repo root (no ffi.nimble in any ancestor)")
|
||||
endif()
|
||||
|
||||
# Build the Nim dylib + vendored TinyCBOR (shared with the C++ backend).
|
||||
set(NIM_FFI_LIB my_timer)
|
||||
set(NIM_FFI_SRC ../timer.nim)
|
||||
include("${REPO_ROOT}/ffi/codegen/templates/nim_ffi_lib.cmake")
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
add_library(my_timer_headers INTERFACE)
|
||||
target_include_directories(my_timer_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
target_link_libraries(my_timer_headers INTERFACE my_timer tinycbor Threads::Threads)
|
||||
# The generated header is async (no blocking helper), but consumer code that
|
||||
# waits on a result callback typically uses nanosleep / pthreads, which need a
|
||||
# POSIX feature level that strict `-std=c11` hides. Define it for consumers.
|
||||
target_compile_definitions(my_timer_headers INTERFACE _POSIX_C_SOURCE=200809L)
|
||||
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.c")
|
||||
add_executable(my_timer_example main.c)
|
||||
target_link_libraries(my_timer_example PRIVATE my_timer_headers)
|
||||
add_dependencies(my_timer_example my_timer_nim_lib)
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
add_custom_command(TARGET my_timer_example POST_BUILD
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${my_timer_RUNTIME_LIB}"
|
||||
"$<TARGET_FILE_DIR:my_timer_example>"
|
||||
COMMENT "Staging my_timer.dll next to my_timer_example.exe")
|
||||
endif()
|
||||
endif()
|
||||
72
examples/timer/c_bindings/README.md
Normal file
72
examples/timer/c_bindings/README.md
Normal file
@ -0,0 +1,72 @@
|
||||
# C Bindings for nim-timer
|
||||
|
||||
## Purpose
|
||||
|
||||
This folder contains **auto-generated C bindings** for the `my_timer` Nim
|
||||
library. It is generated from `../timer.nim` and provides:
|
||||
|
||||
- `my_timer.h`: header-only C binding (`MyTimerCtx` + `my_timer_ctx_*` API)
|
||||
- `main.c`: example executable demonstrating how to use the bindings
|
||||
- `CMakeLists.txt`: build configuration that compiles the Nim library, the
|
||||
vendored TinyCBOR, and the C example
|
||||
|
||||
The bindings speak CBOR on the wire (the same format as the Rust and C++
|
||||
backends) using the TinyCBOR copy vendored at
|
||||
`ffi/codegen/templates/cpp/vendor/tinycbor`.
|
||||
|
||||
## How It's Generated
|
||||
|
||||
Regenerate these bindings by running from the parent directory:
|
||||
|
||||
```sh
|
||||
cd examples/timer
|
||||
nimble genbindings_c
|
||||
```
|
||||
|
||||
This invokes the Nim compiler with `-d:targetLang=c`, triggering
|
||||
`genBindings(...)` in `timer.nim`, which reads the compile-time FFI registries
|
||||
and emits the binding files.
|
||||
|
||||
## Building the Example
|
||||
|
||||
```sh
|
||||
cd examples/timer/c_bindings
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
./build/my_timer_example
|
||||
```
|
||||
|
||||
## Asynchronous API
|
||||
|
||||
Every method and the constructor take a typed **result callback** and return
|
||||
immediately. The callback fires exactly once — synchronously if the request
|
||||
fails to even submit, otherwise from the Nim dispatch thread when the reply
|
||||
arrives:
|
||||
|
||||
```c
|
||||
static void on_echo(int err_code, const EchoResponse* reply,
|
||||
const char* err_msg, void* user_data) {
|
||||
if (err_code != 0) { /* err_msg is set, reply is NULL */ return; }
|
||||
printf("echoed: %s\n", reply->echoed.data);
|
||||
}
|
||||
...
|
||||
my_timer_ctx_echo(ctx, &req, on_echo, /*user_data=*/NULL);
|
||||
```
|
||||
|
||||
See `main.c` for the full pattern, including a small `wait_done()` poll helper
|
||||
that turns each async call back into a sequential step.
|
||||
|
||||
## Memory Ownership
|
||||
|
||||
- Request-side strings/sequences are *borrowed* — wrap C strings with
|
||||
`nimffi_str(...)`; the binding never frees them.
|
||||
- Reply values and error strings passed into a result callback are **owned by
|
||||
the binding** and valid only for the duration of that callback. The caller
|
||||
never frees them — copy out anything you need to keep before returning.
|
||||
- A `MyTimerCtx*` delivered to the constructor callback is the exception:
|
||||
ownership transfers to you, and you release it with `my_timer_ctx_destroy()`.
|
||||
|
||||
## Do Not Edit
|
||||
|
||||
The generated files in this folder are overwritten each time
|
||||
`nimble genbindings_c` runs. Any manual changes will be lost.
|
||||
226
examples/timer/c_bindings/main.c
Normal file
226
examples/timer/c_bindings/main.c
Normal file
@ -0,0 +1,226 @@
|
||||
#include "my_timer.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#if defined(__STDC_NO_ATOMICS__)
|
||||
# error "C11 atomics required (or provide a mutex/condvar fallback)"
|
||||
#endif
|
||||
#include <stdatomic.h>
|
||||
|
||||
/* The `done` flags below are written from the library's dispatch thread and
|
||||
* polled from main, so they cross a thread boundary — atomics, not `volatile`,
|
||||
* give the visibility guarantee. sleep_ms wraps the platform nap so the demo
|
||||
* builds on Windows too. */
|
||||
#if defined(_WIN32)
|
||||
# include <windows.h>
|
||||
static void sleep_ms(unsigned ms) { Sleep(ms); }
|
||||
#else
|
||||
# include <time.h>
|
||||
static void sleep_ms(unsigned ms) {
|
||||
struct timespec t = {(time_t)(ms / 1000), (long)(ms % 1000) * 1000 * 1000};
|
||||
nanosleep(&t, NULL);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* The generated bindings are asynchronous: each call takes a result callback
|
||||
* and returns immediately. The reply and any error string handed to that
|
||||
* callback are owned by the binding and valid only while the callback runs —
|
||||
* the caller never frees them; it copies out whatever it wants to keep. This
|
||||
* demo turns each async call back into a sequential step by polling a `done`
|
||||
* flag (the same pattern the typed event listener already uses). */
|
||||
|
||||
/* Poll up to ~5s for a callback to fire. Returns false if it never did, so the
|
||||
* caller can report a stuck call instead of treating it as an empty success. */
|
||||
static bool wait_done(atomic_int* done) {
|
||||
for (int i = 0; i < 500 && !atomic_load(done); i++) {
|
||||
sleep_ms(10);
|
||||
}
|
||||
return atomic_load(done) != 0;
|
||||
}
|
||||
|
||||
static atomic_int g_echo_count = 0;
|
||||
static char g_echo_message[256];
|
||||
|
||||
static void on_echo_fired(const EchoEvent* evt, void* user_data) {
|
||||
(void)user_data;
|
||||
atomic_store(&g_echo_count, (int)evt->echoCount);
|
||||
snprintf(g_echo_message, sizeof(g_echo_message), "%s",
|
||||
evt->message.data ? evt->message.data : "");
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
atomic_int done;
|
||||
int err_code;
|
||||
MyTimerCtx* ctx;
|
||||
char err[256];
|
||||
} CreateWaiter;
|
||||
|
||||
static void on_created(int ec, MyTimerCtx* ctx, const char* em, void* ud) {
|
||||
CreateWaiter* w = (CreateWaiter*)ud;
|
||||
w->err_code = ec;
|
||||
w->ctx = ctx;
|
||||
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
|
||||
atomic_store(&w->done, 1);
|
||||
}
|
||||
|
||||
/* Generic reply sink: each step copies the fields it cares about out of its
|
||||
* typed reply into these slots (text_a/text_b for strings, num_a/num_b for
|
||||
* integers, flag for a boolean) before the binding reclaims the reply. */
|
||||
typedef struct {
|
||||
atomic_int done;
|
||||
int err_code;
|
||||
char err[256];
|
||||
char text_a[256];
|
||||
char text_b[256];
|
||||
long long num_a;
|
||||
long long num_b;
|
||||
int flag;
|
||||
} ReplyWaiter;
|
||||
|
||||
static void on_version(int ec, const NimFfiStr* reply, const char* em, void* ud) {
|
||||
ReplyWaiter* w = (ReplyWaiter*)ud;
|
||||
w->err_code = ec;
|
||||
if (reply && reply->data) snprintf(w->text_a, sizeof(w->text_a), "%s", reply->data);
|
||||
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
|
||||
atomic_store(&w->done, 1);
|
||||
}
|
||||
|
||||
static void on_echo(int ec, const EchoResponse* reply, const char* em, void* ud) {
|
||||
ReplyWaiter* w = (ReplyWaiter*)ud;
|
||||
w->err_code = ec;
|
||||
if (reply) {
|
||||
if (reply->echoed.data)
|
||||
snprintf(w->text_a, sizeof(w->text_a), "%s", reply->echoed.data);
|
||||
if (reply->timerName.data)
|
||||
snprintf(w->text_b, sizeof(w->text_b), "%s", reply->timerName.data);
|
||||
}
|
||||
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
|
||||
atomic_store(&w->done, 1);
|
||||
}
|
||||
|
||||
static void on_complex(int ec, const ComplexResponse* reply, const char* em, void* ud) {
|
||||
ReplyWaiter* w = (ReplyWaiter*)ud;
|
||||
w->err_code = ec;
|
||||
if (reply) {
|
||||
w->num_a = (long long)reply->itemCount;
|
||||
w->flag = (int)reply->hasNote;
|
||||
if (reply->summary.data)
|
||||
snprintf(w->text_a, sizeof(w->text_a), "%s", reply->summary.data);
|
||||
}
|
||||
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
|
||||
atomic_store(&w->done, 1);
|
||||
}
|
||||
|
||||
static void on_schedule(int ec, const ScheduleResult* reply, const char* em, void* ud) {
|
||||
ReplyWaiter* w = (ReplyWaiter*)ud;
|
||||
w->err_code = ec;
|
||||
if (reply) {
|
||||
w->num_a = (long long)reply->willRunCount;
|
||||
w->num_b = (long long)reply->firstRunAtMs;
|
||||
if (reply->jobId.data)
|
||||
snprintf(w->text_a, sizeof(w->text_a), "%s", reply->jobId.data);
|
||||
}
|
||||
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
|
||||
atomic_store(&w->done, 1);
|
||||
}
|
||||
|
||||
/* Fire an async call, block until its callback lands, and bail to cleanup on a
|
||||
* timeout or error. Relies on `ctx` being in scope for that cleanup — these
|
||||
* steps all run against the one context created in main(). */
|
||||
#define RUN(call, w) \
|
||||
do { \
|
||||
memset(&(w), 0, sizeof(w)); \
|
||||
call; \
|
||||
const char* run_err = NULL; \
|
||||
if (!wait_done(&(w).done)) \
|
||||
run_err = "FFI call did not complete"; \
|
||||
else if ((w).err_code != 0) \
|
||||
run_err = (w).err[0] ? (w).err : "unknown"; \
|
||||
if (run_err) { \
|
||||
fprintf(stderr, "Error: %s\n", run_err); \
|
||||
my_timer_ctx_destroy(ctx); \
|
||||
return 1; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
int main(void) {
|
||||
CreateWaiter cw;
|
||||
memset(&cw, 0, sizeof(cw));
|
||||
TimerConfig config = {nimffi_str("c-demo")};
|
||||
my_timer_ctx_create(&config, on_created, &cw);
|
||||
if (!wait_done(&cw.done) || cw.err_code != 0 || !cw.ctx) {
|
||||
fprintf(stderr, "Error: %s\n",
|
||||
cw.err[0] ? cw.err : "create did not complete");
|
||||
return 1;
|
||||
}
|
||||
MyTimerCtx* ctx = cw.ctx;
|
||||
printf("[1] Context created\n");
|
||||
|
||||
ReplyWaiter w;
|
||||
RUN(my_timer_ctx_version(ctx, on_version, &w), w);
|
||||
printf("[2] Version: %s\n", w.text_a);
|
||||
|
||||
EchoRequest echo_req = {nimffi_str("hello from C"), 50};
|
||||
RUN(my_timer_ctx_echo(ctx, &echo_req, on_echo, &w), w);
|
||||
printf("[3] Echo: echoed=%s, timerName=%s\n", w.text_a, w.text_b);
|
||||
|
||||
EchoRequest items[2] = {
|
||||
{nimffi_str("one"), 10},
|
||||
{nimffi_str("two"), 20},
|
||||
};
|
||||
NimFfiStr tags[2] = {nimffi_str("fast"), nimffi_str("c")};
|
||||
ComplexRequest complex_req;
|
||||
complex_req.messages.data = items;
|
||||
complex_req.messages.len = 2;
|
||||
complex_req.tags.data = tags;
|
||||
complex_req.tags.len = 2;
|
||||
complex_req.note.has_value = true;
|
||||
complex_req.note.value = nimffi_str("extra note");
|
||||
complex_req.retries.has_value = true;
|
||||
complex_req.retries.value = 3;
|
||||
|
||||
RUN(my_timer_ctx_complex(ctx, &complex_req, on_complex, &w), w);
|
||||
printf("[4] Complex: summary=%s, itemCount=%lld, hasNote=%d\n", w.text_a, w.num_a,
|
||||
w.flag);
|
||||
|
||||
NimFfiStr job_payload[2] = {nimffi_str("rollup"), nimffi_str("v2")};
|
||||
JobSpec job;
|
||||
job.name = nimffi_str("nightly-rollup");
|
||||
job.payload.data = job_payload;
|
||||
job.payload.len = 2;
|
||||
job.priority = 10;
|
||||
|
||||
NimFfiStr retry_on[2] = {nimffi_str("timeout"), nimffi_str("5xx")};
|
||||
RetryPolicy retry;
|
||||
retry.maxAttempts = 3;
|
||||
retry.backoffMs = 500;
|
||||
retry.retryOn.data = retry_on;
|
||||
retry.retryOn.len = 2;
|
||||
|
||||
ScheduleConfig schedule;
|
||||
schedule.startAtMs = 1000;
|
||||
schedule.intervalMs = 15000;
|
||||
schedule.jitter.has_value = true;
|
||||
schedule.jitter.value = 250;
|
||||
|
||||
RUN(my_timer_ctx_schedule(ctx, &job, &retry, &schedule, on_schedule, &w), w);
|
||||
printf("[5] Schedule: jobId=%s, willRunCount=%lld, firstRunAtMs=%lld\n",
|
||||
w.text_a, w.num_a, w.num_b);
|
||||
|
||||
uint64_t handle =
|
||||
my_timer_ctx_add_on_echo_fired_listener(ctx, on_echo_fired, NULL);
|
||||
EchoRequest evt_req = {nimffi_str("event-demo"), 1};
|
||||
memset(&w, 0, sizeof(w));
|
||||
my_timer_ctx_echo(ctx, &evt_req, on_echo, &w);
|
||||
wait_done(&w.done);
|
||||
/* The event fires from the library's dispatch thread; give it a moment. */
|
||||
sleep_ms(500);
|
||||
printf("[6] typed event onEchoFired: message=%s, echoCount=%d\n",
|
||||
g_echo_message, atomic_load(&g_echo_count));
|
||||
|
||||
my_timer_ctx_remove_event_listener(ctx, handle);
|
||||
|
||||
my_timer_ctx_destroy(ctx);
|
||||
printf("\nDone.\n");
|
||||
return 0;
|
||||
}
|
||||
1180
examples/timer/c_bindings/my_timer.h
Normal file
1180
examples/timer/c_bindings/my_timer.h
Normal file
File diff suppressed because it is too large
Load Diff
342
examples/timer/c_bindings/nim_ffi_cbor.h
Normal file
342
examples/timer/c_bindings/nim_ffi_cbor.h
Normal file
@ -0,0 +1,342 @@
|
||||
#ifndef NIM_FFI_CBOR_HELPERS_H_INCLUDED
|
||||
#define NIM_FFI_CBOR_HELPERS_H_INCLUDED
|
||||
/* Leaf CBOR codecs (scalars, text strings, byte strings) plus the buffer
|
||||
* drivers. The per-struct / per-container codecs in the library header call
|
||||
* into these by name (C has no overloading, so each leaf gets a distinct
|
||||
* nimffi_enc_* / nimffi_dec_* symbol). Guarded so two nim-ffi headers can
|
||||
* share a translation unit. */
|
||||
#include "nim_ffi_prelude.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Result delivery callback exported by the Nim dylib: `ret` is 0 on success
|
||||
* (then `msg`/`len` carry the CBOR response) or non-zero on failure (then
|
||||
* `msg`/`len` carry the error text, which is NOT NUL-terminated). */
|
||||
typedef void (*FFICallback)(int ret, const char* msg, size_t len, void* user_data);
|
||||
|
||||
/* Return / callback status codes. NIMFFI_RET_OK (0) is success; any non-zero
|
||||
* value handed to a result callback's `err_code` (or returned by a submit call)
|
||||
* is a failure. NIMFFI_RET_MISSING_CALLBACK is a special case from the Nim
|
||||
* dispatcher: the callback will never fire, so the request path must report the
|
||||
* failure itself. */
|
||||
#define NIMFFI_RET_OK 0
|
||||
#define NIMFFI_RET_ERROR 1
|
||||
#define NIMFFI_RET_MISSING_CALLBACK 2
|
||||
|
||||
/* ── leaf encoders ─────────────────────────────────────────────────────── */
|
||||
static inline CborError nimffi_enc_bool(CborEncoder* e, const bool* v) {
|
||||
return cbor_encode_boolean(e, *v);
|
||||
}
|
||||
static inline CborError nimffi_enc_i64(CborEncoder* e, const int64_t* v) {
|
||||
return cbor_encode_int(e, *v);
|
||||
}
|
||||
static inline CborError nimffi_enc_i32(CborEncoder* e, const int32_t* v) {
|
||||
return cbor_encode_int(e, (int64_t)*v);
|
||||
}
|
||||
static inline CborError nimffi_enc_i16(CborEncoder* e, const int16_t* v) {
|
||||
return cbor_encode_int(e, (int64_t)*v);
|
||||
}
|
||||
static inline CborError nimffi_enc_i8(CborEncoder* e, const int8_t* v) {
|
||||
return cbor_encode_int(e, (int64_t)*v);
|
||||
}
|
||||
static inline CborError nimffi_enc_u64(CborEncoder* e, const uint64_t* v) {
|
||||
return cbor_encode_uint(e, *v);
|
||||
}
|
||||
static inline CborError nimffi_enc_u32(CborEncoder* e, const uint32_t* v) {
|
||||
return cbor_encode_uint(e, (uint64_t)*v);
|
||||
}
|
||||
static inline CborError nimffi_enc_u16(CborEncoder* e, const uint16_t* v) {
|
||||
return cbor_encode_uint(e, (uint64_t)*v);
|
||||
}
|
||||
static inline CborError nimffi_enc_u8(CborEncoder* e, const uint8_t* v) {
|
||||
return cbor_encode_uint(e, (uint64_t)*v);
|
||||
}
|
||||
static inline CborError nimffi_enc_f64(CborEncoder* e, const double* v) {
|
||||
return cbor_encode_double(e, *v);
|
||||
}
|
||||
static inline CborError nimffi_enc_f32(CborEncoder* e, const float* v) {
|
||||
return cbor_encode_float(e, *v);
|
||||
}
|
||||
static inline CborError nimffi_enc_str(CborEncoder* e, const NimFfiStr* v) {
|
||||
return cbor_encode_text_string(e, v->data ? v->data : "", v->len);
|
||||
}
|
||||
static inline CborError nimffi_enc_bytes(CborEncoder* e, const NimFfiBytes* v) {
|
||||
return cbor_encode_byte_string(e, v->data, v->len);
|
||||
}
|
||||
|
||||
/* ── leaf decoders ─────────────────────────────────────────────────────── */
|
||||
/* After reading a leaf, the parser must advance past it; both steps
|
||||
* short-circuit on the same CborError, so they travel together. */
|
||||
static inline CborError nimffi_advance_if_ok(CborValue* it, CborError err) {
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
|
||||
static inline CborError nimffi_dec_bool(CborValue* it, bool* out) {
|
||||
if (!cbor_value_is_boolean(it)) {
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
return nimffi_advance_if_ok(it, cbor_value_get_boolean(it, out));
|
||||
}
|
||||
static inline CborError nimffi_dec_i64(CborValue* it, int64_t* out) {
|
||||
if (!cbor_value_is_integer(it)) {
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
return nimffi_advance_if_ok(it, cbor_value_get_int64_checked(it, out));
|
||||
}
|
||||
static inline CborError nimffi_dec_i32(CborValue* it, int32_t* out) {
|
||||
int64_t tmp = 0;
|
||||
CborError err = nimffi_dec_i64(it, &tmp);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (tmp < INT32_MIN || tmp > INT32_MAX) {
|
||||
return CborErrorDataTooLarge;
|
||||
}
|
||||
*out = (int32_t)tmp;
|
||||
return CborNoError;
|
||||
}
|
||||
static inline CborError nimffi_dec_i16(CborValue* it, int16_t* out) {
|
||||
int64_t tmp = 0;
|
||||
CborError err = nimffi_dec_i64(it, &tmp);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (tmp < INT16_MIN || tmp > INT16_MAX) {
|
||||
return CborErrorDataTooLarge;
|
||||
}
|
||||
*out = (int16_t)tmp;
|
||||
return CborNoError;
|
||||
}
|
||||
static inline CborError nimffi_dec_i8(CborValue* it, int8_t* out) {
|
||||
int64_t tmp = 0;
|
||||
CborError err = nimffi_dec_i64(it, &tmp);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (tmp < INT8_MIN || tmp > INT8_MAX) {
|
||||
return CborErrorDataTooLarge;
|
||||
}
|
||||
*out = (int8_t)tmp;
|
||||
return CborNoError;
|
||||
}
|
||||
static inline CborError nimffi_dec_u64(CborValue* it, uint64_t* out) {
|
||||
if (!cbor_value_is_unsigned_integer(it)) {
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
return nimffi_advance_if_ok(it, cbor_value_get_uint64(it, out));
|
||||
}
|
||||
static inline CborError nimffi_dec_u32(CborValue* it, uint32_t* out) {
|
||||
uint64_t tmp = 0;
|
||||
CborError err = nimffi_dec_u64(it, &tmp);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (tmp > UINT32_MAX) {
|
||||
return CborErrorDataTooLarge;
|
||||
}
|
||||
*out = (uint32_t)tmp;
|
||||
return CborNoError;
|
||||
}
|
||||
static inline CborError nimffi_dec_u16(CborValue* it, uint16_t* out) {
|
||||
uint64_t tmp = 0;
|
||||
CborError err = nimffi_dec_u64(it, &tmp);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (tmp > UINT16_MAX) {
|
||||
return CborErrorDataTooLarge;
|
||||
}
|
||||
*out = (uint16_t)tmp;
|
||||
return CborNoError;
|
||||
}
|
||||
static inline CborError nimffi_dec_u8(CborValue* it, uint8_t* out) {
|
||||
uint64_t tmp = 0;
|
||||
CborError err = nimffi_dec_u64(it, &tmp);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (tmp > UINT8_MAX) {
|
||||
return CborErrorDataTooLarge;
|
||||
}
|
||||
*out = (uint8_t)tmp;
|
||||
return CborNoError;
|
||||
}
|
||||
static inline CborError nimffi_dec_f64(CborValue* it, double* out) {
|
||||
if (cbor_value_is_double(it)) {
|
||||
return nimffi_advance_if_ok(it, cbor_value_get_double(it, out));
|
||||
}
|
||||
if (cbor_value_is_float(it)) {
|
||||
float f = 0.0f;
|
||||
CborError err = cbor_value_get_float(it, &f);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
*out = (double)f;
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
static inline CborError nimffi_dec_f32(CborValue* it, float* out) {
|
||||
if (cbor_value_is_float(it)) {
|
||||
return nimffi_advance_if_ok(it, cbor_value_get_float(it, out));
|
||||
}
|
||||
if (cbor_value_is_double(it)) {
|
||||
double d = 0.0;
|
||||
CborError err = cbor_value_get_double(it, &d);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
*out = (float)d;
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
static inline CborError nimffi_dec_str(CborValue* it, NimFfiStr* out) {
|
||||
if (!cbor_value_is_text_string(it)) {
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
size_t len = 0;
|
||||
CborError err = cbor_value_get_string_length(it, &len);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (len == SIZE_MAX) { /* len + 1 would wrap to a 0-byte allocation */
|
||||
return CborErrorDataTooLarge;
|
||||
}
|
||||
/* one extra byte so a NUL-free payload is a valid C string */
|
||||
out->data = (char*)malloc(len + 1);
|
||||
if (!out->data) {
|
||||
return CborErrorOutOfMemory;
|
||||
}
|
||||
out->len = len;
|
||||
size_t copied = len;
|
||||
err = cbor_value_copy_text_string(it, out->data, &copied, NULL);
|
||||
if (err) {
|
||||
free(out->data);
|
||||
out->data = NULL;
|
||||
out->len = 0;
|
||||
return err;
|
||||
}
|
||||
out->data[len] = '\0';
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
static inline CborError nimffi_dec_bytes(CborValue* it, NimFfiBytes* out) {
|
||||
if (!cbor_value_is_byte_string(it)) {
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
size_t len = 0;
|
||||
CborError err = cbor_value_get_string_length(it, &len);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
out->data = (uint8_t*)malloc(len ? len : 1);
|
||||
if (!out->data) {
|
||||
return CborErrorOutOfMemory;
|
||||
}
|
||||
out->len = len;
|
||||
size_t copied = len;
|
||||
err = cbor_value_copy_byte_string(it, out->data, &copied, NULL);
|
||||
if (err) {
|
||||
free(out->data);
|
||||
out->data = NULL;
|
||||
out->len = 0;
|
||||
return err;
|
||||
}
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
|
||||
/* ── buffer drivers ────────────────────────────────────────────────────── */
|
||||
typedef CborError (*nimffi_enc_fn)(CborEncoder*, const void*);
|
||||
typedef CborError (*nimffi_dec_fn)(CborValue*, void*);
|
||||
|
||||
static inline char* nimffi_dup_cstr(const char* s) {
|
||||
size_t n = strlen(s) + 1;
|
||||
char* p = (char*)malloc(n);
|
||||
if (p) {
|
||||
memcpy(p, s, n);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/* NUL-terminated copy of a length-delimited (not NUL-terminated) byte run,
|
||||
* for turning the FFICallback's raw error `msg`/`len` into a C string. */
|
||||
static inline char* nimffi_dup_cstr_n(const char* s, size_t n) {
|
||||
char* p = (char*)malloc(n + 1);
|
||||
if (p) {
|
||||
if (n > 0) {
|
||||
memcpy(p, s, n);
|
||||
}
|
||||
p[n] = '\0';
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/* Encode `val` with `fn` into a freshly malloc'd buffer, doubling on overflow.
|
||||
* Returns 0 and sets out/outlen on success; -1 and *err (heap) on failure. */
|
||||
static inline int nimffi_encode_to_buf(
|
||||
nimffi_enc_fn fn, const void* val,
|
||||
uint8_t** out, size_t* outlen, char** err) {
|
||||
size_t cap = 4096;
|
||||
uint8_t* buf = (uint8_t*)malloc(cap);
|
||||
if (!buf) {
|
||||
if (err) *err = nimffi_dup_cstr("out of memory");
|
||||
return -1;
|
||||
}
|
||||
for (;;) {
|
||||
CborEncoder enc;
|
||||
cbor_encoder_init(&enc, buf, cap, 0);
|
||||
CborError e = fn(&enc, val);
|
||||
if (e == CborNoError) {
|
||||
*outlen = cbor_encoder_get_buffer_size(&enc, buf);
|
||||
*out = buf;
|
||||
return 0;
|
||||
}
|
||||
if (e == CborErrorOutOfMemory) {
|
||||
size_t extra = cbor_encoder_get_extra_bytes_needed(&enc);
|
||||
cap += extra > 0 ? extra : cap;
|
||||
uint8_t* grown = (uint8_t*)realloc(buf, cap);
|
||||
if (!grown) {
|
||||
free(buf);
|
||||
if (err) *err = nimffi_dup_cstr("out of memory");
|
||||
return -1;
|
||||
}
|
||||
buf = grown;
|
||||
continue;
|
||||
}
|
||||
free(buf);
|
||||
if (err) *err = nimffi_dup_cstr(cbor_error_string(e));
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Decode a CBOR buffer into `out` with `fn`. Returns 0 on success; -1 and
|
||||
* *err (heap) on failure. */
|
||||
static inline int nimffi_decode_from_buf(
|
||||
nimffi_dec_fn fn, const uint8_t* buf, size_t len,
|
||||
void* out, char** err) {
|
||||
CborParser parser;
|
||||
CborValue it;
|
||||
CborError e = cbor_parser_init(buf, len, 0, &parser, &it);
|
||||
if (e != CborNoError) {
|
||||
if (err) *err = nimffi_dup_cstr(cbor_error_string(e));
|
||||
return -1;
|
||||
}
|
||||
e = fn(&it, out);
|
||||
if (e != CborNoError) {
|
||||
if (err) *err = nimffi_dup_cstr(cbor_error_string(e));
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NIM_FFI_CBOR_HELPERS_H_INCLUDED */
|
||||
|
||||
89
examples/timer/c_bindings/nim_ffi_prelude.h
Normal file
89
examples/timer/c_bindings/nim_ffi_prelude.h
Normal file
@ -0,0 +1,89 @@
|
||||
#ifndef NIM_FFI_PRELUDE_H_INCLUDED
|
||||
#define NIM_FFI_PRELUDE_H_INCLUDED
|
||||
/* Generated C binding for a nim-ffi library. Requests/responses travel as
|
||||
* CBOR (encoded with vendored TinyCBOR on this side, matching the Nim-side
|
||||
* cbor_serial codec on the wire — both ends speak RFC 8949).
|
||||
*
|
||||
* The API is asynchronous: every method/constructor takes a result callback
|
||||
* and returns immediately. The callback fires exactly once — synchronously on
|
||||
* a submit-time failure, otherwise from the Nim dispatch thread when the reply
|
||||
* arrives.
|
||||
*
|
||||
* Memory ownership contract:
|
||||
* - Request-side strings/sequences are *borrowed*: the binding only reads
|
||||
* them while encoding, so a string literal wrapped with nimffi_str() is
|
||||
* fine and is never freed by the binding.
|
||||
* - Response values and error strings passed into a result callback are
|
||||
* *owned by the binding* and valid only for the duration of that callback;
|
||||
* the binding reclaims them once the callback returns. The caller never
|
||||
* frees them. (The generated <lib>_free_<Type>() helpers are internal — the
|
||||
* trampolines use them to reclaim decoded payloads.)
|
||||
* - A context handle delivered to a constructor callback is the exception:
|
||||
* ownership transfers to the caller, who releases it with
|
||||
* <lib>_ctx_destroy(). It is a lifecycle handle, not returned data.
|
||||
*
|
||||
* Trust boundary: the decoders assume the CBOR they parse was produced by the
|
||||
* paired Nim library. They reject malformed input rather than trusting it, but
|
||||
* they are not hardened against a hostile peer feeding crafted payloads through
|
||||
* the raw nimffi_decode_from_buf entry point.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <tinycbor/cbor.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Owned, length-delimited UTF-8 text (Nim `string`/`cstring`). On the request
|
||||
* side `data` may point at borrowed storage (see nimffi_str); on the response
|
||||
* side it is heap-allocated and freed by nimffi_free_str. Always NUL-padded by
|
||||
* one byte after decode so `data` is usable as a C string when it has no
|
||||
* embedded NULs. */
|
||||
typedef struct {
|
||||
char* data;
|
||||
size_t len;
|
||||
} NimFfiStr;
|
||||
|
||||
/* Owned, length-delimited byte buffer (Nim `seq[byte]`). */
|
||||
typedef struct {
|
||||
uint8_t* data;
|
||||
size_t len;
|
||||
} NimFfiBytes;
|
||||
|
||||
/* Wrap a borrowed C string for use as a request field. The returned view is
|
||||
* not owned by the binding and must outlive the call that encodes it. */
|
||||
static inline NimFfiStr nimffi_str(const char* s) {
|
||||
NimFfiStr v;
|
||||
v.data = (char*)s;
|
||||
v.len = s ? strlen(s) : 0;
|
||||
return v;
|
||||
}
|
||||
|
||||
static inline void nimffi_free_str(NimFfiStr* v) {
|
||||
if (!v || !v->data) {
|
||||
return;
|
||||
}
|
||||
free(v->data);
|
||||
v->data = NULL;
|
||||
v->len = 0;
|
||||
}
|
||||
|
||||
static inline void nimffi_free_bytes(NimFfiBytes* v) {
|
||||
if (!v || !v->data) {
|
||||
return;
|
||||
}
|
||||
free(v->data);
|
||||
v->data = NULL;
|
||||
v->len = 0;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NIM_FFI_PRELUDE_H_INCLUDED */
|
||||
|
||||
@ -27,92 +27,10 @@ if("${REPO_ROOT}" STREQUAL "")
|
||||
message(FATAL_ERROR "Cannot find repo root (no ffi.nimble in any ancestor)")
|
||||
endif()
|
||||
|
||||
get_filename_component(NIM_SRC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../timer.nim"
|
||||
ABSOLUTE)
|
||||
|
||||
find_program(NIM_EXECUTABLE nim REQUIRED)
|
||||
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
set(NIM_LIB_FILE "${REPO_ROOT}/libmy_timer.dylib")
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
set(NIM_LIB_FILE "${REPO_ROOT}/my_timer.dll")
|
||||
# MSVC consumers link against the `.lib` import library, not the DLL.
|
||||
# MinGW's ld emits one when asked via `--out-implib`; the resulting COFF
|
||||
# archive is readable by MSVC's link.exe.
|
||||
set(NIM_IMPLIB_FILE "${REPO_ROOT}/my_timer.lib")
|
||||
else()
|
||||
set(NIM_LIB_FILE "${REPO_ROOT}/libmy_timer.so")
|
||||
endif()
|
||||
|
||||
# On Windows the default Nim toolchain (mingw gcc) doesn't emit an import
|
||||
# library unless told to. Without it, MSVC consumers can't resolve any
|
||||
# symbol exported by the DLL at link time.
|
||||
set(NIM_IMPLIB_PASSL "")
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
set(NIM_IMPLIB_PASSL "--passL:-Wl,--out-implib,${NIM_IMPLIB_FILE}")
|
||||
endif()
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${NIM_LIB_FILE}"
|
||||
COMMAND "${NIM_EXECUTABLE}" c
|
||||
--mm:orc
|
||||
-d:chronicles_log_level=WARN
|
||||
--app:lib
|
||||
--noMain
|
||||
"--nimMainPrefix:libmy_timer"
|
||||
${NIM_IMPLIB_PASSL}
|
||||
"-o:${NIM_LIB_FILE}"
|
||||
"${NIM_SRC}"
|
||||
WORKING_DIRECTORY "${REPO_ROOT}"
|
||||
DEPENDS "${NIM_SRC}"
|
||||
BYPRODUCTS "${NIM_IMPLIB_FILE}"
|
||||
COMMENT "Compiling Nim library libmy_timer"
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(my_timer_nim_lib ALL DEPENDS "${NIM_LIB_FILE}")
|
||||
|
||||
# On Windows, an `IMPORTED SHARED` target needs IMPORTED_IMPLIB pointing at
|
||||
# the `.lib` import library so MSVC's `link.exe` can resolve symbols. The
|
||||
# Visual Studio multi-config generator did not pick up `IMPORTED_IMPLIB` —
|
||||
# nor per-config `IMPORTED_IMPLIB_<CONFIG>` variants — and emitted
|
||||
# `my_timer-NOTFOUND.obj` into every link line. Side-step the IMPORTED
|
||||
# machinery on Windows by exposing the import library through a plain
|
||||
# INTERFACE library that links the `.lib` by path.
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
add_library(my_timer INTERFACE)
|
||||
target_link_libraries(my_timer INTERFACE "${NIM_IMPLIB_FILE}")
|
||||
else()
|
||||
add_library(my_timer SHARED IMPORTED GLOBAL)
|
||||
set_target_properties(my_timer PROPERTIES IMPORTED_LOCATION "${NIM_LIB_FILE}")
|
||||
endif()
|
||||
add_dependencies(my_timer my_timer_nim_lib)
|
||||
|
||||
# Absolute path to the runtime library (DLL/dylib/so). Exposed via the cache
|
||||
# so consumers in other directories (e.g. tests/e2e/cpp) can stage the DLL
|
||||
# next to their executable on Windows.
|
||||
set(my_timer_RUNTIME_LIB "${NIM_LIB_FILE}" CACHE INTERNAL
|
||||
"Absolute path to the my_timer runtime library")
|
||||
|
||||
# ── TinyCBOR (vendored at ffi/codegen/templates/cpp/vendor/tinycbor) ─────────
|
||||
# Guarded so two sibling cpp_bindings dirs in one parent project don't redefine
|
||||
# the `tinycbor` target.
|
||||
set(TINYCBOR_SRC_DIR "${REPO_ROOT}/ffi/codegen/templates/cpp/vendor")
|
||||
if(NOT TARGET tinycbor)
|
||||
add_library(tinycbor STATIC
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor/cborencoder.c"
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor/cborencoder_close_container_checked.c"
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor/cborparser.c"
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor/cborparser_dup_string.c"
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor/cborerrorstrings.c"
|
||||
)
|
||||
target_include_directories(tinycbor PUBLIC
|
||||
"${TINYCBOR_SRC_DIR}" # consumer uses #include <tinycbor/cbor.h>
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor" # internal _p.h includes resolve here
|
||||
)
|
||||
set_property(TARGET tinycbor PROPERTY C_STANDARD 99)
|
||||
set_property(TARGET tinycbor PROPERTY POSITION_INDEPENDENT_CODE ON)
|
||||
endif()
|
||||
# Build the Nim dylib + vendored TinyCBOR (shared with the C backend).
|
||||
set(NIM_FFI_LIB my_timer)
|
||||
set(NIM_FFI_SRC ../timer.nim)
|
||||
include("${REPO_ROOT}/ffi/codegen/templates/nim_ffi_lib.cmake")
|
||||
|
||||
add_library(my_timer_headers INTERFACE)
|
||||
target_include_directories(my_timer_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
|
||||
@ -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"
|
||||
|
||||
45
ffi.nimble
45
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"
|
||||
|
||||
926
ffi/codegen/c.nim
Normal file
926
ffi/codegen/c.nim
Normal file
@ -0,0 +1,926 @@
|
||||
## C99 binding generator for the nim-ffi framework.
|
||||
## Emits a header-only C binding plus a CMakeLists.txt. The binding is split
|
||||
## into three headers so the example reads cleanly: `nim_ffi_prelude.h` (owned
|
||||
## string/byte types + libc includes), `nim_ffi_cbor.h` (leaf CBOR codecs and
|
||||
## buffer drivers, includes the prelude), and `<lib>.h` (the library-specific
|
||||
## structs, codecs and async API, includes the cbor header). Requests/responses
|
||||
## travel as CBOR (encoded with the same vendored TinyCBOR the C++ backend
|
||||
## uses, matching the Nim-side cbor_serial codec — both ends speak RFC 8949).
|
||||
##
|
||||
## C has neither generics nor overloading, so the codecs the C++ backend gets
|
||||
## from templates are monomorphised here: every distinct `seq[T]` / `Option[T]`
|
||||
## becomes its own struct + encode/decode/free triple, and each leaf type has a
|
||||
## distinctly-named codec emitted by the cbor_helpers template.
|
||||
|
||||
import std/[os, strutils, tables, sets]
|
||||
import ./meta, ./string_helpers, ./c_cpp_common
|
||||
|
||||
## Wire-format C type for any Nim `ptr T` / `pointer`. Fixed 64-bit so the CBOR
|
||||
## payload size is stable regardless of host architecture (mirrors CppPtrType).
|
||||
const CPtrType* = "uint64_t"
|
||||
|
||||
const
|
||||
HeaderPreludeTpl = staticRead("templates/c/header_prelude.h.tpl")
|
||||
CborHelpersTpl = staticRead("templates/c/cbor_helpers.h.tpl")
|
||||
CMakeListsTpl = staticRead("templates/c/CMakeLists.txt.tpl")
|
||||
|
||||
# Shared headers written alongside the library header. Their names match the
|
||||
# include guards baked into the templates and the `#include` the cbor header
|
||||
# emits for the prelude.
|
||||
PreludeHeaderName* = "nim_ffi_prelude.h"
|
||||
CborHeaderName* = "nim_ffi_cbor.h"
|
||||
|
||||
type LeafInfo = tuple[ok: bool, cType: string, suffix: string, owns: bool]
|
||||
|
||||
func leafCType(t: string): LeafInfo =
|
||||
## Maps a Nim leaf type to its C type, codec suffix and whether a decoded
|
||||
## value owns heap memory. `ok` is false for composite types (seq/Option/
|
||||
## user structs), which are monomorphised separately.
|
||||
case t
|
||||
of "int", "int64":
|
||||
(true, "int64_t", "i64", false)
|
||||
of "int32":
|
||||
(true, "int32_t", "i32", false)
|
||||
of "int16":
|
||||
(true, "int16_t", "i16", false)
|
||||
of "int8":
|
||||
(true, "int8_t", "i8", false)
|
||||
of "uint", "uint64":
|
||||
(true, "uint64_t", "u64", false)
|
||||
of "uint32":
|
||||
(true, "uint32_t", "u32", false)
|
||||
of "uint16":
|
||||
(true, "uint16_t", "u16", false)
|
||||
of "uint8", "byte":
|
||||
(true, "uint8_t", "u8", false)
|
||||
of "bool":
|
||||
(true, "bool", "bool", false)
|
||||
of "float", "float64":
|
||||
(true, "double", "f64", false)
|
||||
of "float32":
|
||||
(true, "float", "f32", false)
|
||||
of "pointer":
|
||||
(true, CPtrType, "u64", false)
|
||||
of "string", "cstring":
|
||||
(true, "NimFfiStr", "str", true)
|
||||
else:
|
||||
(false, "", "", false)
|
||||
|
||||
func cToken(cType: string): string =
|
||||
## Short PascalCase token used to build monomorphised container names and
|
||||
## codec-adapter symbols. Composite C type names are already unique C
|
||||
## identifiers, so they pass through verbatim.
|
||||
case cType
|
||||
of "int64_t": "I64"
|
||||
of "int32_t": "I32"
|
||||
of "int16_t": "I16"
|
||||
of "int8_t": "I8"
|
||||
of "uint64_t": "U64"
|
||||
of "uint32_t": "U32"
|
||||
of "uint16_t": "U16"
|
||||
of "uint8_t": "U8"
|
||||
of "bool": "Bool"
|
||||
of "double": "F64"
|
||||
of "float": "F32"
|
||||
of "NimFfiStr": "Str"
|
||||
of "NimFfiBytes": "Bytes"
|
||||
else: cType
|
||||
|
||||
func leafSuffix(cType: string): string =
|
||||
## Inverse of leafCType's cType→suffix for the leaf codecs the template
|
||||
## provides; empty string for composite types.
|
||||
case cType
|
||||
of "int64_t": "i64"
|
||||
of "int32_t": "i32"
|
||||
of "int16_t": "i16"
|
||||
of "int8_t": "i8"
|
||||
of "uint64_t": "u64"
|
||||
of "uint32_t": "u32"
|
||||
of "uint16_t": "u16"
|
||||
of "uint8_t": "u8"
|
||||
of "bool": "bool"
|
||||
of "double": "f64"
|
||||
of "float": "f32"
|
||||
of "NimFfiStr": "str"
|
||||
of "NimFfiBytes": "bytes"
|
||||
else: ""
|
||||
|
||||
type CTypeReg = object
|
||||
libName: string ## snake_case symbol prefix, e.g. "my_timer"
|
||||
libType: string ## PascalCase container-name prefix, e.g. "MyTimer"
|
||||
typeTable: Table[string, FFITypeMeta] ## user structs + synthetic Req structs
|
||||
emitted: HashSet[string] ## composite C type names already emitted
|
||||
owns: Table[string, bool] ## C type name → owns-heap-memory
|
||||
decls: seq[string] ## struct typedefs, dependency order
|
||||
codecs: seq[string] ## enc/dec/free defs, dependency order
|
||||
|
||||
func encFn(reg: CTypeReg, cType: string): string =
|
||||
let suffix = leafSuffix(cType)
|
||||
if suffix.len > 0:
|
||||
return "nimffi_enc_" & suffix
|
||||
reg.libName & "_enc_" & cType
|
||||
|
||||
func decFn(reg: CTypeReg, cType: string): string =
|
||||
let suffix = leafSuffix(cType)
|
||||
if suffix.len > 0:
|
||||
return "nimffi_dec_" & suffix
|
||||
reg.libName & "_dec_" & cType
|
||||
|
||||
func freeFn(reg: CTypeReg, cType: string): string =
|
||||
## Free-function name for `cType`, or "" when the type owns no heap memory.
|
||||
case cType
|
||||
of "NimFfiStr":
|
||||
"nimffi_free_str"
|
||||
of "NimFfiBytes":
|
||||
"nimffi_free_bytes"
|
||||
else:
|
||||
if leafSuffix(cType).len > 0:
|
||||
""
|
||||
elif reg.owns.getOrDefault(cType, false):
|
||||
reg.libName & "_free_" & cType
|
||||
else:
|
||||
""
|
||||
|
||||
proc emitSeqType(reg: var CTypeReg, name, elemC: string) =
|
||||
let eEnc = encFn(reg, elemC)
|
||||
let eDec = decFn(reg, elemC)
|
||||
let eFree = freeFn(reg, elemC)
|
||||
reg.decls.add(
|
||||
"typedef struct {\n " & elemC & "* data;\n size_t len;\n} " & name & ";"
|
||||
)
|
||||
var body: seq[string] = @[]
|
||||
body.add("static inline CborError " & reg.libName & "_enc_" & name & "(")
|
||||
body.add(" CborEncoder* e, const " & name & "* v) {")
|
||||
body.add(" CborEncoder arr;")
|
||||
body.add(" CborError err = cbor_encoder_create_array(e, &arr, v->len);")
|
||||
body.add(" if (err) return err;")
|
||||
body.add(" for (size_t i = 0; i < v->len; i++) {")
|
||||
body.add(" err = " & eEnc & "(&arr, &v->data[i]);")
|
||||
body.add(" if (err) return err;")
|
||||
body.add(" }")
|
||||
body.add(" return cbor_encoder_close_container(e, &arr);")
|
||||
body.add("}")
|
||||
body.add("static inline CborError " & reg.libName & "_dec_" & name & "(")
|
||||
body.add(" CborValue* it, " & name & "* out) {")
|
||||
body.add(" if (!cbor_value_is_array(it)) return CborErrorImproperValue;")
|
||||
body.add(" size_t len = 0;")
|
||||
body.add(" CborError err = cbor_value_get_array_length(it, &len);")
|
||||
body.add(" if (err) return err;")
|
||||
body.add(
|
||||
" out->data = (" & elemC & "*)calloc(len ? len : 1, sizeof(" & elemC & "));"
|
||||
)
|
||||
body.add(" if (!out->data) return CborErrorOutOfMemory;")
|
||||
body.add(" out->len = len;")
|
||||
body.add(" CborValue inner;")
|
||||
body.add(" err = cbor_value_enter_container(it, &inner);")
|
||||
body.add(" if (err) return err;")
|
||||
body.add(" for (size_t i = 0; i < len; i++) {")
|
||||
body.add(" err = " & eDec & "(&inner, &out->data[i]);")
|
||||
body.add(" if (err) return err;")
|
||||
body.add(" }")
|
||||
body.add(" return cbor_value_leave_container(it, &inner);")
|
||||
body.add("}")
|
||||
body.add(
|
||||
"static inline void " & reg.libName & "_free_" & name & "(" & name & "* v) {"
|
||||
)
|
||||
body.add(" if (!v || !v->data) return;")
|
||||
if eFree.len > 0:
|
||||
body.add(" for (size_t i = 0; i < v->len; i++) " & eFree & "(&v->data[i]);")
|
||||
body.add(" free(v->data);")
|
||||
body.add(" v->data = NULL;")
|
||||
body.add(" v->len = 0;")
|
||||
body.add("}")
|
||||
reg.codecs.add(body.join("\n"))
|
||||
reg.owns[name] = true
|
||||
|
||||
proc emitOptType(reg: var CTypeReg, name, elemC: string, elemOwns: bool) =
|
||||
let eEnc = encFn(reg, elemC)
|
||||
let eDec = decFn(reg, elemC)
|
||||
let eFree = freeFn(reg, elemC)
|
||||
reg.decls.add(
|
||||
"typedef struct {\n bool has_value;\n " & elemC & " value;\n} " & name & ";"
|
||||
)
|
||||
var body: seq[string] = @[]
|
||||
body.add("static inline CborError " & reg.libName & "_enc_" & name & "(")
|
||||
body.add(" CborEncoder* e, const " & name & "* v) {")
|
||||
body.add(" if (!v->has_value) return cbor_encode_null(e);")
|
||||
body.add(" return " & eEnc & "(e, &v->value);")
|
||||
body.add("}")
|
||||
body.add("static inline CborError " & reg.libName & "_dec_" & name & "(")
|
||||
body.add(" CborValue* it, " & name & "* out) {")
|
||||
body.add(" if (cbor_value_is_null(it)) {")
|
||||
body.add(" out->has_value = false;")
|
||||
body.add(" memset(&out->value, 0, sizeof(out->value));")
|
||||
body.add(" return cbor_value_advance(it);")
|
||||
body.add(" }")
|
||||
body.add(" out->has_value = true;")
|
||||
body.add(" return " & eDec & "(it, &out->value);")
|
||||
body.add("}")
|
||||
if elemOwns and eFree.len > 0:
|
||||
body.add(
|
||||
"static inline void " & reg.libName & "_free_" & name & "(" & name & "* v) {"
|
||||
)
|
||||
body.add(" if (!v || !v->has_value) return;")
|
||||
body.add(" " & eFree & "(&v->value);")
|
||||
body.add(" v->has_value = false;")
|
||||
body.add("}")
|
||||
reg.codecs.add(body.join("\n"))
|
||||
reg.owns[name] = elemOwns
|
||||
|
||||
proc ensureCType(reg: var CTypeReg, nimType: string): tuple[cType: string, owns: bool]
|
||||
|
||||
proc emitStructType(reg: var CTypeReg, t: FFITypeMeta) =
|
||||
var fieldDecls: seq[string] = @[]
|
||||
var members: seq[tuple[name, cType: string, owns: bool]] = @[]
|
||||
for f in t.fields:
|
||||
let (cType, owns) = ensureCType(reg, f.typeName)
|
||||
fieldDecls.add(" " & cType & " " & f.name & ";")
|
||||
members.add((f.name, cType, owns))
|
||||
if members.len == 0:
|
||||
fieldDecls.add(" char _nimffi_empty; /* C forbids empty structs */")
|
||||
reg.decls.add("typedef struct {\n" & fieldDecls.join("\n") & "\n} " & t.name & ";")
|
||||
|
||||
var body: seq[string] = @[]
|
||||
body.add("static inline CborError " & reg.libName & "_enc_" & t.name & "(")
|
||||
body.add(" CborEncoder* e, const " & t.name & "* v) {")
|
||||
if members.len == 0:
|
||||
body.add(" (void)v;")
|
||||
body.add(" CborEncoder m;")
|
||||
body.add(" CborError err = cbor_encoder_create_map(e, &m, " & $members.len & ");")
|
||||
body.add(" if (err) return err;")
|
||||
for mem in members:
|
||||
body.add(" err = cbor_encode_text_stringz(&m, \"" & mem.name & "\");")
|
||||
body.add(" if (err) return err;")
|
||||
body.add(" err = " & encFn(reg, mem.cType) & "(&m, &v->" & mem.name & ");")
|
||||
body.add(" if (err) return err;")
|
||||
body.add(" return cbor_encoder_close_container(e, &m);")
|
||||
body.add("}")
|
||||
|
||||
body.add("static inline CborError " & reg.libName & "_dec_" & t.name & "(")
|
||||
body.add(" CborValue* it, " & t.name & "* out) {")
|
||||
body.add(" if (!cbor_value_is_map(it)) return CborErrorImproperValue;")
|
||||
if members.len == 0:
|
||||
body.add(" (void)out;")
|
||||
body.add(" return cbor_value_advance(it);")
|
||||
else:
|
||||
body.add(" CborValue field;")
|
||||
body.add(" CborError err;")
|
||||
for mem in members:
|
||||
body.add(" err = cbor_value_map_find_value(it, \"" & mem.name & "\", &field);")
|
||||
body.add(" if (err) return err;")
|
||||
body.add(" if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;")
|
||||
body.add(
|
||||
" err = " & decFn(reg, mem.cType) & "(&field, &out->" & mem.name & ");"
|
||||
)
|
||||
body.add(" if (err) return err;")
|
||||
body.add(" return cbor_value_advance(it);")
|
||||
body.add("}")
|
||||
|
||||
var owns = false
|
||||
for mem in members:
|
||||
if mem.owns:
|
||||
owns = true
|
||||
if owns:
|
||||
body.add(
|
||||
"static inline void " & reg.libName & "_free_" & t.name & "(" & t.name & "* v) {"
|
||||
)
|
||||
body.add(" if (!v) return;")
|
||||
for mem in members:
|
||||
let ff = freeFn(reg, mem.cType)
|
||||
if mem.owns and ff.len > 0:
|
||||
body.add(" " & ff & "(&v->" & mem.name & ");")
|
||||
body.add("}")
|
||||
reg.codecs.add(body.join("\n"))
|
||||
reg.owns[t.name] = owns
|
||||
|
||||
proc ensureCType(reg: var CTypeReg, nimType: string): tuple[cType: string, owns: bool] =
|
||||
let t = nimType.strip()
|
||||
if t.startsWith("ptr ") or t == "pointer":
|
||||
return (CPtrType, false)
|
||||
let leaf = leafCType(t)
|
||||
if leaf.ok:
|
||||
return (leaf.cType, leaf.owns)
|
||||
|
||||
let seqInner = genericInnerType(t, "seq[")
|
||||
if seqInner.len > 0:
|
||||
let inner = seqInner.strip()
|
||||
if inner == "byte" or inner == "uint8":
|
||||
return ("NimFfiBytes", true)
|
||||
let (elemC, _) = ensureCType(reg, inner)
|
||||
let name = reg.libType & "Seq_" & cToken(elemC)
|
||||
if name notin reg.emitted:
|
||||
reg.emitted.incl(name)
|
||||
emitSeqType(reg, name, elemC)
|
||||
return (name, true)
|
||||
|
||||
var optInner = genericInnerType(t, "Option[")
|
||||
if optInner.len == 0:
|
||||
optInner = genericInnerType(t, "Maybe[")
|
||||
if optInner.len > 0:
|
||||
let (elemC, elemOwns) = ensureCType(reg, optInner.strip())
|
||||
let name = reg.libType & "Opt_" & cToken(elemC)
|
||||
if name notin reg.emitted:
|
||||
reg.emitted.incl(name)
|
||||
emitOptType(reg, name, elemC, elemOwns)
|
||||
return (name, reg.owns.getOrDefault(name, false))
|
||||
|
||||
if t notin reg.emitted:
|
||||
reg.emitted.incl(t)
|
||||
if t in reg.typeTable:
|
||||
emitStructType(reg, reg.typeTable[t])
|
||||
else:
|
||||
reg.decls.add("/* unknown type referenced: " & t & " */")
|
||||
(t, reg.owns.getOrDefault(t, false))
|
||||
|
||||
proc reqTypeMeta(p: FFIProcMeta): FFITypeMeta =
|
||||
## Synthesises the per-proc Req struct as an FFITypeMeta so it flows through
|
||||
## the same monomorphisation path as user-declared types. Pointer/handle
|
||||
## params ride the wire as the opaque uint64 pointer type.
|
||||
var fields: seq[FFIFieldMeta] = @[]
|
||||
for ep in p.extraParams:
|
||||
let typeName = if ep.ridesAsPtr(): "pointer" else: ep.typeName
|
||||
fields.add(FFIFieldMeta(name: ep.name, typeName: typeName))
|
||||
FFITypeMeta(name: reqStructName(p), fields: fields)
|
||||
|
||||
func paramByValue(nimType: string, ridesAsPtr: bool): bool =
|
||||
## Scalars / opaque pointers / string views pass by value; composite
|
||||
## aggregates (seq, Option, user structs) pass by const pointer.
|
||||
if ridesAsPtr:
|
||||
return true
|
||||
leafCType(nimType.strip()).ok
|
||||
|
||||
proc cReturnType(reg: var CTypeReg, p: FFIProcMeta): string =
|
||||
if p.returnRidesAsPtr():
|
||||
return CPtrType
|
||||
ensureCType(reg, p.returnTypeName).cType
|
||||
|
||||
proc buildReqParams(
|
||||
reg: var CTypeReg, eps: seq[FFIParamMeta]
|
||||
): tuple[params, assigns: seq[string]] =
|
||||
var params: seq[string] = @[]
|
||||
var assigns: seq[string] = @[]
|
||||
for ep in eps:
|
||||
let rides = ep.ridesAsPtr()
|
||||
let cType =
|
||||
if rides:
|
||||
CPtrType
|
||||
else:
|
||||
ensureCType(reg, ep.typeName).cType
|
||||
if paramByValue(ep.typeName, rides):
|
||||
params.add(cType & " " & ep.name)
|
||||
assigns.add(" ffi_req." & ep.name & " = " & ep.name & ";")
|
||||
else:
|
||||
params.add("const " & cType & "* " & ep.name)
|
||||
assigns.add(" ffi_req." & ep.name & " = *" & ep.name & ";")
|
||||
(params, assigns)
|
||||
|
||||
proc evNames(
|
||||
libType, libName: string, ev: FFIEventMeta
|
||||
): tuple[fnType, boxType, tramp, regName: string] =
|
||||
let pascal = capitalizeFirstLetter(ev.nimProcName)
|
||||
let snake = camelToSnakeCase(ev.nimProcName)
|
||||
(
|
||||
libType & pascal & "Fn",
|
||||
libType & pascal & "Box",
|
||||
libName & "_" & snake & "_trampoline",
|
||||
libName & "_ctx_add_" & snake & "_listener",
|
||||
)
|
||||
|
||||
proc emitEventMachinery(
|
||||
lines: var seq[string],
|
||||
reg: CTypeReg,
|
||||
libType, libName: string,
|
||||
events: seq[FFIEventMeta],
|
||||
) =
|
||||
if events.len == 0:
|
||||
return
|
||||
lines.add("/* Event listener machinery */")
|
||||
for ev in events:
|
||||
let n = evNames(libType, libName, ev)
|
||||
let payC = ev.payloadTypeName
|
||||
let payFree = freeFn(reg, payC)
|
||||
lines.add(
|
||||
"typedef void (*" & n.fnType & ")(const " & payC & "* evt, void* user_data);"
|
||||
)
|
||||
lines.add(
|
||||
"typedef struct { " & n.fnType & " fn; void* user_data; } " & n.boxType & ";"
|
||||
)
|
||||
lines.add(
|
||||
"static void " & n.tramp & "(int ret, const char* msg, size_t len, void* ud) {"
|
||||
)
|
||||
lines.add(" if (!ud || ret != 0 || !msg || len == 0) return;")
|
||||
lines.add(" " & n.boxType & "* box = (" & n.boxType & "*)ud;")
|
||||
lines.add(" if (!box->fn) return;")
|
||||
lines.add(" CborParser parser;")
|
||||
lines.add(" CborValue it;")
|
||||
lines.add(
|
||||
" if (cbor_parser_init((const uint8_t*)msg, len, 0, &parser, &it) != CborNoError) return;"
|
||||
)
|
||||
lines.add(" if (!cbor_value_is_map(&it)) return;")
|
||||
lines.add(" CborValue payloadField;")
|
||||
lines.add(
|
||||
" if (cbor_value_map_find_value(&it, \"payload\", &payloadField) != CborNoError) return;"
|
||||
)
|
||||
lines.add(" " & payC & " payload;")
|
||||
lines.add(" memset(&payload, 0, sizeof(payload));")
|
||||
lines.add(
|
||||
" if (" & decFn(reg, payC) & "(&payloadField, &payload) != CborNoError) return;"
|
||||
)
|
||||
lines.add(" box->fn(&payload, box->user_data);")
|
||||
if payFree.len > 0:
|
||||
lines.add(" " & payFree & "(&payload);")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
proc emitContextStruct(
|
||||
lines: var seq[string], ctxType: string, events: seq[FFIEventMeta]
|
||||
) =
|
||||
lines.add("/* ============================================================ */")
|
||||
lines.add("/* High-level context wrapper */")
|
||||
lines.add("/* ============================================================ */")
|
||||
if events.len > 0:
|
||||
lines.add("typedef struct {")
|
||||
lines.add(" uint64_t id;")
|
||||
lines.add(" void* box;")
|
||||
lines.add("} " & ctxType & "Listener;")
|
||||
lines.add("")
|
||||
lines.add("typedef struct {")
|
||||
lines.add(" void* ptr;")
|
||||
if events.len > 0:
|
||||
lines.add(" " & ctxType & "Listener* listeners;")
|
||||
lines.add(" size_t listeners_len;")
|
||||
lines.add(" size_t listeners_cap;")
|
||||
lines.add("} " & ctxType & ";")
|
||||
lines.add("")
|
||||
|
||||
proc emitCallBox(lines: var seq[string], fnType, boxType: string) =
|
||||
lines.add("typedef struct { " & fnType & " fn; void* user_data; } " & boxType & ";")
|
||||
|
||||
proc emitReplyTrampolineHead(lines: var seq[string], tramp, boxType, fallback: string) =
|
||||
## Opens a reply trampoline: cast the user-data back to the call box, bail if
|
||||
## the caller passed no callback (nothing to deliver to, and leaving early
|
||||
## avoids allocating a result nobody receives), then deliver a non-zero `ret`
|
||||
## as an error. The error text in msg/len is not NUL-terminated, so copy it.
|
||||
lines.add(
|
||||
"static void " & tramp & "(int ret, const char* msg, size_t len, void* ud) {"
|
||||
)
|
||||
lines.add(" " & boxType & "* box = (" & boxType & "*)ud;")
|
||||
lines.add(" if (!box->fn) {")
|
||||
lines.add(" free(box);")
|
||||
lines.add(" return;")
|
||||
lines.add(" }")
|
||||
lines.add(" if (ret != 0) {")
|
||||
lines.add(" char* em = nimffi_dup_cstr_n(msg ? msg : \"\", msg ? len : 0);")
|
||||
lines.add(
|
||||
" box->fn(ret, NULL, em ? em : \"" & fallback & "\", box->user_data);"
|
||||
)
|
||||
lines.add(" free(em);")
|
||||
lines.add(" free(box);")
|
||||
lines.add(" return;")
|
||||
lines.add(" }")
|
||||
|
||||
proc emitConstructors(
|
||||
lines: var seq[string],
|
||||
reg: var CTypeReg,
|
||||
ctxType, libType, libName: string,
|
||||
ctors: seq[FFIProcMeta],
|
||||
) =
|
||||
if ctors.len == 0:
|
||||
return
|
||||
let fnType = libType & "CreateFn"
|
||||
let boxType = libType & "CreateBox"
|
||||
let tramp = libName & "_create_trampoline"
|
||||
lines.add(
|
||||
"typedef void (*" & fnType & ")(int err_code, " & ctxType &
|
||||
"* ctx, const char* err_msg, void* user_data);"
|
||||
)
|
||||
emitCallBox(lines, fnType, boxType)
|
||||
emitReplyTrampolineHead(lines, tramp, boxType, "FFI create failed")
|
||||
lines.add(" char* err = NULL;")
|
||||
lines.add(" NimFfiStr addr;")
|
||||
lines.add(" memset(&addr, 0, sizeof(addr));")
|
||||
lines.add(
|
||||
" if (nimffi_decode_from_buf(" & libName &
|
||||
"_decv_Str, (const uint8_t*)msg, len, &addr, &err) != 0) {"
|
||||
)
|
||||
lines.add(" box->fn(-1, NULL, err ? err : \"decode failed\", box->user_data);")
|
||||
lines.add(" free(err);")
|
||||
lines.add(" free(box);")
|
||||
lines.add(" return;")
|
||||
lines.add(" }")
|
||||
lines.add(" char* endp = NULL;")
|
||||
lines.add(
|
||||
" unsigned long long a = addr.data ? strtoull(addr.data, &endp, 10) : 0;"
|
||||
)
|
||||
lines.add(" bool ok = addr.data && addr.len > 0 && endp && *endp == '\\0';")
|
||||
lines.add(" nimffi_free_str(&addr);")
|
||||
lines.add(" if (!ok) {")
|
||||
lines.add(
|
||||
" box->fn(-1, NULL, \"FFI create returned non-numeric address\", box->user_data);"
|
||||
)
|
||||
lines.add(" free(box);")
|
||||
lines.add(" return;")
|
||||
lines.add(" }")
|
||||
lines.add(
|
||||
" " & ctxType & "* ctx = (" & ctxType & "*)calloc(1, sizeof(" & ctxType & "));"
|
||||
)
|
||||
lines.add(" if (!ctx) {")
|
||||
lines.add(" box->fn(-1, NULL, \"out of memory\", box->user_data);")
|
||||
lines.add(" free(box);")
|
||||
lines.add(" return;")
|
||||
lines.add(" }")
|
||||
lines.add(" ctx->ptr = (void*)(uintptr_t)a;")
|
||||
lines.add(" box->fn(NIMFFI_RET_OK, ctx, NULL, box->user_data);")
|
||||
lines.add(" free(box);")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
for ctor in ctors:
|
||||
let reqName = reqStructName(ctor)
|
||||
let (params, assigns) = buildReqParams(reg, ctor.extraParams)
|
||||
let head = "static inline int " & libName & "_ctx_create("
|
||||
let sig =
|
||||
if params.len > 0:
|
||||
head & params.join(", ") & ", " & fnType & " on_created, void* user_data) {"
|
||||
else:
|
||||
head & fnType & " on_created, void* user_data) {"
|
||||
lines.add(sig)
|
||||
lines.add(" " & reqName & " ffi_req;")
|
||||
lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));")
|
||||
for a in assigns:
|
||||
lines.add(a)
|
||||
lines.add(" uint8_t* req_buf = NULL;")
|
||||
lines.add(" size_t req_len = 0;")
|
||||
lines.add(" char* err = NULL;")
|
||||
lines.add(
|
||||
" if (nimffi_encode_to_buf(" & libName & "_encv_" & cToken(reqName) &
|
||||
", &ffi_req, &req_buf, &req_len, &err) != 0) {"
|
||||
)
|
||||
lines.add(
|
||||
" if (on_created) on_created(-1, NULL, err ? err : \"encode failed\", user_data);"
|
||||
)
|
||||
lines.add(" free(err);")
|
||||
lines.add(" return -1;")
|
||||
lines.add(" }")
|
||||
lines.add(
|
||||
" " & boxType & "* box = (" & boxType & "*)malloc(sizeof(" & boxType & "));"
|
||||
)
|
||||
lines.add(" if (!box) {")
|
||||
lines.add(" free(req_buf);")
|
||||
lines.add(
|
||||
" if (on_created) on_created(-1, NULL, \"out of memory\", user_data);"
|
||||
)
|
||||
lines.add(" return -1;")
|
||||
lines.add(" }")
|
||||
lines.add(" box->fn = on_created;")
|
||||
lines.add(" box->user_data = user_data;")
|
||||
lines.add(" (void)" & ctor.procName & "(req_buf, req_len, " & tramp & ", box);")
|
||||
lines.add(" free(req_buf);")
|
||||
lines.add(" return 0;")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
proc emitDestructor(
|
||||
lines: var seq[string],
|
||||
ctxType, libName, dtorProcName: string,
|
||||
events: seq[FFIEventMeta],
|
||||
) =
|
||||
lines.add("static inline void " & libName & "_ctx_destroy(" & ctxType & "* ctx) {")
|
||||
lines.add(" if (!ctx) return;")
|
||||
if dtorProcName.len > 0:
|
||||
lines.add(" if (ctx->ptr) { " & dtorProcName & "(ctx->ptr); ctx->ptr = NULL; }")
|
||||
if events.len > 0:
|
||||
lines.add(
|
||||
" for (size_t i = 0; i < ctx->listeners_len; i++) free(ctx->listeners[i].box);"
|
||||
)
|
||||
lines.add(" free(ctx->listeners);")
|
||||
lines.add(" free(ctx);")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
proc emitListenerApi(
|
||||
lines: var seq[string], ctxType, libType, libName: string, events: seq[FFIEventMeta]
|
||||
) =
|
||||
if events.len == 0:
|
||||
return
|
||||
for ev in events:
|
||||
let n = evNames(libType, libName, ev)
|
||||
lines.add(
|
||||
"static inline uint64_t " & n.regName & "(" & ctxType & "* ctx, " & n.fnType &
|
||||
" fn, void* user_data) {"
|
||||
)
|
||||
lines.add(
|
||||
" " & n.boxType & "* box = (" & n.boxType & "*)malloc(sizeof(" & n.boxType &
|
||||
"));"
|
||||
)
|
||||
lines.add(" if (!box) return 0;")
|
||||
lines.add(" box->fn = fn;")
|
||||
lines.add(" box->user_data = user_data;")
|
||||
lines.add(
|
||||
" uint64_t id = " & libName & "_add_event_listener(ctx->ptr, \"" & ev.wireName &
|
||||
"\", " & n.tramp & ", box);"
|
||||
)
|
||||
lines.add(" if (id == 0) { free(box); return 0; }")
|
||||
lines.add(" if (ctx->listeners_len == ctx->listeners_cap) {")
|
||||
lines.add(" size_t ncap = ctx->listeners_cap ? ctx->listeners_cap * 2 : 4;")
|
||||
lines.add(
|
||||
" " & ctxType & "Listener* grown = (" & ctxType &
|
||||
"Listener*)realloc(ctx->listeners, ncap * sizeof(" & ctxType & "Listener));"
|
||||
)
|
||||
lines.add(
|
||||
" if (!grown) { " & libName &
|
||||
"_remove_event_listener(ctx->ptr, id); free(box); return 0; }"
|
||||
)
|
||||
lines.add(" ctx->listeners = grown;")
|
||||
lines.add(" ctx->listeners_cap = ncap;")
|
||||
lines.add(" }")
|
||||
lines.add(" ctx->listeners[ctx->listeners_len].id = id;")
|
||||
lines.add(" ctx->listeners[ctx->listeners_len].box = box;")
|
||||
lines.add(" ctx->listeners_len++;")
|
||||
lines.add(" return id;")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
lines.add(
|
||||
"static inline bool " & libName & "_ctx_remove_event_listener(" & ctxType &
|
||||
"* ctx, uint64_t id) {"
|
||||
)
|
||||
lines.add(" if (id == 0) return false;")
|
||||
lines.add(" int rc = " & libName & "_remove_event_listener(ctx->ptr, id);")
|
||||
lines.add(" for (size_t i = 0; i < ctx->listeners_len; i++) {")
|
||||
lines.add(" if (ctx->listeners[i].id == id) {")
|
||||
lines.add(" free(ctx->listeners[i].box);")
|
||||
lines.add(" ctx->listeners[i] = ctx->listeners[ctx->listeners_len - 1];")
|
||||
lines.add(" ctx->listeners_len--;")
|
||||
lines.add(" break;")
|
||||
lines.add(" }")
|
||||
lines.add(" }")
|
||||
lines.add(" return rc == 0;")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
proc emitMethod(
|
||||
lines: var seq[string],
|
||||
reg: var CTypeReg,
|
||||
ctxType, libType, libName: string,
|
||||
m: FFIProcMeta,
|
||||
) =
|
||||
let stripped = stripLibPrefix(m.procName, libName)
|
||||
let reqName = reqStructName(m)
|
||||
let retC = cReturnType(reg, m)
|
||||
let retFree = freeFn(reg, retC)
|
||||
let (params, assigns) = buildReqParams(reg, m.extraParams)
|
||||
let methodPascal = snakeToPascalCase(stripped)
|
||||
let fnType = libType & methodPascal & "ReplyFn"
|
||||
let boxType = libType & methodPascal & "CallBox"
|
||||
let tramp = libName & "_" & stripped & "_reply_trampoline"
|
||||
|
||||
lines.add(
|
||||
"typedef void (*" & fnType & ")(int err_code, const " & retC &
|
||||
"* reply, const char* err_msg, void* user_data);"
|
||||
)
|
||||
emitCallBox(lines, fnType, boxType)
|
||||
emitReplyTrampolineHead(lines, tramp, boxType, "FFI call failed")
|
||||
lines.add(" char* err = NULL;")
|
||||
lines.add(" " & retC & " out;")
|
||||
lines.add(" memset(&out, 0, sizeof(out));")
|
||||
lines.add(
|
||||
" int dec = nimffi_decode_from_buf(" & libName & "_decv_" & cToken(retC) &
|
||||
", (const uint8_t*)msg, len, &out, &err);"
|
||||
)
|
||||
lines.add(" if (dec != 0) {")
|
||||
lines.add(" box->fn(-1, NULL, err ? err : \"decode failed\", box->user_data);")
|
||||
lines.add(" free(err);")
|
||||
# A partial decode may have allocated some fields; reclaim them (out is
|
||||
# zeroed, so the typed free skips what was never written).
|
||||
if retFree.len > 0:
|
||||
lines.add(" " & retFree & "(&out);")
|
||||
lines.add(" free(box);")
|
||||
lines.add(" return;")
|
||||
lines.add(" }")
|
||||
lines.add(" box->fn(NIMFFI_RET_OK, &out, NULL, box->user_data);")
|
||||
if retFree.len > 0:
|
||||
lines.add(" " & retFree & "(&out);")
|
||||
lines.add(" free(box);")
|
||||
lines.add("}")
|
||||
|
||||
let head =
|
||||
"static inline int " & libName & "_ctx_" & stripped & "(const " & ctxType & "* ctx, "
|
||||
let sig =
|
||||
if params.len > 0:
|
||||
head & params.join(", ") & ", " & fnType & " on_reply, void* user_data) {"
|
||||
else:
|
||||
head & fnType & " on_reply, void* user_data) {"
|
||||
lines.add(sig)
|
||||
lines.add(" " & reqName & " ffi_req;")
|
||||
lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));")
|
||||
for a in assigns:
|
||||
lines.add(a)
|
||||
lines.add(" uint8_t* req_buf = NULL;")
|
||||
lines.add(" size_t req_len = 0;")
|
||||
lines.add(" char* err = NULL;")
|
||||
lines.add(
|
||||
" if (nimffi_encode_to_buf(" & libName & "_encv_" & cToken(reqName) &
|
||||
", &ffi_req, &req_buf, &req_len, &err) != 0) {"
|
||||
)
|
||||
lines.add(
|
||||
" if (on_reply) on_reply(-1, NULL, err ? err : \"encode failed\", user_data);"
|
||||
)
|
||||
lines.add(" free(err);")
|
||||
lines.add(" return -1;")
|
||||
lines.add(" }")
|
||||
lines.add(
|
||||
" " & boxType & "* box = (" & boxType & "*)malloc(sizeof(" & boxType & "));"
|
||||
)
|
||||
lines.add(" if (!box) {")
|
||||
lines.add(" free(req_buf);")
|
||||
lines.add(" if (on_reply) on_reply(-1, NULL, \"out of memory\", user_data);")
|
||||
lines.add(" return -1;")
|
||||
lines.add(" }")
|
||||
lines.add(" box->fn = on_reply;")
|
||||
lines.add(" box->user_data = user_data;")
|
||||
lines.add(
|
||||
" int ret = " & m.procName & "(ctx->ptr, " & tramp & ", box, req_buf, req_len);"
|
||||
)
|
||||
lines.add(" free(req_buf);")
|
||||
lines.add(" if (ret == NIMFFI_RET_MISSING_CALLBACK) {")
|
||||
lines.add(
|
||||
" if (on_reply) on_reply(-1, NULL, \"RET_MISSING_CALLBACK (internal error)\", user_data);"
|
||||
)
|
||||
lines.add(" free(box);")
|
||||
lines.add(" return -1;")
|
||||
lines.add(" }")
|
||||
lines.add(" return 0;")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
proc newCTypeReg(
|
||||
libName, libType: string, types: seq[FFITypeMeta], procs: seq[FFIProcMeta]
|
||||
): CTypeReg =
|
||||
var reg = CTypeReg(libName: libName, libType: libType)
|
||||
for t in types:
|
||||
reg.typeTable[t.name] = t
|
||||
for p in procs:
|
||||
if p.kind != FFIKind.DTOR:
|
||||
let rt = reqTypeMeta(p)
|
||||
reg.typeTable[rt.name] = rt
|
||||
reg
|
||||
|
||||
proc monomorphiseAll(
|
||||
reg: var CTypeReg,
|
||||
types: seq[FFITypeMeta],
|
||||
procs, methods: seq[FFIProcMeta],
|
||||
events: seq[FFIEventMeta],
|
||||
): tuple[reqTypes, respTypes: seq[string]] =
|
||||
## Walks every user type, per-proc Req envelope, return type and event
|
||||
## payload through ensureCType, emitting their structs/codecs into `reg` in
|
||||
## dependency order. Returns the Req and response C type names the buffer
|
||||
## adapters need.
|
||||
for t in types:
|
||||
discard ensureCType(reg, t.name)
|
||||
var reqTypes: seq[string] = @[]
|
||||
for p in procs:
|
||||
if p.kind != FFIKind.DTOR:
|
||||
let n = reqStructName(p)
|
||||
discard ensureCType(reg, n)
|
||||
reqTypes.add(n)
|
||||
var respTypes: seq[string] = @[]
|
||||
for m in methods:
|
||||
respTypes.add(cReturnType(reg, m))
|
||||
for ev in events:
|
||||
discard ensureCType(reg, ev.payloadTypeName)
|
||||
(reqTypes, respTypes)
|
||||
|
||||
func generateCPreludeHeader*(): string =
|
||||
## The `nim_ffi_prelude.h` shared header: owned string/byte types plus the
|
||||
## libc/TinyCBOR includes every nim-ffi C binding needs. Identical across
|
||||
## libraries, so it is emitted verbatim from the template.
|
||||
HeaderPreludeTpl & "\n"
|
||||
|
||||
func generateCCborHeader*(): string =
|
||||
## The `nim_ffi_cbor.h` shared header: leaf CBOR codecs and buffer drivers.
|
||||
## Includes the prelude (its guard is inside the template) and is library-
|
||||
## agnostic, so it too is emitted verbatim.
|
||||
CborHelpersTpl & "\n"
|
||||
|
||||
proc generateCLibHeader*(
|
||||
procs: seq[FFIProcMeta],
|
||||
types: seq[FFITypeMeta],
|
||||
libName: string,
|
||||
events: seq[FFIEventMeta] = @[],
|
||||
): string =
|
||||
## The `<lib>.h` header: library-specific structs, monomorphised codecs and
|
||||
## the async API. Pulls the two shared headers in via the cbor header.
|
||||
let classified = classifyProcs(procs)
|
||||
let ctors = classified.ctors
|
||||
let methods = classified.methods
|
||||
let libType = libTypeName(ctors, libName)
|
||||
let ctxType = libType & "Ctx"
|
||||
|
||||
var reg = newCTypeReg(libName, libType, types, procs)
|
||||
let (reqTypes, respTypes) = monomorphiseAll(reg, types, procs, methods, events)
|
||||
|
||||
let guard = "NIM_FFI_LIB_" & libName.toUpperAscii() & "_H_INCLUDED"
|
||||
var lines: seq[string] = @[]
|
||||
lines.add("#ifndef " & guard)
|
||||
lines.add("#define " & guard)
|
||||
lines.add("#include \"" & CborHeaderName & "\"")
|
||||
lines.add("")
|
||||
|
||||
lines.add("/* ============================================================ */")
|
||||
lines.add("/* Generated types (user-declared + per-proc request envelopes) */")
|
||||
lines.add("/* ============================================================ */")
|
||||
lines.add("")
|
||||
for decl in reg.decls:
|
||||
lines.add(decl)
|
||||
lines.add("")
|
||||
for codec in reg.codecs:
|
||||
lines.add(codec)
|
||||
lines.add("")
|
||||
|
||||
lines.add("/* ============================================================ */")
|
||||
lines.add("/* C ABI declarations (symbols exported by the Nim dylib) */")
|
||||
lines.add("/* ============================================================ */")
|
||||
lines.add("#ifdef __cplusplus")
|
||||
lines.add("extern \"C\" {")
|
||||
lines.add("#endif")
|
||||
lines.add("")
|
||||
for p in procs:
|
||||
case p.kind
|
||||
of FFIKind.FFI:
|
||||
lines.add(
|
||||
"int " & p.procName & "(void* ctx, FFICallback callback, void* user_data, " &
|
||||
"const uint8_t* req_cbor, size_t req_cbor_len);"
|
||||
)
|
||||
of FFIKind.CTOR:
|
||||
lines.add(
|
||||
"void* " & p.procName & "(const uint8_t* req_cbor, size_t req_cbor_len, " &
|
||||
"FFICallback callback, void* user_data);"
|
||||
)
|
||||
of FFIKind.DTOR:
|
||||
lines.add("int " & p.procName & "(void* ctx);")
|
||||
lines.add(
|
||||
"uint64_t " & libName & "_add_event_listener(void* ctx, const char* event_name, " &
|
||||
"FFICallback callback, void* user_data);"
|
||||
)
|
||||
lines.add(
|
||||
"int " & libName & "_remove_event_listener(void* ctx, uint64_t listener_id);"
|
||||
)
|
||||
lines.add("")
|
||||
lines.add("#ifdef __cplusplus")
|
||||
lines.add("} /* extern \"C\" */")
|
||||
lines.add("#endif")
|
||||
lines.add("")
|
||||
|
||||
# Per-Req encode / per-response decode void* adapters for the buffer drivers.
|
||||
var adaptersDone = initHashSet[string]()
|
||||
lines.add("/* CBOR buffer adapters (typed codec → void* driver signature) */")
|
||||
for n in reqTypes:
|
||||
let tok = cToken(n)
|
||||
if ("enc" & tok) notin adaptersDone:
|
||||
adaptersDone.incl("enc" & tok)
|
||||
lines.add(
|
||||
"static inline CborError " & libName & "_encv_" & tok &
|
||||
"(CborEncoder* e, const void* v) { return " & reg.libName & "_enc_" & n &
|
||||
"(e, (const " & n & "*)v); }"
|
||||
)
|
||||
var respSet = respTypes
|
||||
respSet.add("NimFfiStr") # ctor address payload
|
||||
for n in respSet:
|
||||
let tok = cToken(n)
|
||||
if ("dec" & tok) notin adaptersDone:
|
||||
adaptersDone.incl("dec" & tok)
|
||||
lines.add(
|
||||
"static inline CborError " & libName & "_decv_" & tok &
|
||||
"(CborValue* it, void* v) { return " & decFn(reg, n) & "(it, (" & n & "*)v); }"
|
||||
)
|
||||
lines.add("")
|
||||
|
||||
emitEventMachinery(lines, reg, libType, libName, events)
|
||||
emitContextStruct(lines, ctxType, events)
|
||||
emitConstructors(lines, reg, ctxType, libType, libName, ctors)
|
||||
emitDestructor(lines, ctxType, libName, classified.dtorProcName, events)
|
||||
emitListenerApi(lines, ctxType, libType, libName, events)
|
||||
for m in methods:
|
||||
emitMethod(lines, reg, ctxType, libType, libName, m)
|
||||
|
||||
lines.add("#endif /* " & guard & " */")
|
||||
lines.join("\n") & "\n"
|
||||
|
||||
proc generateCCMakeLists*(libName, nimSrcRelPath: string): string =
|
||||
let src = nimSrcRelPath.replace("\\", "/")
|
||||
CMakeListsTpl.multiReplace(("{{LIB}}", libName), ("{{SRC}}", src))
|
||||
|
||||
proc generateCBindings*(
|
||||
procs: seq[FFIProcMeta],
|
||||
types: seq[FFITypeMeta],
|
||||
libName: string,
|
||||
outputDir: string,
|
||||
nimSrcRelPath: string,
|
||||
events: seq[FFIEventMeta] = @[],
|
||||
) =
|
||||
createDir(outputDir)
|
||||
writeFile(outputDir / PreludeHeaderName, generateCPreludeHeader())
|
||||
writeFile(outputDir / CborHeaderName, generateCCborHeader())
|
||||
writeFile(
|
||||
outputDir / (libName & ".h"), generateCLibHeader(procs, types, libName, events)
|
||||
)
|
||||
writeFile(outputDir / "CMakeLists.txt", generateCCMakeLists(libName, nimSrcRelPath))
|
||||
62
ffi/codegen/c_cpp_common.nim
Normal file
62
ffi/codegen/c_cpp_common.nim
Normal file
@ -0,0 +1,62 @@
|
||||
## Helpers shared by the language-specific binding generators (cpp.nim, c.nim).
|
||||
## Kept here so the per-proc envelope naming, lib-prefix stripping and
|
||||
## proc-classification logic live in one place rather than being copy-pasted
|
||||
## into each backend.
|
||||
|
||||
import std/strutils
|
||||
import ./meta, ./string_helpers
|
||||
|
||||
proc genericInnerType*(typeName, prefix: string): string =
|
||||
## Inner type of a single-parameter generic written `Prefix[Inner]`, e.g.
|
||||
## `genericInnerType("seq[int]", "seq[")` → `"int"`. Empty string when
|
||||
## `typeName` is not of that shape.
|
||||
if typeName.startsWith(prefix) and typeName.endsWith("]"):
|
||||
let start = prefix.len
|
||||
let lastIndex = typeName.len - 2
|
||||
return typeName[start .. lastIndex]
|
||||
return ""
|
||||
|
||||
proc stripLibPrefix*(procName, libName: string): string =
|
||||
## Drops the `<lib>_` prefix from an exported C symbol, e.g.
|
||||
## `stripLibPrefix("timer_echo", "timer")` → `"echo"`.
|
||||
let prefix = libName & "_"
|
||||
if procName.startsWith(prefix):
|
||||
return procName[prefix.len .. ^1]
|
||||
return procName
|
||||
|
||||
proc reqStructName*(p: FFIProcMeta): string =
|
||||
## Mirrors the Nim macro: `<PascalCase(procName)>Req`, or `...CtorReq` for a
|
||||
## constructor. The per-proc envelope every backend encodes onto the wire.
|
||||
let camel = snakeToPascalCase(p.procName)
|
||||
if p.kind == FFIKind.CTOR:
|
||||
camel & "CtorReq"
|
||||
else:
|
||||
camel & "Req"
|
||||
|
||||
type ClassifiedProcs* = object
|
||||
ctors*: seq[FFIProcMeta]
|
||||
methods*: seq[FFIProcMeta]
|
||||
dtorProcName*: string
|
||||
|
||||
proc classifyProcs*(procs: seq[FFIProcMeta]): ClassifiedProcs =
|
||||
## Splits the registry into constructors, instance methods and (the first)
|
||||
## destructor symbol — the split every backend needs before emitting a
|
||||
## high-level context wrapper.
|
||||
var c: ClassifiedProcs
|
||||
for p in procs:
|
||||
case p.kind
|
||||
of FFIKind.CTOR:
|
||||
c.ctors.add(p)
|
||||
of FFIKind.FFI:
|
||||
c.methods.add(p)
|
||||
of FFIKind.DTOR:
|
||||
if c.dtorProcName.len == 0:
|
||||
c.dtorProcName = p.procName
|
||||
c
|
||||
|
||||
proc libTypeName*(ctors: seq[FFIProcMeta], libName: string): string =
|
||||
## The user's library type name (e.g. `MyTimer`), taken from the first ctor
|
||||
## or derived from `libName` when the library declares none.
|
||||
if ctors.len > 0:
|
||||
return ctors[0].libTypeName
|
||||
capitalizeFirstLetter(libName)
|
||||
@ -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
|
||||
|
||||
47
ffi/codegen/templates/c/CMakeLists.txt.tpl
Normal file
47
ffi/codegen/templates/c/CMakeLists.txt.tpl
Normal file
@ -0,0 +1,47 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project({{LIB}}_c_bindings C)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
||||
# ── Locate the repository root (contains ffi.nimble) ─────────────────────────
|
||||
set(_search_dir "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
set(REPO_ROOT "")
|
||||
foreach(_i RANGE 10)
|
||||
if(EXISTS "${_search_dir}/ffi.nimble")
|
||||
set(REPO_ROOT "${_search_dir}")
|
||||
break()
|
||||
endif()
|
||||
get_filename_component(_search_dir "${_search_dir}" DIRECTORY)
|
||||
endforeach()
|
||||
if("${REPO_ROOT}" STREQUAL "")
|
||||
message(FATAL_ERROR "Cannot find repo root (no ffi.nimble in any ancestor)")
|
||||
endif()
|
||||
|
||||
# Build the Nim dylib + vendored TinyCBOR (shared with the C++ backend).
|
||||
set(NIM_FFI_LIB {{LIB}})
|
||||
set(NIM_FFI_SRC {{SRC}})
|
||||
include("${REPO_ROOT}/ffi/codegen/templates/nim_ffi_lib.cmake")
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
add_library({{LIB}}_headers INTERFACE)
|
||||
target_include_directories({{LIB}}_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
target_link_libraries({{LIB}}_headers INTERFACE {{LIB}} tinycbor Threads::Threads)
|
||||
# The generated header is async (no blocking helper), but consumer code that
|
||||
# waits on a result callback typically uses nanosleep / pthreads, which need a
|
||||
# POSIX feature level that strict `-std=c11` hides. Define it for consumers.
|
||||
target_compile_definitions({{LIB}}_headers INTERFACE _POSIX_C_SOURCE=200809L)
|
||||
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.c")
|
||||
add_executable({{LIB}}_example main.c)
|
||||
target_link_libraries({{LIB}}_example PRIVATE {{LIB}}_headers)
|
||||
add_dependencies({{LIB}}_example {{LIB}}_nim_lib)
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
add_custom_command(TARGET {{LIB}}_example POST_BUILD
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${{{LIB}}_RUNTIME_LIB}"
|
||||
"$<TARGET_FILE_DIR:{{LIB}}_example>"
|
||||
COMMENT "Staging {{LIB}}.dll next to {{LIB}}_example.exe")
|
||||
endif()
|
||||
endif()
|
||||
341
ffi/codegen/templates/c/cbor_helpers.h.tpl
Normal file
341
ffi/codegen/templates/c/cbor_helpers.h.tpl
Normal file
@ -0,0 +1,341 @@
|
||||
#ifndef NIM_FFI_CBOR_HELPERS_H_INCLUDED
|
||||
#define NIM_FFI_CBOR_HELPERS_H_INCLUDED
|
||||
/* Leaf CBOR codecs (scalars, text strings, byte strings) plus the buffer
|
||||
* drivers. The per-struct / per-container codecs in the library header call
|
||||
* into these by name (C has no overloading, so each leaf gets a distinct
|
||||
* nimffi_enc_* / nimffi_dec_* symbol). Guarded so two nim-ffi headers can
|
||||
* share a translation unit. */
|
||||
#include "nim_ffi_prelude.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Result delivery callback exported by the Nim dylib: `ret` is 0 on success
|
||||
* (then `msg`/`len` carry the CBOR response) or non-zero on failure (then
|
||||
* `msg`/`len` carry the error text, which is NOT NUL-terminated). */
|
||||
typedef void (*FFICallback)(int ret, const char* msg, size_t len, void* user_data);
|
||||
|
||||
/* Return / callback status codes. NIMFFI_RET_OK (0) is success; any non-zero
|
||||
* value handed to a result callback's `err_code` (or returned by a submit call)
|
||||
* is a failure. NIMFFI_RET_MISSING_CALLBACK is a special case from the Nim
|
||||
* dispatcher: the callback will never fire, so the request path must report the
|
||||
* failure itself. */
|
||||
#define NIMFFI_RET_OK 0
|
||||
#define NIMFFI_RET_ERROR 1
|
||||
#define NIMFFI_RET_MISSING_CALLBACK 2
|
||||
|
||||
/* ── leaf encoders ─────────────────────────────────────────────────────── */
|
||||
static inline CborError nimffi_enc_bool(CborEncoder* e, const bool* v) {
|
||||
return cbor_encode_boolean(e, *v);
|
||||
}
|
||||
static inline CborError nimffi_enc_i64(CborEncoder* e, const int64_t* v) {
|
||||
return cbor_encode_int(e, *v);
|
||||
}
|
||||
static inline CborError nimffi_enc_i32(CborEncoder* e, const int32_t* v) {
|
||||
return cbor_encode_int(e, (int64_t)*v);
|
||||
}
|
||||
static inline CborError nimffi_enc_i16(CborEncoder* e, const int16_t* v) {
|
||||
return cbor_encode_int(e, (int64_t)*v);
|
||||
}
|
||||
static inline CborError nimffi_enc_i8(CborEncoder* e, const int8_t* v) {
|
||||
return cbor_encode_int(e, (int64_t)*v);
|
||||
}
|
||||
static inline CborError nimffi_enc_u64(CborEncoder* e, const uint64_t* v) {
|
||||
return cbor_encode_uint(e, *v);
|
||||
}
|
||||
static inline CborError nimffi_enc_u32(CborEncoder* e, const uint32_t* v) {
|
||||
return cbor_encode_uint(e, (uint64_t)*v);
|
||||
}
|
||||
static inline CborError nimffi_enc_u16(CborEncoder* e, const uint16_t* v) {
|
||||
return cbor_encode_uint(e, (uint64_t)*v);
|
||||
}
|
||||
static inline CborError nimffi_enc_u8(CborEncoder* e, const uint8_t* v) {
|
||||
return cbor_encode_uint(e, (uint64_t)*v);
|
||||
}
|
||||
static inline CborError nimffi_enc_f64(CborEncoder* e, const double* v) {
|
||||
return cbor_encode_double(e, *v);
|
||||
}
|
||||
static inline CborError nimffi_enc_f32(CborEncoder* e, const float* v) {
|
||||
return cbor_encode_float(e, *v);
|
||||
}
|
||||
static inline CborError nimffi_enc_str(CborEncoder* e, const NimFfiStr* v) {
|
||||
return cbor_encode_text_string(e, v->data ? v->data : "", v->len);
|
||||
}
|
||||
static inline CborError nimffi_enc_bytes(CborEncoder* e, const NimFfiBytes* v) {
|
||||
return cbor_encode_byte_string(e, v->data, v->len);
|
||||
}
|
||||
|
||||
/* ── leaf decoders ─────────────────────────────────────────────────────── */
|
||||
/* After reading a leaf, the parser must advance past it; both steps
|
||||
* short-circuit on the same CborError, so they travel together. */
|
||||
static inline CborError nimffi_advance_if_ok(CborValue* it, CborError err) {
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
|
||||
static inline CborError nimffi_dec_bool(CborValue* it, bool* out) {
|
||||
if (!cbor_value_is_boolean(it)) {
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
return nimffi_advance_if_ok(it, cbor_value_get_boolean(it, out));
|
||||
}
|
||||
static inline CborError nimffi_dec_i64(CborValue* it, int64_t* out) {
|
||||
if (!cbor_value_is_integer(it)) {
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
return nimffi_advance_if_ok(it, cbor_value_get_int64_checked(it, out));
|
||||
}
|
||||
static inline CborError nimffi_dec_i32(CborValue* it, int32_t* out) {
|
||||
int64_t tmp = 0;
|
||||
CborError err = nimffi_dec_i64(it, &tmp);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (tmp < INT32_MIN || tmp > INT32_MAX) {
|
||||
return CborErrorDataTooLarge;
|
||||
}
|
||||
*out = (int32_t)tmp;
|
||||
return CborNoError;
|
||||
}
|
||||
static inline CborError nimffi_dec_i16(CborValue* it, int16_t* out) {
|
||||
int64_t tmp = 0;
|
||||
CborError err = nimffi_dec_i64(it, &tmp);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (tmp < INT16_MIN || tmp > INT16_MAX) {
|
||||
return CborErrorDataTooLarge;
|
||||
}
|
||||
*out = (int16_t)tmp;
|
||||
return CborNoError;
|
||||
}
|
||||
static inline CborError nimffi_dec_i8(CborValue* it, int8_t* out) {
|
||||
int64_t tmp = 0;
|
||||
CborError err = nimffi_dec_i64(it, &tmp);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (tmp < INT8_MIN || tmp > INT8_MAX) {
|
||||
return CborErrorDataTooLarge;
|
||||
}
|
||||
*out = (int8_t)tmp;
|
||||
return CborNoError;
|
||||
}
|
||||
static inline CborError nimffi_dec_u64(CborValue* it, uint64_t* out) {
|
||||
if (!cbor_value_is_unsigned_integer(it)) {
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
return nimffi_advance_if_ok(it, cbor_value_get_uint64(it, out));
|
||||
}
|
||||
static inline CborError nimffi_dec_u32(CborValue* it, uint32_t* out) {
|
||||
uint64_t tmp = 0;
|
||||
CborError err = nimffi_dec_u64(it, &tmp);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (tmp > UINT32_MAX) {
|
||||
return CborErrorDataTooLarge;
|
||||
}
|
||||
*out = (uint32_t)tmp;
|
||||
return CborNoError;
|
||||
}
|
||||
static inline CborError nimffi_dec_u16(CborValue* it, uint16_t* out) {
|
||||
uint64_t tmp = 0;
|
||||
CborError err = nimffi_dec_u64(it, &tmp);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (tmp > UINT16_MAX) {
|
||||
return CborErrorDataTooLarge;
|
||||
}
|
||||
*out = (uint16_t)tmp;
|
||||
return CborNoError;
|
||||
}
|
||||
static inline CborError nimffi_dec_u8(CborValue* it, uint8_t* out) {
|
||||
uint64_t tmp = 0;
|
||||
CborError err = nimffi_dec_u64(it, &tmp);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (tmp > UINT8_MAX) {
|
||||
return CborErrorDataTooLarge;
|
||||
}
|
||||
*out = (uint8_t)tmp;
|
||||
return CborNoError;
|
||||
}
|
||||
static inline CborError nimffi_dec_f64(CborValue* it, double* out) {
|
||||
if (cbor_value_is_double(it)) {
|
||||
return nimffi_advance_if_ok(it, cbor_value_get_double(it, out));
|
||||
}
|
||||
if (cbor_value_is_float(it)) {
|
||||
float f = 0.0f;
|
||||
CborError err = cbor_value_get_float(it, &f);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
*out = (double)f;
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
static inline CborError nimffi_dec_f32(CborValue* it, float* out) {
|
||||
if (cbor_value_is_float(it)) {
|
||||
return nimffi_advance_if_ok(it, cbor_value_get_float(it, out));
|
||||
}
|
||||
if (cbor_value_is_double(it)) {
|
||||
double d = 0.0;
|
||||
CborError err = cbor_value_get_double(it, &d);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
*out = (float)d;
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
static inline CborError nimffi_dec_str(CborValue* it, NimFfiStr* out) {
|
||||
if (!cbor_value_is_text_string(it)) {
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
size_t len = 0;
|
||||
CborError err = cbor_value_get_string_length(it, &len);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
if (len == SIZE_MAX) { /* len + 1 would wrap to a 0-byte allocation */
|
||||
return CborErrorDataTooLarge;
|
||||
}
|
||||
/* one extra byte so a NUL-free payload is a valid C string */
|
||||
out->data = (char*)malloc(len + 1);
|
||||
if (!out->data) {
|
||||
return CborErrorOutOfMemory;
|
||||
}
|
||||
out->len = len;
|
||||
size_t copied = len;
|
||||
err = cbor_value_copy_text_string(it, out->data, &copied, NULL);
|
||||
if (err) {
|
||||
free(out->data);
|
||||
out->data = NULL;
|
||||
out->len = 0;
|
||||
return err;
|
||||
}
|
||||
out->data[len] = '\0';
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
static inline CborError nimffi_dec_bytes(CborValue* it, NimFfiBytes* out) {
|
||||
if (!cbor_value_is_byte_string(it)) {
|
||||
return CborErrorImproperValue;
|
||||
}
|
||||
size_t len = 0;
|
||||
CborError err = cbor_value_get_string_length(it, &len);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
out->data = (uint8_t*)malloc(len ? len : 1);
|
||||
if (!out->data) {
|
||||
return CborErrorOutOfMemory;
|
||||
}
|
||||
out->len = len;
|
||||
size_t copied = len;
|
||||
err = cbor_value_copy_byte_string(it, out->data, &copied, NULL);
|
||||
if (err) {
|
||||
free(out->data);
|
||||
out->data = NULL;
|
||||
out->len = 0;
|
||||
return err;
|
||||
}
|
||||
return cbor_value_advance(it);
|
||||
}
|
||||
|
||||
/* ── buffer drivers ────────────────────────────────────────────────────── */
|
||||
typedef CborError (*nimffi_enc_fn)(CborEncoder*, const void*);
|
||||
typedef CborError (*nimffi_dec_fn)(CborValue*, void*);
|
||||
|
||||
static inline char* nimffi_dup_cstr(const char* s) {
|
||||
size_t n = strlen(s) + 1;
|
||||
char* p = (char*)malloc(n);
|
||||
if (p) {
|
||||
memcpy(p, s, n);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/* NUL-terminated copy of a length-delimited (not NUL-terminated) byte run,
|
||||
* for turning the FFICallback's raw error `msg`/`len` into a C string. */
|
||||
static inline char* nimffi_dup_cstr_n(const char* s, size_t n) {
|
||||
char* p = (char*)malloc(n + 1);
|
||||
if (p) {
|
||||
if (n > 0) {
|
||||
memcpy(p, s, n);
|
||||
}
|
||||
p[n] = '\0';
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/* Encode `val` with `fn` into a freshly malloc'd buffer, doubling on overflow.
|
||||
* Returns 0 and sets out/outlen on success; -1 and *err (heap) on failure. */
|
||||
static inline int nimffi_encode_to_buf(
|
||||
nimffi_enc_fn fn, const void* val,
|
||||
uint8_t** out, size_t* outlen, char** err) {
|
||||
size_t cap = 4096;
|
||||
uint8_t* buf = (uint8_t*)malloc(cap);
|
||||
if (!buf) {
|
||||
if (err) *err = nimffi_dup_cstr("out of memory");
|
||||
return -1;
|
||||
}
|
||||
for (;;) {
|
||||
CborEncoder enc;
|
||||
cbor_encoder_init(&enc, buf, cap, 0);
|
||||
CborError e = fn(&enc, val);
|
||||
if (e == CborNoError) {
|
||||
*outlen = cbor_encoder_get_buffer_size(&enc, buf);
|
||||
*out = buf;
|
||||
return 0;
|
||||
}
|
||||
if (e == CborErrorOutOfMemory) {
|
||||
size_t extra = cbor_encoder_get_extra_bytes_needed(&enc);
|
||||
cap += extra > 0 ? extra : cap;
|
||||
uint8_t* grown = (uint8_t*)realloc(buf, cap);
|
||||
if (!grown) {
|
||||
free(buf);
|
||||
if (err) *err = nimffi_dup_cstr("out of memory");
|
||||
return -1;
|
||||
}
|
||||
buf = grown;
|
||||
continue;
|
||||
}
|
||||
free(buf);
|
||||
if (err) *err = nimffi_dup_cstr(cbor_error_string(e));
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Decode a CBOR buffer into `out` with `fn`. Returns 0 on success; -1 and
|
||||
* *err (heap) on failure. */
|
||||
static inline int nimffi_decode_from_buf(
|
||||
nimffi_dec_fn fn, const uint8_t* buf, size_t len,
|
||||
void* out, char** err) {
|
||||
CborParser parser;
|
||||
CborValue it;
|
||||
CborError e = cbor_parser_init(buf, len, 0, &parser, &it);
|
||||
if (e != CborNoError) {
|
||||
if (err) *err = nimffi_dup_cstr(cbor_error_string(e));
|
||||
return -1;
|
||||
}
|
||||
e = fn(&it, out);
|
||||
if (e != CborNoError) {
|
||||
if (err) *err = nimffi_dup_cstr(cbor_error_string(e));
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NIM_FFI_CBOR_HELPERS_H_INCLUDED */
|
||||
88
ffi/codegen/templates/c/header_prelude.h.tpl
Normal file
88
ffi/codegen/templates/c/header_prelude.h.tpl
Normal file
@ -0,0 +1,88 @@
|
||||
#ifndef NIM_FFI_PRELUDE_H_INCLUDED
|
||||
#define NIM_FFI_PRELUDE_H_INCLUDED
|
||||
/* Generated C binding for a nim-ffi library. Requests/responses travel as
|
||||
* CBOR (encoded with vendored TinyCBOR on this side, matching the Nim-side
|
||||
* cbor_serial codec on the wire — both ends speak RFC 8949).
|
||||
*
|
||||
* The API is asynchronous: every method/constructor takes a result callback
|
||||
* and returns immediately. The callback fires exactly once — synchronously on
|
||||
* a submit-time failure, otherwise from the Nim dispatch thread when the reply
|
||||
* arrives.
|
||||
*
|
||||
* Memory ownership contract:
|
||||
* - Request-side strings/sequences are *borrowed*: the binding only reads
|
||||
* them while encoding, so a string literal wrapped with nimffi_str() is
|
||||
* fine and is never freed by the binding.
|
||||
* - Response values and error strings passed into a result callback are
|
||||
* *owned by the binding* and valid only for the duration of that callback;
|
||||
* the binding reclaims them once the callback returns. The caller never
|
||||
* frees them. (The generated <lib>_free_<Type>() helpers are internal — the
|
||||
* trampolines use them to reclaim decoded payloads.)
|
||||
* - A context handle delivered to a constructor callback is the exception:
|
||||
* ownership transfers to the caller, who releases it with
|
||||
* <lib>_ctx_destroy(). It is a lifecycle handle, not returned data.
|
||||
*
|
||||
* Trust boundary: the decoders assume the CBOR they parse was produced by the
|
||||
* paired Nim library. They reject malformed input rather than trusting it, but
|
||||
* they are not hardened against a hostile peer feeding crafted payloads through
|
||||
* the raw nimffi_decode_from_buf entry point.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <tinycbor/cbor.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Owned, length-delimited UTF-8 text (Nim `string`/`cstring`). On the request
|
||||
* side `data` may point at borrowed storage (see nimffi_str); on the response
|
||||
* side it is heap-allocated and freed by nimffi_free_str. Always NUL-padded by
|
||||
* one byte after decode so `data` is usable as a C string when it has no
|
||||
* embedded NULs. */
|
||||
typedef struct {
|
||||
char* data;
|
||||
size_t len;
|
||||
} NimFfiStr;
|
||||
|
||||
/* Owned, length-delimited byte buffer (Nim `seq[byte]`). */
|
||||
typedef struct {
|
||||
uint8_t* data;
|
||||
size_t len;
|
||||
} NimFfiBytes;
|
||||
|
||||
/* Wrap a borrowed C string for use as a request field. The returned view is
|
||||
* not owned by the binding and must outlive the call that encodes it. */
|
||||
static inline NimFfiStr nimffi_str(const char* s) {
|
||||
NimFfiStr v;
|
||||
v.data = (char*)s;
|
||||
v.len = s ? strlen(s) : 0;
|
||||
return v;
|
||||
}
|
||||
|
||||
static inline void nimffi_free_str(NimFfiStr* v) {
|
||||
if (!v || !v->data) {
|
||||
return;
|
||||
}
|
||||
free(v->data);
|
||||
v->data = NULL;
|
||||
v->len = 0;
|
||||
}
|
||||
|
||||
static inline void nimffi_free_bytes(NimFfiBytes* v) {
|
||||
if (!v || !v->data) {
|
||||
return;
|
||||
}
|
||||
free(v->data);
|
||||
v->data = NULL;
|
||||
v->len = 0;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NIM_FFI_PRELUDE_H_INCLUDED */
|
||||
@ -27,92 +27,10 @@ if("${REPO_ROOT}" STREQUAL "")
|
||||
message(FATAL_ERROR "Cannot find repo root (no ffi.nimble in any ancestor)")
|
||||
endif()
|
||||
|
||||
get_filename_component(NIM_SRC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/{{SRC}}"
|
||||
ABSOLUTE)
|
||||
|
||||
find_program(NIM_EXECUTABLE nim REQUIRED)
|
||||
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
set(NIM_LIB_FILE "${REPO_ROOT}/lib{{LIB}}.dylib")
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
set(NIM_LIB_FILE "${REPO_ROOT}/{{LIB}}.dll")
|
||||
# MSVC consumers link against the `.lib` import library, not the DLL.
|
||||
# MinGW's ld emits one when asked via `--out-implib`; the resulting COFF
|
||||
# archive is readable by MSVC's link.exe.
|
||||
set(NIM_IMPLIB_FILE "${REPO_ROOT}/{{LIB}}.lib")
|
||||
else()
|
||||
set(NIM_LIB_FILE "${REPO_ROOT}/lib{{LIB}}.so")
|
||||
endif()
|
||||
|
||||
# On Windows the default Nim toolchain (mingw gcc) doesn't emit an import
|
||||
# library unless told to. Without it, MSVC consumers can't resolve any
|
||||
# symbol exported by the DLL at link time.
|
||||
set(NIM_IMPLIB_PASSL "")
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
set(NIM_IMPLIB_PASSL "--passL:-Wl,--out-implib,${NIM_IMPLIB_FILE}")
|
||||
endif()
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${NIM_LIB_FILE}"
|
||||
COMMAND "${NIM_EXECUTABLE}" c
|
||||
--mm:orc
|
||||
-d:chronicles_log_level=WARN
|
||||
--app:lib
|
||||
--noMain
|
||||
"--nimMainPrefix:lib{{LIB}}"
|
||||
${NIM_IMPLIB_PASSL}
|
||||
"-o:${NIM_LIB_FILE}"
|
||||
"${NIM_SRC}"
|
||||
WORKING_DIRECTORY "${REPO_ROOT}"
|
||||
DEPENDS "${NIM_SRC}"
|
||||
BYPRODUCTS "${NIM_IMPLIB_FILE}"
|
||||
COMMENT "Compiling Nim library lib{{LIB}}"
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target({{LIB}}_nim_lib ALL DEPENDS "${NIM_LIB_FILE}")
|
||||
|
||||
# On Windows, an `IMPORTED SHARED` target needs IMPORTED_IMPLIB pointing at
|
||||
# the `.lib` import library so MSVC's `link.exe` can resolve symbols. The
|
||||
# Visual Studio multi-config generator did not pick up `IMPORTED_IMPLIB` —
|
||||
# nor per-config `IMPORTED_IMPLIB_<CONFIG>` variants — and emitted
|
||||
# `{{LIB}}-NOTFOUND.obj` into every link line. Side-step the IMPORTED
|
||||
# machinery on Windows by exposing the import library through a plain
|
||||
# INTERFACE library that links the `.lib` by path.
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
add_library({{LIB}} INTERFACE)
|
||||
target_link_libraries({{LIB}} INTERFACE "${NIM_IMPLIB_FILE}")
|
||||
else()
|
||||
add_library({{LIB}} SHARED IMPORTED GLOBAL)
|
||||
set_target_properties({{LIB}} PROPERTIES IMPORTED_LOCATION "${NIM_LIB_FILE}")
|
||||
endif()
|
||||
add_dependencies({{LIB}} {{LIB}}_nim_lib)
|
||||
|
||||
# Absolute path to the runtime library (DLL/dylib/so). Exposed via the cache
|
||||
# so consumers in other directories (e.g. tests/e2e/cpp) can stage the DLL
|
||||
# next to their executable on Windows.
|
||||
set({{LIB}}_RUNTIME_LIB "${NIM_LIB_FILE}" CACHE INTERNAL
|
||||
"Absolute path to the {{LIB}} runtime library")
|
||||
|
||||
# ── TinyCBOR (vendored at ffi/codegen/templates/cpp/vendor/tinycbor) ─────────
|
||||
# Guarded so two sibling cpp_bindings dirs in one parent project don't redefine
|
||||
# the `tinycbor` target.
|
||||
set(TINYCBOR_SRC_DIR "${REPO_ROOT}/ffi/codegen/templates/cpp/vendor")
|
||||
if(NOT TARGET tinycbor)
|
||||
add_library(tinycbor STATIC
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor/cborencoder.c"
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor/cborencoder_close_container_checked.c"
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor/cborparser.c"
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor/cborparser_dup_string.c"
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor/cborerrorstrings.c"
|
||||
)
|
||||
target_include_directories(tinycbor PUBLIC
|
||||
"${TINYCBOR_SRC_DIR}" # consumer uses #include <tinycbor/cbor.h>
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor" # internal _p.h includes resolve here
|
||||
)
|
||||
set_property(TARGET tinycbor PROPERTY C_STANDARD 99)
|
||||
set_property(TARGET tinycbor PROPERTY POSITION_INDEPENDENT_CODE ON)
|
||||
endif()
|
||||
# Build the Nim dylib + vendored TinyCBOR (shared with the C backend).
|
||||
set(NIM_FFI_LIB {{LIB}})
|
||||
set(NIM_FFI_SRC {{SRC}})
|
||||
include("${REPO_ROOT}/ffi/codegen/templates/nim_ffi_lib.cmake")
|
||||
|
||||
add_library({{LIB}}_headers INTERFACE)
|
||||
target_include_directories({{LIB}}_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
|
||||
87
ffi/codegen/templates/nim_ffi_lib.cmake
Normal file
87
ffi/codegen/templates/nim_ffi_lib.cmake
Normal file
@ -0,0 +1,87 @@
|
||||
# Shared CMake logic for nim-ffi generated bindings. Builds the Nim library as
|
||||
# a shared object and the vendored TinyCBOR as a static library, and exposes
|
||||
# them as the imported target `${NIM_FFI_LIB}` (+ `${NIM_FFI_LIB}_nim_lib`) and
|
||||
# the `tinycbor` target. Included by the per-language generated CMakeLists,
|
||||
# which set REPO_ROOT, NIM_FFI_LIB (library name) and NIM_FFI_SRC (path to the
|
||||
# .nim root, relative to the including CMakeLists) before including this file.
|
||||
|
||||
get_filename_component(NIM_SRC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/${NIM_FFI_SRC}"
|
||||
ABSOLUTE)
|
||||
|
||||
find_program(NIM_EXECUTABLE nim REQUIRED)
|
||||
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
set(NIM_LIB_FILE "${REPO_ROOT}/lib${NIM_FFI_LIB}.dylib")
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
set(NIM_LIB_FILE "${REPO_ROOT}/${NIM_FFI_LIB}.dll")
|
||||
set(NIM_IMPLIB_FILE "${REPO_ROOT}/${NIM_FFI_LIB}.lib")
|
||||
else()
|
||||
set(NIM_LIB_FILE "${REPO_ROOT}/lib${NIM_FFI_LIB}.so")
|
||||
endif()
|
||||
|
||||
# On Windows the default Nim toolchain (mingw gcc) doesn't emit an import
|
||||
# library unless told to; without it MSVC consumers can't resolve any exported
|
||||
# symbol at link time.
|
||||
set(NIM_IMPLIB_PASSL "")
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
set(NIM_IMPLIB_PASSL "--passL:-Wl,--out-implib,${NIM_IMPLIB_FILE}")
|
||||
endif()
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${NIM_LIB_FILE}"
|
||||
COMMAND "${NIM_EXECUTABLE}" c
|
||||
--mm:orc
|
||||
-d:chronicles_log_level=WARN
|
||||
--app:lib
|
||||
--noMain
|
||||
"--nimMainPrefix:lib${NIM_FFI_LIB}"
|
||||
${NIM_IMPLIB_PASSL}
|
||||
"-o:${NIM_LIB_FILE}"
|
||||
"${NIM_SRC}"
|
||||
WORKING_DIRECTORY "${REPO_ROOT}"
|
||||
DEPENDS "${NIM_SRC}"
|
||||
BYPRODUCTS "${NIM_IMPLIB_FILE}"
|
||||
COMMENT "Compiling Nim library lib${NIM_FFI_LIB}"
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(${NIM_FFI_LIB}_nim_lib ALL DEPENDS "${NIM_LIB_FILE}")
|
||||
|
||||
# On Windows an IMPORTED SHARED target needs IMPORTED_IMPLIB, but the Visual
|
||||
# Studio multi-config generator did not pick it up and emitted
|
||||
# `${NIM_FFI_LIB}-NOTFOUND.obj`. Side-step the IMPORTED machinery there by
|
||||
# exposing the import library through a plain INTERFACE library.
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
add_library(${NIM_FFI_LIB} INTERFACE)
|
||||
target_link_libraries(${NIM_FFI_LIB} INTERFACE "${NIM_IMPLIB_FILE}")
|
||||
else()
|
||||
add_library(${NIM_FFI_LIB} SHARED IMPORTED GLOBAL)
|
||||
set_target_properties(${NIM_FFI_LIB} PROPERTIES IMPORTED_LOCATION "${NIM_LIB_FILE}")
|
||||
endif()
|
||||
add_dependencies(${NIM_FFI_LIB} ${NIM_FFI_LIB}_nim_lib)
|
||||
|
||||
# Absolute path to the runtime library (DLL/dylib/so). Exposed via the cache so
|
||||
# consumers in other directories can stage the DLL next to their executable on
|
||||
# Windows.
|
||||
set(${NIM_FFI_LIB}_RUNTIME_LIB "${NIM_LIB_FILE}" CACHE INTERNAL
|
||||
"Absolute path to the ${NIM_FFI_LIB} runtime library")
|
||||
|
||||
# ── TinyCBOR (vendored at ffi/codegen/templates/cpp/vendor/tinycbor) ─────────
|
||||
# The C and C++ backends share one vendored TinyCBOR copy. Guarded so two
|
||||
# sibling bindings dirs in one parent project don't redefine the target.
|
||||
set(TINYCBOR_SRC_DIR "${REPO_ROOT}/ffi/codegen/templates/cpp/vendor")
|
||||
if(NOT TARGET tinycbor)
|
||||
add_library(tinycbor STATIC
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor/cborencoder.c"
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor/cborencoder_close_container_checked.c"
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor/cborparser.c"
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor/cborparser_dup_string.c"
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor/cborerrorstrings.c"
|
||||
)
|
||||
target_include_directories(tinycbor PUBLIC
|
||||
"${TINYCBOR_SRC_DIR}" # consumer uses #include <tinycbor/cbor.h>
|
||||
"${TINYCBOR_SRC_DIR}/tinycbor" # internal _p.h includes resolve here
|
||||
)
|
||||
set_property(TARGET tinycbor PROPERTY C_STANDARD 99)
|
||||
set_property(TARGET tinycbor PROPERTY POSITION_INDEPENDENT_CODE ON)
|
||||
endif()
|
||||
@ -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()
|
||||
|
||||
55
tests/e2e/c/CMakeLists.txt
Normal file
55
tests/e2e/c/CMakeLists.txt
Normal file
@ -0,0 +1,55 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(nim_ffi_c_e2e C)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
||||
# ── Reuse the timer c_bindings (exposes my_timer_headers / my_timer_nim_lib) ──
|
||||
get_filename_component(_timer_c_bindings_dir
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../../../examples/timer/c_bindings"
|
||||
ABSOLUTE)
|
||||
add_subdirectory("${_timer_c_bindings_dir}" timer_c_bindings_build)
|
||||
|
||||
enable_testing()
|
||||
|
||||
add_executable(timer_e2e_c test_timer_e2e.c)
|
||||
target_link_libraries(timer_e2e_c PRIVATE my_timer_headers)
|
||||
add_dependencies(timer_e2e_c my_timer_nim_lib)
|
||||
|
||||
if(NIM_FFI_SAN_CFLAGS)
|
||||
target_compile_options(timer_e2e_c PRIVATE ${NIM_FFI_SAN_CFLAGS})
|
||||
target_link_options(timer_e2e_c PRIVATE ${NIM_FFI_SAN_LFLAGS})
|
||||
endif()
|
||||
|
||||
# Nim-built dylibs use `@rpath/lib*.so|dylib`, so embed the IMPORTED target's
|
||||
# build-tree dir as an rpath. Windows has no rpath — stage the DLL next to the
|
||||
# exe instead.
|
||||
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
get_target_property(_my_timer_loc my_timer IMPORTED_LOCATION)
|
||||
get_filename_component(_my_timer_dir "${_my_timer_loc}" DIRECTORY)
|
||||
set_target_properties(timer_e2e_c PROPERTIES
|
||||
BUILD_RPATH "${_my_timer_dir}"
|
||||
INSTALL_RPATH "${_my_timer_dir}")
|
||||
else()
|
||||
add_custom_command(TARGET timer_e2e_c POST_BUILD
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
|
||||
"${my_timer_RUNTIME_LIB}"
|
||||
"$<TARGET_FILE_DIR:timer_e2e_c>"
|
||||
COMMENT "Staging my_timer.dll next to timer_e2e_c.exe")
|
||||
endif()
|
||||
|
||||
set(_san_test_env "")
|
||||
if(NIM_FFI_SANITIZER STREQUAL "asan-ubsan")
|
||||
list(APPEND _san_test_env
|
||||
"ASAN_OPTIONS=halt_on_error=1:abort_on_error=1:detect_leaks=1:strict_string_checks=1"
|
||||
"UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1"
|
||||
"LSAN_OPTIONS=suppressions=${CMAKE_CURRENT_SOURCE_DIR}/lsan.supp:print_suppressions=0")
|
||||
elseif(NIM_FFI_SANITIZER STREQUAL "tsan")
|
||||
list(APPEND _san_test_env
|
||||
"TSAN_OPTIONS=halt_on_error=1:second_deadlock_stack=1:history_size=7")
|
||||
endif()
|
||||
|
||||
add_test(NAME timer_e2e_c COMMAND timer_e2e_c)
|
||||
if(_san_test_env)
|
||||
set_tests_properties(timer_e2e_c PROPERTIES ENVIRONMENT "${_san_test_env}")
|
||||
endif()
|
||||
18
tests/e2e/c/README.md
Normal file
18
tests/e2e/c/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
# C end-to-end test
|
||||
|
||||
Builds the generated C bindings for the timer example (`examples/timer/c_bindings`)
|
||||
and drives them against the real Nim dylib, asserting on every response. Run it
|
||||
with:
|
||||
|
||||
```sh
|
||||
nimble test_c_e2e
|
||||
```
|
||||
|
||||
which regenerates the bindings, configures CMake, builds, and runs the test via
|
||||
`ctest`. The test program (`test_timer_e2e.c`) exercises the constructor, the
|
||||
sync and async methods, nested `seq`/`Option` payloads, multi-parameter
|
||||
requests, the error channel, and the typed event listener.
|
||||
|
||||
`test_timer_e2e.c` is hand-written (it is the consumer of the bindings, not a
|
||||
generated artifact). The bindings under `examples/timer/c_bindings` are
|
||||
generated and must not be edited by hand.
|
||||
22
tests/e2e/c/lsan.supp
Normal file
22
tests/e2e/c/lsan.supp
Normal file
@ -0,0 +1,22 @@
|
||||
# LeakSanitizer suppressions for the Nim runtime.
|
||||
#
|
||||
# These are process-lifetime allocations freed implicitly at exit —
|
||||
# not real leaks. Add new entries here only with a comment justifying
|
||||
# why the leak is unavoidable, and only for symbols inside the Nim
|
||||
# standard library, chronos, or chronicles. Anything in our own code
|
||||
# (ffi/*) or the generated bindings must be fixed, not suppressed.
|
||||
|
||||
# Nim runtime initialisation — allocates global state freed at exit.
|
||||
leak:NimMain
|
||||
leak:PreMain
|
||||
leak:systemDatInit
|
||||
|
||||
# GC bootstrap — registers stack bottom / TLS slots once per thread.
|
||||
leak:nimGC_setStackBottom
|
||||
leak:initStackBottomWith
|
||||
leak:setupForeignThreadGc
|
||||
|
||||
# Async / logging library globals (event loop singletons, logger
|
||||
# registries) — owned for the process lifetime.
|
||||
leak:chronos
|
||||
leak:chronicles
|
||||
263
tests/e2e/c/test_timer_e2e.c
Normal file
263
tests/e2e/c/test_timer_e2e.c
Normal file
@ -0,0 +1,263 @@
|
||||
/* End-to-end test for the generated C timer bindings. Exercises the same
|
||||
* surface as the C++ suite — constructor, methods, nested seq/Option payloads,
|
||||
* multi-parameter requests, the error channel and the typed event listener —
|
||||
* and aborts (non-zero exit) on the first failure so ctest reports it.
|
||||
*
|
||||
* The binding is asynchronous: every call takes a result callback and the
|
||||
* reply/error are owned by the binding and valid only for the duration of that
|
||||
* callback. So each callback *copies out* what it needs into a waiter struct,
|
||||
* and the test polls a `done` flag (the same volatile-flag pattern the event
|
||||
* listener uses) to turn each async call back into a sequential check. The
|
||||
* caller never frees reply data or error strings — that is the whole point. */
|
||||
#include "my_timer.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
static void wait_done(volatile int* done) {
|
||||
for (int i = 0; i < 500 && !*done; i++) {
|
||||
struct timespec t = {0, 10 * 1000 * 1000}; /* 10ms */
|
||||
nanosleep(&t, NULL);
|
||||
}
|
||||
assert(*done);
|
||||
}
|
||||
|
||||
static int g_event_count = 0;
|
||||
static char g_event_message[256];
|
||||
|
||||
static void on_echo_fired(const EchoEvent* evt, void* user_data) {
|
||||
int* hits = (int*)user_data;
|
||||
if (hits) {
|
||||
(*hits)++;
|
||||
}
|
||||
g_event_count = (int)evt->echoCount;
|
||||
snprintf(g_event_message, sizeof(g_event_message), "%s",
|
||||
evt->message.data ? evt->message.data : "");
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
volatile int done;
|
||||
int err_code;
|
||||
MyTimerCtx* ctx;
|
||||
char err[256];
|
||||
} CreateWaiter;
|
||||
|
||||
static void on_created(int ec, MyTimerCtx* ctx, const char* em, void* ud) {
|
||||
CreateWaiter* w = (CreateWaiter*)ud;
|
||||
w->err_code = ec;
|
||||
w->ctx = ctx;
|
||||
if (em) {
|
||||
snprintf(w->err, sizeof(w->err), "%s", em);
|
||||
}
|
||||
w->done = 1;
|
||||
}
|
||||
|
||||
static MyTimerCtx* make_ctx(void) {
|
||||
CreateWaiter w;
|
||||
memset(&w, 0, sizeof(w));
|
||||
TimerConfig config = {nimffi_str("c-e2e")};
|
||||
my_timer_ctx_create(&config, on_created, &w);
|
||||
wait_done(&w.done);
|
||||
if (w.err_code != 0) {
|
||||
fprintf(stderr, "create failed: %s\n", w.err[0] ? w.err : "?");
|
||||
}
|
||||
assert(w.err_code == 0);
|
||||
assert(w.ctx != NULL);
|
||||
return w.ctx;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
volatile int done;
|
||||
int err_code;
|
||||
char err[256];
|
||||
char text_a[256];
|
||||
char text_b[256];
|
||||
long long num_a;
|
||||
int flag;
|
||||
} ReplyWaiter;
|
||||
|
||||
static void on_version(int ec, const NimFfiStr* reply, const char* em, void* ud) {
|
||||
ReplyWaiter* w = (ReplyWaiter*)ud;
|
||||
w->err_code = ec;
|
||||
if (reply && reply->data) snprintf(w->text_a, sizeof(w->text_a), "%s", reply->data);
|
||||
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
|
||||
w->done = 1;
|
||||
}
|
||||
|
||||
static void test_version(MyTimerCtx* ctx) {
|
||||
ReplyWaiter w;
|
||||
memset(&w, 0, sizeof(w));
|
||||
my_timer_ctx_version(ctx, on_version, &w);
|
||||
wait_done(&w.done);
|
||||
assert(w.err_code == 0);
|
||||
assert(strcmp(w.text_a, "nim-timer v0.1.0") == 0);
|
||||
}
|
||||
|
||||
static void on_echo(int ec, const EchoResponse* reply, const char* em, void* ud) {
|
||||
ReplyWaiter* w = (ReplyWaiter*)ud;
|
||||
w->err_code = ec;
|
||||
if (reply) {
|
||||
if (reply->echoed.data) snprintf(w->text_a, sizeof(w->text_a), "%s", reply->echoed.data);
|
||||
if (reply->timerName.data)
|
||||
snprintf(w->text_b, sizeof(w->text_b), "%s", reply->timerName.data);
|
||||
}
|
||||
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
|
||||
w->done = 1;
|
||||
}
|
||||
|
||||
static void test_echo(MyTimerCtx* ctx) {
|
||||
ReplyWaiter w;
|
||||
memset(&w, 0, sizeof(w));
|
||||
EchoRequest req = {nimffi_str("hello"), 10};
|
||||
my_timer_ctx_echo(ctx, &req, on_echo, &w);
|
||||
wait_done(&w.done);
|
||||
assert(w.err_code == 0);
|
||||
assert(strcmp(w.text_a, "hello") == 0);
|
||||
assert(strcmp(w.text_b, "c-e2e") == 0);
|
||||
}
|
||||
|
||||
static void on_complex(int ec, const ComplexResponse* reply, const char* em, void* ud) {
|
||||
ReplyWaiter* w = (ReplyWaiter*)ud;
|
||||
w->err_code = ec;
|
||||
if (reply) {
|
||||
w->num_a = (long long)reply->itemCount;
|
||||
w->flag = (int)reply->hasNote;
|
||||
if (reply->summary.data)
|
||||
snprintf(w->text_a, sizeof(w->text_a), "%s", reply->summary.data);
|
||||
}
|
||||
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
|
||||
w->done = 1;
|
||||
}
|
||||
|
||||
static void test_complex(MyTimerCtx* ctx) {
|
||||
EchoRequest items[2] = {{nimffi_str("one"), 1}, {nimffi_str("two"), 2}};
|
||||
NimFfiStr tags[2] = {nimffi_str("a"), nimffi_str("b")};
|
||||
ComplexRequest req;
|
||||
req.messages.data = items;
|
||||
req.messages.len = 2;
|
||||
req.tags.data = tags;
|
||||
req.tags.len = 2;
|
||||
req.note.has_value = true;
|
||||
req.note.value = nimffi_str("note");
|
||||
req.retries.has_value = false;
|
||||
req.retries.value = 0;
|
||||
|
||||
ReplyWaiter w;
|
||||
memset(&w, 0, sizeof(w));
|
||||
my_timer_ctx_complex(ctx, &req, on_complex, &w);
|
||||
wait_done(&w.done);
|
||||
assert(w.err_code == 0);
|
||||
assert(w.num_a == 2);
|
||||
assert(w.flag == true);
|
||||
assert(strstr(w.text_a, "note=note") != NULL);
|
||||
}
|
||||
|
||||
static void on_schedule(int ec, const ScheduleResult* reply, const char* em, void* ud) {
|
||||
ReplyWaiter* w = (ReplyWaiter*)ud;
|
||||
w->err_code = ec;
|
||||
if (reply) {
|
||||
w->num_a = (long long)reply->willRunCount;
|
||||
if (reply->jobId.data) snprintf(w->text_a, sizeof(w->text_a), "%s", reply->jobId.data);
|
||||
}
|
||||
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
|
||||
w->done = 1;
|
||||
}
|
||||
|
||||
static void test_schedule_ok(MyTimerCtx* ctx) {
|
||||
NimFfiStr payload[1] = {nimffi_str("p")};
|
||||
JobSpec job;
|
||||
job.name = nimffi_str("rollup");
|
||||
job.payload.data = payload;
|
||||
job.payload.len = 1;
|
||||
job.priority = 1;
|
||||
|
||||
NimFfiStr retry_on[1] = {nimffi_str("timeout")};
|
||||
RetryPolicy retry;
|
||||
retry.maxAttempts = 3;
|
||||
retry.backoffMs = 100;
|
||||
retry.retryOn.data = retry_on;
|
||||
retry.retryOn.len = 1;
|
||||
|
||||
ScheduleConfig sched;
|
||||
sched.startAtMs = 1000;
|
||||
sched.intervalMs = 0;
|
||||
sched.jitter.has_value = false;
|
||||
sched.jitter.value = 0;
|
||||
|
||||
ReplyWaiter w;
|
||||
memset(&w, 0, sizeof(w));
|
||||
my_timer_ctx_schedule(ctx, &job, &retry, &sched, on_schedule, &w);
|
||||
wait_done(&w.done);
|
||||
assert(w.err_code == 0);
|
||||
assert(strcmp(w.text_a, "c-e2e:rollup") == 0);
|
||||
assert(w.num_a == 1);
|
||||
}
|
||||
|
||||
static void test_schedule_error(MyTimerCtx* ctx) {
|
||||
NimFfiStr payload[1] = {nimffi_str("p")};
|
||||
JobSpec job;
|
||||
job.name = nimffi_str(""); /* empty name → handler returns err */
|
||||
job.payload.data = payload;
|
||||
job.payload.len = 1;
|
||||
job.priority = 1;
|
||||
|
||||
NimFfiStr retry_on[1] = {nimffi_str("timeout")};
|
||||
RetryPolicy retry;
|
||||
retry.maxAttempts = 3;
|
||||
retry.backoffMs = 100;
|
||||
retry.retryOn.data = retry_on;
|
||||
retry.retryOn.len = 1;
|
||||
|
||||
ScheduleConfig sched;
|
||||
sched.startAtMs = 0;
|
||||
sched.intervalMs = 0;
|
||||
sched.jitter.has_value = false;
|
||||
sched.jitter.value = 0;
|
||||
|
||||
ReplyWaiter w;
|
||||
memset(&w, 0, sizeof(w));
|
||||
my_timer_ctx_schedule(ctx, &job, &retry, &sched, on_schedule, &w);
|
||||
wait_done(&w.done);
|
||||
assert(w.err_code != 0);
|
||||
assert(w.err[0] != '\0');
|
||||
assert(strstr(w.err, "job name") != NULL);
|
||||
}
|
||||
|
||||
static void test_event(MyTimerCtx* ctx) {
|
||||
int hits = 0;
|
||||
uint64_t handle =
|
||||
my_timer_ctx_add_on_echo_fired_listener(ctx, on_echo_fired, &hits);
|
||||
assert(handle != 0);
|
||||
|
||||
ReplyWaiter w;
|
||||
memset(&w, 0, sizeof(w));
|
||||
EchoRequest req = {nimffi_str("evt"), 1};
|
||||
my_timer_ctx_echo(ctx, &req, on_echo, &w);
|
||||
wait_done(&w.done);
|
||||
assert(w.err_code == 0);
|
||||
|
||||
/* The event fires from the dispatch thread; poll briefly for delivery. */
|
||||
for (int i = 0; i < 100 && hits == 0; i++) {
|
||||
struct timespec t = {0, 10 * 1000 * 1000};
|
||||
nanosleep(&t, NULL);
|
||||
}
|
||||
assert(hits >= 1);
|
||||
assert(strcmp(g_event_message, "evt") == 0);
|
||||
assert(g_event_count == 1);
|
||||
|
||||
assert(my_timer_ctx_remove_event_listener(ctx, handle) == true);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
MyTimerCtx* ctx = make_ctx();
|
||||
test_version(ctx);
|
||||
test_echo(ctx);
|
||||
test_complex(ctx);
|
||||
test_schedule_ok(ctx);
|
||||
test_schedule_error(ctx);
|
||||
test_event(ctx);
|
||||
my_timer_ctx_destroy(ctx);
|
||||
printf("all C e2e checks passed\n");
|
||||
return 0;
|
||||
}
|
||||
229
tests/unit/test_c_codegen.nim
Normal file
229
tests/unit/test_c_codegen.nim
Normal file
@ -0,0 +1,229 @@
|
||||
## Unit-tests for the C binding generator. Drives generateCLibHeader (and the
|
||||
## shared-header generators) directly against a synthetic registry (no macro
|
||||
## pipeline, no files written) and asserts on the emitted text — the same
|
||||
## approach as test_cddl_codegen.
|
||||
|
||||
import std/strutils
|
||||
import unittest2
|
||||
import ffi/codegen/[meta, c]
|
||||
|
||||
proc field(n, t: string): FFIFieldMeta =
|
||||
FFIFieldMeta(name: n, typeName: t)
|
||||
|
||||
proc param(n, t: string, isPtr = false): FFIParamMeta =
|
||||
FFIParamMeta(name: n, typeName: t, isPtr: isPtr)
|
||||
|
||||
suite "generateCLibHeader: types and codecs":
|
||||
setup:
|
||||
let types = @[
|
||||
FFITypeMeta(
|
||||
name: "EchoRequest",
|
||||
fields: @[field("message", "string"), field("delayMs", "int")],
|
||||
),
|
||||
FFITypeMeta(name: "EchoResponse", fields: @[field("echoed", "string")]),
|
||||
FFITypeMeta(
|
||||
name: "ComplexRequest",
|
||||
fields:
|
||||
@[field("messages", "seq[EchoRequest]"), field("note", "Option[string]")],
|
||||
),
|
||||
]
|
||||
let procs = @[
|
||||
FFIProcMeta(
|
||||
procName: "timer_create",
|
||||
libName: "timer",
|
||||
kind: FFIKind.CTOR,
|
||||
libTypeName: "Timer",
|
||||
extraParams: @[param("config", "EchoRequest")],
|
||||
returnTypeName: "Timer",
|
||||
),
|
||||
FFIProcMeta(
|
||||
procName: "timer_echo",
|
||||
libName: "timer",
|
||||
kind: FFIKind.FFI,
|
||||
libTypeName: "Timer",
|
||||
extraParams: @[param("req", "EchoRequest")],
|
||||
returnTypeName: "EchoResponse",
|
||||
),
|
||||
FFIProcMeta(
|
||||
procName: "timer_destroy",
|
||||
libName: "timer",
|
||||
kind: FFIKind.DTOR,
|
||||
libTypeName: "Timer",
|
||||
extraParams: @[],
|
||||
returnTypeName: "",
|
||||
),
|
||||
]
|
||||
let header = generateCLibHeader(procs, types, "timer")
|
||||
|
||||
test "the lib header pulls in the shared cbor header and uses its codecs":
|
||||
check "#include \"nim_ffi_cbor.h\"" in header
|
||||
check "NimFfiStr" in header
|
||||
check "nimffi_enc_str" in header
|
||||
|
||||
test "user structs become C structs with mapped field types":
|
||||
check "} EchoRequest;" in header
|
||||
check "int64_t delayMs;" in header
|
||||
check "NimFfiStr message;" in header
|
||||
|
||||
test "per-struct encode/decode/free are emitted":
|
||||
check "timer_enc_EchoRequest(" in header
|
||||
check "timer_dec_EchoRequest(" in header
|
||||
check "timer_free_EchoRequest(" in header
|
||||
|
||||
test "seq[T] is monomorphised into a sized struct":
|
||||
check "} TimerSeq_EchoRequest;" in header
|
||||
check "EchoRequest* data;" in header
|
||||
check "timer_enc_TimerSeq_EchoRequest(" in header
|
||||
|
||||
test "Option[T] is monomorphised with a has_value flag":
|
||||
check "} TimerOpt_Str;" in header
|
||||
check "bool has_value;" in header
|
||||
|
||||
test "a struct whose fields own no heap memory gets no free helper":
|
||||
# EchoResponse has only a string field, so it does get a free; assert the
|
||||
# inverse with the per-proc Version-less example via the int-only check:
|
||||
check "timer_free_EchoResponse(" in header
|
||||
|
||||
suite "generateCLibHeader: ABI declarations and context API":
|
||||
setup:
|
||||
let procs = @[
|
||||
FFIProcMeta(
|
||||
procName: "timer_create",
|
||||
libName: "timer",
|
||||
kind: FFIKind.CTOR,
|
||||
libTypeName: "Timer",
|
||||
extraParams: @[param("config", "EchoRequest")],
|
||||
returnTypeName: "Timer",
|
||||
),
|
||||
FFIProcMeta(
|
||||
procName: "timer_version",
|
||||
libName: "timer",
|
||||
kind: FFIKind.FFI,
|
||||
libTypeName: "Timer",
|
||||
extraParams: @[],
|
||||
returnTypeName: "string",
|
||||
),
|
||||
FFIProcMeta(
|
||||
procName: "timer_destroy",
|
||||
libName: "timer",
|
||||
kind: FFIKind.DTOR,
|
||||
libTypeName: "Timer",
|
||||
extraParams: @[],
|
||||
returnTypeName: "",
|
||||
),
|
||||
]
|
||||
let types = @[FFITypeMeta(name: "EchoRequest", fields: @[field("m", "string")])]
|
||||
let header = generateCLibHeader(procs, types, "timer")
|
||||
|
||||
test "raw dylib symbols are declared with the C ABI shape":
|
||||
check "void* timer_create(const uint8_t* req_cbor, size_t req_cbor_len," in header
|
||||
check "int timer_version(void* ctx, FFICallback callback" in header
|
||||
check "int timer_destroy(void* ctx);" in header
|
||||
check "uint64_t timer_add_event_listener(" in header
|
||||
|
||||
test "high-level wrappers are namespaced to avoid the raw symbols":
|
||||
check "timer_ctx_create(" in header
|
||||
check "timer_ctx_version(" in header
|
||||
check "timer_ctx_destroy(" in header
|
||||
|
||||
test "the async API is callback-driven, not blocking":
|
||||
# methods take a typed reply callback + user_data; no out-param, no char** err
|
||||
check "typedef void (*TimerVersionReplyFn)(int err_code, const NimFfiStr* reply, const char* err_msg, void* user_data);" in
|
||||
header
|
||||
check "TimerVersionCallBox" in header
|
||||
check "timer_version_reply_trampoline(" in header
|
||||
check "timer_ctx_version(const TimerCtx* ctx, TimerVersionReplyFn on_reply, void* user_data)" in
|
||||
header
|
||||
|
||||
test "the constructor is async and hands the context to a callback":
|
||||
check "typedef void (*TimerCreateFn)(int err_code, TimerCtx* ctx, const char* err_msg, void* user_data);" in
|
||||
header
|
||||
check "timer_create_trampoline(" in header
|
||||
check "timer_ctx_create(const EchoRequest* config, TimerCreateFn on_created, void* user_data)" in
|
||||
header
|
||||
|
||||
test "no blocking sync-call machinery or per-call timeout survives":
|
||||
check "nimffi_wait_result" notin header
|
||||
check "NimFfiCallState" notin header
|
||||
check "timeout_ms" notin header
|
||||
|
||||
test "an empty request envelope still encodes a (zero-length) map":
|
||||
check "_nimffi_empty" in header
|
||||
|
||||
suite "generateCLibHeader: events":
|
||||
setup:
|
||||
let procs = @[
|
||||
FFIProcMeta(
|
||||
procName: "timer_create",
|
||||
libName: "timer",
|
||||
kind: FFIKind.CTOR,
|
||||
libTypeName: "Timer",
|
||||
extraParams: @[],
|
||||
returnTypeName: "Timer",
|
||||
),
|
||||
FFIProcMeta(
|
||||
procName: "timer_destroy",
|
||||
libName: "timer",
|
||||
kind: FFIKind.DTOR,
|
||||
libTypeName: "Timer",
|
||||
extraParams: @[],
|
||||
returnTypeName: "",
|
||||
),
|
||||
]
|
||||
let types = @[FFITypeMeta(name: "TickEvent", fields: @[field("count", "int")])]
|
||||
let events = @[
|
||||
FFIEventMeta(
|
||||
wireName: "on_tick",
|
||||
nimProcName: "onTick",
|
||||
libName: "timer",
|
||||
payloadTypeName: "TickEvent",
|
||||
)
|
||||
]
|
||||
let header = generateCLibHeader(procs, types, "timer", events)
|
||||
|
||||
test "a typed handler, box and trampoline are emitted per event":
|
||||
check "TimerOnTickFn" in header
|
||||
check "TimerOnTickBox" in header
|
||||
check "timer_on_tick_trampoline(" in header
|
||||
|
||||
test "the registration API uses the wire name and snake-cased proc name":
|
||||
check "timer_ctx_add_on_tick_listener(" in header
|
||||
check "\"on_tick\"" in header
|
||||
check "timer_ctx_remove_event_listener(" in header
|
||||
|
||||
test "the context tracks listeners only when events exist":
|
||||
check "TimerCtxListener* listeners;" in header
|
||||
|
||||
suite "generateCLibHeader: no-event libraries stay lean":
|
||||
test "a library without events has no listener bookkeeping":
|
||||
let procs = @[
|
||||
FFIProcMeta(
|
||||
procName: "timer_create",
|
||||
libName: "timer",
|
||||
kind: FFIKind.CTOR,
|
||||
libTypeName: "Timer",
|
||||
extraParams: @[],
|
||||
returnTypeName: "Timer",
|
||||
)
|
||||
]
|
||||
let header = generateCLibHeader(procs, @[], "timer")
|
||||
check "listeners_len" notin header
|
||||
check "_add_event_listener" in header # raw ABI symbol is always declared
|
||||
|
||||
suite "shared headers: prelude and cbor split":
|
||||
test "the prelude owns the leaf types and libc/TinyCBOR includes":
|
||||
let prelude = generateCPreludeHeader()
|
||||
check "#include <tinycbor/cbor.h>" in prelude
|
||||
check "} NimFfiStr;" in prelude
|
||||
check "nimffi_free_str" in prelude
|
||||
|
||||
test "the cbor header carries the leaf codecs and pulls in the prelude":
|
||||
let cbor = generateCCborHeader()
|
||||
check "#include \"nim_ffi_prelude.h\"" in cbor
|
||||
check "nimffi_enc_str" in cbor
|
||||
check "nimffi_decode_from_buf" in cbor
|
||||
|
||||
test "each generated file is independently include-guarded":
|
||||
check "NIM_FFI_PRELUDE_H_INCLUDED" in generateCPreludeHeader()
|
||||
check "NIM_FFI_CBOR_HELPERS_H_INCLUDED" in generateCCborHeader()
|
||||
check "NIM_FFI_LIB_TIMER_H_INCLUDED" in generateCLibHeader(@[], @[], "timer")
|
||||
Loading…
x
Reference in New Issue
Block a user