feat(ffi): make the scalar-fast-path binding drop loud (#120)

This commit is contained in:
Gabriel Cruz 2026-07-09 16:43:21 -03:00 committed by GitHub
parent 3e57751e3a
commit ea67fb747a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 118 additions and 11 deletions

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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(

View File

@ -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()

View File

@ -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")