mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-08-05 06:23:19 +00:00
144 lines
4.9 KiB
Nim
144 lines
4.9 KiB
Nim
import std/[atomics, os]
|
|
import unittest2
|
|
import results
|
|
import ffi
|
|
|
|
# A {.ffiCtor.} that fails still gives the caller a live context. The ctor body
|
|
# runs on the FFI thread, long after the C entry point returns the pointer.
|
|
# `myLib` is not nil, because the FFI thread points it at a default fallback.
|
|
# For a `ref` library type that fallback is nil. A later {.ffi.} call therefore
|
|
# gave the user body a nil ref and crashed on the first field access.
|
|
|
|
type FailedCtorLib = ref object
|
|
marker: int
|
|
|
|
# A stub for the NimMain proc that declareLibrary imports. The test links as a
|
|
# plain executable.
|
|
{.emit: "void libfailedctorNimMain(void) {}".}
|
|
|
|
declareLibrary("failedctor", FailedCtorLib)
|
|
|
|
type FailedCtorConfig {.ffi.} = object
|
|
shouldFail: bool
|
|
|
|
proc failedctor_create*(
|
|
config: FailedCtorConfig
|
|
): Future[Result[FailedCtorLib, string]] {.ffiCtor.} =
|
|
if config.shouldFail:
|
|
return err("ctor deliberately failed")
|
|
return ok(FailedCtorLib(marker: 1))
|
|
|
|
# Both bodies read a field. A nil ref receiver faults on that read.
|
|
proc failedctor_ping*(lib: FailedCtorLib): Future[Result[string, string]] {.ffi.} =
|
|
return ok("pong:" & $lib.marker)
|
|
|
|
proc failedctor_echo*(
|
|
lib: FailedCtorLib, note: string
|
|
): Future[Result[string, string]] {.ffi.} =
|
|
return ok(note & ":" & $lib.marker)
|
|
|
|
type CallbackState = object
|
|
called: Atomic[bool]
|
|
retCode: Atomic[int]
|
|
|
|
proc resetState(s: var CallbackState) =
|
|
s.called.store(false)
|
|
s.retCode.store(-1)
|
|
|
|
proc recordingCallback(
|
|
retCode: cint, msg: ptr cchar, len: csize_t, userData: pointer
|
|
) {.cdecl, gcsafe, raises: [].} =
|
|
let s = cast[ptr CallbackState](userData)
|
|
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 createFailedCtx(s: var CallbackState): ptr FFIContext[FailedCtorLib] =
|
|
## Sends the ctor down its error path and returns the context, which stays alive.
|
|
resetState(s)
|
|
var cfg =
|
|
cborEncode(FailedctorCreateCtorReq(config: FailedCtorConfig(shouldFail: true)))
|
|
let ret =
|
|
failedctor_create(encodedPtr(cfg), cfg.len.csize_t, recordingCallback, addr s)
|
|
if ret.isNil():
|
|
return nil
|
|
discard waitCalled(s)
|
|
cast[ptr FFIContext[FailedCtorLib]](ret)
|
|
|
|
suite "{.ffi.} call after a failed constructor":
|
|
test "the failed ctor reports the error but leaves the context alive":
|
|
var s: CallbackState
|
|
let ctx = createFailedCtx(s)
|
|
check not ctx.isNil()
|
|
check s.retCode.load() == int(RET_ERR)
|
|
# `myLib` is not nil even here. The FFI thread points it at a default
|
|
# fallback before it dispatches the request. `libReady` shows if the ctor
|
|
# stored a real library.
|
|
check not ctx[].myLib.isNil() # the fallback, not a real library
|
|
check ctx[].myLib[].isNil() # for a `ref` lib the fallback is nil
|
|
check not ctx[].libReady.load()
|
|
|
|
# The synchronous return only reports that the FFI thread accepted the
|
|
# request. The callback delivers the rejection.
|
|
test "a no-arg call on an uninitialized library reports RET_ERR, no crash":
|
|
var s: CallbackState
|
|
let ctx = createFailedCtx(s)
|
|
check not ctx.isNil()
|
|
|
|
resetState(s)
|
|
var req = cborEncode(FailedctorPingReq())
|
|
let ret =
|
|
failedctor_ping(ctx, recordingCallback, addr s, encodedPtr(req), req.len.csize_t)
|
|
check ret == RET_OK
|
|
check waitCalled(s)
|
|
check s.retCode.load() == int(RET_ERR)
|
|
|
|
test "an argument-taking call on an uninitialized library reports RET_ERR, no crash":
|
|
var s: CallbackState
|
|
let ctx = createFailedCtx(s)
|
|
check not ctx.isNil()
|
|
|
|
resetState(s)
|
|
var req = cborEncode(FailedctorEchoReq(note: "hello"))
|
|
let ret =
|
|
failedctor_echo(ctx, recordingCallback, addr s, encodedPtr(req), req.len.csize_t)
|
|
check ret == RET_OK
|
|
check waitCalled(s)
|
|
check s.retCode.load() == int(RET_ERR)
|
|
|
|
# The guard runs on the FFI thread, behind the ctor in the queue. Thus a host
|
|
# can send a call before it waits for the create callback.
|
|
test "a call issued before the successful ctor callback still succeeds":
|
|
var ctorState: CallbackState
|
|
resetState(ctorState)
|
|
var cfg =
|
|
cborEncode(FailedctorCreateCtorReq(config: FailedCtorConfig(shouldFail: false)))
|
|
let raw = failedctor_create(
|
|
encodedPtr(cfg), cfg.len.csize_t, recordingCallback, addr ctorState
|
|
)
|
|
check not raw.isNil()
|
|
let ctx = cast[ptr FFIContext[FailedCtorLib]](raw)
|
|
|
|
# No wait here, on purpose: the request goes into the queue behind the ctor.
|
|
var callState: CallbackState
|
|
resetState(callState)
|
|
var req = cborEncode(FailedctorPingReq())
|
|
let ret = failedctor_ping(
|
|
ctx, recordingCallback, addr callState, encodedPtr(req), req.len.csize_t
|
|
)
|
|
check ret == RET_OK
|
|
check waitCalled(callState)
|
|
check callState.retCode.load() == int(RET_OK)
|