diff --git a/ffi/cbor_serial.nim b/ffi/cbor_serial.nim index 0fc87ce..54b1f8a 100644 --- a/ffi/cbor_serial.nim +++ b/ffi/cbor_serial.nim @@ -35,7 +35,6 @@ export cbor_serialization, options, results const CborNullByte*: byte = 0xf6'u8 ## CBOR encoding of `null` — used as the wire sentinel for empty OK payloads. - proc cborEncode*[T](x: T): seq[byte] = ## CBOR-encode any cbor_serialization-supported type (plus `pointer` / `ptr T` ## via our custom writers) into a fresh `seq[byte]`. diff --git a/ffi/codegen/rust.nim b/ffi/codegen/rust.nim index 6753324..a2acb17 100644 --- a/ffi/codegen/rust.nim +++ b/ffi/codegen/rust.nim @@ -63,7 +63,6 @@ proc reqStructName(p: FFIProcMeta): string = else: camel & "Req" - proc generateCargoToml*(libName: string): string = # `flume` is the unified callback channel (PR #23 Rust review, item 8): one # primitive that supports both `recv_timeout` (blocking trampoline) and diff --git a/ffi/event_thread.nim b/ffi/event_thread.nim index 81d9c77..abe173a 100644 --- a/ffi/event_thread.nim +++ b/ffi/event_thread.nim @@ -47,8 +47,10 @@ proc emitLivenessEvent[T, P](ctx: ptr FFIContext[T], name: string, payload: P) = chronicles.error "liveness event encode failed", name = name, err = exc.msg return let dataPtr: pointer = - if event.len > 0: cast[pointer](unsafeAddr event[0]) - else: cast[pointer](emptyListenerPayload) + if event.len > 0: + cast[pointer](unsafeAddr event[0]) + else: + cast[pointer](emptyListenerPayload) ctx.dispatchToListeners(name, dataPtr, event.len) proc onNotResponding*(ctx: ptr FFIContext) = @@ -105,8 +107,7 @@ proc check[T](hb: var HeartbeatMonitor, ctx: ptr FFIContext[T]) = hb.lastValue = cur hb.lastChange = Moment.now() hb.notifiedStale = false - elif not hb.notifiedStale and - Moment.now() - hb.lastChange > FFIHeartbeatStaleThreshold: + elif not hb.notifiedStale and Moment.now() - hb.lastChange > FFIHeartbeatStaleThreshold: onNotResponding(ctx) hb.notifiedStale = true diff --git a/ffi/ffi_context.nim b/ffi/ffi_context.nim index 705ad23..a4ecaef 100644 --- a/ffi/ffi_context.nim +++ b/ffi/ffi_context.nim @@ -32,13 +32,16 @@ type FFIContext*[T] = object reqReceivedSignal: ThreadSignalPtr # to signal main thread, interfacing with the FFI thread, that FFI thread received the request stopSignal: ThreadSignalPtr - threadExitSignal: ThreadSignalPtr # bounds destroyFFIContext's wait so a blocked loop cannot hang the caller - eventQueueSignal: ThreadSignalPtr # wakes the event thread on enqueue (used once dispatch is rewired in PR #69) + threadExitSignal: ThreadSignalPtr + # bounds destroyFFIContext's wait so a blocked loop cannot hang the caller + eventQueueSignal: ThreadSignalPtr + # wakes the event thread on enqueue (used once dispatch is rewired in PR #69) eventThreadExitSignal: ThreadSignalPtr # mirrors threadExitSignal for the event thread userData*: pointer eventRegistry*: FFIEventRegistry eventQueue*: EventQueue - ffiHeartbeat*: Atomic[int64] # advanced each FFI-thread loop; event thread reads for liveness + ffiHeartbeat*: Atomic[int64] + # advanced each FFI-thread loop; event thread reads for liveness running: Atomic[bool] # To control when the threads are running registeredRequests: ptr Table[cstring, FFIRequestProc] # Pointer to with the registered requests at compile time diff --git a/ffi/ffi_events.nim b/ffi/ffi_events.nim index ca048ea..529a6f3 100644 --- a/ffi/ffi_events.nim +++ b/ffi/ffi_events.nim @@ -10,7 +10,6 @@ import std/[locks, sequtils, options, tables] import chronicles import ./ffi_types, ./cbor_serial - type EventEnvelope*[T] = object ## Standard wire shape for CBOR-encoded FFI events: ## { eventType: tstr, payload: } @@ -18,7 +17,6 @@ type EventEnvelope*[T] = object eventType*: string payload*: T - type FFIEventListener* = object id*: uint64 @@ -33,7 +31,6 @@ type nextId*: uint64 ## Monotonic id source. 0 is reserved as "invalid"; ids start at 1. byEvent*: Table[string, seq[FFIEventListener]] - proc initEventRegistry*(reg: var FFIEventRegistry) = ## Must be called exactly once on the owning thread before the registry ## is shared. The embedded `Lock` wraps a platform primitive that cannot @@ -129,7 +126,6 @@ proc snapshotListeners*( listeners.add(l) listeners - const EventQueueCapacity* = 1024 ## ~24 KiB per context. Sustained backlog at this depth means a ## listener is wedged — what the stuck flag exists to surface. @@ -202,7 +198,6 @@ proc eventQueueLen*(q: var EventQueue): int {.raises: [], gcsafe.} = withLock q.lock: return q.count - const emptyListenerPayload*: cstring = "" ## Non-nil zero-length buffer handed to listeners when a payload is ## empty, so a consumer doing `std::string(data, len)` / `memcpy` never @@ -216,8 +211,10 @@ proc notifyListeners*( ## consumer doing `std::string(data, len)` / `memcpy` never receives nil. let n = max(dataLen, 0) let dataPtr = - if n > 0 and not data.isNil(): cast[ptr cchar](data) - else: cast[ptr cchar](emptyListenerPayload) + if n > 0 and not data.isNil(): + cast[ptr cchar](data) + else: + cast[ptr cchar](emptyListenerPayload) for listener in listeners: listener.callback(retCode, dataPtr, cast[csize_t](n), listener.userData) @@ -225,8 +222,10 @@ proc notifyListenersErr*(listeners: seq[FFIEventListener], msg: string) = ## Error fan-out: adapts the message string to `notifyListeners`, which ## supplies the non-nil pointer for the empty-message case. let p = - if msg.len > 0: cast[pointer](unsafeAddr msg[0]) - else: cast[pointer](emptyListenerPayload) + if msg.len > 0: + cast[pointer](unsafeAddr msg[0]) + else: + cast[pointer](emptyListenerPayload) notifyListeners(listeners, RET_ERR, p, msg.len) var ffiCurrentEventRegistry* {.threadvar.}: ptr FFIEventRegistry @@ -269,8 +268,10 @@ template dispatchFFIEvent*(eventName: string, body: untyped) = withFFIEventDispatch(eventName, listeners): let event = body let dataPtr: pointer = - if event.len > 0: cast[pointer](unsafeAddr event[0]) - else: cast[pointer](emptyListenerPayload) + if event.len > 0: + cast[pointer](unsafeAddr event[0]) + else: + cast[pointer](emptyListenerPayload) notifyListeners(listeners, RET_OK, dataPtr, event.len) template dispatchFFIEventCbor*(eventName: string, eventPayload: typed) = diff --git a/ffi/internal/ffi_macro.nim b/ffi/internal/ffi_macro.nim index 121f2be..b8e7be4 100644 --- a/ffi/internal/ffi_macro.nim +++ b/ffi/internal/ffi_macro.nim @@ -7,7 +7,6 @@ when defined(ffiGenBindings): import ../codegen/cpp import ../codegen/cddl - proc isPtr(typ: NimNode): bool = ## True iff `typ` is a `ptr T` type expression — i.e. an `nnkPtrTy` AST node. ## Used by the binding-generator metadata path to flag pointer-typed params @@ -597,7 +596,6 @@ macro ffiRaw*(prc: untyped): untyped = echo stmts.repr return stmts - macro ffi*(prc: untyped): untyped = ## Simplified FFI macro — applies to procs or types. ## @@ -837,7 +835,6 @@ macro ffi*(prc: untyped): untyped = echo stmts.repr return stmts - proc buildCtorRequestType( reqTypeName: NimNode, paramNames: seq[string], paramTypes: seq[NimNode] ): NimNode = @@ -1248,7 +1245,6 @@ macro ffiCtor*(prc: untyped): untyped = echo stmts.repr return stmts - macro ffiDtor*(prc: untyped): untyped = ## Defines a C-exported destructor that tears down the FFIContext after the ## body runs. @@ -1361,7 +1357,6 @@ macro ffiDtor*(prc: untyped): untyped = echo stmts.repr return stmts - macro ffiEvent*(wireName: static[string], prc: untyped): untyped = ## Declares a library-initiated event. The annotated proc has an empty ## body — the macro fills it with a `dispatchFFIEventCbor` call so the @@ -1449,7 +1444,6 @@ macro ffiEvent*(wireName: static[string], prc: untyped): untyped = echo generated.repr return generated - macro genBindings*( outputDir: static[string] = ffiOutputDir, nimSrcRelPath: static[string] = ffiSrcPath ): untyped = diff --git a/tests/e2e/cpp/CMakeLists.txt b/tests/e2e/cpp/CMakeLists.txt index 7aa3959..e9bd115 100644 --- a/tests/e2e/cpp/CMakeLists.txt +++ b/tests/e2e/cpp/CMakeLists.txt @@ -81,9 +81,18 @@ elseif(NIM_FFI_SANITIZER STREQUAL "tsan") "TSAN_OPTIONS=halt_on_error=1:second_deadlock_stack=1:history_size=7") endif() +# Discover at test time, not build time: a POST_BUILD discovery run launches the +# freshly-linked exe while Windows Defender is still scanning it (and its staged +# DLLs), which routinely overran the default 5s timeout on CI. PRE_TEST defers +# enumeration to `ctest`, and the bumped timeout absorbs first-launch scan delays. include(GoogleTest) if(_san_test_env) - gtest_discover_tests(timer_e2e_tests PROPERTIES ENVIRONMENT "${_san_test_env}") + gtest_discover_tests(timer_e2e_tests + DISCOVERY_MODE PRE_TEST + DISCOVERY_TIMEOUT 120 + PROPERTIES ENVIRONMENT "${_san_test_env}") else() - gtest_discover_tests(timer_e2e_tests) + gtest_discover_tests(timer_e2e_tests + DISCOVERY_MODE PRE_TEST + DISCOVERY_TIMEOUT 120) endif() diff --git a/tests/unit/test_event_dispatch.nim b/tests/unit/test_event_dispatch.nim index 4126e30..eab9a22 100644 --- a/tests/unit/test_event_dispatch.nim +++ b/tests/unit/test_event_dispatch.nim @@ -235,7 +235,6 @@ when not defined(gcRefc): # actually landed so a silently-broken dispatch loop is caught. check evt.called - ## A foreign-thread mutation must not be able to invalidate the ## listener's `userData` while an in-flight dispatch is mid-invocation. ## The dispatch templates hold `reg.lock` for the entire snapshot + @@ -317,16 +316,14 @@ suite "liveness events": defer: deinitCallbackData(evt) - discard addEventListener( - ctx[].eventRegistry, NotRespondingEventName, captureCb, addr evt - ) + discard + addEventListener(ctx[].eventRegistry, NotRespondingEventName, captureCb, addr evt) onNotResponding(ctx) waitCallback(evt) check evt.retCode == RET_OK - let decoded = - cborDecode(callbackBytes(evt), EventEnvelope[NotRespondingEvent]) + let decoded = cborDecode(callbackBytes(evt), EventEnvelope[NotRespondingEvent]) check decoded.isOk() check decoded.value.eventType == NotRespondingEventName @@ -343,9 +340,8 @@ suite "liveness events": defer: deinitCallbackData(evt) - discard addEventListener( - ctx[].eventRegistry, RespondingEventName, captureCb, addr evt - ) + discard + addEventListener(ctx[].eventRegistry, RespondingEventName, captureCb, addr evt) onResponding(ctx) @@ -387,9 +383,7 @@ suite "event thread drains queued events": deinitCallbackData(evt) const QueuedEvtName = "queued_evt" - discard addEventListener( - ctx[].eventRegistry, QueuedEvtName, captureCb, addr evt - ) + discard addEventListener(ctx[].eventRegistry, QueuedEvtName, captureCb, addr evt) # `tryEnqueueEvent` takes ownership of both buffers on success; the # event thread c_frees them after dispatch returns. diff --git a/tests/unit/test_event_listener.nim b/tests/unit/test_event_listener.nim index 4cf15aa..3a74bd3 100644 --- a/tests/unit/test_event_listener.nim +++ b/tests/unit/test_event_listener.nim @@ -48,7 +48,6 @@ proc tagCb( copyMem(addr payload[0], msg, int(len)) record(t[].rec[], t[].name, retCode, payload) - suite "FFIEventRegistry mutation": test "addEventListener assigns monotonically increasing non-zero ids": var reg: FFIEventRegistry diff --git a/tests/unit/test_ffi_context.nim b/tests/unit/test_ffi_context.nim index ec28ccb..2e28591 100644 --- a/tests/unit/test_ffi_context.nim +++ b/tests/unit/test_ffi_context.nim @@ -324,7 +324,6 @@ suite "sendRequestToFFIThread": check d.retCode == RET_OK check cborDecode(callbackBytes(d), string).value == "pong:" & msg - type SimpleLib = object value: int @@ -372,7 +371,6 @@ suite "ffiCtor macro": check SimpleLibFFIPool.destroyFFIContext(ctx).isOk() - type SendConfig {.ffi.} = object message: string