@@ -30,11 +30,14 @@ type FFIThreadRequest* = object
3030 reqId* : cstring # # Per-proc Req type name used to look up the handler.
3131 data* : ptr UncheckedArray [byte ] # # Owned CBOR-encoded request payload.
3232 dataLen* : int
33+ isScalar* : bool
34+ # # Set by `initScalar`: the payload rode inline in `scalarArgs` (no CBOR,
35+ # # no `data` buffer). Lets `handleRes` tell a scalar 0-length return (a real
36+ # # empty string) from a CBOR "no value".
3337 scalarArgs* : array [MaxScalarArgs , uint64 ]
34- # # Scalar-fast-path args inlined in the envelope (one `ffiPackScalar` value
35- # # per slot) so there's no per-call `c_malloc`. A plain array rather than a
36- # # `union` with `data`: costs a fixed 64 bytes per request but keeps the
37- # # `next` link and `deleteRequest` unaliased and branch-free.
38+ # # Scalar-fast-path args inlined in the envelope so there's no per-call
39+ # # `c_malloc`. A plain array rather than a `union` with `data`: costs a fixed
40+ # # 64 bytes per request but keeps `deleteRequest` unaliased and branch-free.
3841 next* : ptr FFIThreadRequest
3942 # # Intrusive ingress-queue link (see `ffi_request_queue.nim`). Touched only
4043 # # under the queue's lock; the request doubles as its own node, so no
@@ -79,6 +82,7 @@ proc allocBaseRequest(
7982 ret[].reqId = reqId.alloc ()
8083 ret[].data = nil
8184 ret[].dataLen = 0
85+ ret[].isScalar = false
8286 ret[].next = nil
8387 ret[].responded = false
8488 return ret
@@ -169,17 +173,17 @@ proc initScalar*(
169173 " initScalar: " & $ args.len & " scalar args exceed MaxScalarArgs (" & $ MaxScalarArgs &
170174 " )"
171175 var ret = allocBaseRequest (callback, userData, reqId)
176+ ret[].isScalar = true
172177 for i in 0 ..< args.len:
173178 ret[].scalarArgs[i] = args[i]
174179 ret
175180
176181func ffiScalarRetBytes * [T](x: T): seq [byte ] =
177- # # Serializes a scalar handler result into the raw response payload the
178- # # callback carries — no CBOR envelope. A `string`/`cstring` rides as its
179- # # own UTF-8 bytes (like the error path); every other scalar rides as the
180- # # 8-byte native-endian image of `ffiPackScalar(x)`. Note: an empty string
181- # # yields a 0-length payload, which `handleRes` sends as the CBOR-null
182- # # sentinel — the foreign scalar reader (a follow-up) must special-case it.
182+ # # Serializes a scalar handler result into the raw response payload — no CBOR
183+ # # envelope. A `string`/`cstring` rides as its own UTF-8 bytes (like the error
184+ # # path); every other scalar rides as the 8-byte native-endian image of
185+ # # `ffiPackScalar(x)`. An empty string yields a 0-length payload (see
186+ # # `handleRes`, which delivers it as `""` rather than the CBOR-null sentinel).
183187 when T is string :
184188 var b = newSeq [byte ](x.len)
185189 if x.len > 0 :
@@ -231,6 +235,15 @@ proc fireCallback*(res: Result[seq[byte], string], request: ptr FFIThreadRequest
231235 cast [csize_t ](bytes.len),
232236 request[].userData,
233237 )
238+ elif request[].isScalar:
239+ # `isScalar` marks a scalar-fast-path request (args rode inline in
240+ # `scalarArgs`, no CBOR): its result bytes come from `ffiScalarRetBytes`,
241+ # not a CBOR encoder. So a 0-byte return is a real empty string, not
242+ # "no value" — hand back a genuine empty buffer, not the CBOR-null sentinel.
243+ var empty: byte
244+ request[].callback (
245+ RET_OK , cast [ptr cchar ](addr empty), 0 .csize_t , request[].userData
246+ )
234247 else :
235248 # Always hand the callback a real buffer; CBOR null marks "no value".
236249 var sentinel = CborNullByte
0 commit comments