diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd25502..f6587a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -170,7 +170,7 @@ jobs: # The CBOR-free abi=c C binding (issue #105). Linux-only: the generated # header is platform-checked by the check-bindings job, and the runtime - # path only needs one OS to exercise the flat-struct dispatch. + # path only needs one OS to exercise the abi=c struct dispatch. - name: Run abi=c C e2e test if: matrix.label == 'Linux' shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index 2afe662..ec6cb5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,25 +60,25 @@ All notable changes to this project are documented in this file. `"abi = cbor"` spec (e.g. `{.ffi: "abi = cbor".}`). `declareLibrary` is now required before any FFI annotation ([#78](https://github.com/logos-messaging/nim-ffi/issues/78)). -- `c` (flat C-struct) ABI **codec**: every `{.ffi: "abi = c".}` type gets a +- `c` (`abi = c` C-struct) ABI **codec**: every `{.ffi: "abi = c".}` type gets a `_CWire` companion plus `cwirePack` / `cwireUnpack` / `cwireFree`. This - first slice covers the flat path — POD scalars and `string` (as `cstring`); - composite fields follow. (The `c` proc-dispatch path and its CBOR-free C - generator landed later in this release — see the `-d:targetLang=c_abi` entry - below; `c` events remain CBOR-only.) -- **CBOR-free `abi = c` C binding generator** (`-d:targetLang=c_abi`): emits a - single self-contained `.h` whose flat `_CWire` structs *are* the C ABI, - so the C consumer passes native structs and links no CBOR at all (contrast - the CBOR `-d:targetLang=c` backend). The `c` proc-dispatch path is now wired - end-to-end: the generated exported wrappers `cwireUnpack` the request into a - Nim object, reuse the existing CBOR thread transport internally, and a Nim - reply trampoline `cwirePack`s the response back into a flat struct for the - caller's typed callback. `abiCodegenImplemented` now accepts `c` for - proc/ctor/dtor annotations (events remain CBOR-only). New - `examples/echo/c_abi_bindings/` (checked in beside the CBOR `c_bindings/` for - comparison), `nimble genbindings_c_abi_echo` / `check_bindings_c_abi` / - `test_c_abi_e2e` / `test_c_abi_e2e_sanitized` tasks, and a `tests/e2e/c_abi` - ctest harness ([#105](https://github.com/logos-messaging/nim-ffi/issues/105)). + first slice covers the `abi = c` path — POD scalars and `string` (as `cstring`); + composite fields follow. (`c` events remain CBOR-only.) +- **CBOR-free (`abi = c`) C bindings, emitted by the single `c` target** + (`-d:targetLang=c`): the one `c` generator now picks its output from the + library's ABI format — the `abi = c` header or the CBOR header. The `abi = c` + header is a single self-contained `.h` whose `_CWire` structs *are* the C + ABI, so the C consumer passes native structs and links no CBOR at all. The `c` + proc-dispatch path is wired end-to-end: the generated exported wrappers + `cwireUnpack` the request into a Nim object, reuse the existing CBOR thread + transport internally, and a Nim reply trampoline `cwirePack`s the response + back into a `_CWire` struct for the caller's typed callback. + `abiCodegenImplemented` accepts `c` for proc/ctor/dtor annotations (events + remain CBOR-only). New `examples/echo/c_abi_bindings/` (checked in beside the + CBOR `c_bindings/` for comparison), `nimble genbindings_c_abi_echo` / + `check_bindings_c_abi` / `test_c_abi_e2e` / `test_c_abi_e2e_sanitized` + tasks, and a `tests/e2e/c_abi` ctest harness + ([#105](https://github.com/logos-messaging/nim-ffi/issues/105)). - `tests/bench/bench_codec.nim` (+ `nimble bench_codec`): a single-process microbenchmark comparing the `cbor` and `c` codecs across payload shapes, isolating codec cost from the (identical) thread/callback round-trip. diff --git a/README.md b/README.md index 5aab5e3..27346cc 100644 --- a/README.md +++ b/README.md @@ -153,9 +153,9 @@ for; it does not change the wire. event serializes through the generic CBOR path, and all binding generators emit working callers for it. -`abi = c` is a newer, flat C-struct wire (no CBOR round-trip). Callers for it -are emitted only by the dedicated `c_abi` generator (`-d:targetLang=c_abi`). It -carries two honest limits today: +`abi = c` is a newer, native C-struct wire (no CBOR round-trip). The single `c` +generator (`-d:targetLang=c`) emits its callers, choosing the `abi = c` or CBOR +header shape from the library's ABI format. It carries two honest limits today: - **Events are CBOR-only.** Applying `abi = c` to an `{.ffiEvent.}` proc is a hard compile error; declare events with `abi = cbor` (they ride CBOR @@ -206,19 +206,19 @@ nim c --app:lib --noMain --nimMainPrefix:libmylib mylib.nim **2. Emit the foreign bindings** — add the binding defines and `--compileOnly`, which stops after codegen: the binding files are written during macro expansion, so there's no library to link (no `--app:lib`/`-o:/dev/null` needed). The -generated files (for `targetLang=c`/`c_abi`: the `.h` header your host -includes, plus a `CMakeLists.txt`) land in `-d:ffiOutputDir`: +generated files (for `targetLang=c`: the `.h` header your host includes, +plus a `CMakeLists.txt`) land in `-d:ffiOutputDir`: ```sh nim c -d:ffiGenBindings -d:targetLang=rust,cpp,c --compileOnly mylib.nim ``` - `-d:targetLang` — which generator(s) run; pass a comma-separated list to emit - several from one compile. Two kinds: - - **Language bindings over the CBOR wire:** `rust` (default), `cpp`, `c`. - - **Non-peer generators:** `c_abi` — C bindings that speak the flat `abi = c` - wire instead of CBOR; `cddl` — a CDDL schema of the CBOR wire, not a - language binding at all. + several from one compile: + - **Language bindings:** `rust` (default), `cpp`, `c`. The `c` target follows + the library's ABI format — an `abi = c` C-struct header for `abi = c`, a CBOR + header otherwise; `rust`/`cpp` speak CBOR. + - **`cddl`** — a CDDL schema of the CBOR wire, not a language binding at all. - `-d:ffiOutputDir` — override where the generated files land. Defaults to `_bindings/` next to the compiled source. - `-d:ffiSrcPath` — override the Nim source path embedded in the generated build diff --git a/examples/echo/c_abi_bindings/CMakeLists.txt b/examples/echo/c_abi_bindings/CMakeLists.txt index f007c8a..90a8064 100644 --- a/examples/echo/c_abi_bindings/CMakeLists.txt +++ b/examples/echo/c_abi_bindings/CMakeLists.txt @@ -4,8 +4,8 @@ project(echo_c_abi_bindings C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) -# The CBOR-free `abi = c` binding links no TinyCBOR — the flat structs in the -# generated header are the ABI. Only the Nim dylib is built. +# The CBOR-free `abi = c` binding links no TinyCBOR — the generated header +# structs are the ABI. Only the Nim dylib is built. set(_search_dir "${CMAKE_CURRENT_SOURCE_DIR}") set(REPO_ROOT "") diff --git a/examples/echo/c_abi_bindings/README.md b/examples/echo/c_abi_bindings/README.md index ee39b15..eea474b 100644 --- a/examples/echo/c_abi_bindings/README.md +++ b/examples/echo/c_abi_bindings/README.md @@ -1,12 +1,14 @@ -# echo — CBOR-free `abi = c` C bindings +# echo — CBOR-free (`abi = c`) C bindings -Generated by `nimble genbindings_c_abi_echo` (`-d:targetLang=c_abi`), this is the -**pure C ABI** rendering of the echo library, meant to be read side-by-side with -the CBOR rendering in [`../c_bindings/`](../c_bindings/) (issue #105). +Generated by `nimble genbindings_c_abi_echo`, this is the **pure C ABI** +rendering of the echo library, meant to be read side-by-side with the CBOR +rendering in [`../c_bindings/`](../c_bindings/) (issue #105). Both come from the +single `c` target (`-d:targetLang=c`); the library's `defaultABIFormat` picks +the shape. -| | `../c_bindings/` (`-d:targetLang=c`) | this dir (`-d:targetLang=c_abi`) | +| | `../c_bindings/` (`abi = cbor`) | this dir (`abi = c`) | |---|---|---| -| Wire format on the C side | CBOR | flat C structs (no serialization) | +| Wire format on the C side | CBOR | native C structs (no serialization) | | Third-party dependency | vendored TinyCBOR | none | | Files | `echo.h` + `nim_ffi_cbor.h` + `nim_ffi_prelude.h` | one self-contained `echo.h` | | String type | `NimFfiStr` (owned) | `const char*` (borrowed for the call) | diff --git a/examples/echo/c_abi_bindings/echo.h b/examples/echo/c_abi_bindings/echo.h index d6810cf..11ac4ff 100644 --- a/examples/echo/c_abi_bindings/echo.h +++ b/examples/echo/c_abi_bindings/echo.h @@ -14,7 +14,7 @@ terminal RET_OK/RET_ERR. Ignore it unless you want progress. */ #define NIMFFI_RET_STALE_WARN 3 -/* Flat wire structs — the C ABI. Strings are borrowed, NUL-terminated +/* `abi = c` wire structs — the C ABI. Strings are borrowed, NUL-terminated `const char*` valid only for the duration of the call they cross. */ typedef struct { const char* prefix; @@ -58,7 +58,6 @@ typedef struct { EchoCreateFn fn; void* user_data; } EchoCreateBox; static void echo_create_trampoline(int ret, const char* ctx_addr, const char* err_msg, void* ud) { EchoCreateBox* box = (EchoCreateBox*)ud; if (!box) return; - /* Non-terminal progress ping: keep the box for the terminal reply. */ if (ret == NIMFFI_RET_STALE_WARN) return; if (!box->fn) { free(box); return; } if (ret != 0) { diff --git a/examples/echo/echo.nim b/examples/echo/echo.nim index 4dcc1af..7313859 100644 --- a/examples/echo/echo.nim +++ b/examples/echo/echo.nim @@ -6,9 +6,9 @@ import ffi, chronos, strutils type Echo = object prefix: string -# `-d:ffiEchoAbiC` builds the CBOR-free `abi = c` variant (flat `_CWire` structs -# on the wire); the default is the CBOR ABI. The same source drives both the -# `c_bindings/` (CBOR) and `c_abi_bindings/` (flat) example outputs. +# `-d:ffiEchoAbiC` builds the `abi = c` variant (`_CWire` structs on the wire); +# the default is the CBOR ABI. The same source drives both the `c_bindings/` +# (CBOR) and `c_abi_bindings/` example outputs. when defined(ffiEchoAbiC): declareLibrary("echo", Echo, defaultABIFormat = "c") else: diff --git a/ffi.nimble b/ffi.nimble index a215395..4c6db70 100644 --- a/ffi.nimble +++ b/ffi.nimble @@ -109,14 +109,9 @@ proc genBindingsCmd(flags, src: string, langs = "rust", outDir = ""): string = cmd proc removeStaleEchoLib() = - ## The CBOR and `abi = c` echo e2e suites both compile examples/echo/echo.nim - ## to the same repo-root `libecho.so`, differing only by `-d:ffiEchoAbiC`. - ## CMake keys the dylib rebuild on echo.nim's mtime, not the ABI flag, so a - ## `libecho.so` left by an earlier run is silently reused when the ABI flips — - ## in *either* direction. The caller then reaches entry points of the wrong - ## ABI (CBOR bytes decoded as a flat struct, or vice versa) and segfaults. - ## Every echo-building e2e task must delete it first to force a fresh rebuild - ## with the ABI its bindings expect. + ## CMake keys the shared `libecho.so` rebuild on echo.nim's mtime, not on + ## `-d:ffiEchoAbiC`, so a stale lib from the other ABI is reused and segfaults. + ## Every echo e2e task deletes it first to force a fresh rebuild. for name in ["libecho.so", "libecho.dylib", "echo.dll"]: let path = thisDir() / name if fileExists(path): @@ -180,7 +175,6 @@ task test_c_e2e, "Build and run the C end-to-end tests for the timer example": runOrQuit "ctest --test-dir tests/e2e/c/build --output-on-failure -C Debug" task test_c_abi_e2e, "Build and run the CBOR-free abi=c C end-to-end test (echo)": - # Regenerate the abi=c bindings so the suite always runs against fresh codegen. runOrQuit "nimble genbindings_c_abi_echo" removeStaleEchoLib() runOrQuit "cmake -S tests/e2e/c_abi -B tests/e2e/c_abi/build" @@ -259,15 +253,12 @@ task genbindings_c_echo, "Generate C bindings for the echo example": exec genBindingsCmd(nimFlagsRefc, echoSrc, "c") task genbindings_c_abi_echo, "Generate CBOR-free abi=c C bindings for the echo example": - # echoVersion is all-scalar under the abi=c default, so it has no foreign - # codegen yet and is omitted from the bindings; -d:ffiAllowScalarSkip accepts - # that omission instead of failing the build (see genBindings()). - exec genBindingsCmd( - nimFlagsOrc & " -d:ffiEchoAbiC -d:ffiAllowScalarSkip", echoSrc, "c_abi" - ) - exec genBindingsCmd( - nimFlagsRefc & " -d:ffiEchoAbiC -d:ffiAllowScalarSkip", echoSrc, "c_abi" - ) + # ffiAllowScalarSkip omits echoVersion (all-scalar, no foreign codegen yet); + # abiOut forces output beside the CBOR `c_bindings/` instead of overwriting it. + const abiOut = "examples/echo/c_abi_bindings" + const abiFlags = " -d:ffiEchoAbiC -d:ffiAllowScalarSkip -d:ffiSrcPath=../echo.nim" + exec genBindingsCmd(nimFlagsOrc & abiFlags, echoSrc, "c", abiOut) + exec genBindingsCmd(nimFlagsRefc & abiFlags, echoSrc, "c", abiOut) task check_bindings_rust, "Verify checked-in Rust bindings match Nim source": runOrQuit "nimble genbindings_rust" diff --git a/ffi/codegen/c.nim b/ffi/codegen/c.nim index 4e8fcc0..5331ed1 100644 --- a/ffi/codegen/c.nim +++ b/ffi/codegen/c.nim @@ -1,22 +1,15 @@ -## C99 binding generator for the nim-ffi framework. -## Emits a header-only C binding plus a CMakeLists.txt. The binding is split -## into three headers so the example reads cleanly: `nim_ffi_prelude.h` (owned -## string/byte types + libc includes), `nim_ffi_cbor.h` (leaf CBOR codecs and -## buffer drivers, includes the prelude), and `.h` (the library-specific -## structs, codecs and async API, includes the cbor header). Requests/responses -## travel as CBOR (encoded with the same vendored TinyCBOR the C++ backend -## uses, matching the Nim-side cbor_serial codec — both ends speak RFC 8949). -## -## C has neither generics nor overloading, so the codecs the C++ backend gets -## from templates are monomorphised here: every distinct `seq[T]` / `Option[T]` -## becomes its own struct + encode/decode/free triple, and each leaf type has a -## distinctly-named codec emitted by the cbor_helpers template. +## C99 binding generator. The library's ABI format picks the shape: `cbor` +## (default) emits three headers (prelude + cbor codecs + `.h`) exchanging +## CBOR via vendored TinyCBOR; `c` (`abi = c`) emits one `.h` whose structs +## are the C ABI directly. C lacks generics, so each distinct `seq[T]`/`Option[T]` +## is monomorphised into its own struct + codec triple (e.g. `seq[uint32]` yields +## an `EchoSeq_U32` struct plus `echo_enc_`/`echo_dec_`/`echo_free_EchoSeq_U32`). import std/[os, strutils, tables, sets] import ./meta, ./string_helpers, ./c_cpp_common, ./types_ir -## 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). +## Fixed 64-bit wire type for any Nim `ptr T`/`pointer`, so payload size is +## host-arch independent (mirrors CppPtrType). const CPtrType* = "uint64_t" const @@ -24,9 +17,7 @@ const 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. + # Shared header names; must match the include guards baked into the templates. PreludeHeaderName* = "nim_ffi_prelude.h" CborHeaderName* = "nim_ffi_cbor.h" @@ -45,63 +36,60 @@ const scalarCInfoTable: array[ScalarKind, tuple[cType, suffix: string]] = [ ] func leafSuffix(cType: string): string = - ## C type name → leaf codec suffix for the leaf codecs the template provides; - ## empty string for composite types. Driven off the shared scalar table so it - ## can't drift from the IR's scalar set. + ## Leaf codec suffix for `cType`; "" for composites. for s in ScalarKind: if scalarCInfoTable[s].cType == cType: return scalarCInfoTable[s].suffix - case cType - of "NimFfiStr": "str" - of "NimFfiBytes": "bytes" - else: "" + return + case cType + of "NimFfiStr": "str" + of "NimFfiBytes": "bytes" + else: "" func cToken(cType: string): string = - ## Short PascalCase token used to build monomorphised container names and - ## codec-adapter symbols. Leaf types reuse their codec suffix (e.g. - ## `int64_t`→`I64`); composite C type names are already unique C identifiers, - ## so they pass through verbatim. + ## PascalCase token for monomorphised names: leaf suffix capitalised, else the + ## (already unique) composite C name verbatim. let suffix = leafSuffix(cType) if suffix.len > 0: - capitalizeFirstLetter(suffix) - else: - cType + return capitalizeFirstLetter(suffix) + return cType 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 + libName: string ## snake_case symbol prefix + libType: string ## PascalCase container-name prefix + typeTable: Table[string, FFITypeMeta] + emitted: HashSet[string] 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 + decls: seq[string] + codecs: seq[string] func encFn(reg: CTypeReg, cType: string): string = let suffix = leafSuffix(cType) if suffix.len > 0: return "nimffi_enc_" & suffix - reg.libName & "_enc_" & cType + return 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 + return 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 + ## Free-function name for `cType`, or "" when it owns no heap memory. + return + 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) @@ -256,12 +244,10 @@ proc emitStructType(reg: var CTypeReg, t: FFITypeMeta) = reg.owns[t.name] = owns proc ensureCType(reg: var CTypeReg, t: FFIType): tuple[cType: string, owns: bool] = - ## Walks the shared type IR into a C type, monomorphising each distinct - ## `seq[T]` / `Option[T]` into its own struct + codec triple on first sight. - ## `owns` marks a C type that carries heap-allocated payload the caller must - ## release with its generated free function (strings, byte buffers, and any - ## seq/opt/struct transitively containing one); plain scalars and pointers own - ## nothing and need no cleanup. + ## Lowers the type intermediate representation (see types_ir.nim) to a C type, + ## monomorphising each distinct `seq[T]`/`Option[T]` on first sight. `owns` + ## marks a type carrying heap payload the caller must release via its generated + ## free function. case t.kind of ftPtr: return (CPtrType, false) @@ -296,31 +282,27 @@ proc ensureCType(reg: var CTypeReg, t: FFIType): tuple[cType: string, owns: bool return (name, reg.owns.getOrDefault(name, false)) proc ensureCType(reg: var CTypeReg, nimType: string): tuple[cType: string, owns: bool] = - ensureCType(reg, parseFFIType(nimType)) + return ensureCType(reg, parseFFIType(nimType)) 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. + ## Synthesises the per-proc Req struct so it flows through the same + ## monomorphisation path as user types; pointer/handle params ride as uint64. 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) + return 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. Note `ptr T` - ## rides by value as the 64-bit wire int (like `pointer`); production params - ## reach here as `pointer` since handles are pre-converted upstream. + ## Scalars/pointers/string views pass by value; aggregates by const pointer. if ridesAsPtr: return true - parseFFIType(nimType).kind in {ftScalar, ftStr, ftPtr} + return parseFFIType(nimType).kind in {ftScalar, ftStr, ftPtr} proc cReturnType(reg: var CTypeReg, p: FFIProcMeta): string = if p.returnRidesAsPtr(): return CPtrType - ensureCType(reg, p.returnTypeName).cType + return ensureCType(reg, p.returnTypeName).cType proc buildReqParams( reg: var CTypeReg, eps: seq[FFIParamMeta] @@ -340,14 +322,14 @@ proc buildReqParams( else: params.add("const " & cType & "* " & ep.name) assigns.add(" ffi_req." & ep.name & " = *" & ep.name & ";") - (params, assigns) + return (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) - ( + return ( libType & pascal & "Fn", libType & pascal & "Box", libName & "_" & snake & "_trampoline", @@ -425,10 +407,8 @@ 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. + ## Opens a reply trampoline: recover the box, fail if no callback, deliver a + ## non-zero `ret` as an error (msg/len isn't NUL-terminated, so copy it). lines.add( "static void " & tramp & "(int ret, const char* msg, size_t len, void* ud) {" ) @@ -661,8 +641,8 @@ proc emitMethod( 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). + # Reclaim any fields a partial decode allocated (out is zeroed, so free skips + # what was never written). if retFree.len > 0: lines.add(" " & retFree & "(&out);") lines.add(" free(box);") @@ -734,7 +714,7 @@ proc newCTypeReg( if p.kind != FFIKind.DTOR: let rt = reqTypeMeta(p) reg.typeTable[rt.name] = rt - reg + return reg proc monomorphiseAll( reg: var CTypeReg, @@ -742,10 +722,9 @@ proc monomorphiseAll( 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. + ## Runs every user type, Req envelope, return type and event payload through + ## ensureCType, emitting 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] = @[] @@ -759,19 +738,17 @@ proc monomorphiseAll( respTypes.add(cReturnType(reg, m)) for ev in events: discard ensureCType(reg, ev.payloadTypeName) - (reqTypes, respTypes) + return (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" + ## The library-agnostic `nim_ffi_prelude.h`: owned string/byte types + libc/ + ## TinyCBOR includes, emitted verbatim. + return 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" + ## The library-agnostic `nim_ffi_cbor.h`: leaf CBOR codecs and buffer drivers, + ## emitted verbatim. + return CborHelpersTpl & "\n" proc generateCLibHeader*( procs: seq[FFIProcMeta], @@ -779,8 +756,7 @@ proc generateCLibHeader*( libName: string, events: seq[FFIEventMeta] = @[], ): string = - ## The `.h` header: library-specific structs, monomorphised codecs and - ## the async API. Pulls the two shared headers in via the cbor header. + ## The `.h` header: library structs, monomorphised codecs and async API. let classified = classifyProcs(procs) let ctors = classified.ctors let methods = classified.methods @@ -875,11 +851,432 @@ proc generateCLibHeader*( emitMethod(lines, reg, ctxType, libType, libName, m) lines.add("#endif /* " & guard & " */") - lines.join("\n") & "\n" + return lines.join("\n") & "\n" proc generateCCMakeLists*(libName, nimSrcRelPath: string): string = let src = nimSrcRelPath.replace("\\", "/") - CMakeListsTpl.multiReplace(("{{LIB}}", libName), ("{{SRC}}", src)) + return CMakeListsTpl.multiReplace(("{{LIB}}", libName), ("{{SRC}}", src)) + +# `abi = c` binding: the `_CWire` structs are the C ABI (no CBOR). Layout +# mirrors `wireValueType`/`wireFieldsFor` byte-for-byte: `string`→`const char*`, +# `seq[T]`→`* _items` + `ptrdiff_t _len`, `Option[T]`→`*` +# (NULL = none), nested type→its `_CWire` struct, `ptr`/`pointer`→`void*`. + +const AbiCPtrType = "void*" +const AbiCMakeListsTpl = staticRead("templates/c/CMakeLists_abi.txt.tpl") + +func abiLeafCType(t: string): tuple[ok: bool, cType: string] = + ## Nim leaf type → `abi = c` wire C type; `ok` is false for composites. + return + case t + of "int", "int64": + (true, "int64_t") + of "int32": + (true, "int32_t") + of "int16": + (true, "int16_t") + of "int8": + (true, "int8_t") + of "uint", "uint64": + (true, "uint64_t") + of "uint32": + (true, "uint32_t") + of "uint16": + (true, "uint16_t") + of "uint8", "byte": + (true, "uint8_t") + of "bool": + (true, "bool") + of "float", "float64": + (true, "double") + of "float32": + (true, "float") + of "pointer": + (true, AbiCPtrType) + of "string", "cstring": + (true, "const char*") + else: + (false, "") + +type AbiReg = object + typeTable: Table[string, FFITypeMeta] + emitted: HashSet[string] + decls: seq[string] + +proc ensureAbiStruct(reg: var AbiReg, typeName: string) + +proc abiWireValueCType(reg: var AbiReg, nimType: string): string = + ## `abi = c` C type for a value-position field (a top-level `seq` splits in two). + let t = nimType.strip() + if t.startsWith("ptr ") or t == "pointer": + return AbiCPtrType + let leaf = abiLeafCType(t) + if leaf.ok: + return leaf.cType + var optInner = genericInnerType(t, "Option[") + if optInner.len == 0: + optInner = genericInnerType(t, "Maybe[") + if optInner.len > 0: + return abiWireValueCType(reg, optInner.strip()) & "*" + if genericInnerType(t, "seq[").len > 0: + raise newException( + ValueError, "abi = c: `seq` has no single-field wire form, so it can't nest: " & t + ) + if genericInnerType(t, "array[").len > 0: + raise newException( + ValueError, "abi = c: array fields are not yet supported by the C backend: " & t + ) + if t in reg.typeTable: + ensureAbiStruct(reg, t) + return t + raise newException(ValueError, "abi = c: unknown field type: " & t) + +proc abiFieldDecls(reg: var AbiReg, name, nimType: string): seq[string] = + let seqInner = genericInnerType(nimType.strip(), "seq[") + if seqInner.len > 0: + let elemC = abiWireValueCType(reg, seqInner.strip()) + return @[elemC & "* " & name & "_items;", "ptrdiff_t " & name & "_len;"] + return @[abiWireValueCType(reg, nimType) & " " & name & ";"] + +proc emitAbiStruct(reg: var AbiReg, t: FFITypeMeta) = + var members: seq[string] = @[] + for f in t.fields: + for line in abiFieldDecls(reg, f.name, f.typeName): + members.add(" " & line) + if members.len == 0: + members.add(" uint8_t _placeholder; /* C forbids empty structs */") + reg.decls.add("typedef struct {\n" & members.join("\n") & "\n} " & t.name & ";") + +proc ensureAbiStruct(reg: var AbiReg, typeName: string) = + if typeName in reg.emitted: + return + reg.emitted.incl(typeName) + if typeName in reg.typeTable: + emitAbiStruct(reg, reg.typeTable[typeName]) + else: + reg.decls.add("/* unknown type referenced: " & typeName & " */") + +proc newAbiReg(types: seq[FFITypeMeta], procs: seq[FFIProcMeta]): AbiReg = + var reg = AbiReg() + 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 + return reg + +func abiParamByValue(nimType: string, ridesAsPtr: bool): bool = + ## Scalars/pointers/string views pass by value; aggregates by const pointer. + if ridesAsPtr: + return true + return abiLeafCType(nimType.strip()).ok + +proc abiReqParamsAndAssigns( + reg: var AbiReg, extraParams: seq[FFIParamMeta] +): tuple[params, assigns: seq[string]] = + var params, assigns: seq[string] = @[] + for ep in extraParams: + let rides = ep.ridesAsPtr() + let cType = + if rides: + AbiCPtrType + else: + abiWireValueCType(reg, ep.typeName) + if abiParamByValue(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 & ";") + return (params, assigns) + +proc abiMethodReplyInfo( + reg: var AbiReg, libType: string, m: FFIProcMeta +): tuple[fnType, replyParam: string] = + ## Reply-callback typedef name plus the C type of its `reply` argument. + let pascal = snakeToPascalCase(stripLibPrefix(m.procName, m.libName)) + let fnType = libType & pascal & "ReplyFn" + if m.returnRidesAsPtr(): + raise newException( + ValueError, + "abi = c: handle/pointer returns are not yet supported by the C backend: " & + m.procName, + ) + let rt = m.returnTypeName.strip() + let leaf = abiLeafCType(rt) + let replyParam = + if rt == "string" or rt == "cstring": + "const char*" + elif leaf.ok: + "const " & leaf.cType & "*" + else: + ensureAbiStruct(reg, rt) + "const " & rt & "*" + return (fnType, replyParam) + +proc emitAbiReplyTypedefs( + lines: var seq[string], reg: var AbiReg, libType: string, methods: seq[FFIProcMeta] +) = + for m in methods: + let info = abiMethodReplyInfo(reg, libType, m) + lines.add( + "typedef void (*" & info.fnType & ")(int err_code, " & info.replyParam & + " reply, const char* err_msg, void* user_data);" + ) + +proc emitAbiExternDecls( + lines: var seq[string], + reg: var AbiReg, + libName, libType: string, + procs: seq[FFIProcMeta], +) = + let createRawFn = libType & "CreateRawFn" + var haveCtor = false + for p in procs: + if p.kind == FFIKind.CTOR: + haveCtor = true + if haveCtor: + lines.add( + "typedef void (*" & createRawFn & + ")(int err_code, const char* ctx_addr, const char* err_msg, void* user_data);" + ) + lines.add("#ifdef __cplusplus") + lines.add("extern \"C\" {") + lines.add("#endif") + lines.add("") + for p in procs: + let reqStruct = reqStructName(p) + case p.kind + of FFIKind.FFI: + let info = abiMethodReplyInfo(reg, libType, p) + lines.add( + "int " & p.procName & "(void* ctx, " & info.fnType & + " on_reply, void* user_data, const " & reqStruct & "* req);" + ) + of FFIKind.CTOR: + lines.add( + "void* " & p.procName & "(const " & reqStruct & "* req, " & createRawFn & + " on_created, void* user_data);" + ) + of FFIKind.DTOR: + lines.add("int " & p.procName & "(void* ctx);") + lines.add("") + lines.add("#ifdef __cplusplus") + lines.add("} /* extern \"C\" */") + lines.add("#endif") + lines.add("") + +proc emitAbiCtxAndCtor( + lines: var seq[string], + reg: var AbiReg, + libName, libType, ctxType: string, + ctors: seq[FFIProcMeta], +) = + lines.add("typedef struct {") + lines.add(" void* ptr;") + lines.add("} " & ctxType & ";") + lines.add("") + if ctors.len == 0: + return + let createFn = libType & "CreateFn" + let createBox = libType & "CreateBox" + let createRawFn = libType & "CreateRawFn" + let tramp = libName & "_create_trampoline" + lines.add( + "typedef void (*" & createFn & ")(int err_code, " & ctxType & + "* ctx, const char* err_msg, void* user_data);" + ) + lines.add( + "typedef struct { " & createFn & " fn; void* user_data; } " & createBox & ";" + ) + lines.add( + "static void " & tramp & + "(int ret, const char* ctx_addr, const char* err_msg, void* ud) {" + ) + lines.add(" " & createBox & "* box = (" & createBox & "*)ud;") + lines.add(" if (!box) return;") + lines.add(" if (ret == NIMFFI_RET_STALE_WARN) return;") + lines.add(" if (!box->fn) { free(box); return; }") + lines.add(" if (ret != 0) {") + lines.add( + " box->fn(ret, NULL, err_msg ? err_msg : \"FFI create failed\", box->user_data);" + ) + lines.add(" free(box);") + lines.add(" return;") + lines.add(" }") + lines.add(" char* endp = NULL;") + lines.add(" unsigned long long a = ctx_addr ? strtoull(ctx_addr, &endp, 10) : 0;") + lines.add(" bool ok = ctx_addr && *ctx_addr && endp && *endp == '\\0';") + 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 reqStruct = reqStructName(ctor) + let (params, assigns) = abiReqParamsAndAssigns(reg, ctor.extraParams) + let head = "static inline int " & libName & "_ctx_create(" + let sig = + if params.len > 0: + head & params.join(", ") & ", " & createFn & " on_created, void* user_data) {" + else: + head & createFn & " on_created, void* user_data) {" + lines.add(sig) + lines.add(" " & reqStruct & " ffi_req;") + lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));") + for a in assigns: + lines.add(a) + lines.add( + " " & createBox & "* box = (" & createBox & "*)malloc(sizeof(" & createBox & + "));" + ) + lines.add(" if (!box) {") + 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 & "(&ffi_req, " & tramp & ", box);") + lines.add(" return 0;") + lines.add("}") + lines.add("") + +proc emitAbiDestructor(lines: var seq[string], ctxType, libName, dtorProcName: string) = + 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; }") + lines.add(" free(ctx);") + lines.add("}") + lines.add("") + +proc emitAbiMethod( + lines: var seq[string], + reg: var AbiReg, + ctxType, libName, libType: string, + m: FFIProcMeta, +) = + let stripped = stripLibPrefix(m.procName, m.libName) + let reqStruct = reqStructName(m) + let info = abiMethodReplyInfo(reg, libType, m) + let (params, assigns) = abiReqParamsAndAssigns(reg, m.extraParams) + let head = + "static inline int " & libName & "_ctx_" & stripped & "(const " & ctxType & "* ctx, " + let sig = + if params.len > 0: + head & params.join(", ") & ", " & info.fnType & " on_reply, void* user_data) {" + else: + head & info.fnType & " on_reply, void* user_data) {" + lines.add(sig) + lines.add(" " & reqStruct & " ffi_req;") + lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));") + for a in assigns: + lines.add(a) + lines.add(" return " & m.procName & "(ctx->ptr, on_reply, user_data, &ffi_req);") + lines.add("}") + lines.add("") + +proc generateCAbiLibHeader*( + procs: seq[FFIProcMeta], + types: seq[FFITypeMeta], + libName: string, + events: seq[FFIEventMeta] = @[], +): string = + if events.len > 0: + raise newException( + ValueError, "abi = c: the C backend does not yet support {.ffiEvent.} listeners" + ) + let classified = classifyProcs(procs) + let libType = libTypeName(classified.ctors, libName) + let ctxType = libType & "Ctx" + + var reg = newAbiReg(types, procs) + for t in types: + ensureAbiStruct(reg, t.name) + for p in procs: + if p.kind != FFIKind.DTOR: + ensureAbiStruct(reg, reqStructName(p)) + + let guard = "NIM_FFI_LIB_" & libName.toUpperAscii() & "_C_ABI_H_INCLUDED" + var lines: seq[string] = @[] + lines.add("#ifndef " & guard) + lines.add("#define " & guard) + lines.add("#include ") + lines.add("#include ") + lines.add("#include ") + lines.add("#include ") + lines.add("#include ") + lines.add("") + lines.add("#define NIMFFI_RET_OK 0") + lines.add("#define NIMFFI_RET_ERR 1") + lines.add("#define NIMFFI_RET_MISSING_CALLBACK 2") + lines.add("/* Non-terminal: the request is still running. Fires every ~5s with `msg`") + lines.add( + " carrying the elapsed milliseconds as decimal text; always followed by a" + ) + lines.add(" terminal RET_OK/RET_ERR. Ignore it unless you want progress. */") + lines.add("#define NIMFFI_RET_STALE_WARN 3") + lines.add("") + lines.add( + "/* `abi = c` wire structs — the C ABI. Strings are borrowed, NUL-terminated" + ) + lines.add(" `const char*` valid only for the duration of the call they cross. */") + for decl in reg.decls: + lines.add(decl) + lines.add("") + + emitAbiReplyTypedefs(lines, reg, libType, classified.methods) + lines.add("") + emitAbiExternDecls(lines, reg, libName, libType, procs) + + lines.add("/* High-level context wrapper */") + emitAbiCtxAndCtor(lines, reg, libName, libType, ctxType, classified.ctors) + emitAbiDestructor(lines, ctxType, libName, classified.dtorProcName) + for m in classified.methods: + emitAbiMethod(lines, reg, ctxType, libName, libType, m) + + lines.add("#endif /* " & guard & " */") + return lines.join("\n") & "\n" + +proc generateCAbiCMakeLists*(libName, nimSrcRelPath: string): string = + let src = nimSrcRelPath.replace("\\", "/") + return AbiCMakeListsTpl.multiReplace(("{{LIB}}", libName), ("{{SRC}}", src)) + +func libWireFormat(procs: seq[FFIProcMeta], types: seq[FFITypeMeta]): ABIFormat = + ## The single wire format the C header targets; a library can't mix `abi = c` + ## and `abi = cbor` in one header. + var seen: set[ABIFormat] = {} + for p in procs: + if p.kind != FFIKind.DTOR: + seen.incl(p.abiFormat) + if seen.len == 0: + for t in types: + seen.incl(t.abiFormat) + if seen.len > 1: + raise newException( + ValueError, + "abi = c/cbor mismatch: a C library must use one ABI format for all its " & + "procs and types; a mixed header is not supported", + ) + return (if ABIFormat.C in seen: ABIFormat.C else: ABIFormat.Cbor) proc generateCBindings*( procs: seq[FFIProcMeta], @@ -889,10 +1286,21 @@ proc generateCBindings*( nimSrcRelPath: string, events: seq[FFIEventMeta] = @[], ) = + ## Emits the C binding for `libName`, picking the `abi = c` or CBOR shape from + ## the library's ABI format. 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)) + case libWireFormat(procs, types) + of ABIFormat.C: + writeFile( + outputDir / (libName & ".h"), generateCAbiLibHeader(procs, types, libName, events) + ) + writeFile( + outputDir / "CMakeLists.txt", generateCAbiCMakeLists(libName, nimSrcRelPath) + ) + of ABIFormat.Cbor: + writeFile(outputDir / PreludeHeaderName, generateCPreludeHeader()) + writeFile(outputDir / CborHeaderName, generateCCborHeader()) + writeFile( + outputDir / (libName & ".h"), generateCLibHeader(procs, types, libName, events) + ) + writeFile(outputDir / "CMakeLists.txt", generateCCMakeLists(libName, nimSrcRelPath)) diff --git a/ffi/codegen/c_abi.nim b/ffi/codegen/c_abi.nim deleted file mode 100644 index 1582891..0000000 --- a/ffi/codegen/c_abi.nim +++ /dev/null @@ -1,453 +0,0 @@ -## CBOR-free C99 binding generator for the nim-ffi framework (`-d:targetLang=c_abi`). -## Where the `c` backend speaks CBOR on the wire (vendoring TinyCBOR), this one -## emits a single self-contained header whose flat structs *are* the C ABI: -## they mirror the macro-generated `*_CWire` layout byte-for-byte, so the C -## consumer passes native structs and links no CBOR at all. The Nim dylib -## converts flat struct ⇄ Nim object at the boundary (see the `abi = c` dispatch -## in `ffi/internal/c_macro_helpers.nim`) and keeps CBOR purely as an internal -## transport detail. -## -## Layout contract (must stay in lock-step with `c_macro_helpers.wireValueType` -## / `wireFieldsFor`): `string`→`const char*`, `seq[T]`→`* _items` + -## `ptrdiff_t _len`, `Option[T]`→`*` (NULL = none), nested `{.ffi.}` -## type → its flat struct, `ptr`/`pointer`→`void*`, POD unchanged. - -import std/[os, strutils, tables, sets] -import ./meta, ./string_helpers, ./c_cpp_common, ./types_ir - -const CPtrType = "void*" - ## Wire C type for any Nim `ptr T` / `pointer` (mirrors the `_CWire` `pointer`). - -const CMakeListsTpl = staticRead("templates/c_abi/CMakeLists.txt.tpl") - -func leafCTypeAbi(t: string): tuple[ok: bool, cType: string] = - ## Maps a Nim leaf type to the flat C type used in a wire struct. `ok` is - ## false for composites (seq/Option/user structs), handled separately. - case t - of "int", "int64": - (true, "int64_t") - of "int32": - (true, "int32_t") - of "int16": - (true, "int16_t") - of "int8": - (true, "int8_t") - of "uint", "uint64": - (true, "uint64_t") - of "uint32": - (true, "uint32_t") - of "uint16": - (true, "uint16_t") - of "uint8", "byte": - (true, "uint8_t") - of "bool": - (true, "bool") - of "float", "float64": - (true, "double") - of "float32": - (true, "float") - of "pointer": - (true, CPtrType) - of "string", "cstring": - (true, "const char*") - else: - (false, "") - -type AbiReg = object - typeTable: Table[string, FFITypeMeta] ## user structs + synthetic Req structs - emitted: HashSet[string] ## struct names already emitted - decls: seq[string] ## struct typedefs, dependency order - -proc ensureAbiStruct(reg: var AbiReg, typeName: string) - -proc wireValueCType(reg: var AbiReg, nimType: string): string = - ## Flat C type for a value-position field (everything except a top-level - ## `seq`, which splits into two fields — see `fieldDecls`). - let t = nimType.strip() - if t.startsWith("ptr ") or t == "pointer": - return CPtrType - let leaf = leafCTypeAbi(t) - if leaf.ok: - return leaf.cType - var optInner = genericInnerType(t, "Option[") - if optInner.len == 0: - optInner = genericInnerType(t, "Maybe[") - if optInner.len > 0: - return wireValueCType(reg, optInner.strip()) & "*" - if genericInnerType(t, "seq[").len > 0: - raise newException( - ValueError, - "abi = c: `seq` has no single-field wire form, so it can't nest inside " & - "another container: " & t, - ) - if genericInnerType(t, "array[").len > 0: - raise newException( - ValueError, "abi = c: array fields are not yet supported by the C backend: " & t - ) - if t in reg.typeTable: - ensureAbiStruct(reg, t) - return t - raise newException(ValueError, "abi = c: unknown field type: " & t) - -proc fieldDecls(reg: var AbiReg, name, nimType: string): seq[string] = - ## C struct member line(s) for one Nim field. A `seq[T]` becomes the - ## `_items` pointer + `_len` count pair; everything else is one - ## member. - let seqInner = genericInnerType(nimType.strip(), "seq[") - if seqInner.len > 0: - let elemC = wireValueCType(reg, seqInner.strip()) - return @[elemC & "* " & name & "_items;", "ptrdiff_t " & name & "_len;"] - @[wireValueCType(reg, nimType) & " " & name & ";"] - -proc emitAbiStruct(reg: var AbiReg, t: FFITypeMeta) = - var members: seq[string] = @[] - for f in t.fields: - for line in fieldDecls(reg, f.name, f.typeName): - members.add(" " & line) - if members.len == 0: - members.add(" uint8_t _placeholder; /* C forbids empty structs */") - reg.decls.add("typedef struct {\n" & members.join("\n") & "\n} " & t.name & ";") - -proc ensureAbiStruct(reg: var AbiReg, typeName: string) = - if typeName in reg.emitted: - return - reg.emitted.incl(typeName) - if typeName in reg.typeTable: - emitAbiStruct(reg, reg.typeTable[typeName]) - else: - reg.decls.add("/* unknown type referenced: " & typeName & " */") - -proc reqTypeMeta(p: FFIProcMeta): FFITypeMeta = - ## The per-proc Req envelope as an FFITypeMeta, mirroring the Nim macro. A - ## pointer/handle param rides as the opaque `pointer` wire 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) - -proc newAbiReg(types: seq[FFITypeMeta], procs: seq[FFIProcMeta]): AbiReg = - var reg = AbiReg() - 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 - -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 - leafCTypeAbi(nimType.strip()).ok - -proc reqParamsAndAssigns( - reg: var AbiReg, extraParams: seq[FFIParamMeta] -): tuple[params, assigns: seq[string]] = - ## The C parameter list + `ffi_req` field assignments shared by the ctor and - ## method wrappers: by-value params copy straight into the request struct, - ## by-const-pointer aggregates are dereferenced in. - var params, assigns: seq[string] = @[] - for ep in extraParams: - let rides = ep.ridesAsPtr() - let cType = - if rides: - CPtrType - else: - wireValueCType(reg, ep.typeName) - 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 methodReplyInfo( - reg: var AbiReg, libType: string, m: FFIProcMeta -): tuple[fnType, replyParam: string] = - ## The reply-callback typedef name plus the C type of its `reply` argument. - ## An object return hands back a `const *`; a string return a - ## `const char*`. Both are the raw callback the dylib invokes directly. - let pascal = snakeToPascalCase(stripLibPrefix(m.procName, m.libName)) - let fnType = libType & pascal & "ReplyFn" - if m.returnRidesAsPtr(): - raise newException( - ValueError, - "abi = c: handle/pointer returns are not yet supported by the C backend: " & - m.procName, - ) - let rt = m.returnTypeName.strip() - let replyParam = - if rt == "string" or rt == "cstring": - "const char*" - elif leafCTypeAbi(rt).ok: - "const " & leafCTypeAbi(rt).cType & "*" - else: - ensureAbiStruct(reg, rt) - "const " & rt & "*" - (fnType, replyParam) - -proc emitReplyTypedefs( - lines: var seq[string], reg: var AbiReg, libType: string, methods: seq[FFIProcMeta] -) = - for m in methods: - let info = methodReplyInfo(reg, libType, m) - lines.add( - "typedef void (*" & info.fnType & ")(int err_code, " & info.replyParam & - " reply, const char* err_msg, void* user_data);" - ) - -proc emitExternDecls( - lines: var seq[string], - reg: var AbiReg, - libName, libType: string, - procs: seq[FFIProcMeta], -) = - let createRawFn = libType & "CreateRawFn" - var haveCtor = false - for p in procs: - if p.kind == FFIKind.CTOR: - haveCtor = true - if haveCtor: - lines.add( - "typedef void (*" & createRawFn & - ")(int err_code, const char* ctx_addr, const char* err_msg, void* user_data);" - ) - lines.add("#ifdef __cplusplus") - lines.add("extern \"C\" {") - lines.add("#endif") - lines.add("") - for p in procs: - let reqStruct = reqStructName(p) - case p.kind - of FFIKind.FFI: - let info = methodReplyInfo(reg, libType, p) - lines.add( - "int " & p.procName & "(void* ctx, " & info.fnType & - " on_reply, void* user_data, const " & reqStruct & "* req);" - ) - of FFIKind.CTOR: - lines.add( - "void* " & p.procName & "(const " & reqStruct & "* req, " & createRawFn & - " on_created, void* user_data);" - ) - of FFIKind.DTOR: - lines.add("int " & p.procName & "(void* ctx);") - lines.add("") - lines.add("#ifdef __cplusplus") - lines.add("} /* extern \"C\" */") - lines.add("#endif") - lines.add("") - -proc emitCtxAndCtor( - lines: var seq[string], - reg: var AbiReg, - libName, libType, ctxType: string, - ctors: seq[FFIProcMeta], -) = - lines.add("typedef struct {") - lines.add(" void* ptr;") - lines.add("} " & ctxType & ";") - lines.add("") - if ctors.len == 0: - return - let createFn = libType & "CreateFn" - let createBox = libType & "CreateBox" - let createRawFn = libType & "CreateRawFn" - let tramp = libName & "_create_trampoline" - lines.add( - "typedef void (*" & createFn & ")(int err_code, " & ctxType & - "* ctx, const char* err_msg, void* user_data);" - ) - lines.add( - "typedef struct { " & createFn & " fn; void* user_data; } " & createBox & ";" - ) - lines.add( - "static void " & tramp & - "(int ret, const char* ctx_addr, const char* err_msg, void* ud) {" - ) - lines.add(" " & createBox & "* box = (" & createBox & "*)ud;") - lines.add(" if (!box) return;") - lines.add( - " /* Non-terminal progress ping: keep the box for the terminal reply. */" - ) - lines.add(" if (ret == NIMFFI_RET_STALE_WARN) return;") - lines.add(" if (!box->fn) { free(box); return; }") - lines.add(" if (ret != 0) {") - lines.add( - " box->fn(ret, NULL, err_msg ? err_msg : \"FFI create failed\", box->user_data);" - ) - lines.add(" free(box);") - lines.add(" return;") - lines.add(" }") - lines.add(" char* endp = NULL;") - lines.add(" unsigned long long a = ctx_addr ? strtoull(ctx_addr, &endp, 10) : 0;") - lines.add(" bool ok = ctx_addr && *ctx_addr && endp && *endp == '\\0';") - 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 reqStruct = reqStructName(ctor) - let (params, assigns) = reqParamsAndAssigns(reg, ctor.extraParams) - let head = "static inline int " & libName & "_ctx_create(" - let sig = - if params.len > 0: - head & params.join(", ") & ", " & createFn & " on_created, void* user_data) {" - else: - head & createFn & " on_created, void* user_data) {" - lines.add(sig) - lines.add(" " & reqStruct & " ffi_req;") - lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));") - for a in assigns: - lines.add(a) - lines.add( - " " & createBox & "* box = (" & createBox & "*)malloc(sizeof(" & createBox & - "));" - ) - lines.add(" if (!box) {") - 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 & "(&ffi_req, " & tramp & ", box);") - lines.add(" return 0;") - lines.add("}") - lines.add("") - -proc emitDestructor(lines: var seq[string], ctxType, libName, dtorProcName: string) = - 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; }") - lines.add(" free(ctx);") - lines.add("}") - lines.add("") - -proc emitMethod( - lines: var seq[string], - reg: var AbiReg, - ctxType, libName, libType: string, - m: FFIProcMeta, -) = - let stripped = stripLibPrefix(m.procName, m.libName) - let reqStruct = reqStructName(m) - let info = methodReplyInfo(reg, libType, m) - let (params, assigns) = reqParamsAndAssigns(reg, m.extraParams) - let head = - "static inline int " & libName & "_ctx_" & stripped & "(const " & ctxType & "* ctx, " - let sig = - if params.len > 0: - head & params.join(", ") & ", " & info.fnType & " on_reply, void* user_data) {" - else: - head & info.fnType & " on_reply, void* user_data) {" - lines.add(sig) - lines.add(" " & reqStruct & " ffi_req;") - lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));") - for a in assigns: - lines.add(a) - lines.add(" return " & m.procName & "(ctx->ptr, on_reply, user_data, &ffi_req);") - lines.add("}") - lines.add("") - -proc generateCAbiLibHeader*( - procs: seq[FFIProcMeta], - types: seq[FFITypeMeta], - libName: string, - events: seq[FFIEventMeta] = @[], -): string = - if events.len > 0: - raise newException( - ValueError, "abi = c: the C backend does not yet support {.ffiEvent.} listeners" - ) - let classified = classifyProcs(procs) - let libType = libTypeName(classified.ctors, libName) - let ctxType = libType & "Ctx" - - var reg = newAbiReg(types, procs) - for t in types: - ensureAbiStruct(reg, t.name) - for p in procs: - if p.kind != FFIKind.DTOR: - ensureAbiStruct(reg, reqStructName(p)) - - let guard = "NIM_FFI_LIB_" & libName.toUpperAscii() & "_C_ABI_H_INCLUDED" - var lines: seq[string] = @[] - lines.add("#ifndef " & guard) - lines.add("#define " & guard) - lines.add("#include ") - lines.add("#include ") - lines.add("#include ") - lines.add("#include ") - lines.add("#include ") - lines.add("") - lines.add("#define NIMFFI_RET_OK 0") - lines.add("#define NIMFFI_RET_ERR 1") - lines.add("#define NIMFFI_RET_MISSING_CALLBACK 2") - lines.add("/* Non-terminal: the request is still running. Fires every ~5s with `msg`") - lines.add( - " carrying the elapsed milliseconds as decimal text; always followed by a" - ) - lines.add(" terminal RET_OK/RET_ERR. Ignore it unless you want progress. */") - lines.add("#define NIMFFI_RET_STALE_WARN 3") - lines.add("") - lines.add("/* Flat wire structs — the C ABI. Strings are borrowed, NUL-terminated") - lines.add(" `const char*` valid only for the duration of the call they cross. */") - for decl in reg.decls: - lines.add(decl) - lines.add("") - - emitReplyTypedefs(lines, reg, libType, classified.methods) - lines.add("") - emitExternDecls(lines, reg, libName, libType, procs) - - lines.add("/* High-level context wrapper */") - emitCtxAndCtor(lines, reg, libName, libType, ctxType, classified.ctors) - emitDestructor(lines, ctxType, libName, classified.dtorProcName) - for m in classified.methods: - emitMethod(lines, reg, ctxType, libName, libType, m) - - lines.add("#endif /* " & guard & " */") - lines.join("\n") & "\n" - -proc generateCAbiCMakeLists*(libName, nimSrcRelPath: string): string = - let src = nimSrcRelPath.replace("\\", "/") - CMakeListsTpl.multiReplace(("{{LIB}}", libName), ("{{SRC}}", src)) - -proc generateCAbiBindings*( - procs: seq[FFIProcMeta], - types: seq[FFITypeMeta], - libName: string, - outputDir: string, - nimSrcRelPath: string, - events: seq[FFIEventMeta] = @[], -) = - createDir(outputDir) - writeFile( - outputDir / (libName & ".h"), generateCAbiLibHeader(procs, types, libName, events) - ) - writeFile( - outputDir / "CMakeLists.txt", generateCAbiCMakeLists(libName, nimSrcRelPath) - ) diff --git a/ffi/codegen/meta.nim b/ffi/codegen/meta.nim index 7c5088d..4622a79 100644 --- a/ffi/codegen/meta.nim +++ b/ffi/codegen/meta.nim @@ -6,7 +6,7 @@ import std/strutils type ABIFormat* {.pure.} = enum ## Wire format for an FFI payload. Only `Cbor` is wired end-to-end; `C` - ## (flat C-struct) has a type codec but no proc-dispatch path yet. + ## (`abi = c` C-struct) has a type codec but no proc-dispatch path yet. Cbor = "cbor" C = "c" @@ -74,7 +74,7 @@ var currentDefaultABIFormat* {.compileTime.}: ABIFormat = ABIFormat.Cbor proc abiCodegenImplemented*(fmt: ABIFormat): bool = ## Whether `fmt` has a working proc-dispatch path. Both `Cbor` and `C` are - ## wired: `Cbor` rides the generic overloads, `C` rides the flat `_CWire` + ## wired: `Cbor` rides the generic overloads, `C` rides the `_CWire` ## companions (a CBOR-free foreign surface with CBOR transport internally). fmt in {ABIFormat.Cbor, ABIFormat.C} diff --git a/ffi/codegen/templates/c_abi/CMakeLists.txt.tpl b/ffi/codegen/templates/c/CMakeLists_abi.txt.tpl similarity index 94% rename from ffi/codegen/templates/c_abi/CMakeLists.txt.tpl rename to ffi/codegen/templates/c/CMakeLists_abi.txt.tpl index 09c6a2c..2fa6a90 100644 --- a/ffi/codegen/templates/c_abi/CMakeLists.txt.tpl +++ b/ffi/codegen/templates/c/CMakeLists_abi.txt.tpl @@ -4,8 +4,8 @@ project({{LIB}}_c_abi_bindings C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) -# The CBOR-free `abi = c` binding links no TinyCBOR — the flat structs in the -# generated header are the ABI. Only the Nim dylib is built. +# The CBOR-free `abi = c` binding links no TinyCBOR — the generated header +# structs are the ABI. Only the Nim dylib is built. set(_search_dir "${CMAKE_CURRENT_SOURCE_DIR}") set(REPO_ROOT "") diff --git a/ffi/codegen/types_ir.nim b/ffi/codegen/types_ir.nim index fd1b533..6454c8c 100644 --- a/ffi/codegen/types_ir.nim +++ b/ffi/codegen/types_ir.nim @@ -86,7 +86,8 @@ func scalarKind(t: string): Option[ScalarKind] = none(ScalarKind) func parseFFIType*(typeName: string): FFIType = - ## Single source of truth for turning a Nim type string into the shared IR: + ## Single source of truth for turning a Nim type string into the shared + ## intermediate representation: ## ptr/pointer, seq[byte]→bytes, seq/Option/Maybe, scalars, string, else struct. let t = typeName.strip() if t.startsWith("ptr ") or t == "pointer": diff --git a/ffi/internal/c_macro_helpers.nim b/ffi/internal/c_macro_helpers.nim index a5ef23c..2b484c9 100644 --- a/ffi/internal/c_macro_helpers.nim +++ b/ffi/internal/c_macro_helpers.nim @@ -1,4 +1,4 @@ -## Compile-time helpers used by `ffi_macro.nim` for the `c` (flat C-struct) ABI. +## Compile-time helpers used by `ffi_macro.nim` for the `c` (`abi = c` C-struct) ABI. ## For each `{.ffi: "abi = c".}` object T, emits a `T_CWire` companion plus ## `cwirePack` / `cwireUnpack` / `cwireFree`. Field mapping: `string`→`cstring`, ## `seq[T]`→`_items`+`_len`, `Option[T]`/`Maybe[T]`→`ptr T_w` @@ -554,7 +554,7 @@ proc flushCWireCompanions*(): NimNode {.compileTime.} = ensureCWireFor(typeMeta.name, sink) sink -## abi = c proc dispatch. The foreign surface is CBOR-free — the flat `_CWire` +## abi = c proc dispatch. The foreign surface is CBOR-free — the `_CWire` ## structs are the C ABI — but transport reuses the proven CBOR request path ## internally: the generated exported wrapper `cwireUnpack`s the request into a ## Nim object, `cborEncodeShared`s it onto the FFI thread, and a Nim reply @@ -679,7 +679,7 @@ proc replyTrampProc(trampName, body: NimNode): NimNode = proc objectTrampBody(boxName, respType, respWire: NimNode): NimNode = ## Reply trampoline for an object return: recover the box, deliver a transport ## error as a copied NUL-terminated string, else CBOR-decode the reply, - ## `cwirePack` it into the flat wire struct, hand a pointer to the caller, and + ## `cwirePack` it into the `_CWire` struct, hand a pointer to the caller, and ## release the wire. `err_msg` is always a non-nil string; the `reply` struct ## pointer is nil only on error, gated by a non-`RET_OK` `err_code`. quote: diff --git a/ffi/internal/ffi_macro.nim b/ffi/internal/ffi_macro.nim index 420cd59..2a10b8e 100644 --- a/ffi/internal/ffi_macro.nim +++ b/ffi/internal/ffi_macro.nim @@ -11,7 +11,6 @@ when defined(ffiGenBindings): import ../codegen/rust import ../codegen/cpp import ../codegen/c - import ../codegen/c_abi import ../codegen/cddl proc requireLibraryDeclared(where: string) {.compileTime.} = @@ -92,7 +91,7 @@ proc gateABIFormat(fmt: ABIFormat, where: string) {.compileTime.} = proc gateFFITypeABIFormat(fmt: ABIFormat, where: string) {.compileTime.} = ## Type annotations only register metadata. `cbor` uses the generic CBOR - ## overloads, while `c` emits its flat `_CWire` companion from `genBindings()`. + ## overloads, while `c` emits its `_CWire` companion from `genBindings()`. case fmt of ABIFormat.Cbor, ABIFormat.C: discard @@ -931,7 +930,7 @@ macro ffi*(args: varargs[untyped]): untyped = # Does this proc qualify for the CBOR-free scalar fast path? Only `abi = c` # opts in, and only when every wire param + the return is a plain scalar # (see `isScalarOnly`) and the args fit the inline slots. A non-scalar - # `abi = c` proc rides the flat `_CWire` C-dispatch emitted by `asyncPath`. + # `abi = c` proc rides the `_CWire` C-dispatch emitted by `asyncPath`. let scalarEligible = abiFormat == ABIFormat.C and isScalarOnly(procMeta) and extraParamNames.len <= MaxScalarArgs @@ -1061,7 +1060,7 @@ macro ffi*(args: varargs[untyped]): untyped = ffiProcRegistry.add(procMeta) if abiFormat == ABIFormat.C: - # The flat-struct exported wrapper + reply trampoline are emitted at + # The `abi = c` exported wrapper + reply trampoline are emitted at # genBindings() time (see flushCAbiDispatch); the CBOR `ffiProc` is not. registerCAbiMethod( cExportName, libTypeName, reqTypeName, extraParamNames, extraParamTypes, @@ -1515,7 +1514,7 @@ macro ffiCtor*(args: varargs[untyped]): untyped = let stmts = if abiFormat == ABIFormat.C: - # The flat-struct exported wrapper is emitted at genBindings() time (see + # The `abi = c` exported wrapper is emitted at genBindings() time (see # flushCAbiDispatch); the CBOR `ffiProc` is not. registerCAbiCtor(cExportName, libTypeName, reqTypeName, paramNames, paramTypes) newStmtList(typeDef, ffiNewReqProc, helperProc, processProc, addToReg, poolDecl) @@ -1858,16 +1857,12 @@ when defined(ffiGenBindings): generateCBindings( genProcs, ffiTypeRegistry, libName, outDir, srcRel, ffiEventRegistry ) - of "c_abi": - generateCAbiBindings( - genProcs, ffiTypeRegistry, libName, outDir, srcRel, ffiEventRegistry - ) of "cddl": generateCddlBindings(genProcs, ffiTypeRegistry, libName, outDir, srcRel) else: error( "genBindings: unknown targetLang '" & lang & - "'. Use 'rust', 'cpp', 'c', 'c_abi', or 'cddl'." + "'. Use 'rust', 'cpp', 'c', or 'cddl'." ) macro genBindings*( @@ -1886,9 +1881,10 @@ 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", "c", - ## "c_abi", "cddl". Pass a comma-separated list to emit several at once from - ## a single compile — the backend dispatch loops over each language. + ## Supported languages (-d:targetLang): "rust" (default), "cpp", "c", "cddl". + ## Pass a comma-separated list to emit several at once from a single compile — + ## the backend dispatch loops over each language. The `c` target emits the + ## `abi = c` or CBOR C shape based on the library's ABI format (`defaultABIFormat`). ## ## Output dir defaults to `_bindings/` next to the compiled source; the ## embedded nim source path is derived by making that source relative to the diff --git a/tests/bench/README.md b/tests/bench/README.md index 09312fe..4087504 100644 --- a/tests/bench/README.md +++ b/tests/bench/README.md @@ -57,7 +57,7 @@ wire-format codecs head-to-head on identical payloads: - **cbor** — `cborEncode` / `cborDecode`, self-describing bytes over `seq[byte]`. The codec the `cbor` ABI uses on every boundary crossing. -- **c (cwire)** — `cwirePack` / `cwireUnpack` / `cwireFree`, flat C-struct +- **c (cwire)** — `cwirePack` / `cwireUnpack` / `cwireFree`, `abi = c` C-struct shared-memory packing. The codec the `c` ABI uses, emitted for every `{.ffi: "abi = c".}` type as its `_CWire` companion. diff --git a/tests/e2e/c_abi/test_echo_c_abi.c b/tests/e2e/c_abi/test_echo_c_abi.c index 87d6f97..34db73c 100644 --- a/tests/e2e/c_abi/test_echo_c_abi.c +++ b/tests/e2e/c_abi/test_echo_c_abi.c @@ -1,13 +1,8 @@ -/* End-to-end test for the CBOR-free `abi = c` echo bindings. Unlike the CBOR C - * backend, this header links no TinyCBOR: the flat structs in echo.h are the C - * ABI, strings are plain borrowed `const char*`. The test drives the same - * async, callback-per-call surface — constructor, an object-returning method - * and teardown — copying out what each callback delivers (owned by the binding, - * valid only for the call) and polling a `done` flag to sequence the async - * calls. A string-returning method (echoVersion) rides the CBOR-free scalar - * fast path instead of a flat `_CWire` wrapper, so it has no c_abi binding yet - * (foreign codegen for the scalar shape is a follow-up) and isn't exercised - * here. */ +/* End-to-end test for the CBOR-free `abi = c` echo bindings: the `_CWire` + * structs in echo.h are the C ABI, strings are borrowed `const char*`, no + * TinyCBOR. Drives the async callback-per-call surface (ctor, object-returning + * method, teardown). echoVersion rides the scalar fast path (no foreign binding + * yet) and isn't exercised. */ #include "echo.h" #include #include diff --git a/tests/unit/test_abi_format.nim b/tests/unit/test_abi_format.nim index 825aa57..4e62c35 100644 --- a/tests/unit/test_abi_format.nim +++ b/tests/unit/test_abi_format.nim @@ -101,7 +101,7 @@ suite "ABI proc-dispatch readiness": test "both cbor and c proc-dispatch are wired": # This predicate is what the proc-form macros consult. Both ABIs now have a # working dispatch path: `cbor` rides the generic overloads, `c` rides the - # flat `_CWire` companions (a CBOR-free foreign surface, CBOR transport + # `_CWire` companions (a CBOR-free foreign surface, CBOR transport # internally). Events are the one `c` gap, gated separately in the macro. check abiCodegenImplemented(ABIFormat.Cbor) check abiCodegenImplemented(ABIFormat.C) diff --git a/tests/unit/test_c_abi_codegen.nim b/tests/unit/test_c_abi_codegen.nim index 7a10630..f5c6390 100644 --- a/tests/unit/test_c_abi_codegen.nim +++ b/tests/unit/test_c_abi_codegen.nim @@ -1,11 +1,10 @@ -## Unit tests for the CBOR-free `abi = c` C binding generator. Drives -## generateCAbiLibHeader directly against a synthetic registry (no macro -## pipeline, no files written) and asserts on the emitted text — same approach -## as test_c_codegen / test_cddl_codegen. +## Unit tests for the CBOR-free (`abi = c`) C binding shape. Drives +## generateCAbiLibHeader against a synthetic registry (no macro pipeline, no +## files written), asserting on the emitted text. import std/strutils import unittest2 -import ffi/codegen/[meta, c_abi] +import ffi/codegen/[meta, c] proc field(n, t: string): FFIFieldMeta = FFIFieldMeta(name: n, typeName: t) @@ -69,7 +68,7 @@ suite "generateCAbiLibHeader": check "CborError" notin header check "tinycbor" notin header.toLowerAscii() - test "flat wire structs mirror the _CWire layout": + test "abi = c wire structs mirror the _CWire layout": # string -> const char*; POD unchanged. check "const char* message;" in header check "int64_t delayMs;" in header @@ -78,14 +77,14 @@ suite "generateCAbiLibHeader": check "ptrdiff_t messages_len;" in header # Option[string] -> pointer to the element wire type (NULL = none). check "const char** note;" in header - # nested {.ffi.} type rides as its flat struct. + # nested {.ffi.} type rides as its _CWire struct. check "EchoRequest config;" in header test "per-proc Req envelopes are emitted as structs": check "} TimerEchoReq;" in header check "} TimerCreateCtorReq;" in header - test "exported symbols use the flat structs, not CBOR buffers": + test "exported symbols use the _CWire structs, not CBOR buffers": check "req_cbor" notin header check "const TimerEchoReq* req" in header check "void* timer_create(const TimerCreateCtorReq* req," in header diff --git a/tests/unit/test_c_wire.nim b/tests/unit/test_c_wire.nim index 8b561c4..15a435e 100644 --- a/tests/unit/test_c_wire.nim +++ b/tests/unit/test_c_wire.nim @@ -1,4 +1,4 @@ -## Round-trip correctness for the `c` (flat C-struct) ABI codec. +## Round-trip correctness for the `c` (`abi = c` C-struct) ABI codec. ## ## Each `{.ffi: "abi = c".}` type gets a `_CWire` companion plus ## `cwirePack` / `cwireUnpack` / `cwireFree`. This asserts diff --git a/tests/unit/test_rust_codegen.nim b/tests/unit/test_rust_codegen.nim index e2caa79..5f80f00 100644 --- a/tests/unit/test_rust_codegen.nim +++ b/tests/unit/test_rust_codegen.nim @@ -2,7 +2,7 @@ ## own scalar table that had drifted from C/C++ — `int8`/`int16`/`uint8`/ ## `uint16`/`uint32`/`byte`/`float32` fell through to `capitalizeFirstLetter` ## and emitted invalid Rust. It now renders through the shared `parseFFIType` -## IR, so the full scalar set is pinned here. +## intermediate representation, so the full scalar set is pinned here. import unittest2 import ffi/codegen/rust diff --git a/tests/unit/test_types_ir.nim b/tests/unit/test_types_ir.nim index dab7699..319d1d2 100644 --- a/tests/unit/test_types_ir.nim +++ b/tests/unit/test_types_ir.nim @@ -1,6 +1,7 @@ -## Unit tests for the shared type IR that the C / C++ / Rust binding generators -## parse Nim type strings through. `parseFFIType` is the single source of truth -## the three backends consume, so its shape mappings are pinned here directly. +## Unit tests for the shared type intermediate representation that the C / C++ / +## Rust binding generators parse Nim type strings through. `parseFFIType` is the +## single source of truth the three backends consume, so its shape mappings are +## pinned here directly. import unittest2 import ffi/codegen/types_ir @@ -83,7 +84,7 @@ suite "parseFFIType: structs": check t.kind == ftStruct check t.name == "EchoRequest" -suite "renderNative: walks the IR with a backend map": +suite "renderNative: walks the intermediate representation with a backend map": let rustish = NativeTypeMap( scalar: proc(s: ScalarKind): string = (