feat: scalar fast-path codegen

This commit is contained in:
Gabriel Cruz 2026-07-09 19:59:16 -03:00
parent 9ed1fedf96
commit 265f38208f
No known key found for this signature in database
GPG Key ID: 3C6977037D5A1EF5
11 changed files with 380 additions and 48 deletions

View File

@ -171,13 +171,17 @@ 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.
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).
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).
## Placement of `genBindings()`

View File

@ -34,14 +34,20 @@ 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 raw 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);
#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 +118,44 @@ 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 = (char*)malloc(len + 1);
if (em) {
if (len > 0) memcpy(em, msg, len);
em[len] = '\0';
}
fn(caller_ret, "", em ? em : "FFI call failed", user_data);
free(em);
return;
}
char* reply = (char*)malloc(len + 1);
if (!reply) {
fn(NIMFFI_RET_ERR, "", "out of memory", user_data);
return;
}
if (len > 0) memcpy(reply, msg, len);
reply[len] = '\0';
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 */

View File

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

View File

@ -946,7 +946,9 @@ 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:
# A scalar-fast-path proc has no Req envelope: its export takes the scalar
# args inline (see emitAbiScalarMethod).
if p.kind != FFIKind.DTOR and not p.scalarFastPath:
let rt = reqTypeMeta(p)
reg.typeTable[rt.name] = rt
return reg
@ -1010,6 +1012,20 @@ proc emitAbiReplyTypedefs(
" reply, const char* err_msg, void* user_data);"
)
func abiScalarRawFnName(libType: string): string =
## The `FFICallBack`-shaped raw callback typedef a scalar-fast-path export
## takes: the dylib replies with raw bytes (no CBOR, no flat struct) that the
## per-method trampoline converts into the typed reply.
return libType & "ScalarRawFn"
proc 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] = @[]
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 +1033,48 @@ 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 raw bytes (a")
lines.add(
" string 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);"
)
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 +1212,130 @@ proc emitAbiMethod(
lines.add("}")
lines.add("")
func abiScalarOkLines(m: FFIProcMeta, fnType: string): seq[string] =
## Trampoline RET_OK branch: convert the raw reply bytes into the typed
## reply. A string return rides as its own UTF-8 bytes (copied and
## NUL-terminated here); every other scalar is the 8-byte native-endian image
## of the Nim-side pack (signed ints sign-extended to 64 bits, floats widened
## to double, bool as 0/1 — see `ffiScalarRetBytes`).
let rt = m.returnTypeName.strip()
if rt == "string" or rt == "cstring":
return @[
" char* reply = (char*)malloc(len + 1);", " if (!reply) {",
" fn(NIMFFI_RET_ERR, \"\", \"out of memory\", user_data);",
" return;", " }", " if (len > 0) memcpy(reply, msg, len);",
" reply[len] = '\\0';", " 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,
) =
## A scalar-fast-path method: no Req struct crosses — the wrapper hands the
## scalar args straight to the raw export and a per-method trampoline adapts
## the raw-bytes reply into the same typed `ReplyFn` surface the flat-struct
## methods use. The heap box carrying the caller's callback is freed by the
## trampoline, which the dylib invokes exactly once on every path (the ctx
## guard and enqueue failures reply synchronously; success replies from the
## FFI thread).
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 = (char*)malloc(len + 1);")
lines.add(" if (em) {")
lines.add(" if (len > 0) memcpy(em, msg, len);")
lines.add(" em[len] = '\\0';")
lines.add(" }")
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 +1354,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 +1393,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"

View File

@ -32,8 +32,11 @@ type
returnIsHandle*: bool
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.
## 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`).
FFIFieldMeta* = object
name*: string

View File

@ -1519,8 +1519,9 @@ 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.
## 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).
var skipped: seq[string] = @[]
for p in procs:
if p.scalarFastPath:
@ -1535,9 +1536,11 @@ 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: " &
"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" &
" - 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."
@ -1595,12 +1598,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
# `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).
let genProcs =
if lang == "c_abi":
ffiProcRegistry
else:
bindableProcs(ffiProcRegistry)
if lang != "c_abi":
reportScalarFastPathDrops(ffiProcRegistry)
let outDir = bindingsOutputDir(lang, outputDir)
emitBindingsFor(
lang, genProcs, libName, outDir, bindingsSrcPath(outDir, nimSrcRelPath)

View File

@ -32,8 +32,11 @@ 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
## dropped (their inline-scalar export doesn't match the CBOR codegen shape).
## 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
## has scalar codegen and binds the full registry (see genBindings()).
var kept: seq[FFIProcMeta] = @[]
for p in procs:
if not p.scalarFastPath:

View File

@ -1,8 +1,12 @@
/* 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. */
/* End-to-end test for the CBOR-free `abi = c` echo bindings. Unlike the CBOR C
* backend, this header links no TinyCBOR: the flat structs in echo.h are the C
* ABI, strings are plain borrowed `const char*`. The test drives the same
* async, callback-per-call surface constructor, an object-returning method,
* a scalar-fast-path method and teardown copying out what each callback
* delivers (owned by the binding, valid only for the call) and polling a
* `done` flag to sequence the async calls. echoVersion rides the CBOR-free
* scalar fast path: its 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 <assert.h>
#include <stdatomic.h>
@ -85,9 +89,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;

View File

@ -1,6 +1,8 @@
## 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.
## 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.
import ffi, chronos
@ -18,7 +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: CBOR-free fast path, no foreign-binding codegen yet.
## All-scalar signature: dispatches through the CBOR-free fast path; only the
## `c_abi` target generates a foreign binding for it.
return ok(lib.base + a + b)
genBindings()

View File

@ -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,41 @@ 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 and NUL-terminate":
# int return: the slot is the sign-extended int64 image.
check "memcpy(&reply, &slot, sizeof(reply));" in header
# float32 return: packed as a widened double, narrowed back here.
check "float reply = (float)wide;" in header
# string return: raw UTF-8 bytes, not NUL-terminated on the wire.
check "reply[len] = '\\0';" in header
test "events are rejected (CBOR-only for now)":
expect ValueError:
discard generateCAbiLibHeader(

View File

@ -1,6 +1,13 @@
## 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.
## 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.
##
## 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
## assertion, not this file's own compile error.
import std/[os, osproc, strutils, compilesettings]
import unittest2
@ -10,14 +17,15 @@ const
nimExe = getCurrentCompilerExe()
ffiSearchPaths = querySettingSeq(searchPaths)
proc genFixture(extraDefs: seq[string]): tuple[output: string, exitCode: int] =
let outDir = getTempDir() / "ffi_scalar_skip_out"
proc genFixture(
lang: string, extraDefs: seq[string], outDir: string
): tuple[output: string, exitCode: int] =
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:ffiGenBindings -d:targetLang=" & lang)
cmd.add(" -d:ffiOutputDir=" & quoteShell(outDir))
for d in extraDefs:
cmd.add(" " & d)
@ -26,14 +34,26 @@ proc genFixture(extraDefs: seq[string]): tuple[output: string, exitCode: int] =
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(@[])
test "a CBOR target errors and names the dropped scalar proc":
let (output, code) = genFixture("c", @[], getTempDir() / "ffi_scalar_skip_out_c")
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(@["-d:ffiAllowScalarSkip"])
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")
check code == 0
check not output.contains("Error")