diff --git a/README.md b/README.md index 47f1add..27e9ec7 100644 --- a/README.md +++ b/README.md @@ -171,17 +171,18 @@ header shape from the library's ABI format. It carries two honest limits today: 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. -The `-d:targetLang=c_abi` generator emits real bindings for that shape: the -wrapper passes the scalar args inline (no request struct) and adapts the -raw-bytes reply into the same typed callback surface the flat-struct methods -use. The CBOR-speaking targets (`c`, `cpp`, `rust`, `cddl`) have no scalar -codegen, so under `-d:ffiGenBindings` they would omit such a proc from the -generated bindings — and `genBindings()` fails with an error naming the -affected procs. Resolve it by generating with `-d:targetLang=c_abi`, 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). +The `abi = c` C header (an `abi = c` library generated with `-d:targetLang=c`) +emits real bindings for that shape: the wrapper passes the scalar args inline +(no request struct) and adapts the raw-bytes reply into the same typed callback +surface the flat-struct methods use. The CBOR-speaking outputs — the CBOR C +header and the `cpp`, `rust`, `cddl` targets — have no scalar codegen, so under +`-d:ffiGenBindings` they would omit such a proc from the generated bindings — +and `genBindings()` fails with an error naming the affected procs. Resolve it by +making the whole library `abi = c` and generating C bindings, 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()` diff --git a/ffi/codegen/c.nim b/ffi/codegen/c.nim index bd6141a..d76bd2b 100644 --- a/ffi/codegen/c.nim +++ b/ffi/codegen/c.nim @@ -1018,7 +1018,7 @@ func abiScalarRawFnName(libType: string): string = ## per-method trampoline converts into the typed reply. return libType & "ScalarRawFn" -proc abiScalarArgParams(m: FFIProcMeta): seq[string] = +func abiScalarArgParams(m: FFIProcMeta): seq[string] = ## C parameters for a scalar method's args — passed inline by value, in both ## the raw export and the high-level wrapper (no Req struct). var params: seq[string] = @[] diff --git a/ffi/codegen/meta.nim b/ffi/codegen/meta.nim index bef564e..b493611 100644 --- a/ffi/codegen/meta.nim +++ b/ffi/codegen/meta.nim @@ -34,9 +34,9 @@ type scalarFastPath*: bool ## True for an `abi = c` proc whose whole signature is scalar (see ## `isScalarOnly`): dispatches through the CBOR-free scalar fast path. - ## Only the `c_abi` generator emits foreign bindings for it (inline - ## scalar args + raw-bytes reply trampoline); the CBOR-speaking - ## generators drop it (see `bindableProcs`). + ## Only the `abi = c` C header emits a foreign binding for it (inline + ## scalar args + raw-bytes reply trampoline); every other target — and + ## the CBOR C header — drops it (see `bindableProcs`). FFIFieldMeta* = object name*: string @@ -138,5 +138,8 @@ const ffiOutputDir* {.strdefine.} = "" # Nim src path override relative to outputDir (-d:ffiSrcPath); empty derives it. const ffiSrcPath* {.strdefine.} = "" -# When true, scalar-only `abi = c` procs are silently omitted rather than failing the build. Off by default so the drop is loud; see genBindings(). +# When set, scalar-only `abi = c` procs are silently omitted from bindings +# generated by targets without scalar codegen (every target except the +# `abi = c` C header) 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 fc1b1b8..973c7fa 100644 --- a/ffi/internal/ffi_macro.nim +++ b/ffi/internal/ffi_macro.nim @@ -1519,9 +1519,10 @@ macro ffiEvent*(args: varargs[untyped]): untyped = return generated proc reportScalarFastPathDrops(procs: seq[FFIProcMeta]) {.compileTime.} = - ## Only the `c_abi` generator emits foreign bindings for scalar-fast-path - ## procs; every other target drops them. Fail loudly, naming them, unless - ## `-d:ffiAllowScalarSkip` opts into the silent omission (then just hint). + ## Only the `abi = c` C header emits foreign bindings for scalar-fast-path + ## procs; every other target (and the CBOR C header) drops them. 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: @@ -1537,11 +1538,12 @@ proc reportScalarFastPathDrops(procs: seq[FFIProcMeta]) {.compileTime.} = return error( "genBindings: this target has no foreign-binding codegen for " & - "scalar-fast-path `abi = c` procs (only -d:targetLang=c_abi emits them), " & - "so these would be silently omitted from the generated bindings: " & - skipped.join(", ") & ".\n" & "Fix by one of:\n" & - " - generate with -d:targetLang=c_abi, or\n" & - " - switch the proc to `abi = cbor`, or\n" & + "scalar-fast-path `abi = c` procs, so these would be silently omitted " & + "from the generated bindings: " & skipped.join(", ") & + ".\nThey are emitted only into the `abi = c` C header (an `abi = c` " & + "library generated with -d:targetLang=c).\n" & "Fix by one of:\n" & + " - make the library `abi = c` (declareLibrary(..., \"c\")) and generate " & + "C bindings, or\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." ) @@ -1602,14 +1604,16 @@ macro genBindings*( let lang = string_helpers.toLower(rawLang.strip()) if lang.len == 0: continue - # `c_abi` is the one target with scalar-fast-path codegen, so it binds the - # full registry; the others drop scalar procs (loudly, unless skipped). + # The `abi = c` C header is the one output with scalar-fast-path codegen, + # so it binds the full registry. Every other target — and the CBOR C + # header — drops scalar procs (loudly, unless skipped). + let emitsScalars = lang == "c" and currentDefaultABIFormat == ABIFormat.C let genProcs = - if lang == "c_abi": + if emitsScalars: ffiProcRegistry else: bindableProcs(ffiProcRegistry) - if lang != "c_abi": + if not emitsScalars: reportScalarFastPathDrops(ffiProcRegistry) let outDir = bindingsOutputDir(lang, outputDir) emitBindingsFor( diff --git a/ffi/internal/ffi_scalar.nim b/ffi/internal/ffi_scalar.nim index 42e6e25..414a69a 100644 --- a/ffi/internal/ffi_scalar.nim +++ b/ffi/internal/ffi_scalar.nim @@ -35,7 +35,7 @@ func bindableProcs*(procs: seq[FFIProcMeta]): seq[FFIProcMeta] = ## The procs the CBOR-speaking foreign-binding generators emit for. ## Scalar-fast-path procs are dropped: their C export takes inline scalar ## args, not the CBOR `(reqCbor, reqCborLen)` shape those backends assume, so - ## emitting a CBOR caller for them would be wrong. Only the `c_abi` generator + ## emitting a CBOR caller for them would be wrong. Only the `abi = c` C header ## has scalar codegen and binds the full registry (see genBindings()). var kept: seq[FFIProcMeta] = @[] for p in procs: diff --git a/tests/unit/fixtures/scalar_skip_fixture.nim b/tests/unit/fixtures/scalar_skip_fixture.nim index 49538ac..508c02c 100644 --- a/tests/unit/fixtures/scalar_skip_fixture.nim +++ b/tests/unit/fixtures/scalar_skip_fixture.nim @@ -1,8 +1,8 @@ -## Compile fixture for the scalar-fast-path genBindings() behavior (see -## tests/unit/test_scalar_skip_gen.nim). Under `-d:ffiGenBindings` only the -## `c_abi` target has foreign-binding codegen for the scalar `abi = c` proc -## below; any other target must fail — unless `-d:ffiAllowScalarSkip` is -## passed, which downgrades the drop to a hint. +## Compile fixture for the scalar-fast-path drop error (see +## tests/unit/test_scalar_skip_gen.nim). This is a CBOR-default library with one +## stray scalar `abi = c` proc, so no target can emit a binding for it: under +## `-d:ffiGenBindings` genBindings() must fail — unless `-d:ffiAllowScalarSkip` +## is passed, which downgrades the drop to a hint. import ffi, chronos @@ -20,8 +20,8 @@ proc scalarskip_create*(cfg: SkipConfig): Future[Result[SkipLib, string]] {.ffiC 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; only the - ## `c_abi` target generates a foreign binding for it. + ## All-scalar signature: dispatches through the CBOR-free fast path. In this + ## CBOR-default library no target can emit a foreign binding for it. 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 index 7505f85..5e6164e 100644 --- a/tests/unit/test_scalar_skip_gen.nim +++ b/tests/unit/test_scalar_skip_gen.nim @@ -1,12 +1,12 @@ -## Drives the scalar-fast-path genBindings() behavior end to end: compiles -## `fixtures/scalar_skip_fixture.nim` (a library with an all-scalar `abi = c` -## proc) with `-d:ffiGenBindings` and asserts a CBOR-speaking target -## (`targetLang=c`) fails loudly, `-d:ffiAllowScalarSkip` downgrades the drop -## to a clean build, and `targetLang=c_abi` needs no skip at all — it emits a -## real binding for the scalar proc. +## Drives the scalar-fast-path drop error end to end: compiles +## `fixtures/scalar_skip_fixture.nim` (a CBOR-default library with a stray +## 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 positive path — the `abi = c` C header emitting a real scalar +## binding — is covered by test_c_abi_codegen and the echo c_abi e2e. ## ## The fixture is compiled in a child `nim check` (search paths and compiler -## captured at compile time) so an expected failure is observed as a test +## 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] @@ -17,15 +17,14 @@ const nimExe = getCurrentCompilerExe() ffiSearchPaths = querySettingSeq(searchPaths) -proc genFixture( - lang: string, extraDefs: seq[string], outDir: string -): tuple[output: string, exitCode: int] = +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=" & lang) + cmd.add(" -d:ffiGenBindings -d:targetLang=c") cmd.add(" -d:ffiOutputDir=" & quoteShell(outDir)) for d in extraDefs: cmd.add(" " & d) @@ -34,26 +33,14 @@ proc genFixture( execCmdEx(cmd) suite "scalar-fast-path drop is loud under -d:ffiGenBindings": - test "a CBOR target errors and names the dropped scalar proc": - let (output, code) = genFixture("c", @[], getTempDir() / "ffi_scalar_skip_out_c") + 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("targetLang=c_abi") check output.contains("-d:ffiAllowScalarSkip") test "-d:ffiAllowScalarSkip downgrades the drop to a clean build": - let (output, code) = genFixture( - "c", @["-d:ffiAllowScalarSkip"], getTempDir() / "ffi_scalar_skip_out_c_skip" - ) - check code == 0 - check not output.contains("Error") - - test "targetLang=c_abi needs no skip: the scalar proc has real codegen": - # `nim check` runs genBindings() but VM file writes are skipped, so this - # asserts the clean build only; the emitted wrapper text is covered by - # test_c_abi_codegen and the checked-in echo c_abi bindings. - let (output, code) = - genFixture("c_abi", @[], getTempDir() / "ffi_scalar_skip_out_c_abi") + let (output, code) = genFixture(@["-d:ffiAllowScalarSkip"]) check code == 0 check not output.contains("Error")