mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-07-24 08:43:16 +00:00
feat(codegen): scalar-fast-path bindings for the abi = c header (#124)
This commit is contained in:
parent
9ed1fedf96
commit
e60f771be6
29
README.md
29
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()`
|
||||
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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 <assert.h>
|
||||
#include <stdatomic.h>
|
||||
@ -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;
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user