mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-07-16 21:10:19 +00:00
feat: shrink the genbindings incantation (#119)
This commit is contained in:
parent
c0f6f2e802
commit
7ef58f4bf5
24
README.md
24
README.md
@ -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
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
86
ffi.nimble
86
ffi.nimble
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user