Skip to content

Commit 0d42636

Browse files
committed
chore: scalar fast path
1 parent 7e3fd96 commit 0d42636

4 files changed

Lines changed: 610 additions & 80 deletions

File tree

ffi/codegen/meta.nim

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ type
3131
returnIsPtr*: bool # true if return type is ptr T
3232
returnIsHandle*: bool # true if return type is an {.ffiHandle.} type
3333
abiFormat*: ABIFormat # wire format for this interaction (default Cbor)
34+
scalarFastPath*: bool
35+
## True for an `abi = c` proc whose whole signature is scalar (see
36+
## `isScalarOnly`): it dispatches through the CBOR-free scalar fast path
37+
## and is skipped by the foreign-binding generators (no dispatch codegen
38+
## yet — the request rides inline POD args, no `_CWire`, no CBOR).
3439

3540
FFIFieldMeta* = object
3641
name*: string # e.g. "delayMs"
@@ -62,6 +67,53 @@ var libraryDeclared* {.compileTime.}: bool = false
6267
# Library-wide default ABI, inherited by each annotation unless it overrides.
6368
var currentDefaultABIFormat* {.compileTime.}: ABIFormat = ABIFormat.Cbor
6469

70+
const scalarPodTypeNames = [
71+
"int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32",
72+
"uint64", "byte", "float", "float32", "float64", "bool",
73+
]
74+
## Fixed-width POD scalars that fit a single `uint64` slot and survive the
75+
## async hop by value — the payload the scalar fast path inlines into the
76+
## request (no heap copy). `cstring`/`string` are intentionally absent as
77+
## *params*: they are pointers to caller memory the FFI thread reads later,
78+
## so they'd need a copy, defeating the zero-alloc promise.
79+
80+
func isScalarParamTypeName*(name: string): bool =
81+
## A param type eligible for the CBOR-free scalar fast path.
82+
name in scalarPodTypeNames
83+
84+
func isScalarReturnTypeName*(name: string): bool =
85+
## A return type eligible for the scalar fast path. Unlike params, a
86+
## `string`/`cstring` return is fine: the handler produces the bytes and they
87+
## ride back raw (like the error path), so no caller memory is aliased.
88+
name in scalarPodTypeNames or name == "string" or name == "cstring"
89+
90+
func isScalarOnly*(p: FFIProcMeta): bool =
91+
## True iff `p` is a plain `{.ffi.}` method whose every wire param and return
92+
## is scalar — the whole signature crosses without CBOR or `_CWire`. Handles
93+
## and raw pointers are excluded (a handle needs a ctx-registry round-trip;
94+
## a pointer never crosses). Pure over the compile-time metadata.
95+
if p.kind != FFIKind.FFI:
96+
return false
97+
if p.returnIsPtr or p.returnIsHandle:
98+
return false
99+
if not isScalarReturnTypeName(p.returnTypeName):
100+
return false
101+
for ep in p.extraParams:
102+
if ep.isPtr or ep.isHandle or not isScalarParamTypeName(ep.typeName):
103+
return false
104+
true
105+
106+
func bindableProcs*(procs: seq[FFIProcMeta]): seq[FFIProcMeta] =
107+
## The procs the foreign-binding generators emit for. Scalar-fast-path procs
108+
## are dropped: their C export takes inline scalar args, not the CBOR
109+
## `(reqCbor, reqCborLen)` shape the current codegen assumes, so emitting a
110+
## CBOR caller for them would be wrong. Foreign codegen is a follow-up.
111+
var kept: seq[FFIProcMeta] = @[]
112+
for p in procs:
113+
if not p.scalarFastPath:
114+
kept.add(p)
115+
kept
116+
65117
proc abiCodegenImplemented*(fmt: ABIFormat): bool =
66118
## Whether `fmt` has a working proc-dispatch path. Only `Cbor` does today; the
67119
## seam a future PR flips once the `c` dispatch path is wired.

ffi/ffi_thread_request.nim

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,53 @@ const EmptyErrorMarker = "unknown error"
2020
## the callback's msg ptr non-nil and gives the foreign side a recognizable
2121
## fallback to log.
2222

23+
const MaxScalarArgs* = 8
24+
## Inline capacity for the scalar fast path. A `.ffi.` method with more than
25+
## this many scalar params can't use the fast path (checked at compile time).
26+
2327
type FFIThreadRequest* = object
2428
callback*: FFICallBack
2529
userData*: pointer
2630
reqId*: cstring ## Per-proc Req type name used to look up the handler.
2731
data*: ptr UncheckedArray[byte] ## Owned CBOR-encoded request payload.
2832
dataLen*: int
33+
isScalar*: bool
34+
## Set by `initScalar`: the payload rode inline in `scalarArgs` (no CBOR,
35+
## no `data` buffer), so `deleteRequest` has nothing extra to free.
36+
scalarArgs*: array[MaxScalarArgs, uint64]
37+
## Scalar-fast-path args inlined in the envelope (one `ffiPackScalar` value
38+
## per slot) so there's no per-call `c_malloc`. A plain array rather than a
39+
## `union` with `data`: costs a fixed 64 bytes per request but keeps the
40+
## `next` link and `deleteRequest` unaliased and branch-free.
2941
next*: ptr FFIThreadRequest
3042
## Intrusive ingress-queue link (see `ffi_request_queue.nim`). Touched only
3143
## under the queue's lock; the request doubles as its own node, so no
3244
## separate node alloc lands on the per-thread ORC MemRegion.
3345

46+
func ffiPackScalar*[T](x: T): uint64 =
47+
## Bit-cast one scalar into a `uint64` request slot. Signed ints sign-extend
48+
## to 64 bits; `float32` widens to `float64` (exactly representable, so the
49+
## value round-trips); `bool` becomes 0/1. Reverse with `ffiUnpackScalar`.
50+
when T is SomeFloat:
51+
cast[uint64](float64(x))
52+
elif T is bool:
53+
uint64(ord(x))
54+
elif T is SomeSignedInt:
55+
cast[uint64](int64(x))
56+
else:
57+
uint64(x)
58+
59+
func ffiUnpackScalar*[T](u: uint64, _: typedesc[T]): T =
60+
## Inverse of `ffiPackScalar`: reinterpret a request slot back into `T`.
61+
when T is SomeFloat:
62+
T(cast[float64](u))
63+
elif T is bool:
64+
u != 0'u64
65+
elif T is SomeSignedInt:
66+
T(cast[int64](u))
67+
else:
68+
T(u)
69+
3470
proc allocBaseRequest(
3571
callback: FFICallBack, userData: pointer, reqId: cstring
3672
): ptr FFIThreadRequest =
@@ -43,6 +79,7 @@ proc allocBaseRequest(
4379
ret[].reqId = reqId.alloc()
4480
ret[].data = nil
4581
ret[].dataLen = 0
82+
ret[].isScalar = false
4683
ret[].next = nil
4784
return ret
4885

@@ -118,6 +155,49 @@ proc initFromOwnedShared*(
118155
adoptOwnedSharedPayload(ret, data, dataLen)
119156
return ret
120157

158+
proc initScalar*(
159+
T: typedesc[FFIThreadRequest],
160+
callback: FFICallBack,
161+
userData: pointer,
162+
reqId: cstring,
163+
args: varargs[uint64],
164+
): ptr type T =
165+
## Builds a scalar-fast-path request: the packed scalar args ride inline in
166+
## `scalarArgs` with no payload `c_malloc`. `args` come from `ffiPackScalar`.
167+
## Only the routing `reqId` cstring is heap-allocated, same as the CBOR path.
168+
doAssert args.len <= MaxScalarArgs,
169+
"initScalar: " & $args.len & " scalar args exceed MaxScalarArgs (" & $MaxScalarArgs &
170+
")"
171+
var ret = allocBaseRequest(callback, userData, reqId)
172+
ret[].isScalar = true
173+
for i in 0 ..< args.len:
174+
ret[].scalarArgs[i] = args[i]
175+
ret
176+
177+
func ffiScalarRetBytes*[T](x: T): seq[byte] =
178+
## Serializes a scalar handler result into the raw response payload the
179+
## callback carries — no CBOR envelope. A `string`/`cstring` rides as its
180+
## own UTF-8 bytes (like the error path); every other scalar rides as the
181+
## 8-byte native-endian image of `ffiPackScalar(x)`. Note: an empty string
182+
## yields a 0-length payload, which `handleRes` sends as the CBOR-null
183+
## sentinel — the foreign scalar reader (a follow-up) must special-case it.
184+
when T is string:
185+
var b = newSeq[byte](x.len)
186+
if x.len > 0:
187+
copyMem(addr b[0], unsafeAddr x[0], x.len)
188+
b
189+
elif T is cstring:
190+
let n = x.len
191+
var b = newSeq[byte](n)
192+
if n > 0:
193+
copyMem(addr b[0], cast[pointer](x), n)
194+
b
195+
else:
196+
let u = ffiPackScalar(x)
197+
var b = newSeq[byte](sizeof(uint64))
198+
copyMem(addr b[0], unsafeAddr u, sizeof(uint64))
199+
b
200+
121201
proc deleteRequest*(request: ptr FFIThreadRequest) =
122202
if not request[].data.isNil:
123203
c_free(request[].data)

0 commit comments

Comments
 (0)