diff --git a/examples/nim_timer/nim_timer.nim b/examples/nim_timer/nim_timer.nim index 947e749..f845da7 100644 --- a/examples/nim_timer/nim_timer.nim +++ b/examples/nim_timer/nim_timer.nim @@ -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 diff --git a/ffi/internal/ffi_macro.nim b/ffi/internal/ffi_macro.nim index 464644c..7333e44 100644 --- a/ffi/internal/ffi_macro.nim +++ b/ffi/internal/ffi_macro.nim @@ -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