nim-ffi/tests/e2e/ipc/run_roundtrip.nim
Ivan FB db46bb9aa2
feat(examples): in-library chronos CBOR server + cross-platform IPC CI
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>
2026-06-01 08:42:56 +02:00

58 lines
2.3 KiB
Nim

## Cross-platform IPC round-trip integration test (Linux / macOS / Windows).
##
## Builds the timer library with the in-process CBOR serve loop (-d:ffiIpcServe),
## the Nim host that starts it, and the lib-free Nim client; then starts the
## server, runs the client over a loopback TCP socket, and asserts the client's
## exit code. Everything uses chronos / Nim tooling, so there is no POSIX-only
## C or shell in the path. Driven by `nimble test_ipc`.
import std/[os, osproc, strutils]
const
Port = 47097
Address = "tcp:127.0.0.1:" & $Port
let
root = getCurrentDir() # nimble runs tasks from the repo root
ipc = root / "examples" / "timer" / "ipc_chronos"
libExt =
when defined(windows): "dll" elif defined(macosx): "dylib" else: "so"
exeExt = (when defined(windows): ".exe" else: "")
lib = ipc / ("libmy_timer." & libExt)
serveHost = ipc / ("serve_host" & exeExt)
client = ipc / ("client" & exeExt)
nimBase = "nim c --mm:orc -d:chronicles_log_level=WARN"
proc sh(cmd: string) =
echo "+ ", cmd
let code = execCmd(cmd)
doAssert code == 0, "command failed (" & $code & "): " & cmd
# 1) Library with the serve loop compiled in.
sh nimBase & " --app:lib --noMain --nimMainPrefix:libmy_timer -d:ffiIpcServe" & " -o:" &
lib.quoteShell & " " & (root / "examples" / "timer" / "timer.nim").quoteShell
# 2) Host links the lib (rpath so it is found next to the binary on POSIX; on
# Windows the dll sits in the same directory and is found automatically).
var hostCmd =
nimBase & " --passL:-L" & ipc.quoteShell & " --passL:-lmy_timer"
when not defined(windows):
hostCmd &= " --passL:-Wl,-rpath," & ipc.quoteShell
sh hostCmd & " -o:" & serveHost.quoteShell & " " & (ipc / "serve_host.nim").quoteShell
# 3) Client (no lib).
sh nimBase & " -o:" & client.quoteShell & " " & (ipc / "client.nim").quoteShell
# 4) Start the server, run the client (it retries the connect), assert, stop.
echo "+ start ", serveHost, " ", Address
let srv = startProcess(serveHost, args = [Address], options = {poParentStreams})
var clientCode = 1
try:
clientCode = execCmd(client.quoteShell & " " & Address)
finally:
srv.terminate()
discard srv.waitForExit(2000)
srv.close()
doAssert clientCode == 0, "client round-trip failed (exit " & $clientCode & ")"
echo "IPC cross-platform round-trip OK"