Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/linters.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion config.nims
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ switch("path", thisDir())
--noNimblePath
when withDir(thisDir(), system.fileExists("nimble.paths")):
include "nimble.paths"
# end Nimble config
# end Nimble config
6 changes: 3 additions & 3 deletions examples/echo/echo.nimble
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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" &
Expand Down
3 changes: 1 addition & 2 deletions examples/timer/timer.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -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" &
Expand Down
109 changes: 48 additions & 61 deletions ffi.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -95,17 +90,24 @@ task test_cpp_e2e, "Build and run the C++ end-to-end tests for the timer example
runOrQuit "nimble genbindings_cpp"
runOrQuit "nimble genbindings_cpp_echo"
runOrQuit "cmake -S tests/e2e/cpp -B tests/e2e/cpp/build"
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)":
runOrQuit "cmake --build tests/e2e/cpp/build --config Debug"
# `-C Debug` is required on Windows multi-config generators because
# gtest_discover_tests(PRE_TEST) loads per-config include files; harmless on
# single-config generators (Make/Ninja) on Linux/macOS.
runOrQuit "ctest --test-dir tests/e2e/cpp/build --output-on-failure -C Debug"

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")
Expand All @@ -117,82 +119,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 --build tests/e2e/cpp/build -j"
runOrQuit "ctest --test-dir tests/e2e/cpp/build --output-on-failure"
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 --config Debug -j"
runOrQuit "ctest --test-dir tests/e2e/cpp/build --output-on-failure -C Debug"

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":
Expand Down
1 change: 0 additions & 1 deletion ffi/cbor_serial.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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]`.
Expand Down
54 changes: 30 additions & 24 deletions ffi/codegen/cpp.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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]
Expand All @@ -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`:
##
Expand All @@ -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;")
Expand All @@ -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("")
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -515,15 +519,19 @@ 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(
" return $1(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size());" %
[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("")
Expand All @@ -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); });" %
Expand Down Expand Up @@ -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))
Loading
Loading