mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-08-05 14:33:13 +00:00
chore: unify c and c_abi
This commit is contained in:
parent
7b028e64c4
commit
331e2d4c6d
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
||||
36
CHANGELOG.md
36
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
|
||||
`<T>_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 `<lib>.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 `<lib>.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.
|
||||
|
||||
20
README.md
20
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 `<name>.h` header your host
|
||||
includes, plus a `CMakeLists.txt`) land in `-d:ffiOutputDir`:
|
||||
generated files (for `targetLang=c`: the `<name>.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
|
||||
`<lang>_bindings/` next to the compiled source.
|
||||
- `-d:ffiSrcPath` — override the Nim source path embedded in the generated build
|
||||
|
||||
@ -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 "")
|
||||
|
||||
@ -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) |
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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:
|
||||
|
||||
27
ffi.nimble
27
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"
|
||||
|
||||
@ -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 `<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.
|
||||
## C99 binding generator. The library's ABI format picks the shape: `cbor`
|
||||
## (default) emits three headers (prelude + cbor codecs + `<lib>.h`) exchanging
|
||||
## CBOR via vendored TinyCBOR; `c` (`abi = c`) emits one `<lib>.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 `<lib>.h` header: library-specific structs, monomorphised codecs and
|
||||
## the async API. Pulls the two shared headers in via the cbor header.
|
||||
## The `<lib>.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]`→`<wireT>* <f>_items` + `ptrdiff_t <f>_len`, `Option[T]`→`<wireT>*`
|
||||
# (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 <stdint.h>")
|
||||
lines.add("#include <stddef.h>")
|
||||
lines.add("#include <stdbool.h>")
|
||||
lines.add("#include <stdlib.h>")
|
||||
lines.add("#include <string.h>")
|
||||
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))
|
||||
|
||||
@ -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]`→`<wireT>* <f>_items` +
|
||||
## `ptrdiff_t <f>_len`, `Option[T]`→`<wireT>*` (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
|
||||
## `<name>_items` pointer + `<name>_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 <Struct>*`; 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 <stdint.h>")
|
||||
lines.add("#include <stddef.h>")
|
||||
lines.add("#include <stdbool.h>")
|
||||
lines.add("#include <stdlib.h>")
|
||||
lines.add("#include <string.h>")
|
||||
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)
|
||||
)
|
||||
@ -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}
|
||||
|
||||
|
||||
@ -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 "")
|
||||
@ -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":
|
||||
|
||||
@ -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]`→`<name>_items`+`<name>_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:
|
||||
|
||||
@ -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 `<lang>_bindings/` next to the compiled source; the
|
||||
## embedded nim source path is derived by making that source relative to the
|
||||
|
||||
@ -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 `<T>_CWire` companion.
|
||||
|
||||
|
||||
@ -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 <assert.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 `<T>_CWire` companion plus
|
||||
## `cwirePack` / `cwireUnpack` / `cwireFree`. This asserts
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 =
|
||||
(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user