mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-08-05 06:23:19 +00:00
191 lines
5.6 KiB
Nim
191 lines
5.6 KiB
Nim
## `{.ffi.}` picks the path from the shape of the signature. This file writes one
|
|
## proc per path with the same pragma, then calls each generated C wrapper.
|
|
|
|
import std/[atomics, os]
|
|
import unittest2
|
|
import results
|
|
import ffi
|
|
|
|
type RouterLib = ref object
|
|
marker: int
|
|
|
|
type RouterTick {.ffi.} = object
|
|
count: int
|
|
|
|
# A stub for the NimMain proc that declareLibrary imports. The test links as a
|
|
# plain executable.
|
|
{.emit: "void librouterNimMain(void) {}".}
|
|
|
|
declareLibrary("router", RouterLib)
|
|
|
|
proc router_create*(seed: int): Future[Result[RouterLib, string]] {.ffiCtor.} =
|
|
return ok(RouterLib(marker: seed))
|
|
|
|
# A library receiver, so the router picks the context method.
|
|
proc router_marker*(lib: RouterLib): Future[Result[int, string]] {.ffi.} =
|
|
return ok(lib.marker)
|
|
|
|
# No receiver, so the router picks the static call.
|
|
proc router_version*(): Future[Result[string, string]] {.ffi.} =
|
|
return ok("router v1")
|
|
|
|
# No arguments and a plain return type, so the router picks the synchronous
|
|
# export.
|
|
proc router_alive*(): int {.ffi.} =
|
|
7
|
|
|
|
# `int` is pointer-wide, so the export must not narrow it to a C `int`.
|
|
proc router_big*(): int {.ffi.} =
|
|
int(high(int32)) + 1
|
|
|
|
proc router_banner*(): string {.ffi.} =
|
|
"router banner"
|
|
|
|
var routerBeats = 0
|
|
|
|
proc router_beat*() {.ffi.} =
|
|
inc routerBeats
|
|
|
|
proc router_raises*(): int {.ffi.} =
|
|
raise newException(ValueError, "boom")
|
|
|
|
# A library receiver and no result, so the router picks the destructor.
|
|
proc router_destroy*(lib: RouterLib) {.ffi.} =
|
|
discard
|
|
|
|
# A payload parameter and no result, so the router picks the event. The leading
|
|
# literal sets the wire name, exactly as {.ffiEvent.} accepts it.
|
|
proc onRouterTick*(evt: RouterTick) {.ffi: "on_router_tick".} =
|
|
discard
|
|
|
|
# The event queue is per-thread, so only a handler on the FFI thread can fire.
|
|
proc router_tick*(lib: RouterLib): Future[Result[int, string]] {.ffi.} =
|
|
onRouterTick(RouterTick(count: 3))
|
|
return ok(lib.marker)
|
|
|
|
type CallbackState = object
|
|
called: Atomic[bool]
|
|
retCode: Atomic[int]
|
|
msg: string
|
|
|
|
proc resetState(s: var CallbackState) =
|
|
s.called.store(false)
|
|
s.retCode.store(-1)
|
|
s.msg = ""
|
|
|
|
proc recordingCallback(
|
|
retCode: cint, msg: ptr cchar, len: csize_t, userData: pointer
|
|
) {.cdecl, gcsafe, raises: [].} =
|
|
let s = cast[ptr CallbackState](userData)
|
|
if not msg.isNil() and len > 0:
|
|
s[].msg = newString(int(len))
|
|
copyMem(addr s[].msg[0], msg, int(len))
|
|
s[].retCode.store(int(retCode))
|
|
s[].called.store(true)
|
|
|
|
proc encodedPtr(bytes: var seq[byte]): ptr byte =
|
|
if bytes.len == 0:
|
|
nil
|
|
else:
|
|
cast[ptr byte](addr bytes[0])
|
|
|
|
proc waitCalled(s: var CallbackState): bool =
|
|
var tries = 0
|
|
while not s.called.load() and tries < 500:
|
|
os.sleep(5)
|
|
inc tries
|
|
s.called.load()
|
|
|
|
proc createCtx(s: var CallbackState): pointer =
|
|
resetState(s)
|
|
var cfg = cborEncode(RouterCreateCtorReq(seed: 42))
|
|
let ret = router_create(encodedPtr(cfg), cfg.len.csize_t, recordingCallback, addr s)
|
|
discard waitCalled(s)
|
|
ret
|
|
|
|
suite "{.ffi.} routes on the shape of the signature":
|
|
test "a library receiver routes to the context method":
|
|
var s: CallbackState
|
|
let ctx = createCtx(s)
|
|
check not ctx.isNil()
|
|
defer:
|
|
discard router_destroy(ctx)
|
|
|
|
resetState(s)
|
|
var req = cborEncode(RouterMarkerReq())
|
|
check router_marker(
|
|
cast[ptr FFIContext[RouterLib]](ctx),
|
|
recordingCallback,
|
|
addr s,
|
|
encodedPtr(req),
|
|
req.len.csize_t,
|
|
) == RET_OK
|
|
check waitCalled(s)
|
|
check s.retCode.load() == int(RET_OK)
|
|
check cborDecode(cast[seq[byte]](s.msg), int).value == 42
|
|
|
|
test "no receiver routes to the static call, which needs no context":
|
|
var s: CallbackState
|
|
resetState(s)
|
|
var req = cborEncode(RouterVersionReq())
|
|
check router_version(recordingCallback, addr s, encodedPtr(req), req.len.csize_t) ==
|
|
RET_OK
|
|
check waitCalled(s)
|
|
check s.retCode.load() == int(RET_OK)
|
|
check cborDecode(cast[seq[byte]](s.msg), string).value == "router v1"
|
|
|
|
test "no arguments and a plain return type route to the synchronous export":
|
|
# The export returns its value directly, with no context and no callback.
|
|
check router_alive() == clonglong(7)
|
|
|
|
test "an int export keeps its full width across the C ABI":
|
|
check router_big() == clonglong(int32.high) + 1
|
|
|
|
test "a string export returns bytes the caller can read":
|
|
check $router_banner() == "router banner"
|
|
|
|
test "a no-return export routes to a void C symbol":
|
|
let before = routerBeats
|
|
router_beat()
|
|
check routerBeats == before + 1
|
|
|
|
test "an exception in the body never crosses the C ABI":
|
|
check router_raises() == clonglong(0)
|
|
|
|
test "a payload parameter and no result route to the event":
|
|
var s: CallbackState
|
|
let ctx = createCtx(s)
|
|
check not ctx.isNil()
|
|
defer:
|
|
discard router_destroy(ctx)
|
|
|
|
var evt: CallbackState
|
|
resetState(evt)
|
|
check router_add_event_listener(
|
|
cast[ptr FFIContext[RouterLib]](ctx),
|
|
"on_router_tick".cstring,
|
|
recordingCallback,
|
|
addr evt,
|
|
) != 0'u64
|
|
|
|
resetState(s)
|
|
var req = cborEncode(RouterTickReq())
|
|
check router_tick(
|
|
cast[ptr FFIContext[RouterLib]](ctx),
|
|
recordingCallback,
|
|
addr s,
|
|
encodedPtr(req),
|
|
req.len.csize_t,
|
|
) == RET_OK
|
|
check waitCalled(evt)
|
|
let env = cborDecode(cast[seq[byte]](evt.msg), EventEnvelope[RouterTick])
|
|
check env.value.eventType == "on_router_tick"
|
|
check env.value.payload.count == 3
|
|
|
|
test "a library receiver and no result route to the destructor":
|
|
var s: CallbackState
|
|
let ctx = createCtx(s)
|
|
check not ctx.isNil()
|
|
check router_destroy(ctx) == RET_OK
|
|
check router_destroy(nil) == RET_ERR
|