fix Cross-file FFI exports silently disappear from bindings. genBindings() only sees procs registered in modules that were imported transitively before its call. A downstream

user who adds {.ffi.} in a sub-module that isn't pulled into the root file gets a successful build with missing methods. The README warns about this, but there's no
  compile-time diagnostic — the registry could at least error if genBindings() is called and the count looks suspicious, or expose a way to assert "I expect N exports."
This commit is contained in:
Ivan FB 2026-05-11 22:45:35 +02:00
parent b598255f74
commit e6c09d25b1
No known key found for this signature in database
GPG Key ID: DF0C67A04C543270
2 changed files with 30 additions and 1 deletions

View File

@ -70,7 +70,10 @@ proc nimtimerComplex*(
# the compiler processes the AST. genBindings() reads those registries to emit
# the binding files, so placing it any earlier would produce incomplete output.
# In a multi-file library, import all sub-modules first and call genBindings()
# once, at the bottom of the top-level compilation-root file.
# once, at the bottom of the top-level compilation-root file. For multi-file
# libraries it's strongly recommended to pin `expectedExports = N`: a sub-
# module that isn't imported here would silently disappear from the bindings,
# and the pin turns that into a loud compile error.
# This call is a no-op unless -d:ffiGenBindings is passed to the compiler.
genBindings() # reads -d:ffiOutputDir, -d:ffiNimSrcRelPath, -d:targetLang from compile flags

View File

@ -1605,6 +1605,7 @@ macro genBindings*(
outputDir: static[string] = ffiOutputDir,
nimSrcRelPath: static[string] = ffiNimSrcRelPath,
nimBuildFlags: static[string] = ffiNimBuildFlags,
expectedExports: static[int] = -1,
): untyped =
## Emits C++ or Rust binding files from the compile-time FFI registries.
##
@ -1622,6 +1623,14 @@ macro genBindings*(
## -d:ffiNimSrcRelPath, or can be passed as explicit arguments.
## This macro is a no-op unless -d:ffiGenBindings is set.
##
## expectedExports: optional pin on the number of registered procs. The
## generator can only see {.ffi.} / {.ffiCtor.} annotations from modules
## the compilation root has *imported* by the time genBindings is called;
## a sub-module that isn't pulled in produces silently-incomplete bindings.
## Set this to your known export count and the macro fails loudly when the
## tally drifts (typical cause: a new export landed in an unimported file).
## Default -1 disables the pin (only the zero-procs sanity check applies).
##
## Example (all via compile flags):
## genBindings()
## # nim c -d:ffiGenBindings -d:targetLang=rust \
@ -1641,6 +1650,23 @@ macro genBindings*(
" registered. The library name cannot be safely derived from proc names" &
" (e.g. \"nim_timer_create\" is ambiguous between \"nim\" and \"nim_timer\")."
)
if ffiProcRegistry.len == 0:
error(
"genBindings: zero {.ffi.} / {.ffiCtor.} procs registered. The most" &
" common cause is calling genBindings() from a file that hasn't" &
" imported the modules that declare the exports -- the registry only" &
" sees annotations from code the compilation root has transitively" &
" imported. Move the call to the root file, or import the relevant" &
" sub-modules above it."
)
if expectedExports >= 0 and ffiProcRegistry.len != expectedExports:
error(
"genBindings: expected " & $expectedExports & " FFI export(s), found " &
$ffiProcRegistry.len & ". A {.ffi.} / {.ffiCtor.} annotation may have" &
" landed in a module the compilation root doesn't import (silently" &
" omitted), or the expectedExports pin is stale. Update the pin or" &
" add the missing import."
)
let lang = targetLang.toLowerAscii()
let libName = deriveLibName(ffiProcRegistry)
case lang