From e6c09d25b1d947ef70f850104d895d0d84d45acb Mon Sep 17 00:00:00 2001 From: Ivan FB Date: Mon, 11 May 2026 22:45:35 +0200 Subject: [PATCH] =?UTF-8?q?fix=20Cross-file=20FFI=20exports=20silently=20d?= =?UTF-8?q?isappear=20from=20bindings.=20genBindings()=20only=20sees=20pro?= =?UTF-8?q?cs=20registered=20in=20modules=20that=20were=20imported=20trans?= =?UTF-8?q?itively=20before=20its=20call.=20A=20downstream=20=20=20user=20?= =?UTF-8?q?who=20adds=20{.ffi.}=20in=20a=20sub-module=20that=20isn't=20pul?= =?UTF-8?q?led=20into=20the=20root=20file=20gets=20a=20successful=20build?= =?UTF-8?q?=20with=20missing=20methods.=20The=20README=20warns=20about=20t?= =?UTF-8?q?his,=20but=20there's=20no=20=20=20compile-time=20diagnostic=20?= =?UTF-8?q?=E2=80=94=20the=20registry=20could=20at=20least=20error=20if=20?= =?UTF-8?q?genBindings()=20is=20called=20and=20the=20count=20looks=20suspi?= =?UTF-8?q?cious,=20or=20expose=20a=20way=20to=20assert=20"I=20expect=20N?= =?UTF-8?q?=20exports."?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/nim_timer/nim_timer.nim | 5 ++++- ffi/internal/ffi_macro.nim | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) 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