## Unit-tests for the C binding generator: drives generateCLibHeader (and the ## shared-header generators) against a synthetic registry and asserts on the text. import std/[strutils, sequtils] import unittest2 import ffi/codegen/[meta, c] import ffi/internal/ffi_scalar 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 a string field, so it gets a free helper. 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 context destructor propagates the destructor's status code": check( """ static inline int timer_ctx_destroy(TimerCtx* ctx) { if (!ctx) return NIMFFI_RET_OK; int rc = NIMFFI_RET_OK; if (ctx->ptr) { rc = timer_destroy(ctx->ptr); ctx->ptr = NULL; } free(ctx); return rc; }""" 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: context-independent procs": 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_parse", libName: "timer", kind: FFIKind.STATIC, libTypeName: "Timer", extraParams: @[param("req", "EchoRequest")], returnTypeName: "EchoResponse", ), ] let types = @[ FFITypeMeta(name: "EchoRequest", fields: @[field("m", "string")]), FFITypeMeta(name: "EchoResponse", fields: @[field("echoed", "string")]), ] let header = generateCLibHeader(procs, types, "timer") test "the static's raw symbol takes no ctx": check "int timer_parse(FFICallback callback, void* user_data, " & "const uint8_t* req_cbor, size_t req_cbor_len);" in header test "its wrapper is _static_-namespaced and takes neither ctx nor timeout": check "timer_static_parse(const EchoRequest* req, TimerParseReplyFn on_reply, void* user_data)" in header check "timer_ctx_parse(" notin header test "the wrapper calls the raw symbol without a ctx argument": check "timer_parse(timer_parse_reply_trampoline, box, req_buf, req_len);" in header test "a static gets the same reply machinery as a method": check "typedef void (*TimerParseReplyFn)(int err_code, const EchoResponse* reply, " & "const char* err_msg, void* user_data);" in header check "TimerParseCallBox" in header check "timer_parse_reply_trampoline(" in header test "its return type is monomorphised into the codecs": check "timer_decv_EchoResponse" in header test "methods keep their ctx": check "int timer_version(void* ctx, FFICallback callback" in header check "timer_ctx_version(const TimerCtx* ctx," 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 test "a failed teardown leaks the listener boxes instead of dangling them": # A non-OK rc leaves the worker threads live, still holding each box as # callback user_data, so the sweep must sit behind the rc guard. check( """ static inline int timer_ctx_destroy(TimerCtx* ctx) { if (!ctx) return NIMFFI_RET_OK; int rc = NIMFFI_RET_OK; if (ctx->ptr) { rc = timer_destroy(ctx->ptr); ctx->ptr = NULL; } if (rc == NIMFFI_RET_OK) { for (size_t i = 0; i < ctx->listeners_len; i++) free(ctx->listeners[i].box); } free(ctx->listeners); free(ctx); return rc; }""" 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 test "a library without a dtor still reports success from ctx_destroy": let procs = @[ FFIProcMeta( procName: "timer_create", libName: "timer", kind: FFIKind.CTOR, libTypeName: "Timer", extraParams: @[], returnTypeName: "Timer", ) ] let header = generateCLibHeader(procs, @[], "timer") check( """ static inline int timer_ctx_destroy(TimerCtx* ctx) { if (!ctx) return NIMFFI_RET_OK; int rc = NIMFFI_RET_OK; free(ctx); return rc; }""" in header ) suite "generateCLibHeader: scalar-fast-path procs are excluded": setup: let procs = @[ FFIProcMeta( procName: "calc_create", libName: "calc", kind: FFIKind.CTOR, libTypeName: "Calc", returnTypeName: "Calc", ), FFIProcMeta( procName: "calc_echo", libName: "calc", kind: FFIKind.FFI, libTypeName: "Calc", extraParams: @[param("req", "EchoRequest")], returnTypeName: "EchoResponse", ), FFIProcMeta( procName: "calc_add", libName: "calc", kind: FFIKind.FFI, libTypeName: "Calc", extraParams: @[param("a", "int"), param("b", "int")], returnTypeName: "int", abiFormat: ABIFormat.C, scalarFastPath: true, ), ] let types = @[ FFITypeMeta(name: "EchoRequest", fields: @[field("m", "string")]), FFITypeMeta(name: "EchoResponse", fields: @[field("echoed", "string")]), ] test "bindableProcs keeps the CBOR procs and drops the scalar one": let kept = bindableProcs(procs) check kept.anyIt(it.procName == "calc_create") check kept.anyIt(it.procName == "calc_echo") check not kept.anyIt(it.procName == "calc_add") test "the C header emitted from the bindable set carries no scalar symbol": let header = generateCLibHeader(bindableProcs(procs), types, "calc") check "int calc_echo(void* ctx, FFICallback callback" in header check "int calc_add(" notin header # note: calc_add_event_listener is unrelated test "unfiltered, the generator would emit a wrong-ABI CBOR caller for it": # Unfiltered, the generator emits a wrong-ABI CBOR prototype for the scalar proc. let header = generateCLibHeader(procs, types, "calc") check "int calc_add(void* ctx, FFICallback callback, void* user_data, " & "const uint8_t* req_cbor, size_t req_cbor_len);" in header suite "shared headers: prelude and cbor split": test "the prelude owns the leaf types and libc/TinyCBOR includes": let prelude = generateCPreludeHeader() check "#include " in prelude check "} NimFfiStr;" in prelude check "nimffi_free_str" in prelude test "the cbor header carries the leaf codecs and pulls in the prelude": let cbor = generateCCborHeader() check "#include \"nim_ffi_prelude.h\"" in cbor check "nimffi_enc_str" in cbor check "nimffi_decode_from_buf" in cbor test "each generated file is independently include-guarded": check "NIM_FFI_PRELUDE_H_INCLUDED" in generateCPreludeHeader() check "NIM_FFI_CBOR_HELPERS_H_INCLUDED" in generateCCborHeader() check "NIM_FFI_LIB_TIMER_H_INCLUDED" in generateCLibHeader(@[], @[], "timer")