Skip to content

Commit d0ed85c

Browse files
committed
feat: scalar fast-path codegen
1 parent d99df88 commit d0ed85c

11 files changed

Lines changed: 372 additions & 59 deletions

File tree

README.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,17 @@ The default wire format is `cbor`. Override the library default with
114114
An `abi = c` proc whose whole signature is scalar — fixed-width integer, float,
115115
or bool params (a `string` return is fine, a `string` param is not) and no
116116
structs, handles, or pointers — dispatches through a CBOR-free scalar fast path.
117-
Foreign-binding codegen for that shape isn't implemented yet, so
118-
under `-d:ffiGenBindings` such a proc would be omitted from the generated
119-
bindings — and `genBindings()` fails with an error naming the affected procs.
120-
Resolve it by switching the proc to `abi = cbor`, adding a non-scalar param so it
121-
takes the CBOR wire shape, or passing `-d:ffiAllowScalarSkip` to accept the
122-
omission (the proc still works over the scalar fast path; it's just absent from
123-
the generated foreign bindings).
117+
The `-d:targetLang=c_abi` generator emits real bindings for that shape: the
118+
wrapper passes the scalar args inline (no request struct) and adapts the
119+
raw-bytes reply into the same typed callback surface the flat-struct methods
120+
use. The CBOR-speaking targets (`c`, `cpp`, `rust`, `cddl`) have no scalar
121+
codegen, so under `-d:ffiGenBindings` they would omit such a proc from the
122+
generated bindings — and `genBindings()` fails with an error naming the
123+
affected procs. Resolve it by generating with `-d:targetLang=c_abi`, switching
124+
the proc to `abi = cbor`, adding a non-scalar param so it takes the CBOR wire
125+
shape, or passing `-d:ffiAllowScalarSkip` to accept the omission (the proc
126+
still works over the scalar fast path; it's just absent from the generated
127+
foreign bindings).
124128

125129
## Placement of `genBindings()`
126130

examples/echo/c_abi_bindings/echo.h

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,20 @@ typedef struct {
3030
} EchoShoutReq;
3131

3232
typedef void (*EchoShoutReplyFn)(int err_code, const ShoutResponse* reply, const char* err_msg, void* user_data);
33+
typedef void (*EchoVersionReplyFn)(int err_code, const char* reply, const char* err_msg, void* user_data);
3334

3435
typedef void (*EchoCreateRawFn)(int err_code, const char* ctx_addr, const char* err_msg, void* user_data);
36+
/* Raw reply of a scalar-fast-path export: `msg`/`len` are raw bytes (a
37+
string return's UTF-8, or the 8-byte native-endian scalar image), not
38+
NUL-terminated and valid only for the duration of the call. */
39+
typedef void (*EchoScalarRawFn)(int caller_ret, char* msg, size_t len, void* user_data);
3540
#ifdef __cplusplus
3641
extern "C" {
3742
#endif
3843

3944
void* echo_create(const EchoCreateCtorReq* req, EchoCreateRawFn on_created, void* user_data);
4045
int echo_shout(void* ctx, EchoShoutReplyFn on_reply, void* user_data, const EchoShoutReq* req);
46+
int echo_version(void* ctx, EchoScalarRawFn callback, void* user_data);
4147
int echo_destroy(void* ctx);
4248

4349
#ifdef __cplusplus
@@ -107,4 +113,44 @@ static inline int echo_ctx_shout(const EchoCtx* ctx, const ShoutRequest* req, Ec
107113
return echo_shout(ctx->ptr, on_reply, user_data, &ffi_req);
108114
}
109115

116+
typedef struct { EchoVersionReplyFn fn; void* user_data; } EchoVersionScalarBox;
117+
static void echo_version_scalar_reply(int caller_ret, char* msg, size_t len, void* ud) {
118+
EchoVersionScalarBox* box = (EchoVersionScalarBox*)ud;
119+
if (!box) return;
120+
EchoVersionReplyFn fn = box->fn;
121+
void* user_data = box->user_data;
122+
free(box);
123+
if (!fn) return;
124+
if (caller_ret != NIMFFI_RET_OK) {
125+
char* em = (char*)malloc(len + 1);
126+
if (em) {
127+
if (len > 0) memcpy(em, msg, len);
128+
em[len] = '\0';
129+
}
130+
fn(caller_ret, "", em ? em : "FFI call failed", user_data);
131+
free(em);
132+
return;
133+
}
134+
char* reply = (char*)malloc(len + 1);
135+
if (!reply) {
136+
fn(NIMFFI_RET_ERR, "", "out of memory", user_data);
137+
return;
138+
}
139+
if (len > 0) memcpy(reply, msg, len);
140+
reply[len] = '\0';
141+
fn(NIMFFI_RET_OK, reply, "", user_data);
142+
free(reply);
143+
}
144+
145+
static inline int echo_ctx_version(const EchoCtx* ctx, EchoVersionReplyFn on_reply, void* user_data) {
146+
EchoVersionScalarBox* box = (EchoVersionScalarBox*)malloc(sizeof(EchoVersionScalarBox));
147+
if (!box) {
148+
if (on_reply) on_reply(-1, "", "out of memory", user_data);
149+
return -1;
150+
}
151+
box->fn = on_reply;
152+
box->user_data = user_data;
153+
return echo_version(ctx->ptr, echo_version_scalar_reply, box);
154+
}
155+
110156
#endif /* NIM_FFI_LIB_ECHO_C_ABI_H_INCLUDED */

ffi.nimble

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -259,15 +259,12 @@ task genbindings_c_echo, "Generate C bindings for the echo example":
259259
" -d:ffiSrcPath=../echo.nim" & " -o:/dev/null examples/echo/echo.nim"
260260

261261
task genbindings_c_abi_echo, "Generate CBOR-free abi=c C bindings for the echo example":
262-
# echoVersion is all-scalar under the abi=c default, so it has no foreign
263-
# codegen yet and is omitted from the bindings; -d:ffiAllowScalarSkip accepts
264-
# that omission instead of failing the build (see genBindings()).
265262
exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libecho" &
266-
" -d:ffiEchoAbiC -d:ffiGenBindings -d:targetLang=c_abi -d:ffiAllowScalarSkip" &
263+
" -d:ffiEchoAbiC -d:ffiGenBindings -d:targetLang=c_abi" &
267264
" -d:ffiOutputDir=examples/echo/c_abi_bindings" & " -d:ffiSrcPath=../echo.nim" &
268265
" -o:/dev/null examples/echo/echo.nim"
269266
exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libecho" &
270-
" -d:ffiEchoAbiC -d:ffiGenBindings -d:targetLang=c_abi -d:ffiAllowScalarSkip" &
267+
" -d:ffiEchoAbiC -d:ffiGenBindings -d:targetLang=c_abi" &
271268
" -d:ffiOutputDir=examples/echo/c_abi_bindings" & " -d:ffiSrcPath=../echo.nim" &
272269
" -o:/dev/null examples/echo/echo.nim"
273270

ffi/codegen/c_abi.nim

Lines changed: 171 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,9 @@ proc newAbiReg(types: seq[FFITypeMeta], procs: seq[FFIProcMeta]): AbiReg =
131131
for t in types:
132132
reg.typeTable[t.name] = t
133133
for p in procs:
134-
if p.kind != FFIKind.DTOR:
134+
# A scalar-fast-path proc has no Req envelope: its export takes the scalar
135+
# args inline (see emitScalarMethod).
136+
if p.kind != FFIKind.DTOR and not p.scalarFastPath:
135137
let rt = reqTypeMeta(p)
136138
reg.typeTable[rt.name] = rt
137139
reg
@@ -200,38 +202,69 @@ proc emitReplyTypedefs(
200202
" reply, const char* err_msg, void* user_data);"
201203
)
202204

205+
func scalarRawFnName(libType: string): string =
206+
## The `FFICallBack`-shaped raw callback typedef a scalar-fast-path export
207+
## takes: the dylib replies with raw bytes (no CBOR, no flat struct) that the
208+
## per-method trampoline converts into the typed reply.
209+
libType & "ScalarRawFn"
210+
211+
proc scalarArgParams(m: FFIProcMeta): seq[string] =
212+
## C parameters for a scalar method's args — passed inline by value, in both
213+
## the raw export and the high-level wrapper (no Req struct).
214+
var params: seq[string] = @[]
215+
for ep in m.extraParams:
216+
params.add(leafCTypeAbi(ep.typeName.strip()).cType & " " & ep.name)
217+
params
218+
203219
proc emitExternDecls(
204220
lines: var seq[string],
205221
reg: var AbiReg,
206222
libName, libType: string,
207223
procs: seq[FFIProcMeta],
208224
) =
209225
let createRawFn = libType & "CreateRawFn"
210-
var haveCtor = false
226+
var haveCtor, haveScalar = false
211227
for p in procs:
212228
if p.kind == FFIKind.CTOR:
213229
haveCtor = true
230+
if p.scalarFastPath:
231+
haveScalar = true
214232
if haveCtor:
215233
lines.add(
216234
"typedef void (*" & createRawFn &
217235
")(int err_code, const char* ctx_addr, const char* err_msg, void* user_data);"
218236
)
237+
if haveScalar:
238+
lines.add("/* Raw reply of a scalar-fast-path export: `msg`/`len` are raw bytes (a")
239+
lines.add(
240+
" string return's UTF-8, or the 8-byte native-endian scalar image), not"
241+
)
242+
lines.add(" NUL-terminated and valid only for the duration of the call. */")
243+
lines.add(
244+
"typedef void (*" & scalarRawFnName(libType) &
245+
")(int caller_ret, char* msg, size_t len, void* user_data);"
246+
)
219247
lines.add("#ifdef __cplusplus")
220248
lines.add("extern \"C\" {")
221249
lines.add("#endif")
222250
lines.add("")
223251
for p in procs:
224-
let reqStruct = reqStructName(p)
225252
case p.kind
226253
of FFIKind.FFI:
227-
let info = methodReplyInfo(reg, libType, p)
228-
lines.add(
229-
"int " & p.procName & "(void* ctx, " & info.fnType &
230-
" on_reply, void* user_data, const " & reqStruct & "* req);"
231-
)
254+
if p.scalarFastPath:
255+
var params =
256+
@["void* ctx", scalarRawFnName(libType) & " callback", "void* user_data"]
257+
params.add(scalarArgParams(p))
258+
lines.add("int " & p.procName & "(" & params.join(", ") & ");")
259+
else:
260+
let info = methodReplyInfo(reg, libType, p)
261+
lines.add(
262+
"int " & p.procName & "(void* ctx, " & info.fnType &
263+
" on_reply, void* user_data, const " & reqStructName(p) & "* req);"
264+
)
232265
of FFIKind.CTOR:
233266
lines.add(
234-
"void* " & p.procName & "(const " & reqStruct & "* req, " & createRawFn &
267+
"void* " & p.procName & "(const " & reqStructName(p) & "* req, " & createRawFn &
235268
" on_created, void* user_data);"
236269
)
237270
of FFIKind.DTOR:
@@ -368,6 +401,130 @@ proc emitMethod(
368401
lines.add("}")
369402
lines.add("")
370403

404+
func scalarOkLines(m: FFIProcMeta, fnType: string): seq[string] =
405+
## Trampoline RET_OK branch: convert the raw reply bytes into the typed
406+
## reply. A string return rides as its own UTF-8 bytes (copied and
407+
## NUL-terminated here); every other scalar is the 8-byte native-endian image
408+
## of the Nim-side pack (signed ints sign-extended to 64 bits, floats widened
409+
## to double, bool as 0/1 — see `ffiScalarRetBytes`).
410+
let rt = m.returnTypeName.strip()
411+
if rt == "string" or rt == "cstring":
412+
return @[
413+
" char* reply = (char*)malloc(len + 1);", " if (!reply) {",
414+
" fn(NIMFFI_RET_ERR, \"\", \"out of memory\", user_data);",
415+
" return;", " }", " if (len > 0) memcpy(reply, msg, len);",
416+
" reply[len] = '\\0';", " fn(NIMFFI_RET_OK, reply, \"\", user_data);",
417+
" free(reply);",
418+
]
419+
var lines = @[
420+
" uint64_t slot = 0;", " if (!msg || len != sizeof(slot)) {",
421+
" fn(NIMFFI_RET_ERR, NULL, \"scalar reply: unexpected payload size\", user_data);",
422+
" return;", " }", " memcpy(&slot, msg, sizeof(slot));",
423+
]
424+
let cType = leafCTypeAbi(rt).cType
425+
case rt
426+
of "int", "int64":
427+
lines.add(" int64_t reply;")
428+
lines.add(" memcpy(&reply, &slot, sizeof(reply));")
429+
of "int8", "int16", "int32":
430+
lines.add(" int64_t wide;")
431+
lines.add(" memcpy(&wide, &slot, sizeof(wide));")
432+
lines.add(" " & cType & " reply = (" & cType & ")wide;")
433+
of "uint", "uint64":
434+
lines.add(" uint64_t reply = slot;")
435+
of "uint8", "uint16", "uint32", "byte":
436+
lines.add(" " & cType & " reply = (" & cType & ")slot;")
437+
of "bool":
438+
lines.add(" bool reply = slot != 0;")
439+
of "float", "float64":
440+
lines.add(" double reply;")
441+
lines.add(" memcpy(&reply, &slot, sizeof(reply));")
442+
of "float32":
443+
lines.add(" double wide;")
444+
lines.add(" memcpy(&wide, &slot, sizeof(wide));")
445+
lines.add(" float reply = (float)wide;")
446+
else:
447+
raise newException(
448+
ValueError, "abi = c: unexpected scalar-fast-path return type: " & rt
449+
)
450+
lines.add(" fn(NIMFFI_RET_OK, &reply, \"\", user_data);")
451+
lines
452+
453+
proc emitScalarMethod(
454+
lines: var seq[string],
455+
reg: var AbiReg,
456+
ctxType, libName, libType: string,
457+
m: FFIProcMeta,
458+
) =
459+
## A scalar-fast-path method: no Req struct crosses — the wrapper hands the
460+
## scalar args straight to the raw export and a per-method trampoline adapts
461+
## the raw-bytes reply into the same typed `ReplyFn` surface the flat-struct
462+
## methods use. The heap box carrying the caller's callback is freed by the
463+
## trampoline, which the dylib invokes exactly once on every path (the ctx
464+
## guard and enqueue failures reply synchronously; success replies from the
465+
## FFI thread).
466+
let stripped = stripLibPrefix(m.procName, m.libName)
467+
let pascal = snakeToPascalCase(stripped)
468+
let info = methodReplyInfo(reg, libType, m)
469+
let boxType = libType & pascal & "ScalarBox"
470+
let tramp = m.procName & "_scalar_reply"
471+
let isStr = m.returnTypeName.strip() in ["string", "cstring"]
472+
let errReply = if isStr: "\"\"" else: "NULL"
473+
lines.add(
474+
"typedef struct { " & info.fnType & " fn; void* user_data; } " & boxType & ";"
475+
)
476+
lines.add(
477+
"static void " & tramp & "(int caller_ret, char* msg, size_t len, void* ud) {"
478+
)
479+
lines.add(" " & boxType & "* box = (" & boxType & "*)ud;")
480+
lines.add(" if (!box) return;")
481+
lines.add(" " & info.fnType & " fn = box->fn;")
482+
lines.add(" void* user_data = box->user_data;")
483+
lines.add(" free(box);")
484+
lines.add(" if (!fn) return;")
485+
lines.add(" if (caller_ret != NIMFFI_RET_OK) {")
486+
lines.add(" char* em = (char*)malloc(len + 1);")
487+
lines.add(" if (em) {")
488+
lines.add(" if (len > 0) memcpy(em, msg, len);")
489+
lines.add(" em[len] = '\\0';")
490+
lines.add(" }")
491+
lines.add(
492+
" fn(caller_ret, " & errReply & ", em ? em : \"FFI call failed\", user_data);"
493+
)
494+
lines.add(" free(em);")
495+
lines.add(" return;")
496+
lines.add(" }")
497+
for l in scalarOkLines(m, info.fnType):
498+
lines.add(l)
499+
lines.add("}")
500+
lines.add("")
501+
let params = scalarArgParams(m)
502+
let head =
503+
"static inline int " & libName & "_ctx_" & stripped & "(const " & ctxType & "* ctx, "
504+
let sig =
505+
if params.len > 0:
506+
head & params.join(", ") & ", " & info.fnType & " on_reply, void* user_data) {"
507+
else:
508+
head & info.fnType & " on_reply, void* user_data) {"
509+
lines.add(sig)
510+
lines.add(
511+
" " & boxType & "* box = (" & boxType & "*)malloc(sizeof(" & boxType & "));"
512+
)
513+
lines.add(" if (!box) {")
514+
lines.add(
515+
" if (on_reply) on_reply(-1, " & errReply & ", \"out of memory\", user_data);"
516+
)
517+
lines.add(" return -1;")
518+
lines.add(" }")
519+
lines.add(" box->fn = on_reply;")
520+
lines.add(" box->user_data = user_data;")
521+
var callArgs = @["ctx->ptr", tramp, "box"]
522+
for ep in m.extraParams:
523+
callArgs.add(ep.name)
524+
lines.add(" return " & m.procName & "(" & callArgs.join(", ") & ");")
525+
lines.add("}")
526+
lines.add("")
527+
371528
proc generateCAbiLibHeader*(
372529
procs: seq[FFIProcMeta],
373530
types: seq[FFITypeMeta],
@@ -386,7 +543,7 @@ proc generateCAbiLibHeader*(
386543
for t in types:
387544
ensureAbiStruct(reg, t.name)
388545
for p in procs:
389-
if p.kind != FFIKind.DTOR:
546+
if p.kind != FFIKind.DTOR and not p.scalarFastPath:
390547
ensureAbiStruct(reg, reqStructName(p))
391548

392549
let guard = "NIM_FFI_LIB_" & libName.toUpperAscii() & "_C_ABI_H_INCLUDED"
@@ -417,7 +574,10 @@ proc generateCAbiLibHeader*(
417574
emitCtxAndCtor(lines, reg, libName, libType, ctxType, classified.ctors)
418575
emitDestructor(lines, ctxType, libName, classified.dtorProcName)
419576
for m in classified.methods:
420-
emitMethod(lines, reg, ctxType, libName, libType, m)
577+
if m.scalarFastPath:
578+
emitScalarMethod(lines, reg, ctxType, libName, libType, m)
579+
else:
580+
emitMethod(lines, reg, ctxType, libName, libType, m)
421581

422582
lines.add("#endif /* " & guard & " */")
423583
lines.join("\n") & "\n"

ffi/codegen/meta.nim

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ type
3333
abiFormat*: ABIFormat # wire format for this interaction (default Cbor)
3434
scalarFastPath*: bool
3535
## True for an `abi = c` proc whose whole signature is scalar (see
36-
## `isScalarOnly`): dispatches through the CBOR-free scalar fast path and
37-
## is skipped by the foreign-binding generators (their codegen is a
38-
## follow-up).
36+
## `isScalarOnly`): dispatches through the CBOR-free scalar fast path.
37+
## Only the `c_abi` generator emits foreign bindings for it (inline
38+
## scalar args + raw-bytes reply trampoline); the CBOR-speaking
39+
## generators drop it (see `bindableProcs`).
3940

4041
FFIFieldMeta* = object
4142
name*: string # e.g. "delayMs"

0 commit comments

Comments
 (0)