From ea67fb747ae2b8c4b22994f3a4e970f37338206b Mon Sep 17 00:00:00 2001 From: Gabriel Cruz <8129788+gmelodie@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:43:21 -0300 Subject: [PATCH] feat(ffi): make the scalar-fast-path binding drop loud (#120) --- README.md | 11 ++++++ ffi.nimble | 7 +++- ffi/codegen/meta.nim | 5 +++ ffi/internal/ffi_macro.nim | 36 ++++++++++++----- tests/unit/fixtures/scalar_skip_fixture.nim | 26 ++++++++++++ tests/unit/test_scalar_skip_gen.nim | 44 +++++++++++++++++++++ 6 files changed, 118 insertions(+), 11 deletions(-) create mode 100644 tests/unit/fixtures/scalar_skip_fixture.nim create mode 100644 tests/unit/test_scalar_skip_gen.nim diff --git a/README.md b/README.md index 1a7b46c..eb3835b 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,17 @@ The default wire format is `cbor`. Override the library default with `declareLibrary("lib", Lib, defaultABIFormat = "c")`, or per annotation with an `"abi = ..."` spec, e.g. `{.ffi: "abi = c".}`. +An `abi = c` proc whose whole signature is scalar — fixed-width integer, float, +or bool params (a `string` return is fine, a `string` param is not) and no +structs, handles, or pointers — dispatches through a CBOR-free scalar fast path. +Foreign-binding codegen for that shape isn't implemented yet, so +under `-d:ffiGenBindings` such a proc would be omitted from the generated +bindings — and `genBindings()` fails with an error naming the affected procs. +Resolve it by switching the proc to `abi = cbor`, adding a non-scalar param so it +takes the CBOR wire shape, or passing `-d:ffiAllowScalarSkip` to accept the +omission (the proc still works over the scalar fast path; it's just absent from +the generated foreign bindings). + ## Placement of `genBindings()` `genBindings()` reads the compile-time registries that the pragmas populate as diff --git a/ffi.nimble b/ffi.nimble index 7988a8f..84e6c7a 100644 --- a/ffi.nimble +++ b/ffi.nimble @@ -259,12 +259,15 @@ task genbindings_c_echo, "Generate C bindings for the echo example": " -d:ffiSrcPath=../echo.nim" & " -o:/dev/null examples/echo/echo.nim" 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: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: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" diff --git a/ffi/codegen/meta.nim b/ffi/codegen/meta.nim index a247aa5..4706915 100644 --- a/ffi/codegen/meta.nim +++ b/ffi/codegen/meta.nim @@ -172,3 +172,8 @@ const ffiOutputDir* {.strdefine.} = "" # Nim source path (relative to outputDir) embedded in generated build files; # set with -d:ffiSrcPath=../relative/path.nim const ffiSrcPath* {.strdefine.} = "" + +# When set to true, scalar-only `abi = c` procs (which have no foreign-binding codegen +# yet) are silently omitted from the generated bindings instead of failing the +# build. Off by default so the drop is loud; see genBindings(). +const ffiAllowScalarSkip* {.booldefine.} = false diff --git a/ffi/internal/ffi_macro.nim b/ffi/internal/ffi_macro.nim index 6b263b9..eee8ab2 100644 --- a/ffi/internal/ffi_macro.nim +++ b/ffi/internal/ffi_macro.nim @@ -1841,6 +1841,32 @@ macro ffiEvent*(args: varargs[untyped]): untyped = echo generated.repr return generated +proc reportScalarFastPathDrops(procs: seq[FFIProcMeta]) {.compileTime.} = + ## Scalar-fast-path procs have no foreign-binding codegen yet, so they can't + ## ride the generated bindings. Fail loudly, naming them, unless + ## `-d:ffiAllowScalarSkip` opts into the silent omission (then just hint). + var skipped: seq[string] = @[] + for p in procs: + if p.scalarFastPath: + skipped.add(p.procName) + if skipped.len == 0: + return + if ffiAllowScalarSkip: + for name in skipped: + hint( + "genBindings: omitting scalar-fast-path proc '" & name & + "' from the bindings (-d:ffiAllowScalarSkip)" + ) + return + error( + "genBindings: no foreign-binding codegen for scalar-fast-path `abi = c` " & + "procs yet, so these would be silently omitted from the generated " & "bindings: " & + skipped.join(", ") & ".\n" & "Fix by one of:\n" & + " - switch the proc to `abi = cbor`, or\n" & + " - add a non-scalar param (e.g. a struct or handle) so it takes the " & + "CBOR wire shape, or\n" & " - pass -d:ffiAllowScalarSkip to accept the omission." + ) + macro genBindings*( outputDir: static[string] = ffiOutputDir, nimSrcRelPath: static[string] = ffiSrcPath ): untyped = @@ -1880,16 +1906,8 @@ macro genBindings*( ) let lang = string_helpers.toLower(targetLang) let libName = deriveLibName(ffiProcRegistry) - # Scalar-fast-path procs have no foreign-dispatch codegen yet (their C - # export doesn't take the CBOR `(reqCbor, reqCborLen)` shape); drop them so - # the generators don't emit a broken CBOR caller for them. let genProcs = bindableProcs(ffiProcRegistry) - for p in ffiProcRegistry: - if p.scalarFastPath: - hint( - "genBindings: skipping scalar-fast-path proc '" & p.procName & - "' (no foreign-binding codegen for the scalar shape yet)" - ) + reportScalarFastPathDrops(ffiProcRegistry) case lang of "rust": generateRustCrate( diff --git a/tests/unit/fixtures/scalar_skip_fixture.nim b/tests/unit/fixtures/scalar_skip_fixture.nim new file mode 100644 index 0000000..91c4b2d --- /dev/null +++ b/tests/unit/fixtures/scalar_skip_fixture.nim @@ -0,0 +1,26 @@ +## Compile fixture for the scalar-fast-path drop error (see +## tests/unit/test_scalar_skip_gen.nim). Under `-d:ffiGenBindings` the scalar +## `abi = c` proc below has no foreign-binding codegen, so genBindings() must +## fail — unless `-d:ffiAllowScalarSkip` is passed, which downgrades it to a hint. + +import ffi, chronos + +type SkipLib = object + base: int + +declareLibrary("scalarskip", SkipLib) + +type SkipConfig {.ffi.} = object + base: int + +proc scalarskip_create*(cfg: SkipConfig): Future[Result[SkipLib, string]] {.ffiCtor.} = + return ok(SkipLib(base: cfg.base)) + +proc scalarskip_add*( + lib: SkipLib, a: int, b: int +): Future[Result[int, string]] {.ffi: "abi = c".} = + ## All-scalar signature: dispatches through the CBOR-free fast path and has no + ## foreign-binding codegen yet. + return ok(lib.base + a + b) + +genBindings() diff --git a/tests/unit/test_scalar_skip_gen.nim b/tests/unit/test_scalar_skip_gen.nim new file mode 100644 index 0000000..8017b14 --- /dev/null +++ b/tests/unit/test_scalar_skip_gen.nim @@ -0,0 +1,44 @@ +## Drives the scalar-fast-path drop error end to end: compiles +## `fixtures/scalar_skip_fixture.nim` (a library with an all-scalar `abi = c` +## proc) with `-d:ffiGenBindings` and asserts genBindings() fails loudly, and +## that `-d:ffiAllowScalarSkip` downgrades the drop to a clean build. +## +## The fixture is compiled in a child `nim check` (search paths and compiler +## captured at compile time) so its expected failure is observed as a test +## assertion, not this file's own compile error. + +import std/[os, osproc, strutils, compilesettings] +import unittest2 + +const + fixture = currentSourcePath().parentDir() / "fixtures" / "scalar_skip_fixture.nim" + nimExe = getCurrentCompilerExe() + ffiSearchPaths = querySettingSeq(searchPaths) + +proc genFixture(extraDefs: seq[string]): tuple[output: string, exitCode: int] = + let outDir = getTempDir() / "ffi_scalar_skip_out" + let cacheDir = getTempDir() / "ffi_scalar_skip_cache" + createDir(outDir) + var cmd = quoteShell(nimExe) & " check --hints:off --warnings:off" + for p in ffiSearchPaths: + cmd.add(" --path:" & quoteShell(p)) + cmd.add(" -d:ffiGenBindings -d:targetLang=c") + cmd.add(" -d:ffiOutputDir=" & quoteShell(outDir)) + for d in extraDefs: + cmd.add(" " & d) + cmd.add(" --nimcache:" & quoteShell(cacheDir)) + cmd.add(" " & quoteShell(fixture)) + execCmdEx(cmd) + +suite "scalar-fast-path drop is loud under -d:ffiGenBindings": + test "genBindings errors and names the dropped scalar proc": + let (output, code) = genFixture(@[]) + check code != 0 + check output.contains("scalarskip_add") + check output.contains("scalar-fast-path") + check output.contains("-d:ffiAllowScalarSkip") + + test "-d:ffiAllowScalarSkip downgrades the drop to a clean build": + let (output, code) = genFixture(@["-d:ffiAllowScalarSkip"]) + check code == 0 + check not output.contains("Error")