mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-22 01:10:03 +00:00
Increment 3: the {.ffiHost.} pragma. A bodyless
proc fetchToken(key: string): Future[Result[string, string]] {.ffiHost.}
expands into an async proc that resolves the thread-local host registry +
pending table, looks the fn up by snake_case wire name, allocates a token,
invokes the host with the raw request bytes, and awaits the answer.
This is the inverse of {.ffi.} and the first end-to-end use of the registry
(increment 1) + completion bridge (increment 2). First slice is deliberately
narrow — raw ABI, one string param, Future[Result[string, string]] — to prove
the round-trip with zero serialization; struct params/returns and the
{.ffiHost: cbor.} format arg are follow-ups.
The body reads two new threadvars (ffiCurrentHostRegistry / ffiCurrentPendingTable)
set by ffiThreadBody alongside ffiCurrentEventRegistry, so the user's signature
stays ctx-free. The host fn is invoked synchronously before the await, while the
string arg is still alive (honouring the "req valid only for the call" contract).
5 macro tests pass under orc+refc; host + ffi_context suites stay green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
102 lines
3.3 KiB
Nim
102 lines
3.3 KiB
Nim
## Unit tests for the `{.ffiHost.}` macro (roadmap #1, increment 3) — the
|
|
## generated proc that dispatches to a host-registered function and awaits the
|
|
## answer over the raw (zero-serialization) ABI.
|
|
##
|
|
## These drive the generated proc directly with a synchronous "host": the
|
|
## registered fn completes the pending future inline (its userData carries the
|
|
## pending table), so the await resolves without the full FFI thread. The
|
|
## cross-thread bridge is covered separately by the FFIContext wiring.
|
|
|
|
import std/strutils
|
|
import unittest2
|
|
import chronos
|
|
import ffi
|
|
|
|
# A {.ffiHost.} declaration: the host implements `echoHost`, Nim awaits it.
|
|
proc echoHost(s: string): Future[Result[string, string]] {.ffiHost.}
|
|
|
|
# Synchronous host impls. `userData` carries the pending table so the fn can
|
|
# resolve the token inline (a real host answers later via <lib>_host_complete).
|
|
proc echoFn(
|
|
token: uint64, req: ptr cchar, reqLen: csize_t, userData: pointer
|
|
) {.cdecl, gcsafe, raises: [].} =
|
|
let tbl = cast[ptr FFIPendingTable](userData)
|
|
var b = newSeq[byte](int(reqLen))
|
|
if reqLen > 0'u:
|
|
copyMem(addr b[0], req, int(reqLen))
|
|
discard completePending(tbl[], token, okResult(b))
|
|
|
|
proc failFn(
|
|
token: uint64, req: ptr cchar, reqLen: csize_t, userData: pointer
|
|
) {.cdecl, gcsafe, raises: [].} =
|
|
let tbl = cast[ptr FFIPendingTable](userData)
|
|
discard completePending(tbl[], token, errResult("host said no"))
|
|
|
|
suite "ffiHost macro":
|
|
test "round-trips the value through the registered host fn":
|
|
var reg: FFIHostRegistry
|
|
var tbl: FFIPendingTable
|
|
initHostRegistry(reg)
|
|
initPendingTable(tbl)
|
|
defer:
|
|
deinitHostRegistry(reg)
|
|
deinitPendingTable(tbl)
|
|
ffiCurrentHostRegistry = addr reg
|
|
ffiCurrentPendingTable = addr tbl
|
|
check registerHostFn(reg, "echo_host", echoFn, addr tbl)
|
|
|
|
let r = waitFor echoHost("hello host")
|
|
check r.isOk
|
|
check r.get == "hello host"
|
|
|
|
test "empty argument is handled":
|
|
var reg: FFIHostRegistry
|
|
var tbl: FFIPendingTable
|
|
initHostRegistry(reg)
|
|
initPendingTable(tbl)
|
|
defer:
|
|
deinitHostRegistry(reg)
|
|
deinitPendingTable(tbl)
|
|
ffiCurrentHostRegistry = addr reg
|
|
ffiCurrentPendingTable = addr tbl
|
|
discard registerHostFn(reg, "echo_host", echoFn, addr tbl)
|
|
let r = waitFor echoHost("")
|
|
check r.isOk
|
|
check r.get == ""
|
|
|
|
test "unregistered host fn yields an error":
|
|
var reg: FFIHostRegistry
|
|
var tbl: FFIPendingTable
|
|
initHostRegistry(reg)
|
|
initPendingTable(tbl)
|
|
defer:
|
|
deinitHostRegistry(reg)
|
|
deinitPendingTable(tbl)
|
|
ffiCurrentHostRegistry = addr reg
|
|
ffiCurrentPendingTable = addr tbl
|
|
let r = waitFor echoHost("x")
|
|
check r.isErr
|
|
check "not registered" in r.error
|
|
|
|
test "host-reported error propagates as the Result error":
|
|
var reg: FFIHostRegistry
|
|
var tbl: FFIPendingTable
|
|
initHostRegistry(reg)
|
|
initPendingTable(tbl)
|
|
defer:
|
|
deinitHostRegistry(reg)
|
|
deinitPendingTable(tbl)
|
|
ffiCurrentHostRegistry = addr reg
|
|
ffiCurrentPendingTable = addr tbl
|
|
discard registerHostFn(reg, "echo_host", failFn, addr tbl)
|
|
let r = waitFor echoHost("x")
|
|
check r.isErr
|
|
check r.error == "host said no"
|
|
|
|
test "no host context on the thread yields an error":
|
|
ffiCurrentHostRegistry = nil
|
|
ffiCurrentPendingTable = nil
|
|
let r = waitFor echoHost("x")
|
|
check r.isErr
|
|
check "no host context" in r.error
|