feat: shrink the genbindings incantation (#119)

This commit is contained in:
Gabriel Cruz 2026-07-10 17:13:06 -03:00 committed by GitHub
parent c0f6f2e802
commit 7ef58f4bf5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 135 additions and 112 deletions

View File

@ -196,26 +196,26 @@ library and nothing else.
nim c --app:lib --noMain --nimMainPrefix:libmylib mylib.nim
```
**2. Emit the foreign bindings** — same flags, plus the binding defines. This
compile runs the generators as a compile-time side effect and produces no
runnable output, so send the binary to `/dev/null`. The generated files (for
`targetLang=c`/`c_abi`: the `<name>.h` header your host includes, plus a
`CMakeLists.txt`) land in `-d:ffiOutputDir`:
**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`:
```sh
nim c --app:lib --noMain --nimMainPrefix:libmylib \
-d:ffiGenBindings -d:targetLang=c \
-d:ffiOutputDir=path/to/output -d:ffiSrcPath=../mylib.nim \
-o:/dev/null mylib.nim
nim c -d:ffiGenBindings -d:targetLang=rust,cpp,c --compileOnly mylib.nim
```
- `-d:targetLang` — which generator runs. Two kinds:
- `-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.
- `-d:ffiOutputDir` — where the generated files land.
- `-d:ffiSrcPath` — the Nim source path embedded in the generated build files.
- `-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
files. Defaults to the compiled source made relative to the output dir.
### The `--nimMainPrefix:lib<name>` rule

View File

@ -12,20 +12,24 @@ requires "https://github.com/logos-messaging/nim-ffi >= 0.2.0"
const nimFlags = "--mm:orc -d:chronicles_log_level=WARN"
proc genBindingsCmd(langs: string): string =
## One `nim c` that emits `langs` (comma-separated) from timer.nim, each into
## `<lang>_bindings/`. `--compileOnly` is enough — the files are written during
## macro expansion, nothing is linked.
"nim c " & nimFlags & " -d:ffiGenBindings -d:targetLang=" & langs &
" --compileOnly timer.nim"
task build, "Compile the timer library":
exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libmy_timer timer.nim"
task genbindings, "Generate Rust, C++ and C bindings for the timer example":
exec genBindingsCmd("rust,cpp,c")
task genbindings_rust, "Generate Rust bindings for the timer example":
exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libmy_timer" &
" -d:ffiGenBindings -d:targetLang=rust" & " -d:ffiOutputDir=rust_bindings" &
" -d:ffiSrcPath=timer.nim" & " -o:/dev/null timer.nim"
exec genBindingsCmd("rust")
task genbindings_cpp, "Generate C++ bindings for the timer example":
exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libmy_timer" &
" -d:ffiGenBindings -d:targetLang=cpp" & " -d:ffiOutputDir=cpp_bindings" &
" -d:ffiSrcPath=timer.nim" & " -o:/dev/null timer.nim"
exec genBindingsCmd("cpp")
task genbindings_c, "Generate C bindings for the timer example":
exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libmy_timer" &
" -d:ffiGenBindings -d:targetLang=c" & " -d:ffiOutputDir=c_bindings" &
" -d:ffiSrcPath=timer.nim" & " -o:/dev/null timer.nim"
exec genBindingsCmd("c")

View File

@ -16,6 +16,9 @@ requires "cbor_serialization == 0.3.0"
const nimFlagsOrc = "--mm:orc -d:chronicles_log_level=WARN"
const nimFlagsRefc = "--mm:refc -d:chronicles_log_level=WARN"
const timerSrc = "examples/timer/timer.nim"
const echoSrc = "examples/echo/echo.nim"
import std/[algorithm, os, strutils]
proc discoverUnitTests(): seq[string] =
@ -93,6 +96,18 @@ proc applyTsanSuppressions() =
elif "suppressions=" notin existing:
putEnv("TSAN_OPTIONS", existing & ":suppressions=" & suppPath)
proc genBindingsCmd(flags, src: string, langs = "rust", outDir = ""): string =
## One `nim c` that emits `langs` (comma-separated) from `src`. Output dir and
## embedded source path default to `<lang>_bindings/` next to `src`; `outDir`
## overrides every language. `--compileOnly` is enough because the binding
## files are written during macro expansion — nothing is linked.
var cmd =
"nim c " & flags & " -d:ffiGenBindings -d:targetLang=" & langs & " --compileOnly"
if outDir.len > 0:
cmd.add " -d:ffiOutputDir=" & outDir
cmd.add " " & src
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`.
@ -210,75 +225,42 @@ task test_c_abi_e2e_sanitized,
runOrQuit "ctest --test-dir tests/e2e/c_abi/build --output-on-failure -C Debug"
task genbindings_example, "Generate Rust bindings for the timer example":
exec "nim c " & nimFlagsOrc &
" --app:lib --noMain --nimMainPrefix:libmy_timer -d:ffiGenBindings -o:/dev/null examples/timer/timer.nim"
exec "nim c " & nimFlagsRefc &
" --app:lib --noMain --nimMainPrefix:libmy_timer -d:ffiGenBindings -o:/dev/null examples/timer/timer.nim"
exec genBindingsCmd(nimFlagsOrc, timerSrc)
exec genBindingsCmd(nimFlagsRefc, timerSrc)
task genbindings_rust, "Generate Rust bindings for the timer example":
exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libmy_timer" &
" -d:ffiGenBindings -d:targetLang=rust" &
" -d:ffiOutputDir=examples/timer/rust_bindings" & " -d:ffiSrcPath=../timer.nim" &
" -o:/dev/null examples/timer/timer.nim"
exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libmy_timer" &
" -d:ffiGenBindings -d:targetLang=rust" &
" -d:ffiOutputDir=examples/timer/rust_bindings" & " -d:ffiSrcPath=../timer.nim" &
" -o:/dev/null examples/timer/timer.nim"
exec genBindingsCmd(nimFlagsOrc, timerSrc, "rust")
exec genBindingsCmd(nimFlagsRefc, timerSrc, "rust")
task genbindings_cddl, "Generate CDDL schema for the timer example":
exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libmy_timer" &
" -d:ffiGenBindings -d:targetLang=cddl" &
" -d:ffiOutputDir=examples/timer/cddl_bindings" & " -d:ffiSrcPath=../timer.nim" &
" -o:/dev/null examples/timer/timer.nim"
exec genBindingsCmd(nimFlagsOrc, timerSrc, "cddl")
task genbindings_cpp, "Generate C++ bindings for the timer example":
exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libmy_timer" &
" -d:ffiGenBindings -d:targetLang=cpp" &
" -d:ffiOutputDir=examples/timer/cpp_bindings" & " -d:ffiSrcPath=../timer.nim" &
" -o:/dev/null examples/timer/timer.nim"
exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libmy_timer" &
" -d:ffiGenBindings -d:targetLang=cpp" &
" -d:ffiOutputDir=examples/timer/cpp_bindings" & " -d:ffiSrcPath=../timer.nim" &
" -o:/dev/null examples/timer/timer.nim"
exec genBindingsCmd(nimFlagsOrc, timerSrc, "cpp")
exec genBindingsCmd(nimFlagsRefc, timerSrc, "cpp")
task genbindings_cpp_echo, "Generate C++ bindings for the echo example":
exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libecho" &
" -d:ffiGenBindings -d:targetLang=cpp" &
" -d:ffiOutputDir=examples/echo/cpp_bindings" & " -d:ffiSrcPath=../echo.nim" &
" -o:/dev/null examples/echo/echo.nim"
exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libecho" &
" -d:ffiGenBindings -d:targetLang=cpp" &
" -d:ffiOutputDir=examples/echo/cpp_bindings" & " -d:ffiSrcPath=../echo.nim" &
" -o:/dev/null examples/echo/echo.nim"
exec genBindingsCmd(nimFlagsOrc, echoSrc, "cpp")
exec genBindingsCmd(nimFlagsRefc, echoSrc, "cpp")
task genbindings_c, "Generate C bindings for the timer example":
exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libmy_timer" &
" -d:ffiGenBindings -d:targetLang=c" & " -d:ffiOutputDir=examples/timer/c_bindings" &
" -d:ffiSrcPath=../timer.nim" & " -o:/dev/null examples/timer/timer.nim"
exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libmy_timer" &
" -d:ffiGenBindings -d:targetLang=c" & " -d:ffiOutputDir=examples/timer/c_bindings" &
" -d:ffiSrcPath=../timer.nim" & " -o:/dev/null examples/timer/timer.nim"
exec genBindingsCmd(nimFlagsOrc, timerSrc, "c")
exec genBindingsCmd(nimFlagsRefc, timerSrc, "c")
task genbindings_c_echo, "Generate C bindings for the echo example":
exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libecho" &
" -d:ffiGenBindings -d:targetLang=c" & " -d:ffiOutputDir=examples/echo/c_bindings" &
" -d:ffiSrcPath=../echo.nim" & " -o:/dev/null examples/echo/echo.nim"
exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libecho" &
" -d:ffiGenBindings -d:targetLang=c" & " -d:ffiOutputDir=examples/echo/c_bindings" &
" -d:ffiSrcPath=../echo.nim" & " -o:/dev/null examples/echo/echo.nim"
exec genBindingsCmd(nimFlagsOrc, echoSrc, "c")
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 "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libecho" &
" -d:ffiEchoAbiC -d:ffiGenBindings -d:targetLang=c_abi -d:ffiAllowScalarSkip" &
" -d:ffiOutputDir=examples/echo/c_abi_bindings" & " -d:ffiSrcPath=../echo.nim" &
" -o:/dev/null examples/echo/echo.nim"
exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libecho" &
" -d:ffiEchoAbiC -d:ffiGenBindings -d:targetLang=c_abi -d:ffiAllowScalarSkip" &
" -d:ffiOutputDir=examples/echo/c_abi_bindings" & " -d:ffiSrcPath=../echo.nim" &
" -o:/dev/null examples/echo/echo.nim"
exec genBindingsCmd(
nimFlagsOrc & " -d:ffiEchoAbiC -d:ffiAllowScalarSkip", echoSrc, "c_abi"
)
exec genBindingsCmd(
nimFlagsRefc & " -d:ffiEchoAbiC -d:ffiAllowScalarSkip", echoSrc, "c_abi"
)
task check_bindings_rust, "Verify checked-in Rust bindings match Nim source":
runOrQuit "nimble genbindings_rust"

View File

@ -163,14 +163,19 @@ proc returnRidesAsPtr*(p: FFIProcMeta): bool =
## True if the return crosses the wire as an opaque uint64 (raw `ptr` or handle).
p.returnIsPtr or p.returnIsHandle
# Target language for binding generation; override with -d:targetLang=cpp
# Target language(s) for binding generation; override with -d:targetLang=cpp.
# Accepts a comma-separated list (e.g. -d:targetLang=rust,cpp,c) to emit
# several languages from a single compile.
const targetLang* {.strdefine.} = "rust"
# Output directory for generated bindings; set with -d:ffiOutputDir=path/to/dir
# Output directory override for generated bindings; set with
# -d:ffiOutputDir=path/to/dir. Empty (the default) derives `<lang>_bindings/`
# next to the compiled source.
const ffiOutputDir* {.strdefine.} = ""
# Nim source path (relative to outputDir) embedded in generated build files;
# set with -d:ffiSrcPath=../relative/path.nim
# Nim source path override (relative to outputDir) embedded in generated build
# files; set with -d:ffiSrcPath=../relative/path.nim. Empty (the default)
# derives it from the compiled source relative to the output dir.
const ffiSrcPath* {.strdefine.} = ""
# When set to true, scalar-only `abi = c` procs (which have no foreign-binding codegen

View File

@ -1,4 +1,6 @@
import std/[macros, tables, strutils]
from std/os import `/`, relativePath
from std/compilesettings import querySetting, SingleValueSetting
import chronos
import ../ffi_types
import ../ffi_thread_request
@ -1867,6 +1869,55 @@ proc reportScalarFastPathDrops(procs: seq[FFIProcMeta]) {.compileTime.} =
"CBOR wire shape, or\n" & " - pass -d:ffiAllowScalarSkip to accept the omission."
)
proc bindingsOutputDir(lang, explicit: string): string {.compileTime.} =
## Output dir for `lang`. Defaults to `<lang>_bindings/` next to the source
## file being compiled (`querySetting(projectPath)`); an explicit
## -d:ffiOutputDir override wins.
if explicit.len > 0:
explicit
else:
return querySetting(SingleValueSetting.projectPath) / (lang & "_bindings")
proc bindingsSrcPath(outDir, explicit: string): string {.compileTime.} =
## Nim source path embedded in generated build files, expressed relative to
## the output dir. Defaults to the compiled file (`querySetting(projectFull)`)
## made relative to `outDir`; an explicit -d:ffiSrcPath override wins.
if explicit.len > 0:
explicit
else:
relativePath(querySetting(SingleValueSetting.projectFull), outDir)
when defined(ffiGenBindings):
proc emitBindingsFor(
lang: string, genProcs: seq[FFIProcMeta], libName, outDir, srcRel: string
) {.compileTime.} =
## Route one language token to its generator; unknown tokens are a compile
## error listing the valid set.
case lang
of "rust":
generateRustCrate(
genProcs, ffiTypeRegistry, libName, outDir, srcRel, ffiEventRegistry
)
of "cpp", "c++":
generateCppBindings(
genProcs, ffiTypeRegistry, libName, outDir, srcRel, ffiEventRegistry
)
of "c":
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'."
)
macro genBindings*(
outputDir: static[string] = ffiOutputDir, nimSrcRelPath: static[string] = ffiSrcPath
): untyped =
@ -1883,54 +1934,35 @@ 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", "cddl".
## Output path and nim source path default to -d:ffiOutputDir and
## -d:ffiSrcPath, or can be passed as explicit arguments.
## 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.
##
## 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
## output dir. Both can be overridden with -d:ffiOutputDir / -d:ffiSrcPath
## (or the explicit arguments) — an override applies to every language.
## Foreign-binding file emission is a no-op unless -d:ffiGenBindings is set;
## the `abi = c` `_CWire` companions are emitted unconditionally (runtime
## code, not generated files).
##
## Example (all via compile flags):
## genBindings()
## # nim c -d:ffiGenBindings -d:targetLang=rust \
## # -d:ffiOutputDir=examples/timer/rust_bindings \
## # -d:ffiSrcPath=../timer.nim mylib.nim
## # nim c -d:ffiGenBindings -d:targetLang=rust,cpp,c mylib.nim
genBindingsEmitted = true
when defined(ffiGenBindings):
if outputDir.len == 0:
error(
"genBindings: output directory is empty." &
" Pass it as an argument or set -d:ffiOutputDir=path/to/output"
)
let lang = string_helpers.toLower(targetLang)
let libName = deriveLibName(ffiProcRegistry)
let genProcs = bindableProcs(ffiProcRegistry)
reportScalarFastPathDrops(ffiProcRegistry)
case lang
of "rust":
generateRustCrate(
genProcs, ffiTypeRegistry, libName, outputDir, nimSrcRelPath, ffiEventRegistry
)
of "cpp", "c++":
generateCppBindings(
genProcs, ffiTypeRegistry, libName, outputDir, nimSrcRelPath, ffiEventRegistry
)
of "c":
generateCBindings(
genProcs, ffiTypeRegistry, libName, outputDir, nimSrcRelPath, ffiEventRegistry
)
of "c_abi":
generateCAbiBindings(
genProcs, ffiTypeRegistry, libName, outputDir, nimSrcRelPath, ffiEventRegistry
)
of "cddl":
generateCddlBindings(genProcs, ffiTypeRegistry, libName, outputDir, nimSrcRelPath)
else:
error(
"genBindings: unknown targetLang '" & lang &
"'. Use 'rust', 'cpp', 'c', 'c_abi', or 'cddl'."
for rawLang in targetLang.split(','):
let lang = string_helpers.toLower(rawLang.strip())
if lang.len == 0:
continue
let outDir = bindingsOutputDir(lang, outputDir)
emitBindingsFor(
lang, genProcs, libName, outDir, bindingsSrcPath(outDir, nimSrcRelPath)
)
let emitted = flushCWireCompanions()