From e60f771be671d71cd355d1ce244831e6b84b2765 Mon Sep 17 00:00:00 2001 From: Gabriel Cruz <8129788+gmelodie@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:58:45 -0300 Subject: [PATCH] feat(codegen): scalar-fast-path bindings for the abi = c header (#124) --- README.md | 29 ++- examples/echo/c_abi_bindings/echo.h | 53 ++++++ examples/echo/c_bindings/nim_ffi_cbor.h | 6 +- examples/timer/c_bindings/nim_ffi_cbor.h | 6 +- ffi.nimble | 3 +- ffi/codegen/c.nim | 191 ++++++++++++++++++-- ffi/codegen/meta.nim | 4 +- ffi/codegen/templates/c/cbor_helpers.h.tpl | 6 +- ffi/internal/ffi_macro.nim | 32 +++- ffi/internal/ffi_scalar.nim | 3 +- tests/e2e/c_abi/test_echo_c_abi.c | 23 ++- tests/unit/fixtures/scalar_skip_fixture.nim | 8 +- tests/unit/test_c_abi_codegen.nim | 61 +++++++ tests/unit/test_scalar_skip_gen.nim | 8 +- 14 files changed, 378 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 27346cc..00637cd 100644 --- a/README.md +++ b/README.md @@ -160,24 +160,17 @@ header shape from the library's ABI format. It carries two honest limits today: - **Events are CBOR-only.** Applying `abi = c` to an `{.ffiEvent.}` proc is a hard compile error; declare events with `abi = cbor` (they ride CBOR internally regardless of the library default). -- **All-scalar `abi = c` procs are dropped from the foreign bindings.** A - `{.ffi: "abi = c".}` method whose every param and return is a plain scalar - takes the CBOR-free scalar fast path at runtime, but the foreign codegen for - that inline-args shape is a follow-up (tracked in #120) — such procs are - omitted from the generated `.h`. Give a proc at least one non-scalar - (struct / `seq` / `Option`) param or return, or use `abi = cbor`, if you need - it in the bindings. - -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). +- **All-scalar `abi = c` procs bind only in the `abi = c` C header.** A + `{.ffi: "abi = c".}` method whose params and return are all scalars — ints, + floats, bools; a `string` return is fine, a `string` param is not — takes a + CBOR-free fast path, and its C wrapper passes the args inline instead of + packing a request struct. Only the `abi = c` C header emits that shape. The + CBOR C header and the `cpp`, `rust` and `cddl` targets have no scalar codegen + and would silently omit the proc, so `genBindings()` fails and names it. Fix + it by generating C bindings from an `abi = c` library, switching the proc to + `abi = cbor`, giving it a non-scalar param, or passing + `-d:ffiAllowScalarSkip` to accept the omission — the proc still works over the + fast path, it's just absent from the bindings. ## Placement of `genBindings()` diff --git a/examples/echo/c_abi_bindings/echo.h b/examples/echo/c_abi_bindings/echo.h index 11ac4ff..def7d56 100644 --- a/examples/echo/c_abi_bindings/echo.h +++ b/examples/echo/c_abi_bindings/echo.h @@ -34,14 +34,33 @@ typedef struct { } EchoShoutReq; typedef void (*EchoShoutReplyFn)(int err_code, const ShoutResponse* reply, const char* err_msg, void* user_data); +typedef void (*EchoVersionReplyFn)(int err_code, const char* reply, const char* err_msg, void* user_data); typedef void (*EchoCreateRawFn)(int err_code, const char* ctx_addr, const char* err_msg, void* user_data); +/* Raw reply of a scalar-fast-path export: `msg`/`len` are bytes (a string + return's UTF-8, or the 8-byte native-endian scalar image), not + NUL-terminated and valid only for the duration of the call. */ +typedef void (*EchoScalarRawFn)(int caller_ret, char* msg, size_t len, void* user_data); +#ifndef NIMFFI_ABI_DUP_CSTR_N +#define NIMFFI_ABI_DUP_CSTR_N +/* NUL-terminated copy of a length-delimited byte run; NULL if it can't. */ +static inline char* nimffi_abi_dup_cstr_n(const char* s, size_t n) { + if (n == SIZE_MAX) return NULL; + char* p = (char*)malloc(n + 1); + if (p) { + if (n > 0) memcpy(p, s, n); + p[n] = '\0'; + } + return p; +} +#endif #ifdef __cplusplus extern "C" { #endif void* echo_create(const EchoCreateCtorReq* req, EchoCreateRawFn on_created, void* user_data); int echo_shout(void* ctx, EchoShoutReplyFn on_reply, void* user_data, const EchoShoutReq* req); +int echo_version(void* ctx, EchoScalarRawFn callback, void* user_data); int echo_destroy(void* ctx); #ifdef __cplusplus @@ -112,4 +131,38 @@ static inline int echo_ctx_shout(const EchoCtx* ctx, const ShoutRequest* req, Ec return echo_shout(ctx->ptr, on_reply, user_data, &ffi_req); } +typedef struct { EchoVersionReplyFn fn; void* user_data; } EchoVersionScalarBox; +static void echo_version_scalar_reply(int caller_ret, char* msg, size_t len, void* ud) { + EchoVersionScalarBox* box = (EchoVersionScalarBox*)ud; + if (!box) return; + EchoVersionReplyFn fn = box->fn; + void* user_data = box->user_data; + free(box); + if (!fn) return; + if (caller_ret != NIMFFI_RET_OK) { + char* em = nimffi_abi_dup_cstr_n(msg ? msg : "", msg ? len : 0); + fn(caller_ret, "", em ? em : "FFI call failed", user_data); + free(em); + return; + } + char* reply = nimffi_abi_dup_cstr_n(msg ? msg : "", msg ? len : 0); + if (!reply) { + fn(NIMFFI_RET_ERR, "", "out of memory", user_data); + return; + } + fn(NIMFFI_RET_OK, reply, "", user_data); + free(reply); +} + +static inline int echo_ctx_version(const EchoCtx* ctx, EchoVersionReplyFn on_reply, void* user_data) { + EchoVersionScalarBox* box = (EchoVersionScalarBox*)malloc(sizeof(EchoVersionScalarBox)); + if (!box) { + if (on_reply) on_reply(-1, "", "out of memory", user_data); + return -1; + } + box->fn = on_reply; + box->user_data = user_data; + return echo_version(ctx->ptr, echo_version_scalar_reply, box); +} + #endif /* NIM_FFI_LIB_ECHO_C_ABI_H_INCLUDED */ diff --git a/examples/echo/c_bindings/nim_ffi_cbor.h b/examples/echo/c_bindings/nim_ffi_cbor.h index 98d375c..059f7a3 100644 --- a/examples/echo/c_bindings/nim_ffi_cbor.h +++ b/examples/echo/c_bindings/nim_ffi_cbor.h @@ -271,8 +271,12 @@ static inline char* nimffi_dup_cstr(const char* s) { } /* NUL-terminated copy of a length-delimited (not NUL-terminated) byte run, - * for turning the FFICallback's raw error `msg`/`len` into a C string. */ + * for turning the FFICallback's raw error `msg`/`len` into a C string; NULL if + * it can't. */ static inline char* nimffi_dup_cstr_n(const char* s, size_t n) { + if (n == SIZE_MAX) { + return NULL; + } char* p = (char*)malloc(n + 1); if (p) { if (n > 0) { diff --git a/examples/timer/c_bindings/nim_ffi_cbor.h b/examples/timer/c_bindings/nim_ffi_cbor.h index 98d375c..059f7a3 100644 --- a/examples/timer/c_bindings/nim_ffi_cbor.h +++ b/examples/timer/c_bindings/nim_ffi_cbor.h @@ -271,8 +271,12 @@ static inline char* nimffi_dup_cstr(const char* s) { } /* NUL-terminated copy of a length-delimited (not NUL-terminated) byte run, - * for turning the FFICallback's raw error `msg`/`len` into a C string. */ + * for turning the FFICallback's raw error `msg`/`len` into a C string; NULL if + * it can't. */ static inline char* nimffi_dup_cstr_n(const char* s, size_t n) { + if (n == SIZE_MAX) { + return NULL; + } char* p = (char*)malloc(n + 1); if (p) { if (n > 0) { diff --git a/ffi.nimble b/ffi.nimble index 4c6db70..bc01f2f 100644 --- a/ffi.nimble +++ b/ffi.nimble @@ -253,10 +253,9 @@ task genbindings_c_echo, "Generate C bindings for the echo example": exec genBindingsCmd(nimFlagsRefc, echoSrc, "c") task genbindings_c_abi_echo, "Generate CBOR-free abi=c C bindings for the echo example": - # ffiAllowScalarSkip omits echoVersion (all-scalar, no foreign codegen yet); # abiOut forces output beside the CBOR `c_bindings/` instead of overwriting it. const abiOut = "examples/echo/c_abi_bindings" - const abiFlags = " -d:ffiEchoAbiC -d:ffiAllowScalarSkip -d:ffiSrcPath=../echo.nim" + const abiFlags = " -d:ffiEchoAbiC -d:ffiSrcPath=../echo.nim" exec genBindingsCmd(nimFlagsOrc & abiFlags, echoSrc, "c", abiOut) exec genBindingsCmd(nimFlagsRefc & abiFlags, echoSrc, "c", abiOut) diff --git a/ffi/codegen/c.nim b/ffi/codegen/c.nim index 71075bb..b248ad5 100644 --- a/ffi/codegen/c.nim +++ b/ffi/codegen/c.nim @@ -946,7 +946,7 @@ proc newAbiReg(types: seq[FFITypeMeta], procs: seq[FFIProcMeta]): AbiReg = for t in types: reg.typeTable[t.name] = t for p in procs: - if p.kind != FFIKind.DTOR: + if p.kind != FFIKind.DTOR and not p.scalarFastPath: let rt = reqTypeMeta(p) reg.typeTable[rt.name] = rt return reg @@ -1010,6 +1010,37 @@ proc emitAbiReplyTypedefs( " reply, const char* err_msg, void* user_data);" ) +func abiScalarRawFnName(libType: string): string = + ## Raw-bytes callback typedef a scalar-fast-path export takes. + return libType & "ScalarRawFn" + +const abiScalarDupCStr = "nimffi_abi_dup_cstr_n" + +func abiScalarDupHelper(): seq[string] = + ## CBOR-free twin of `nimffi_dup_cstr_n`, include-guarded so two `abi = c` + ## headers can co-exist in one TU. + return @[ + "#ifndef NIMFFI_ABI_DUP_CSTR_N", + "#define NIMFFI_ABI_DUP_CSTR_N", + "/* NUL-terminated copy of a length-delimited byte run; NULL if it can't. */", + "static inline char* " & abiScalarDupCStr & "(const char* s, size_t n) {", + " if (n == SIZE_MAX) return NULL;", + " char* p = (char*)malloc(n + 1);", + " if (p) {", + " if (n > 0) memcpy(p, s, n);", + " p[n] = '\\0';", + " }", + " return p;", + "}", + "#endif", + ] + +func abiScalarArgParams(m: FFIProcMeta): seq[string] = + var params: seq[string] = @[] + for ep in m.extraParams: + params.add(abiLeafCType(ep.typeName.strip()).cType & " " & ep.name) + return params + proc emitAbiExternDecls( lines: var seq[string], reg: var AbiReg, @@ -1017,31 +1048,50 @@ proc emitAbiExternDecls( procs: seq[FFIProcMeta], ) = let createRawFn = libType & "CreateRawFn" - var haveCtor = false + var haveCtor, haveScalar = false for p in procs: if p.kind == FFIKind.CTOR: haveCtor = true + if p.scalarFastPath: + haveScalar = true if haveCtor: lines.add( "typedef void (*" & createRawFn & ")(int err_code, const char* ctx_addr, const char* err_msg, void* user_data);" ) + if haveScalar: + lines.add( + "/* Raw reply of a scalar-fast-path export: `msg`/`len` are bytes (a string" + ) + lines.add(" return's UTF-8, or the 8-byte native-endian scalar image), not") + lines.add(" NUL-terminated and valid only for the duration of the call. */") + lines.add( + "typedef void (*" & abiScalarRawFnName(libType) & + ")(int caller_ret, char* msg, size_t len, void* user_data);" + ) + for l in abiScalarDupHelper(): + lines.add(l) lines.add("#ifdef __cplusplus") lines.add("extern \"C\" {") lines.add("#endif") lines.add("") for p in procs: - let reqStruct = reqStructName(p) case p.kind of FFIKind.FFI: - let info = abiMethodReplyInfo(reg, libType, p) - lines.add( - "int " & p.procName & "(void* ctx, " & info.fnType & - " on_reply, void* user_data, const " & reqStruct & "* req);" - ) + if p.scalarFastPath: + var params = + @["void* ctx", abiScalarRawFnName(libType) & " callback", "void* user_data"] + params.add(abiScalarArgParams(p)) + lines.add("int " & p.procName & "(" & params.join(", ") & ");") + else: + let info = abiMethodReplyInfo(reg, libType, p) + lines.add( + "int " & p.procName & "(void* ctx, " & info.fnType & + " on_reply, void* user_data, const " & reqStructName(p) & "* req);" + ) of FFIKind.CTOR: lines.add( - "void* " & p.procName & "(const " & reqStruct & "* req, " & createRawFn & + "void* " & p.procName & "(const " & reqStructName(p) & "* req, " & createRawFn & " on_created, void* user_data);" ) of FFIKind.DTOR: @@ -1179,6 +1229,122 @@ proc emitAbiMethod( lines.add("}") lines.add("") +func abiScalarOkLines(m: FFIProcMeta, fnType: string): seq[string] = + ## Trampoline RET_OK branch. A string return rides as its own UTF-8; every + ## other scalar is the 8-byte image `ffiScalarRetBytes` packs (ints + ## sign-extended, floats widened to double, bool as 0/1). + let rt = m.returnTypeName.strip() + if rt == "string" or rt == "cstring": + return @[ + " char* reply = " & abiScalarDupCStr & "(msg ? msg : \"\", msg ? len : 0);", + " if (!reply) {", + " fn(NIMFFI_RET_ERR, \"\", \"out of memory\", user_data);", + " return;", " }", " fn(NIMFFI_RET_OK, reply, \"\", user_data);", + " free(reply);", + ] + var lines = @[ + " uint64_t slot = 0;", " if (!msg || len != sizeof(slot)) {", + " fn(NIMFFI_RET_ERR, NULL, \"scalar reply: unexpected payload size\", user_data);", + " return;", " }", " memcpy(&slot, msg, sizeof(slot));", + ] + let cType = abiLeafCType(rt).cType + case rt + of "int", "int64": + lines.add(" int64_t reply;") + lines.add(" memcpy(&reply, &slot, sizeof(reply));") + of "int8", "int16", "int32": + lines.add(" int64_t wide;") + lines.add(" memcpy(&wide, &slot, sizeof(wide));") + lines.add(" " & cType & " reply = (" & cType & ")wide;") + of "uint", "uint64": + lines.add(" uint64_t reply = slot;") + of "uint8", "uint16", "uint32", "byte": + lines.add(" " & cType & " reply = (" & cType & ")slot;") + of "bool": + lines.add(" bool reply = slot != 0;") + of "float", "float64": + lines.add(" double reply;") + lines.add(" memcpy(&reply, &slot, sizeof(reply));") + of "float32": + lines.add(" double wide;") + lines.add(" memcpy(&wide, &slot, sizeof(wide));") + lines.add(" float reply = (float)wide;") + else: + raise newException( + ValueError, "abi = c: unexpected scalar-fast-path return type: " & rt + ) + lines.add(" fn(NIMFFI_RET_OK, &reply, \"\", user_data);") + return lines + +proc emitAbiScalarMethod( + lines: var seq[string], + reg: var AbiReg, + ctxType, libName, libType: string, + m: FFIProcMeta, +) = + ## Args go inline to the raw export and a trampoline adapts the raw-bytes + ## reply into the typed `ReplyFn` surface. The trampoline frees the callback + ## box, relying on the dylib invoking it exactly once on every path. + let stripped = stripLibPrefix(m.procName, m.libName) + let pascal = snakeToPascalCase(stripped) + let info = abiMethodReplyInfo(reg, libType, m) + let boxType = libType & pascal & "ScalarBox" + let tramp = m.procName & "_scalar_reply" + let isStr = m.returnTypeName.strip() in ["string", "cstring"] + let errReply = if isStr: "\"\"" else: "NULL" + lines.add( + "typedef struct { " & info.fnType & " fn; void* user_data; } " & boxType & ";" + ) + lines.add( + "static void " & tramp & "(int caller_ret, char* msg, size_t len, void* ud) {" + ) + lines.add(" " & boxType & "* box = (" & boxType & "*)ud;") + lines.add(" if (!box) return;") + lines.add(" " & info.fnType & " fn = box->fn;") + lines.add(" void* user_data = box->user_data;") + lines.add(" free(box);") + lines.add(" if (!fn) return;") + lines.add(" if (caller_ret != NIMFFI_RET_OK) {") + lines.add( + " char* em = " & abiScalarDupCStr & "(msg ? msg : \"\", msg ? len : 0);" + ) + lines.add( + " fn(caller_ret, " & errReply & ", em ? em : \"FFI call failed\", user_data);" + ) + lines.add(" free(em);") + lines.add(" return;") + lines.add(" }") + for l in abiScalarOkLines(m, info.fnType): + lines.add(l) + lines.add("}") + lines.add("") + let params = abiScalarArgParams(m) + let head = + "static inline int " & libName & "_ctx_" & stripped & "(const " & ctxType & "* ctx, " + let sig = + if params.len > 0: + head & params.join(", ") & ", " & info.fnType & " on_reply, void* user_data) {" + else: + head & info.fnType & " on_reply, void* user_data) {" + lines.add(sig) + lines.add( + " " & boxType & "* box = (" & boxType & "*)malloc(sizeof(" & boxType & "));" + ) + lines.add(" if (!box) {") + lines.add( + " if (on_reply) on_reply(-1, " & errReply & ", \"out of memory\", user_data);" + ) + lines.add(" return -1;") + lines.add(" }") + lines.add(" box->fn = on_reply;") + lines.add(" box->user_data = user_data;") + var callArgs = @["ctx->ptr", tramp, "box"] + for ep in m.extraParams: + callArgs.add(ep.name) + lines.add(" return " & m.procName & "(" & callArgs.join(", ") & ");") + lines.add("}") + lines.add("") + proc generateCAbiLibHeader*( procs: seq[FFIProcMeta], types: seq[FFITypeMeta], @@ -1197,7 +1363,7 @@ proc generateCAbiLibHeader*( for t in types: ensureAbiStruct(reg, t.name) for p in procs: - if p.kind != FFIKind.DTOR: + if p.kind != FFIKind.DTOR and not p.scalarFastPath: ensureAbiStruct(reg, reqStructName(p)) let guard = "NIM_FFI_LIB_" & libName.toUpperAscii() & "_C_ABI_H_INCLUDED" @@ -1236,7 +1402,10 @@ proc generateCAbiLibHeader*( emitAbiCtxAndCtor(lines, reg, libName, libType, ctxType, classified.ctors) emitAbiDestructor(lines, ctxType, libName, classified.dtorProcName) for m in classified.methods: - emitAbiMethod(lines, reg, ctxType, libName, libType, m) + if m.scalarFastPath: + emitAbiScalarMethod(lines, reg, ctxType, libName, libType, m) + else: + emitAbiMethod(lines, reg, ctxType, libName, libType, m) lines.add("#endif /* " & guard & " */") return lines.join("\n") & "\n" diff --git a/ffi/codegen/meta.nim b/ffi/codegen/meta.nim index 63c8a84..1aa7314 100644 --- a/ffi/codegen/meta.nim +++ b/ffi/codegen/meta.nim @@ -33,7 +33,7 @@ type abiFormat*: ABIFormat scalarFastPath*: bool ## `abi = c` proc with an all-scalar signature: uses the CBOR-free fast - ## path and is skipped by the foreign-binding generators. + ## path, and binds only in the `abi = c` C header (see `bindableProcs`). FFIFieldMeta* = object name*: string @@ -135,5 +135,5 @@ 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 true, targets without scalar codegen silently omit scalar-only `abi = c` procs rather than failing the build. Off by default so the drop is loud; see genBindings(). const ffiAllowScalarSkip* {.booldefine.} = false diff --git a/ffi/codegen/templates/c/cbor_helpers.h.tpl b/ffi/codegen/templates/c/cbor_helpers.h.tpl index 3a5e29e..3056aab 100644 --- a/ffi/codegen/templates/c/cbor_helpers.h.tpl +++ b/ffi/codegen/templates/c/cbor_helpers.h.tpl @@ -271,8 +271,12 @@ static inline char* nimffi_dup_cstr(const char* s) { } /* NUL-terminated copy of a length-delimited (not NUL-terminated) byte run, - * for turning the FFICallback's raw error `msg`/`len` into a C string. */ + * for turning the FFICallback's raw error `msg`/`len` into a C string; NULL if + * it can't. */ static inline char* nimffi_dup_cstr_n(const char* s, size_t n) { + if (n == SIZE_MAX) { + return NULL; + } char* p = (char*)malloc(n + 1); if (p) { if (n > 0) { diff --git a/ffi/internal/ffi_macro.nim b/ffi/internal/ffi_macro.nim index 5fe7874..5911ea0 100644 --- a/ffi/internal/ffi_macro.nim +++ b/ffi/internal/ffi_macro.nim @@ -1519,8 +1519,8 @@ macro ffiEvent*(args: varargs[untyped]): untyped = return generated proc reportScalarFastPathDrops(procs: seq[FFIProcMeta]) {.compileTime.} = - ## Scalar-fast-path procs have no foreign-binding codegen yet; fail loudly - ## naming them unless `-d:ffiAllowScalarSkip` downgrades it to a hint. + ## Fail loudly on scalar-fast-path procs a target can't bind, unless + ## `-d:ffiAllowScalarSkip` downgrades it to a hint. var skipped: seq[string] = @[] for p in procs: if p.scalarFastPath: @@ -1535,12 +1535,17 @@ proc reportScalarFastPathDrops(procs: seq[FFIProcMeta]) {.compileTime.} = ) 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." + """genBindings: this target has no foreign-binding codegen for scalar-fast-path +`abi = c` procs, so these would be silently omitted from the generated bindings: +$1 +They are emitted only into the `abi = c` C header (an `abi = c` library generated +with -d:targetLang=c). +Fix by one of: + - make the library `abi = c` (declareLibrary(..., "c")) and generate C bindings, or + - switch the proc to `abi = cbor`, or + - add a non-scalar param (e.g. a struct or handle) so it takes the CBOR wire shape, or + - pass -d:ffiAllowScalarSkip to accept the omission.""" % + [skipped.join(", ")] ) proc bindingsOutputDir(lang, explicit: string): string {.compileTime.} = @@ -1595,12 +1600,19 @@ macro genBindings*( when defined(ffiGenBindings): let libName = deriveLibName(ffiProcRegistry) - let genProcs = bindableProcs(ffiProcRegistry) - reportScalarFastPathDrops(ffiProcRegistry) for rawLang in targetLang.split(','): let lang = string_helpers.toLower(rawLang.strip()) if lang.len == 0: continue + # The `abi = c` C header is the only output with scalar-fast-path codegen. + let emitsScalars = lang == "c" and currentDefaultABIFormat == ABIFormat.C + let genProcs = + if emitsScalars: + ffiProcRegistry + else: + bindableProcs(ffiProcRegistry) + if not emitsScalars: + reportScalarFastPathDrops(ffiProcRegistry) let outDir = bindingsOutputDir(lang, outputDir) emitBindingsFor( lang, genProcs, libName, outDir, bindingsSrcPath(outDir, nimSrcRelPath) diff --git a/ffi/internal/ffi_scalar.nim b/ffi/internal/ffi_scalar.nim index a390590..3fdde24 100644 --- a/ffi/internal/ffi_scalar.nim +++ b/ffi/internal/ffi_scalar.nim @@ -32,8 +32,9 @@ func isScalarOnly*(p: FFIProcMeta): bool = true func bindableProcs*(procs: seq[FFIProcMeta]): seq[FFIProcMeta] = - ## Procs the foreign-binding generators emit for; scalar-fast-path procs are + ## Procs the CBOR-speaking generators emit for; scalar-fast-path procs are ## dropped (their inline-scalar export doesn't match the CBOR codegen shape). + ## The `abi = c` C header binds the full registry instead. var kept: seq[FFIProcMeta] = @[] for p in procs: if not p.scalarFastPath: diff --git a/tests/e2e/c_abi/test_echo_c_abi.c b/tests/e2e/c_abi/test_echo_c_abi.c index 34db73c..893ff43 100644 --- a/tests/e2e/c_abi/test_echo_c_abi.c +++ b/tests/e2e/c_abi/test_echo_c_abi.c @@ -1,8 +1,9 @@ /* End-to-end test for the CBOR-free `abi = c` echo bindings: the `_CWire` * structs in echo.h are the C ABI, strings are borrowed `const char*`, no * TinyCBOR. Drives the async callback-per-call surface (ctor, object-returning - * method, teardown). echoVersion rides the scalar fast path (no foreign binding - * yet) and isn't exercised. */ + * method, teardown). echoVersion rides the scalar fast path, whose binding + * passes no struct and adapts the raw-bytes reply into the same typed callback + * shape as the flat `_CWire` methods. */ #include "echo.h" #include #include @@ -85,9 +86,27 @@ static void test_shout(EchoCtx* ctx) { assert(strcmp(w.text_b, "c-abi") == 0); } +static void on_version(int ec, const char* reply, const char* em, void* ud) { + ReplyWaiter* w = (ReplyWaiter*)ud; + w->err_code = ec; + if (reply) snprintf(w->text_a, sizeof(w->text_a), "%s", reply); + if (em) snprintf(w->err, sizeof(w->err), "%s", em); + atomic_store_explicit(&w->done, 1, memory_order_release); +} + +static void test_version(EchoCtx* ctx) { + ReplyWaiter w; + memset(&w, 0, sizeof(w)); + echo_ctx_version(ctx, on_version, &w); + wait_done(&w.done); + assert(w.err_code == 0); + assert(strcmp(w.text_a, "nim-echo v0.1.0") == 0); +} + int main(void) { EchoCtx* ctx = make_ctx(); test_shout(ctx); + test_version(ctx); echo_ctx_destroy(ctx); printf("all abi=c echo e2e checks passed\n"); return 0; diff --git a/tests/unit/fixtures/scalar_skip_fixture.nim b/tests/unit/fixtures/scalar_skip_fixture.nim index 80c9a45..8322981 100644 --- a/tests/unit/fixtures/scalar_skip_fixture.nim +++ b/tests/unit/fixtures/scalar_skip_fixture.nim @@ -1,6 +1,7 @@ ## Compile fixture for the scalar-fast-path drop error (see -## tests/unit/test_scalar_skip_gen.nim): the scalar `abi = c` proc has no -## foreign-binding codegen, so genBindings() fails unless -d:ffiAllowScalarSkip. +## tests/unit/test_scalar_skip_gen.nim): a CBOR-default library, so no target +## binds its scalar `abi = c` proc and genBindings() fails unless +## -d:ffiAllowScalarSkip. import ffi, chronos @@ -18,7 +19,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: CBOR-free fast path, no foreign-binding codegen yet. + ## All-scalar signature: CBOR-free fast path, unbindable outside an `abi = c` + ## library. return ok(lib.base + a + b) genBindings() diff --git a/tests/unit/test_c_abi_codegen.nim b/tests/unit/test_c_abi_codegen.nim index 9b18af7..3383ed7 100644 --- a/tests/unit/test_c_abi_codegen.nim +++ b/tests/unit/test_c_abi_codegen.nim @@ -50,6 +50,33 @@ suite "generateCAbiLibHeader": extraParams: @[], returnTypeName: "string", ), + FFIProcMeta( + procName: "timer_add", + libName: "timer", + kind: FFIKind.FFI, + libTypeName: "Timer", + extraParams: @[param("a", "int"), param("b", "int32")], + returnTypeName: "int", + scalarFastPath: true, + ), + FFIProcMeta( + procName: "timer_name", + libName: "timer", + kind: FFIKind.FFI, + libTypeName: "Timer", + extraParams: @[], + returnTypeName: "string", + scalarFastPath: true, + ), + FFIProcMeta( + procName: "timer_ratio", + libName: "timer", + kind: FFIKind.FFI, + libTypeName: "Timer", + extraParams: @[param("enabled", "bool")], + returnTypeName: "float32", + scalarFastPath: true, + ), FFIProcMeta( procName: "timer_destroy", libName: "timer", @@ -106,6 +133,40 @@ suite "generateCAbiLibHeader": check "timer_ctx_version(" in header check "timer_ctx_destroy(" in header + test "scalar-fast-path methods take inline args, not a Req struct": + check "TimerAddReq" notin header + check "TimerNameReq" notin header + check "TimerRatioReq" notin header + check "typedef void (*TimerScalarRawFn)(int caller_ret, char* msg, size_t len, void* user_data);" in + header + check "int timer_add(void* ctx, TimerScalarRawFn callback, void* user_data, int64_t a, int32_t b);" in + header + check "int timer_name(void* ctx, TimerScalarRawFn callback, void* user_data);" in + header + + test "scalar-fast-path replies ride the same typed ReplyFn surface": + check "typedef void (*TimerAddReplyFn)(int err_code, const int64_t* reply," in header + check "typedef void (*TimerNameReplyFn)(int err_code, const char* reply," in header + check "typedef void (*TimerRatioReplyFn)(int err_code, const float* reply," in header + + test "scalar-fast-path wrappers box the callback and adapt the raw reply": + check "typedef struct { TimerAddReplyFn fn; void* user_data; } TimerAddScalarBox;" in + header + check "static void timer_add_scalar_reply(int caller_ret, char* msg, size_t len, void* ud)" in + header + check "timer_ctx_add(const TimerCtx* ctx, int64_t a, int32_t b, TimerAddReplyFn on_reply, void* user_data)" in + header + check "timer_ctx_name(const TimerCtx* ctx, TimerNameReplyFn on_reply, void* user_data)" in + header + check "return timer_add(ctx->ptr, timer_add_scalar_reply, box, a, b);" in header + + test "scalar returns unpack the 8-byte image; strings copy through the dup helper": + check "memcpy(&reply, &slot, sizeof(reply));" in header + check "float reply = (float)wide;" in header + check "char* reply = nimffi_abi_dup_cstr_n(msg ? msg : \"\", msg ? len : 0);" in + header + check "if (n == SIZE_MAX) return NULL;" in header + test "events are rejected (CBOR-only for now)": expect ValueError: discard generateCAbiLibHeader( diff --git a/tests/unit/test_scalar_skip_gen.nim b/tests/unit/test_scalar_skip_gen.nim index 156608f..8b853e8 100644 --- a/tests/unit/test_scalar_skip_gen.nim +++ b/tests/unit/test_scalar_skip_gen.nim @@ -1,6 +1,8 @@ -## Compiles fixtures/scalar_skip_fixture.nim (all-scalar `abi = c`) in a child -## `nim check` under `-d:ffiGenBindings`, asserting genBindings() fails loudly -## and that `-d:ffiAllowScalarSkip` downgrades the drop to a clean build. +## Asserts genBindings() fails loudly on a scalar `abi = c` proc no target can +## bind, and that -d:ffiAllowScalarSkip downgrades it to a clean build. +## +## The fixture compiles in a child `nim check` so its expected failure is a test +## assertion, not this file's own compile error. import std/[os, osproc, strutils, compilesettings] import unittest2