mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-20 16:29:31 +00:00
Adds a standalone IPC example: the library serving itself over a CBOR socket. examples/timer/ipc_chronos/serve.nim compiles into libmy_timer only under -d:ffiIpcServe (every other build untouched) and runs a chronos socket server that, per request, decodes CBOR at the socket edge and calls the library's own async procs directly — native, in-process, zero serialization between the socket and the logic, no FFI boundary, no callback bridge. Exposed as my_timer_serve(address). CBOR (not the native struct ABI) is correct at the wire here: a relay's data is serialized regardless, so native would only relocate the decode and add marshalling for no gain — native locally, CBOR for IPC. serve_host.nim starts it; client.nim is a lib-free chronos client. Both use chronos sockets, so the example builds and runs on Linux, macOS and Windows over TCP (unix sockets are a POSIX bonus). CI: tests/e2e/ipc/run_roundtrip.nim builds the dylib + host + client, spawns the server and round-trips over loopback TCP asserting the replies; wired as `nimble test_ipc` and a 3-OS CI matrix (ubuntu/macos/windows). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
119 lines
4.1 KiB
Nim
119 lines
4.1 KiB
Nim
## Cross-platform CBOR-over-socket client for the timer IPC server.
|
|
##
|
|
## Speaks a simple length-prefixed CBOR framing (see the frame layout below) and uses chronos for
|
|
## the socket so it builds and runs on Linux, macOS and Windows. It does not
|
|
## link the library — it only needs the CBOR codec to build requests and read
|
|
## replies (exactly what a remote client has).
|
|
##
|
|
## client tcp:127.0.0.1:9099 # any platform
|
|
## client unix:/tmp/timer.sock # POSIX
|
|
##
|
|
## Exits 0 only if the round-trip values match the expectations, so it doubles
|
|
## as the integration check the CI driver runs.
|
|
import std/[os, strutils]
|
|
import chronos
|
|
import chronos/transports/stream
|
|
import results
|
|
import ffi/cbor_serial
|
|
|
|
# Local mirrors of the wire shapes (a remote client defines its own — it never
|
|
# imports the library). Field names match the generated CBOR ABI.
|
|
type
|
|
EchoReqWire = object
|
|
message: string
|
|
delayMs: int
|
|
EchoReqEnv = object
|
|
req: EchoReqWire
|
|
EchoRespWire = object
|
|
echoed: string
|
|
timerName: string
|
|
|
|
proc writeU32(t: StreamTransport, v: uint32) {.async.} =
|
|
let b = @[byte(v shr 24), byte(v shr 16), byte(v shr 8), byte(v and 0xFF)]
|
|
discard await t.write(b)
|
|
|
|
proc readU32(t: StreamTransport): Future[uint32] {.async.} =
|
|
var b: array[4, byte]
|
|
await t.readExactly(addr b[0], 4)
|
|
return
|
|
(uint32(b[0]) shl 24) or (uint32(b[1]) shl 16) or (uint32(b[2]) shl 8) or
|
|
uint32(b[3])
|
|
|
|
proc call(
|
|
t: StreamTransport, meth: string, payload: seq[byte]
|
|
): Future[(int32, seq[byte])] {.async.} =
|
|
# Request frame: [u32 method_len][method][u32 payload_len][payload].
|
|
await writeU32(t, uint32(meth.len))
|
|
if meth.len > 0:
|
|
discard await t.write(meth)
|
|
await writeU32(t, uint32(payload.len))
|
|
if payload.len > 0:
|
|
discard await t.write(payload)
|
|
# Response frame: [i32 ret][u32 len][body].
|
|
let ret = cast[int32](await readU32(t))
|
|
let blen = await readU32(t)
|
|
var body = newSeq[byte](int(blen))
|
|
if blen > 0'u32:
|
|
await t.readExactly(addr body[0], int(blen))
|
|
return (ret, body)
|
|
|
|
proc parseAddress(a: string): TransportAddress =
|
|
if a.startsWith("unix:"):
|
|
return initTAddress(a[5 ..^ 1])
|
|
elif a.startsWith("tcp:"):
|
|
let hp = a[4 ..^ 1]
|
|
let sep = hp.rfind(':')
|
|
doAssert sep > 0, "tcp address must be tcp:<host>:<port>"
|
|
return initTAddress(hp[0 ..< sep], Port(parseInt(hp[sep + 1 ..^ 1])))
|
|
else:
|
|
raise newException(ValueError, "address must be unix:<path> or tcp:<host>:<port>")
|
|
|
|
proc connectWithRetry(ta: TransportAddress): Future[StreamTransport] {.async.} =
|
|
# The server may still be starting; retry briefly so the example/CI is not
|
|
# racing socket setup.
|
|
for _ in 0 ..< 100:
|
|
try:
|
|
return await connect(ta)
|
|
except CatchableError:
|
|
await sleepAsync(50.milliseconds)
|
|
return await connect(ta) # last attempt: surface the real error
|
|
|
|
proc run(address: string): Future[bool] {.async.} =
|
|
let t = await connectWithRetry(parseAddress(address))
|
|
var ok = true
|
|
echo "[client] connected"
|
|
|
|
# 1) version — empty request, response is a CBOR text string.
|
|
block:
|
|
let (ret, body) = await t.call("version", @[byte 0xA0]) # empty CBOR map {}
|
|
let v = cborDecode(body, string)
|
|
if ret == 0 and v.isOk:
|
|
echo "[client] version = ", v.get()
|
|
ok = ok and v.get() == "nim-timer v0.1.0"
|
|
else:
|
|
echo "[client] version failed (ret=", ret, ")"
|
|
ok = false
|
|
|
|
# 2) echo — nested request, response is an EchoResponse map.
|
|
block:
|
|
let req = cborEncode(EchoReqEnv(req: EchoReqWire(message: "hello over the wire", delayMs: 5)))
|
|
let (ret, body) = await t.call("echo", req)
|
|
let r = cborDecode(body, EchoRespWire)
|
|
if ret == 0 and r.isOk:
|
|
echo "[client] echo.echoed= ", r.get().echoed
|
|
echo "[client] echo.timer = ", r.get().timerName
|
|
ok = ok and r.get().echoed == "hello over the wire" and r.get().timerName == "ipc-server"
|
|
else:
|
|
echo "[client] echo failed (ret=", ret, ")"
|
|
ok = false
|
|
|
|
await t.closeWait()
|
|
echo "[client] done"
|
|
return ok
|
|
|
|
when isMainModule:
|
|
if paramCount() != 1:
|
|
stderr.writeLine "usage: client <tcp:host:port | unix:path>"
|
|
quit(2)
|
|
quit(if waitFor run(paramStr(1)): 0 else: 1)
|