chore(ci): add nph linting

This commit is contained in:
Gabriel Cruz 2026-06-08 08:21:17 -03:00
parent 721f244312
commit fbbfe08aec
No known key found for this signature in database
GPG Key ID: 2E467754A6BA9BA5
13 changed files with 158 additions and 154 deletions

27
.github/workflows/linters.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: Linters
on:
pull_request:
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
nph:
name: nph
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 2 # In PR, has extra merge commit: ^1 = PR, ^2 = base
- name: Check nph formatting
uses: arnetheduck/nph-action@v1
with:
version: 0.7.0
options: "./. *.nim*"
fail: true
suggest: true

View File

@ -6,4 +6,4 @@ switch("path", thisDir())
--noNimblePath
when withDir(thisDir(), system.fileExists("nimble.paths")):
include "nimble.paths"
# end Nimble config
# end Nimble config

View File

@ -1,7 +1,8 @@
version = "0.1.0"
packageName = "echo"
author = "Institute of Free Technology"
description = "Second nim-ffi example library, used as the cross-library partner of the timer example in C++ e2e tests"
description =
"Second nim-ffi example library, used as the cross-library partner of the timer example in C++ e2e tests"
license = "MIT or Apache License 2.0"
requires "nim >= 2.2.4"
@ -13,8 +14,7 @@ requires "https://github.com/logos-messaging/nim-ffi >= 0.2.0"
const nimFlags = "--mm:orc -d:chronicles_log_level=WARN"
task build, "Compile the echo library":
exec "nim c " & nimFlags &
" --app:lib --noMain --nimMainPrefix:libecho echo.nim"
exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libecho echo.nim"
task genbindings_cpp, "Generate C++ bindings for the echo example":
exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libecho" &

View File

@ -13,8 +13,7 @@ requires "https://github.com/logos-messaging/nim-ffi >= 0.2.0"
const nimFlags = "--mm:orc -d:chronicles_log_level=WARN"
task build, "Compile the timer library":
exec "nim c " & nimFlags &
" --app:lib --noMain --nimMainPrefix:libmy_timer timer.nim"
exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libmy_timer timer.nim"
task genbindings_rust, "Generate Rust bindings for the timer example":
exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libmy_timer" &

View File

@ -56,17 +56,12 @@ proc sanFlags(san: string): string =
of "none", "":
""
of "asan-ubsan":
" --passC:-fsanitize=address,undefined" &
" --passC:-fno-sanitize-recover=all" &
" --passC:-fno-omit-frame-pointer" &
" --passC:-g" &
" --passL:-fsanitize=address,undefined"
" --passC:-fsanitize=address,undefined" & " --passC:-fno-sanitize-recover=all" &
" --passC:-fno-omit-frame-pointer" & " --passC:-g" &
" --passL:-fsanitize=address,undefined"
of "tsan":
" --passC:-fsanitize=thread" &
" --passC:-fno-omit-frame-pointer" &
" --passC:-g" &
" --passC:-O1" &
" --passL:-fsanitize=thread"
" --passC:-fsanitize=thread" & " --passC:-fno-omit-frame-pointer" & " --passC:-g" &
" --passC:-O1" & " --passL:-fsanitize=thread"
else:
raise newException(ValueError, "unknown NIM_FFI_SAN: " & san)
@ -98,14 +93,18 @@ task test_cpp_e2e, "Build and run the C++ end-to-end tests for the timer example
runOrQuit "cmake --build tests/e2e/cpp/build"
runOrQuit "ctest --test-dir tests/e2e/cpp/build --output-on-failure"
task test_sanitized, "Run all unit tests under a sanitizer (NIM_FFI_SAN) and mm (NIM_FFI_MM)":
task test_sanitized,
"Run all unit tests under a sanitizer (NIM_FFI_SAN) and mm (NIM_FFI_MM)":
let san = getEnv("NIM_FFI_SAN", "none")
let mm = getEnv("NIM_FFI_MM", "")
let mm = getEnv("NIM_FFI_MM", "")
let extra = sanFlags(san)
let modes =
if mm == "orc": @[nimFlagsOrc]
elif mm == "refc": @[nimFlagsRefc]
else: @[nimFlagsOrc, nimFlagsRefc]
if mm == "orc":
@[nimFlagsOrc]
elif mm == "refc":
@[nimFlagsRefc]
else:
@[nimFlagsOrc, nimFlagsRefc]
if san == "tsan":
let suppPath = thisDir() & "/tsan.supp"
let existing = getEnv("TSAN_OPTIONS")
@ -117,82 +116,67 @@ task test_sanitized, "Run all unit tests under a sanitizer (NIM_FFI_SAN) and mm
for t in unitTests:
exec "nim c -r " & flags & extra & " tests/unit/" & t & ".nim"
task test_cpp_e2e_sanitized, "Build and run the C++ e2e tests with a sanitizer (NIM_FFI_SAN) and mm (NIM_FFI_MM)":
let mm = getEnv("NIM_FFI_MM", "orc")
task test_cpp_e2e_sanitized,
"Build and run the C++ e2e tests with a sanitizer (NIM_FFI_SAN) and mm (NIM_FFI_MM)":
let mm = getEnv("NIM_FFI_MM", "orc")
let san = getEnv("NIM_FFI_SAN", "none")
runOrQuit "nimble genbindings_cpp"
runOrQuit "nimble genbindings_cpp_echo"
runOrQuit "cmake -S tests/e2e/cpp -B tests/e2e/cpp/build" &
" -DNIM_FFI_MM=" & mm &
" -DNIM_FFI_SANITIZER=" & san
runOrQuit "cmake -S tests/e2e/cpp -B tests/e2e/cpp/build" & " -DNIM_FFI_MM=" & mm &
" -DNIM_FFI_SANITIZER=" & san
runOrQuit "cmake --build tests/e2e/cpp/build -j"
runOrQuit "ctest --test-dir tests/e2e/cpp/build --output-on-failure"
task genbindings_example, "Generate Rust bindings for the timer example":
exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libmy_timer -d:ffiGenBindings -o:/dev/null examples/timer/timer.nim"
exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libmy_timer -d:ffiGenBindings -o:/dev/null examples/timer/timer.nim"
exec "nim c " & nimFlagsOrc &
" --app:lib --noMain --nimMainPrefix:libmy_timer -d:ffiGenBindings -o:/dev/null examples/timer/timer.nim"
exec "nim c " & nimFlagsRefc &
" --app:lib --noMain --nimMainPrefix:libmy_timer -d:ffiGenBindings -o:/dev/null examples/timer/timer.nim"
task genbindings_rust, "Generate Rust bindings for the timer example":
exec "nim c " & nimFlagsOrc &
" --app:lib --noMain --nimMainPrefix:libmy_timer" &
exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libmy_timer" &
" -d:ffiGenBindings -d:targetLang=rust" &
" -d:ffiOutputDir=examples/timer/rust_bindings" &
" -d:ffiSrcPath=../timer.nim" &
" -d:ffiOutputDir=examples/timer/rust_bindings" & " -d:ffiSrcPath=../timer.nim" &
" -o:/dev/null examples/timer/timer.nim"
exec "nim c " & nimFlagsRefc &
" --app:lib --noMain --nimMainPrefix:libmy_timer" &
exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libmy_timer" &
" -d:ffiGenBindings -d:targetLang=rust" &
" -d:ffiOutputDir=examples/timer/rust_bindings" &
" -d:ffiSrcPath=../timer.nim" &
" -d:ffiOutputDir=examples/timer/rust_bindings" & " -d:ffiSrcPath=../timer.nim" &
" -o:/dev/null examples/timer/timer.nim"
task genbindings_cddl, "Generate CDDL schema for the timer example":
exec "nim c " & nimFlagsOrc &
" --app:lib --noMain --nimMainPrefix:libtimer" &
exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libtimer" &
" -d:ffiGenBindings -d:targetLang=cddl" &
" -d:ffiOutputDir=examples/timer/cddl_bindings" &
" -d:ffiSrcPath=../timer.nim" &
" -d:ffiOutputDir=examples/timer/cddl_bindings" & " -d:ffiSrcPath=../timer.nim" &
" -o:/dev/null examples/timer/timer.nim"
task genbindings_cpp, "Generate C++ bindings for the timer example":
exec "nim c " & nimFlagsOrc &
" --app:lib --noMain --nimMainPrefix:libmy_timer" &
exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libmy_timer" &
" -d:ffiGenBindings -d:targetLang=cpp" &
" -d:ffiOutputDir=examples/timer/cpp_bindings" &
" -d:ffiSrcPath=../timer.nim" &
" -d:ffiOutputDir=examples/timer/cpp_bindings" & " -d:ffiSrcPath=../timer.nim" &
" -o:/dev/null examples/timer/timer.nim"
exec "nim c " & nimFlagsRefc &
" --app:lib --noMain --nimMainPrefix:libmy_timer" &
exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libmy_timer" &
" -d:ffiGenBindings -d:targetLang=cpp" &
" -d:ffiOutputDir=examples/timer/cpp_bindings" &
" -d:ffiSrcPath=../timer.nim" &
" -d:ffiOutputDir=examples/timer/cpp_bindings" & " -d:ffiSrcPath=../timer.nim" &
" -o:/dev/null examples/timer/timer.nim"
task genbindings_cpp_echo, "Generate C++ bindings for the echo example":
exec "nim c " & nimFlagsOrc &
" --app:lib --noMain --nimMainPrefix:libecho" &
exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libecho" &
" -d:ffiGenBindings -d:targetLang=cpp" &
" -d:ffiOutputDir=examples/echo/cpp_bindings" &
" -d:ffiSrcPath=../echo.nim" &
" -d:ffiOutputDir=examples/echo/cpp_bindings" & " -d:ffiSrcPath=../echo.nim" &
" -o:/dev/null examples/echo/echo.nim"
exec "nim c " & nimFlagsRefc &
" --app:lib --noMain --nimMainPrefix:libecho" &
exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libecho" &
" -d:ffiGenBindings -d:targetLang=cpp" &
" -d:ffiOutputDir=examples/echo/cpp_bindings" &
" -d:ffiSrcPath=../echo.nim" &
" -d:ffiOutputDir=examples/echo/cpp_bindings" & " -d:ffiSrcPath=../echo.nim" &
" -o:/dev/null examples/echo/echo.nim"
task check_bindings_rust, "Verify checked-in Rust bindings match Nim source":
exec "nimble genbindings_rust"
exec "git diff --exit-code --" &
" examples/timer/rust_bindings/Cargo.toml" &
" examples/timer/rust_bindings/build.rs" &
" examples/timer/rust_bindings/src"
exec "git diff --exit-code --" & " examples/timer/rust_bindings/Cargo.toml" &
" examples/timer/rust_bindings/build.rs" & " examples/timer/rust_bindings/src"
task check_bindings_cpp, "Verify checked-in C++ bindings match Nim source":
exec "nimble genbindings_cpp"
exec "git diff --exit-code --" &
" examples/timer/cpp_bindings/my_timer.hpp" &
exec "git diff --exit-code --" & " examples/timer/cpp_bindings/my_timer.hpp" &
" examples/timer/cpp_bindings/CMakeLists.txt"
task check_bindings, "Verify all checked-in example bindings match Nim source":

View File

@ -158,7 +158,9 @@ proc emitEventDispatcher(
## until `removeEventListener` removes it.
if events.len == 0:
return
lines.add(" // ── Event listener API ──────────────────────────────────")
lines.add(
" // ── Event listener API ──────────────────────────────────"
)
lines.add(" struct ListenerHandle { std::uint64_t id = 0; };")
lines.add("")
# Per-event typed registration helpers.
@ -174,9 +176,7 @@ proc emitEventDispatcher(
[ev.payloadTypeName]
)
lines.add(" auto* raw = owned.get();")
lines.add(
" const auto id = $1_add_event_listener(" % [libName]
)
lines.add(" const auto id = $1_add_event_listener(" % [libName])
lines.add(
" ptr_, \"$1\", &$2::typedTrampoline<$3>, raw);" %
[ev.wireName, ctxTypeName, ev.payloadTypeName]
@ -197,9 +197,7 @@ proc emitEventDispatcher(
lines.add(" }")
lines.add("")
proc emitEventTrampoline(
lines: var seq[string], events: seq[FFIEventMeta]
) =
proc emitEventTrampoline(lines: var seq[string], events: seq[FFIEventMeta]) =
## Private listener machinery for the public API emitted by
## `emitEventDispatcher`:
##
@ -217,12 +215,16 @@ proc emitEventTrampoline(
lines.add(" template <class T>")
lines.add(" struct TypedListener : ListenerBase {")
lines.add(" std::function<void(const T&)> fn;")
lines.add(" explicit TypedListener(std::function<void(const T&)> f) : fn(std::move(f)) {}")
lines.add(
" explicit TypedListener(std::function<void(const T&)> f) : fn(std::move(f)) {}"
)
lines.add(" };")
lines.add("")
# Typed trampoline — one instantiation per payload type, all sharing a body.
lines.add(" template <class T>")
lines.add(" static void typedTrampoline(int ret, const char* msg, std::size_t len, void* ud) {")
lines.add(
" static void typedTrampoline(int ret, const char* msg, std::size_t len, void* ud) {"
)
lines.add(" if (!ud || ret != 0 || !msg || len == 0) return;")
lines.add(" auto* listener = static_cast<TypedListener<T>*>(ud);")
lines.add(" if (!listener->fn) return;")
@ -236,9 +238,7 @@ proc emitEventTrampoline(
" if (cbor_value_map_find_value(&it, \"payload\", &payloadField) != CborNoError) return;"
)
lines.add(" T payload{};")
lines.add(
" if (decode_cbor(payloadField, payload) != CborNoError) return;"
)
lines.add(" if (decode_cbor(payloadField, payload) != CborNoError) return;")
lines.add(" listener->fn(payload);")
lines.add(" }")
lines.add("")
@ -416,12 +416,12 @@ proc generateCppHeader*(
# itself (see ContextRuleOf5Tpl) and hand out ownership through a
# smart pointer that callers can move, store in containers, etc.
let createRet = "Result<std::unique_ptr<$1>>" % [ctxTypeName]
lines.add(
" static $1 create($2) {" % [createRet, ctorParamsWithTimeout]
)
lines.add(" static $1 create($2) {" % [createRet, ctorParamsWithTimeout])
lines.add(" const auto ffi_req_ = $1;" % [reqInit])
lines.add(" auto ffi_enc_ = encodeCborFFI(ffi_req_);")
lines.add(" if (ffi_enc_.isErr()) return $1::err(ffi_enc_.error());" % [createRet])
lines.add(
" if (ffi_enc_.isErr()) return $1::err(ffi_enc_.error());" % [createRet]
)
lines.add(" const auto& ffi_req_bytes_ = ffi_enc_.value();")
lines.add(" auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {")
lines.add(
@ -430,9 +430,13 @@ proc generateCppHeader*(
)
lines.add(" return 0;")
lines.add(" }, timeout);")
lines.add(" if (ffi_raw_.isErr()) return $1::err(ffi_raw_.error());" % [createRet])
lines.add(
" if (ffi_raw_.isErr()) return $1::err(ffi_raw_.error());" % [createRet]
)
lines.add(" auto ffi_addr_ = decodeCborFFI<std::string>(ffi_raw_.value());")
lines.add(" if (ffi_addr_.isErr()) return $1::err(ffi_addr_.error());" % [createRet])
lines.add(
" if (ffi_addr_.isErr()) return $1::err(ffi_addr_.error());" % [createRet]
)
lines.add(" const auto& addr_str = ffi_addr_.value();")
# Parse the ctx address without exceptions: std::stoull would throw on a
# non-numeric payload, so use std::from_chars and surface the failure as
@ -515,7 +519,9 @@ proc generateCppHeader*(
lines.add(" $1 $2($3) const {" % [methRet, methodName, methParamsStr])
lines.add(" const auto ffi_req_ = $1;" % [reqInit])
lines.add(" auto ffi_enc_ = encodeCborFFI(ffi_req_);")
lines.add(" if (ffi_enc_.isErr()) return $1::err(ffi_enc_.error());" % [methRet])
lines.add(
" if (ffi_enc_.isErr()) return $1::err(ffi_enc_.error());" % [methRet]
)
lines.add(" const auto& ffi_req_bytes_ = ffi_enc_.value();")
lines.add(" auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {")
lines.add(
@ -523,7 +529,9 @@ proc generateCppHeader*(
[m.procName]
)
lines.add(" }, timeout_);")
lines.add(" if (ffi_raw_.isErr()) return $1::err(ffi_raw_.error());" % [methRet])
lines.add(
" if (ffi_raw_.isErr()) return $1::err(ffi_raw_.error());" % [methRet]
)
lines.add(" return decodeCborFFI<$1>(ffi_raw_.value());" % [retCppType])
lines.add(" }")
lines.add("")
@ -533,8 +541,7 @@ proc generateCppHeader*(
# parse as invoking the `schedule` parameter).
if methParamsStr.len > 0:
lines.add(
" std::future<$1> $2Async($3) const {" %
[methRet, methodName, methParamsStr]
" std::future<$1> $2Async($3) const {" % [methRet, methodName, methParamsStr]
)
lines.add(
" return std::async(std::launch::async, [this, $1]() { return this->$2($3); });" %
@ -591,7 +598,6 @@ proc generateCppBindings*(
) =
createDir(outputDir)
writeFile(
outputDir / (libName & ".hpp"),
generateCppHeader(procs, types, libName, events),
outputDir / (libName & ".hpp"), generateCppHeader(procs, types, libName, events)
)
writeFile(outputDir / "CMakeLists.txt", generateCppCMakeLists(libName, nimSrcRelPath))

View File

@ -461,9 +461,7 @@ proc generateApiRs*(
let handlerStruct = capitalizeFirstLetter(ev.nimProcName) & "Handler"
let trampolineName = camelToSnakeCase(ev.nimProcName) & "_trampoline"
lines.add("struct $1 {" % [handlerStruct])
lines.add(
" f: Box<dyn Fn(&$1) + Send + Sync>," % [ev.payloadTypeName]
)
lines.add(" f: Box<dyn Fn(&$1) + Send + Sync>," % [ev.payloadTypeName])
lines.add("}")
lines.add("")
lines.add("unsafe extern \"C\" fn $1(" % [trampolineName])
@ -475,9 +473,7 @@ proc generateApiRs*(
lines.add(" let h = &*(ud as *const $1);" % [handlerStruct])
lines.add(" let bytes = slice::from_raw_parts(msg as *const u8, len);")
lines.add(" #[derive(serde::Deserialize)]")
lines.add(
" struct Envelope { payload: $1 }" % [ev.payloadTypeName]
)
lines.add(" struct Envelope { payload: $1 }" % [ev.payloadTypeName])
lines.add(
" if let Ok(env) = ciborium::de::from_reader::<Envelope, _>(bytes) {"
)
@ -600,7 +596,9 @@ proc generateApiRs*(
" let addr: usize = addr_str.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;"
)
if events.len > 0:
lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout, listeners: std::sync::Mutex::new(std::collections::HashMap::new()) })")
lines.add(
" Ok(Self { ptr: addr as *mut c_void, timeout, listeners: std::sync::Mutex::new(std::collections::HashMap::new()) })"
)
else:
lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout })")
lines.add(" }")
@ -626,7 +624,9 @@ proc generateApiRs*(
" let addr: usize = addr_str.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;"
)
if events.len > 0:
lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout, listeners: std::sync::Mutex::new(std::collections::HashMap::new()) })")
lines.add(
" Ok(Self { ptr: addr as *mut c_void, timeout, listeners: std::sync::Mutex::new(std::collections::HashMap::new()) })"
)
else:
lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout })")
lines.add(" }")
@ -669,19 +669,16 @@ proc generateApiRs*(
[ev.wireName]
)
lines.add(" /// passed to `remove_event_listener` to unregister.")
lines.add(
" pub fn $1<F>(&self, handler: F) -> ListenerHandle" % [methodName]
)
lines.add(
" where F: Fn(&$1) + Send + Sync + 'static," % [ev.payloadTypeName]
)
lines.add(" pub fn $1<F>(&self, handler: F) -> ListenerHandle" % [methodName])
lines.add(" where F: Fn(&$1) + Send + Sync + 'static," % [ev.payloadTypeName])
lines.add(" {")
lines.add(
" let owned: Box<$1> = Box::new($1 { f: Box::new(handler) });" %
[handlerStruct]
)
lines.add(" let raw = &*owned as *const $1 as *mut c_void;" %
[handlerStruct])
lines.add(
" let raw = &*owned as *const $1 as *mut c_void;" % [handlerStruct]
)
lines.add(
" self.add_listener_inner(b\"$1\\0\".as_ptr() as *const c_char, $2, raw, owned)" %
[ev.wireName, trampolineName]
@ -691,20 +688,15 @@ proc generateApiRs*(
# Remove by handle. Drops the Box (and the user's closure) after the
# C ABI confirms the listener has been unregistered.
lines.add(
" /// Remove a previously-registered listener by handle. Returns true"
)
lines.add(
" /// if the listener existed and was removed; false otherwise."
)
lines.add(" /// Remove a previously-registered listener by handle. Returns true")
lines.add(" /// if the listener existed and was removed; false otherwise.")
lines.add(
" pub fn remove_event_listener(&self, handle: ListenerHandle) -> bool {"
)
lines.add(" if handle.id == 0 { return false; }")
lines.add(" let rc = unsafe {")
lines.add(
" ffi::$1_remove_event_listener(self.ptr, handle.id)" %
[libName]
" ffi::$1_remove_event_listener(self.ptr, handle.id)" % [libName]
)
lines.add(" };")
lines.add(" self.listeners.lock().unwrap().remove(&handle.id);")

View File

@ -3,7 +3,11 @@
import std/[atomics, locks, json, tables]
import chronicles, chronos, chronos/threadsync, taskpools/channels_spsc_single, results
import
./ffi_types, ./ffi_events, ./ffi_thread_request, ./internal/ffi_macro, ./logging,
./ffi_types,
./ffi_events,
./ffi_thread_request,
./internal/ffi_macro,
./logging,
./cbor_serial
export ffi_events
@ -268,8 +272,7 @@ proc ffiThreadBody[T](ctx: ptr FFIContext[T]) {.thread.} =
try:
await allFutures(pending)
except CatchableError as exc:
error "draining pending FFI requests on shutdown raised",
error = exc.msg
error "draining pending FFI requests on shutdown raised", error = exc.msg
waitFor ffiRun(ctx)

View File

@ -158,9 +158,7 @@ var ffiCurrentEventRegistry* {.threadvar.}: ptr FFIEventRegistry
## Set by the FFI thread at startup so dispatchFFIEvent / dispatchFFIEventCbor
## can find their registry without taking a context pointer per call site.
template withFFIEventDispatch(
eventName: string, listeners, body: untyped
) =
template withFFIEventDispatch(eventName: string, listeners, body: untyped) =
## Shared scaffold for `dispatchFFIEvent` / `dispatchFFIEventCbor`:
## resolves the thread-local registry, snapshots listeners under
## `reg.lock` into the caller-named `listeners` binding, then runs
@ -219,9 +217,7 @@ template dispatchFFIEventCbor*(eventName: string, eventPayload: typed) =
## also replace the `payload:` field name inside `EventEnvelope`.
withFFIEventDispatch(eventName, listeners):
var (data, dataLen) = cborEncodeShared(
EventEnvelope[typeof(eventPayload)](
eventType: eventName, payload: eventPayload
)
EventEnvelope[typeof(eventPayload)](eventType: eventName, payload: eventPayload)
)
defer:
cborFreeShared(data)

View File

@ -143,7 +143,11 @@ macro declareLibrary*(libraryName: static[string], libType: untyped): untyped =
if isNil(ctx):
echo `addErr`
return ret
let evtName = if eventName.isNil(): "" else: $eventName
let evtName =
if eventName.isNil():
""
else:
$eventName
ret = addEventListener(ctx[].eventRegistry, evtName, callback, userData)
return ret

View File

@ -1419,8 +1419,10 @@ macro ffiEvent*(wireName: static[string], prc: untyped): untyped =
let payloadTypeNameStr =
case payloadTypeNode.kind
of nnkIdent: $payloadTypeNode
else: payloadTypeNode.repr
of nnkIdent:
$payloadTypeNode
else:
payloadTypeNode.repr
var userProcName = procName
if procName.kind == nnkPostfix:
@ -1428,13 +1430,8 @@ macro ffiEvent*(wireName: static[string], prc: untyped): untyped =
# The generated body: dispatchFFIEventCbor("wire_name", payload).
let wireNameLit = newStrLitNode(wireName)
let dispatchBody = newStmtList(
newCall(
ident("dispatchFFIEventCbor"),
wireNameLit,
payloadParamName,
)
)
let dispatchBody =
newStmtList(newCall(ident("dispatchFFIEventCbor"), wireNameLit, payloadParamName))
var newParams = newSeq[NimNode]()
newParams.add(formalParams[0]) # return type (typically empty/void)

View File

@ -72,8 +72,7 @@ proc callbackBytes(d: var CallbackData): seq[byte] =
registerReqFFI(EmitCborEventRequest, lib: ptr TestEvtLib):
proc(): Future[Result[string, string]] {.async.} =
dispatchFFIEventCbor(
"message_sent",
MessageSentBody(requestId: "req-1", messageHash: "0xdeadbeef"),
"message_sent", MessageSentBody(requestId: "req-1", messageHash: "0xdeadbeef")
)
return ok("emitted")
@ -90,16 +89,15 @@ registerReqFFI(EmitRawBytesEventRequest, lib: ptr TestEvtLib):
## event so a TSan-instrumented build can confirm `FFIEventRegistry.lock`
## serialises the cross-thread mutation against dispatch-time
## `snapshotListeners` reads from the FFI thread.
type SetterArgs = tuple
ctx: ptr FFIContext[TestEvtLib]
stop: ptr Atomic[bool]
target: ptr CallbackData
type SetterArgs =
tuple[
ctx: ptr FFIContext[TestEvtLib], stop: ptr Atomic[bool], target: ptr CallbackData
]
proc setterThreadBody(args: SetterArgs) {.thread.} =
while not args.stop[].load():
let id = addEventListener(
args.ctx[].eventRegistry, "message_sent", captureCb, args.target
)
let id =
addEventListener(args.ctx[].eventRegistry, "message_sent", captureCb, args.target)
discard removeEventListener(args.ctx[].eventRegistry, id)
suite "dispatchFFIEventCbor":
@ -117,9 +115,7 @@ suite "dispatchFFIEventCbor":
deinitCallbackData(evt)
# Subscribe to the specific event the request below dispatches.
discard addEventListener(
ctx[].eventRegistry, "message_sent", captureCb, addr evt
)
discard addEventListener(ctx[].eventRegistry, "message_sent", captureCb, addr evt)
# Trigger the dispatch from the FFI thread; the response callback is
# ignored (we only care that the request completed so we know the event
@ -157,9 +153,7 @@ suite "dispatchFFIEvent with seq[byte]":
defer:
deinitCallbackData(evt)
discard addEventListener(
ctx[].eventRegistry, "raw_bytes", captureCb, addr evt
)
discard addEventListener(ctx[].eventRegistry, "raw_bytes", captureCb, addr evt)
var rsp: CallbackData
initCallbackData(rsp)
@ -204,9 +198,7 @@ when not defined(gcRefc):
# target. The setter threads will then repeatedly re-install the same
# (callback, userData) pair — what matters is the cross-thread write
# racing the FFI thread's read, not which pair "wins".
discard addEventListener(
ctx[].eventRegistry, "message_sent", captureCb, addr evt
)
discard addEventListener(ctx[].eventRegistry, "message_sent", captureCb, addr evt)
const NumSetterThreads = 4
const NumDispatchIters = 200
@ -267,9 +259,7 @@ proc slowEventCb(
os.sleep(15)
st[].exited.store(true)
type DispatcherArgs = tuple
reg: ptr FFIEventRegistry
done: ptr Atomic[bool]
type DispatcherArgs = tuple[reg: ptr FFIEventRegistry, done: ptr Atomic[bool]]
proc dispatcherBody(args: DispatcherArgs) {.thread.} =
ffiCurrentEventRegistry = args.reg

View File

@ -301,8 +301,14 @@ suite "CBOR boundaries":
check cborDecode(bytes, uint64).value == v
test "float32 finite values incl. ±FLT_MAX":
for v in [float32(0.0), float32(-0.0), float32(1.5), float32(-1.5),
float32(3.4028235e38), float32(-3.4028235e38)]:
for v in [
float32(0.0),
float32(-0.0),
float32(1.5),
float32(-1.5),
float32(3.4028235e38),
float32(-3.4028235e38),
]:
let bytes = cborEncode(v)
check cborDecode(bytes, float32).value == v
@ -410,8 +416,8 @@ suite "CBOR round-trips":
check back.value == o
test "three-level struct nesting":
let d = DeepInner(tag: "outer",
nested: Nested(label: "mid", point: Point(x: 11, y: 22)))
let d =
DeepInner(tag: "outer", nested: Nested(label: "mid", point: Point(x: 11, y: 22)))
let bytes = cborEncode(d)
let back = cborDecode(bytes, DeepInner)
check back.isOk