nim-ffi/tests/unit/test_ctx_after_failed_ctor.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)