diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e39193..f67d52f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,3 +36,50 @@ jobs: uses: ./.github/workflows/test.yml with: test: test_ctx_validation + + cpp-e2e: + name: C++ E2E + # Codegen output doesn't vary with mm and CMake/FetchContent is most + # reliable on Linux, so we run a single config rather than the full matrix. + runs-on: ubuntu-22.04 + env: + NIMBLE_VERSION: '0.22.3' + NIM_VERSION: '2.2.4' + steps: + - uses: actions/checkout@v4 + + - name: Setup Nim + uses: jiro4989/setup-nim-action@v2 + with: + nim-version: ${{ env.NIM_VERSION }} + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Nimble ${{ env.NIMBLE_VERSION }} + run: | + cd /tmp && nimble install "nimble@${{ env.NIMBLE_VERSION }}" -y + echo "$HOME/.nimble/bin" >> $GITHUB_PATH + + - name: Cache nimble deps + id: cache-nimbledeps + uses: actions/cache@v4 + with: + path: | + nimbledeps/ + nimble.paths + key: ${{ runner.os }}-nimbledeps-${{ env.NIM_VERSION }}-${{ hashFiles('*.nimble') }} + restore-keys: | + ${{ runner.os }}-nimbledeps-${{ env.NIM_VERSION }}- + ${{ runner.os }}-nimbledeps- + + - name: Install nimble deps + if: steps.cache-nimbledeps.outputs.cache-hit != 'true' + run: nimble setup --localdeps -y + + - name: Cache CMake FetchContent (GoogleTest + nlohmann_json) + uses: actions/cache@v4 + with: + path: tests/e2e/cpp/build/_deps + key: ${{ runner.os }}-cpp-e2e-deps-${{ hashFiles('tests/e2e/cpp/CMakeLists.txt') }} + + - name: Run C++ e2e tests + run: nimble test_cpp_e2e -y diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9295670..d854f5f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -82,4 +82,4 @@ jobs: if [ "$RUNNER_OS" == "Windows" ]; then export PATH="$GITHUB_WORKSPACE/.nim_runtime/bin:$HOME/.nimble/bin:$PATH" fi - nim c -r --mm:${{ matrix.mm }} -d:chronicles_log_level=WARN tests/${{ inputs.test }}.nim + nim c -r --mm:${{ matrix.mm }} -d:chronicles_log_level=WARN tests/unit/${{ inputs.test }}.nim diff --git a/.gitignore b/.gitignore index f2896a6..b8dbe58 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ PLAN.md .vscode/ .idea/ .DS_Store + +# Compiled test binaries (extensionless executables, also under tests/unit/) +tests/unit/test_* +!tests/unit/test_*.nim diff --git a/config.nims b/config.nims index 15157aa..ccb78e3 100644 --- a/config.nims +++ b/config.nims @@ -1,3 +1,7 @@ +# Make the project root importable so test/example code can write +# `import ffi/alloc` instead of `import ../../ffi/alloc`. +switch("path", thisDir()) + # begin Nimble config (version 2) when withDir(thisDir(), system.fileExists("nimble.paths")): include "nimble.paths" diff --git a/examples/timer/cpp_bindings/CMakeLists.txt b/examples/timer/cpp_bindings/CMakeLists.txt index 36ca8e6..07eae57 100644 --- a/examples/timer/cpp_bindings/CMakeLists.txt +++ b/examples/timer/cpp_bindings/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.14) -project(timer_cpp_bindings CXX C) +project(my_timer_cpp_bindings CXX C) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -25,11 +25,11 @@ get_filename_component(NIM_SRC find_program(NIM_EXECUTABLE nim REQUIRED) if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") - set(NIM_LIB_FILE "${REPO_ROOT}/libtimer.dylib") + set(NIM_LIB_FILE "${REPO_ROOT}/libmy_timer.dylib") elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") - set(NIM_LIB_FILE "${REPO_ROOT}/timer.dll") + set(NIM_LIB_FILE "${REPO_ROOT}/my_timer.dll") else() - set(NIM_LIB_FILE "${REPO_ROOT}/libtimer.so") + set(NIM_LIB_FILE "${REPO_ROOT}/libmy_timer.so") endif() add_custom_command( @@ -39,19 +39,19 @@ add_custom_command( -d:chronicles_log_level=WARN --app:lib --noMain - "--nimMainPrefix:libtimer" + "--nimMainPrefix:libmy_timer" "-o:${NIM_LIB_FILE}" "${NIM_SRC}" WORKING_DIRECTORY "${REPO_ROOT}" DEPENDS "${NIM_SRC}" - COMMENT "Compiling Nim library libtimer" + COMMENT "Compiling Nim library libmy_timer" VERBATIM ) add_custom_target(nim_lib ALL DEPENDS "${NIM_LIB_FILE}") -add_library(timer SHARED IMPORTED GLOBAL) -set_target_properties(timer PROPERTIES IMPORTED_LOCATION "${NIM_LIB_FILE}") -add_dependencies(timer nim_lib) +add_library(my_timer SHARED IMPORTED GLOBAL) +set_target_properties(my_timer PROPERTIES IMPORTED_LOCATION "${NIM_LIB_FILE}") +add_dependencies(my_timer nim_lib) # ── TinyCBOR (vendored at ffi/codegen/templates/cpp/vendor/tinycbor) ───────── set(TINYCBOR_SRC_DIR "${REPO_ROOT}/ffi/codegen/templates/cpp/vendor") @@ -69,12 +69,12 @@ target_include_directories(tinycbor PUBLIC set_property(TARGET tinycbor PROPERTY C_STANDARD 99) set_property(TARGET tinycbor PROPERTY POSITION_INDEPENDENT_CODE ON) -add_library(timer_headers INTERFACE) -target_include_directories(timer_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}") -target_link_libraries(timer_headers INTERFACE timer tinycbor) +add_library(my_timer_headers INTERFACE) +target_include_directories(my_timer_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(my_timer_headers INTERFACE my_timer tinycbor) if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp") add_executable(example main.cpp) - target_link_libraries(example PRIVATE timer_headers) + target_link_libraries(example PRIVATE my_timer_headers) add_dependencies(example nim_lib) endif() diff --git a/examples/timer/cpp_bindings/README.md b/examples/timer/cpp_bindings/README.md index b831fa4..ba61c49 100644 --- a/examples/timer/cpp_bindings/README.md +++ b/examples/timer/cpp_bindings/README.md @@ -2,9 +2,9 @@ ## Purpose -This folder contains **auto-generated C++ bindings** for the `timer` Nim library. It is generated from `../timer.nim` and provides: +This folder contains **auto-generated C++ bindings** for the `my_timer` Nim library. It is generated from `../timer.nim` and provides: -- `timer.hpp`: High-level C++ class (`TimerCtx`) wrapping the FFI interface +- `my_timer.hpp`: High-level C++ class (`MyTimerCtx`) wrapping the FFI interface - `main.cpp`: Example executable demonstrating how to use the bindings - `CMakeLists.txt`: Build configuration that compiles the Nim library and links the C++ example diff --git a/examples/timer/cpp_bindings/main.cpp b/examples/timer/cpp_bindings/main.cpp index 5694568..5a7e3c8 100644 --- a/examples/timer/cpp_bindings/main.cpp +++ b/examples/timer/cpp_bindings/main.cpp @@ -1,10 +1,10 @@ -#include "timer.hpp" +#include "my_timer.hpp" #include #include int main() { try { - auto ctx = TimerCtx::create(TimerConfig{"cpp-demo"}); + auto ctx = MyTimerCtx::create(TimerConfig{"cpp-demo"}); std::cout << "[1] Context created\n"; auto versionFuture = ctx.versionAsync(); diff --git a/examples/timer/cpp_bindings/timer.hpp b/examples/timer/cpp_bindings/my_timer.hpp similarity index 91% rename from examples/timer/cpp_bindings/timer.hpp rename to examples/timer/cpp_bindings/my_timer.hpp index 39d9788..93173cd 100644 --- a/examples/timer/cpp_bindings/timer.hpp +++ b/examples/timer/cpp_bindings/my_timer.hpp @@ -478,10 +478,10 @@ inline CborError decode_cbor(CborValue& it, ScheduleResult& v) { // Per-proc request envelopes (CBOR encoded on the wire) // ============================================================ -struct TimerCreateCtorReq { +struct MyTimerCreateCtorReq { TimerConfig config; }; -inline CborError encode_cbor(CborEncoder& e, const TimerCreateCtorReq& v) { +inline CborError encode_cbor(CborEncoder& e, const MyTimerCreateCtorReq& v) { CborEncoder m; CborError err = cbor_encoder_create_map(&e, &m, 1); if (err) return err; @@ -489,7 +489,7 @@ inline CborError encode_cbor(CborEncoder& e, const TimerCreateCtorReq& v) { err = encode_cbor(m, v.config); if (err) return err; return cbor_encoder_close_container(&e, &m); } -inline CborError decode_cbor(CborValue& it, TimerCreateCtorReq& v) { +inline CborError decode_cbor(CborValue& it, MyTimerCreateCtorReq& v) { if (!cbor_value_is_map(&it)) return CborErrorImproperValue; CborValue field; CborError err; @@ -499,10 +499,10 @@ inline CborError decode_cbor(CborValue& it, TimerCreateCtorReq& v) { return cbor_value_advance(&it); } -struct TimerEchoReq { +struct MyTimerEchoReq { EchoRequest req; }; -inline CborError encode_cbor(CborEncoder& e, const TimerEchoReq& v) { +inline CborError encode_cbor(CborEncoder& e, const MyTimerEchoReq& v) { CborEncoder m; CborError err = cbor_encoder_create_map(&e, &m, 1); if (err) return err; @@ -510,7 +510,7 @@ inline CborError encode_cbor(CborEncoder& e, const TimerEchoReq& v) { err = encode_cbor(m, v.req); if (err) return err; return cbor_encoder_close_container(&e, &m); } -inline CborError decode_cbor(CborValue& it, TimerEchoReq& v) { +inline CborError decode_cbor(CborValue& it, MyTimerEchoReq& v) { if (!cbor_value_is_map(&it)) return CborErrorImproperValue; CborValue field; CborError err; @@ -520,23 +520,23 @@ inline CborError decode_cbor(CborValue& it, TimerEchoReq& v) { return cbor_value_advance(&it); } -struct TimerVersionReq { +struct MyTimerVersionReq { }; -inline CborError encode_cbor(CborEncoder& e, const TimerVersionReq&) { +inline CborError encode_cbor(CborEncoder& e, const MyTimerVersionReq&) { CborEncoder m; CborError err = cbor_encoder_create_map(&e, &m, 0); if (err) return err; return cbor_encoder_close_container(&e, &m); } -inline CborError decode_cbor(CborValue& it, TimerVersionReq&) { +inline CborError decode_cbor(CborValue& it, MyTimerVersionReq&) { if (!cbor_value_is_map(&it)) return CborErrorImproperValue; return cbor_value_advance(&it); } -struct TimerComplexReq { +struct MyTimerComplexReq { ComplexRequest req; }; -inline CborError encode_cbor(CborEncoder& e, const TimerComplexReq& v) { +inline CborError encode_cbor(CborEncoder& e, const MyTimerComplexReq& v) { CborEncoder m; CborError err = cbor_encoder_create_map(&e, &m, 1); if (err) return err; @@ -544,7 +544,7 @@ inline CborError encode_cbor(CborEncoder& e, const TimerComplexReq& v) { err = encode_cbor(m, v.req); if (err) return err; return cbor_encoder_close_container(&e, &m); } -inline CborError decode_cbor(CborValue& it, TimerComplexReq& v) { +inline CborError decode_cbor(CborValue& it, MyTimerComplexReq& v) { if (!cbor_value_is_map(&it)) return CborErrorImproperValue; CborValue field; CborError err; @@ -554,12 +554,12 @@ inline CborError decode_cbor(CborValue& it, TimerComplexReq& v) { return cbor_value_advance(&it); } -struct TimerScheduleReq { +struct MyTimerScheduleReq { JobSpec job; RetryPolicy retry; ScheduleConfig schedule; }; -inline CborError encode_cbor(CborEncoder& e, const TimerScheduleReq& v) { +inline CborError encode_cbor(CborEncoder& e, const MyTimerScheduleReq& v) { CborEncoder m; CborError err = cbor_encoder_create_map(&e, &m, 3); if (err) return err; @@ -571,7 +571,7 @@ inline CborError encode_cbor(CborEncoder& e, const TimerScheduleReq& v) { err = encode_cbor(m, v.schedule); if (err) return err; return cbor_encoder_close_container(&e, &m); } -inline CborError decode_cbor(CborValue& it, TimerScheduleReq& v) { +inline CborError decode_cbor(CborValue& it, MyTimerScheduleReq& v) { if (!cbor_value_is_map(&it)) return CborErrorImproperValue; CborValue field; CborError err; @@ -594,12 +594,12 @@ inline CborError decode_cbor(CborValue& it, TimerScheduleReq& v) { extern "C" { typedef void (*FFICallback)(int ret, const char* msg, size_t len, void* user_data); -void* timer_create(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback callback, void* user_data); -int timer_echo(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); -int timer_version(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); -int timer_complex(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); -int timer_schedule(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); -int timer_destroy(void* ctx); +void* my_timer_create(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback callback, void* user_data); +int my_timer_echo(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); +int my_timer_version(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); +int my_timer_complex(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); +int my_timer_schedule(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); +int my_timer_destroy(void* ctx); } // extern "C" // ============================================================ @@ -659,29 +659,29 @@ inline std::vector ffi_call_(std::function(ffi_raw_); try { const auto addr = std::stoull(addr_str); - return TimerCtx(reinterpret_cast(static_cast(addr)), timeout); + return MyTimerCtx(reinterpret_cast(static_cast(addr)), timeout); } catch (const std::exception&) { throw std::runtime_error("FFI create returned non-numeric address: " + addr_str); } } - static std::future createAsync(const TimerConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) { + static std::future createAsync(const TimerConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) { return std::async(std::launch::async, [config, timeout]() { return create(config, timeout); }); } - // Rule of Five: because this class owns a raw resource (the timer + // Rule of Five: because this class owns a raw resource (the my_timer // context pointer freed in the destructor), the compiler-generated copy // and move special members would do the wrong thing — copies would // double-free, and a default move would leave both objects pointing at @@ -693,22 +693,22 @@ public: // 5. move assignment — destroys the current context, then // transfers ownership from `other`. // See: https://en.cppreference.com/w/cpp/language/rule_of_three - ~TimerCtx() { + ~MyTimerCtx() { if (ptr_) { - timer_destroy(ptr_); + my_timer_destroy(ptr_); ptr_ = nullptr; } } - TimerCtx(const TimerCtx&) = delete; - TimerCtx& operator=(const TimerCtx&) = delete; + MyTimerCtx(const MyTimerCtx&) = delete; + MyTimerCtx& operator=(const MyTimerCtx&) = delete; - TimerCtx(TimerCtx&& other) noexcept : ptr_(other.ptr_), timeout_(other.timeout_) { + MyTimerCtx(MyTimerCtx&& other) noexcept : ptr_(other.ptr_), timeout_(other.timeout_) { other.ptr_ = nullptr; } - TimerCtx& operator=(TimerCtx&& other) noexcept { + MyTimerCtx& operator=(MyTimerCtx&& other) noexcept { if (this != &other) { - if (ptr_) timer_destroy(ptr_); + if (ptr_) my_timer_destroy(ptr_); ptr_ = other.ptr_; timeout_ = other.timeout_; other.ptr_ = nullptr; @@ -717,10 +717,10 @@ public: } EchoResponse echo(const EchoRequest& req) const { - const auto ffi_req_ = TimerEchoReq{req}; + const auto ffi_req_ = MyTimerEchoReq{req}; const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_); const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { - return timer_echo(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); + return my_timer_echo(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); }, timeout_); return decodeCborFFI(ffi_raw_); } @@ -730,10 +730,10 @@ public: } std::string version() const { - const auto ffi_req_ = TimerVersionReq{}; + const auto ffi_req_ = MyTimerVersionReq{}; const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_); const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { - return timer_version(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); + return my_timer_version(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); }, timeout_); return decodeCborFFI(ffi_raw_); } @@ -743,10 +743,10 @@ public: } ComplexResponse complex(const ComplexRequest& req) const { - const auto ffi_req_ = TimerComplexReq{req}; + const auto ffi_req_ = MyTimerComplexReq{req}; const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_); const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { - return timer_complex(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); + return my_timer_complex(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); }, timeout_); return decodeCborFFI(ffi_raw_); } @@ -756,10 +756,10 @@ public: } ScheduleResult schedule(const JobSpec& job, const RetryPolicy& retry, const ScheduleConfig& schedule) const { - const auto ffi_req_ = TimerScheduleReq{job, retry, schedule}; + const auto ffi_req_ = MyTimerScheduleReq{job, retry, schedule}; const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_); const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) { - return timer_schedule(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); + return my_timer_schedule(ptr_, cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size()); }, timeout_); return decodeCborFFI(ffi_raw_); } @@ -771,5 +771,5 @@ public: private: void* ptr_; std::chrono::milliseconds timeout_; - explicit TimerCtx(void* p, std::chrono::milliseconds t) : ptr_(p), timeout_(t) {} + explicit MyTimerCtx(void* p, std::chrono::milliseconds t) : ptr_(p), timeout_(t) {} }; diff --git a/examples/timer/rust_bindings/Cargo.toml b/examples/timer/rust_bindings/Cargo.toml index 1e4dde1..b34251a 100644 --- a/examples/timer/rust_bindings/Cargo.toml +++ b/examples/timer/rust_bindings/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "timer" +name = "my_timer" version = "0.1.0" edition = "2021" diff --git a/examples/timer/rust_bindings/README.md b/examples/timer/rust_bindings/README.md index 8c5ba38..1a337fe 100644 --- a/examples/timer/rust_bindings/README.md +++ b/examples/timer/rust_bindings/README.md @@ -2,13 +2,13 @@ ## Purpose -This folder contains **auto-generated Rust bindings** (the `timer` crate) for the `timer` Nim library. It is generated from `../timer.nim` and provides: +This folder contains **auto-generated Rust bindings** (the `my_timer` crate) for the `my_timer` Nim library. It is generated from `../timer.nim` and provides: -- `src/lib.rs`: Main library exposing high-level Rust types and the `TimerCtx` API +- `src/lib.rs`: Main library exposing high-level Rust types and the `MyTimerCtx` API - `src/api.rs`: High-level async/sync wrapper around the FFI - `src/ffi.rs`: Raw `extern "C"` declarations for the Nim library - `src/types.rs`: Serializable Rust types matching the Nim FFI types -- `build.rs`: Build script that compiles the Nim library to `libtimer.dylib` (or `.so`/`.dll`) +- `build.rs`: Build script that compiles the Nim library to `libmy_timer.dylib` (or `.so`/`.dll`) - `Cargo.toml`: Package manifest with serde and serde_json dependencies ## How It's Generated @@ -31,7 +31,7 @@ The `rust_client` example consumes this crate: ```toml [dependencies] -timer = { path = "../rust_bindings" } +my_timer = { path = "../rust_bindings" } ``` ## Do Not Edit diff --git a/examples/timer/rust_bindings/build.rs b/examples/timer/rust_bindings/build.rs index bc7a1b0..ce9c76f 100644 --- a/examples/timer/rust_bindings/build.rs +++ b/examples/timer/rust_bindings/build.rs @@ -26,7 +26,7 @@ fn main() { #[cfg(target_os = "linux")] let lib_ext = "so"; - let out_lib = repo_root.join(format!("libtimer.{lib_ext}")); + let out_lib = repo_root.join(format!("libmy_timer.{lib_ext}")); let mut cmd = Command::new("nim"); cmd.arg("c") @@ -34,7 +34,7 @@ fn main() { .arg("-d:chronicles_log_level=WARN") .arg("--app:lib") .arg("--noMain") - .arg(format!("--nimMainPrefix:libtimer")) + .arg(format!("--nimMainPrefix:libmy_timer")) .arg(format!("-o:{}", out_lib.display())); cmd.arg(&nim_src).current_dir(&repo_root); @@ -42,6 +42,6 @@ fn main() { assert!(status.success(), "Nim compilation failed"); println!("cargo:rustc-link-search={}", repo_root.display()); - println!("cargo:rustc-link-lib=timer"); + println!("cargo:rustc-link-lib=my_timer"); println!("cargo:rerun-if-changed={}", nim_src.display()); } diff --git a/examples/timer/rust_bindings/src/api.rs b/examples/timer/rust_bindings/src/api.rs index 35cba77..ba1a9a6 100644 --- a/examples/timer/rust_bindings/src/api.rs +++ b/examples/timer/rust_bindings/src/api.rs @@ -98,8 +98,8 @@ where } } -/// High-level context for `Timer`. -pub struct TimerCtx { +/// High-level context for `MyTimer`. +pub struct MyTimerCtx { ptr: *mut c_void, timeout: Duration, } @@ -112,24 +112,24 @@ pub struct TimerCtx { // guard (`onFFIThread` threadvar) prevents handlers from re-entering the // dispatcher and self-deadlocking. These invariants make it sound to mark // the wrapper as Send + Sync. -unsafe impl Send for TimerCtx {} -unsafe impl Sync for TimerCtx {} +unsafe impl Send for MyTimerCtx {} +unsafe impl Sync for MyTimerCtx {} -impl Drop for TimerCtx { +impl Drop for MyTimerCtx { fn drop(&mut self) { if !self.ptr.is_null() { - unsafe { ffi::timer_destroy(self.ptr); } + unsafe { ffi::my_timer_destroy(self.ptr); } self.ptr = std::ptr::null_mut(); } } } -impl TimerCtx { +impl MyTimerCtx { pub fn create(config: TimerConfig, timeout: Duration) -> Result { - let req = TimerCreateCtorReq { config }; + let req = MyTimerCreateCtorReq { config }; let req_bytes = encode_cbor(&req)?; let raw_bytes = ffi_call_sync(timeout, |cb, ud| unsafe { - let _ = ffi::timer_create(req_bytes.as_ptr(), req_bytes.len(), cb, ud); + let _ = ffi::my_timer_create(req_bytes.as_ptr(), req_bytes.len(), cb, ud); 0 })?; let addr_str: String = decode_cbor(&raw_bytes)?; @@ -138,10 +138,10 @@ impl TimerCtx { } pub async fn new_async(config: TimerConfig, timeout: Duration) -> Result { - let req = TimerCreateCtorReq { config }; + let req = MyTimerCreateCtorReq { config }; let req_bytes = encode_cbor(&req)?; let raw_bytes = ffi_call_async(timeout, move |cb, ud| unsafe { - let _ = ffi::timer_create(req_bytes.as_ptr(), req_bytes.len(), cb, ud); + let _ = ffi::my_timer_create(req_bytes.as_ptr(), req_bytes.len(), cb, ud); 0 }).await?; let addr_str: String = decode_cbor(&raw_bytes)?; @@ -150,77 +150,77 @@ impl TimerCtx { } pub fn echo(&self, req: EchoRequest) -> Result { - let req = TimerEchoReq { req }; + let req = MyTimerEchoReq { req }; let req_bytes = encode_cbor(&req)?; let raw_bytes = ffi_call_sync(self.timeout, |cb, ud| unsafe { - ffi::timer_echo(self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len()) + ffi::my_timer_echo(self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len()) })?; decode_cbor::(&raw_bytes) } pub async fn echo_async(&self, req: EchoRequest) -> Result { - let req = TimerEchoReq { req }; + let req = MyTimerEchoReq { req }; let req_bytes = encode_cbor(&req)?; let ptr = self.ptr as usize; let raw_bytes = ffi_call_async(self.timeout, move |cb, ud| unsafe { - ffi::timer_echo(ptr as *mut c_void, cb, ud, req_bytes.as_ptr(), req_bytes.len()) + ffi::my_timer_echo(ptr as *mut c_void, cb, ud, req_bytes.as_ptr(), req_bytes.len()) }).await?; decode_cbor::(&raw_bytes) } pub fn version(&self) -> Result { - let req = TimerVersionReq {}; + let req = MyTimerVersionReq {}; let req_bytes = encode_cbor(&req)?; let raw_bytes = ffi_call_sync(self.timeout, |cb, ud| unsafe { - ffi::timer_version(self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len()) + ffi::my_timer_version(self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len()) })?; decode_cbor::(&raw_bytes) } pub async fn version_async(&self) -> Result { - let req = TimerVersionReq {}; + let req = MyTimerVersionReq {}; let req_bytes = encode_cbor(&req)?; let ptr = self.ptr as usize; let raw_bytes = ffi_call_async(self.timeout, move |cb, ud| unsafe { - ffi::timer_version(ptr as *mut c_void, cb, ud, req_bytes.as_ptr(), req_bytes.len()) + ffi::my_timer_version(ptr as *mut c_void, cb, ud, req_bytes.as_ptr(), req_bytes.len()) }).await?; decode_cbor::(&raw_bytes) } pub fn complex(&self, req: ComplexRequest) -> Result { - let req = TimerComplexReq { req }; + let req = MyTimerComplexReq { req }; let req_bytes = encode_cbor(&req)?; let raw_bytes = ffi_call_sync(self.timeout, |cb, ud| unsafe { - ffi::timer_complex(self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len()) + ffi::my_timer_complex(self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len()) })?; decode_cbor::(&raw_bytes) } pub async fn complex_async(&self, req: ComplexRequest) -> Result { - let req = TimerComplexReq { req }; + let req = MyTimerComplexReq { req }; let req_bytes = encode_cbor(&req)?; let ptr = self.ptr as usize; let raw_bytes = ffi_call_async(self.timeout, move |cb, ud| unsafe { - ffi::timer_complex(ptr as *mut c_void, cb, ud, req_bytes.as_ptr(), req_bytes.len()) + ffi::my_timer_complex(ptr as *mut c_void, cb, ud, req_bytes.as_ptr(), req_bytes.len()) }).await?; decode_cbor::(&raw_bytes) } pub fn schedule(&self, job: JobSpec, retry: RetryPolicy, schedule: ScheduleConfig) -> Result { - let req = TimerScheduleReq { job, retry, schedule }; + let req = MyTimerScheduleReq { job, retry, schedule }; let req_bytes = encode_cbor(&req)?; let raw_bytes = ffi_call_sync(self.timeout, |cb, ud| unsafe { - ffi::timer_schedule(self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len()) + ffi::my_timer_schedule(self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len()) })?; decode_cbor::(&raw_bytes) } pub async fn schedule_async(&self, job: JobSpec, retry: RetryPolicy, schedule: ScheduleConfig) -> Result { - let req = TimerScheduleReq { job, retry, schedule }; + let req = MyTimerScheduleReq { job, retry, schedule }; let req_bytes = encode_cbor(&req)?; let ptr = self.ptr as usize; let raw_bytes = ffi_call_async(self.timeout, move |cb, ud| unsafe { - ffi::timer_schedule(ptr as *mut c_void, cb, ud, req_bytes.as_ptr(), req_bytes.len()) + ffi::my_timer_schedule(ptr as *mut c_void, cb, ud, req_bytes.as_ptr(), req_bytes.len()) }).await?; decode_cbor::(&raw_bytes) } diff --git a/examples/timer/rust_bindings/src/ffi.rs b/examples/timer/rust_bindings/src/ffi.rs index 9b6f4f7..4d11d0d 100644 --- a/examples/timer/rust_bindings/src/ffi.rs +++ b/examples/timer/rust_bindings/src/ffi.rs @@ -7,12 +7,12 @@ pub type FFICallback = unsafe extern "C" fn( user_data: *mut c_void, ); -#[link(name = "timer")] +#[link(name = "my_timer")] extern "C" { - pub fn timer_create(req_cbor: *const u8, req_cbor_len: usize, callback: FFICallback, user_data: *mut c_void) -> *mut c_void; - pub fn timer_echo(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int; - pub fn timer_version(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int; - pub fn timer_complex(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int; - pub fn timer_schedule(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int; - pub fn timer_destroy(ctx: *mut c_void) -> c_int; + pub fn my_timer_create(req_cbor: *const u8, req_cbor_len: usize, callback: FFICallback, user_data: *mut c_void) -> *mut c_void; + pub fn my_timer_echo(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int; + pub fn my_timer_version(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int; + pub fn my_timer_complex(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int; + pub fn my_timer_schedule(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int; + pub fn my_timer_destroy(ctx: *mut c_void) -> c_int; } diff --git a/examples/timer/rust_bindings/src/types.rs b/examples/timer/rust_bindings/src/types.rs index c5383af..dac8cbe 100644 --- a/examples/timer/rust_bindings/src/types.rs +++ b/examples/timer/rust_bindings/src/types.rs @@ -75,25 +75,25 @@ pub struct ScheduleResult { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TimerCreateCtorReq { +pub struct MyTimerCreateCtorReq { pub config: TimerConfig, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TimerEchoReq { +pub struct MyTimerEchoReq { pub req: EchoRequest, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TimerVersionReq {} +pub struct MyTimerVersionReq {} #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TimerComplexReq { +pub struct MyTimerComplexReq { pub req: ComplexRequest, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TimerScheduleReq { +pub struct MyTimerScheduleReq { pub job: JobSpec, pub retry: RetryPolicy, pub schedule: ScheduleConfig, diff --git a/examples/timer/rust_client/Cargo.toml b/examples/timer/rust_client/Cargo.toml index 08c3865..ac2c0b3 100644 --- a/examples/timer/rust_client/Cargo.toml +++ b/examples/timer/rust_client/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -timer = { path = "../rust_bindings" } +my_timer = { path = "../rust_bindings" } serde_json = "1" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } diff --git a/examples/timer/rust_client/src/main.rs b/examples/timer/rust_client/src/main.rs index 42f90a8..e437f6d 100644 --- a/examples/timer/rust_client/src/main.rs +++ b/examples/timer/rust_client/src/main.rs @@ -1,26 +1,25 @@ -// Rust client for the timer shared library built with nim-ffi + chronos. +// Rust client for the my_timer shared library built with nim-ffi + chronos. // -// This file uses the generated `timer` crate, which wraps all the raw FFI +// This file uses the generated `my_timer` crate, which wraps all the raw FFI // boilerplate (extern "C" declarations, callback machinery, CBOR encode/decode). // // To regenerate the `rust_bindings` crate: -// nim c --mm:orc -d:chronicles_log_level=WARN --nimMainPrefix:libtimer \ -// -d:ffiGenBindings examples/timer/timer.nim +// nimble genbindings_rust use std::time::Duration; -use timer::{ - EchoRequest, JobSpec, RetryPolicy, ScheduleConfig, TimerConfig, TimerCtx, +use my_timer::{ + EchoRequest, JobSpec, MyTimerCtx, RetryPolicy, ScheduleConfig, TimerConfig, }; fn main() { let timeout = Duration::from_secs(5); // ── 1. Create the timer service ──────────────────────────────────────── - let ctx = TimerCtx::create(TimerConfig { name: "demo".into() }, timeout) - .expect("timer_create failed"); + let ctx = MyTimerCtx::create(TimerConfig { name: "demo".into() }, timeout) + .expect("my_timer_create failed"); println!("[1] Context created"); // ── 2. Sync call: version ────────────────────────────────────────────── - let version = ctx.version().expect("timer_version failed"); + let version = ctx.version().expect("my_timer_version failed"); println!("[2] Version (sync call, callback fired inline): {version}"); // ── 3. Async call: echo (200 ms delay) ──────────────────────────────── @@ -29,7 +28,7 @@ fn main() { message: "hello from Rust".into(), delay_ms: 200, }) - .expect("timer_echo failed"); + .expect("my_timer_echo failed"); println!( "[3] Echo (async, 200 ms chronos delay): echoed={}, timerName={}", echo.echoed, echo.timer_name @@ -41,7 +40,7 @@ fn main() { message: "second request".into(), delay_ms: 50, }) - .expect("second timer_echo failed"); + .expect("second my_timer_echo failed"); println!("[4] Echo: echoed={}, timerName={}", echo2.echoed, echo2.timer_name); // ── 5. Call with three complex parameters ───────────────────────────── @@ -66,7 +65,7 @@ fn main() { jitter: Some(250), }, ) - .expect("timer_schedule failed"); + .expect("my_timer_schedule failed"); println!( "[5] Schedule (3 complex params): jobId={}, willRunCount={}, firstRunAtMs={}, effectiveBackoffMs={}", schedule.job_id, @@ -76,5 +75,5 @@ fn main() { ); println!("\nDone. The Nim FFI thread and watchdog are still running."); - println!("(In a real app, call timer_destroy to join them gracefully.)"); + println!("(In a real app, call my_timer_destroy to join them gracefully.)"); } diff --git a/examples/timer/rust_client/src/tokio_main.rs b/examples/timer/rust_client/src/tokio_main.rs index e3c7c46..e1bdb17 100644 --- a/examples/timer/rust_client/src/tokio_main.rs +++ b/examples/timer/rust_client/src/tokio_main.rs @@ -1,11 +1,11 @@ use std::time::Duration; -use timer::{ - EchoRequest, JobSpec, RetryPolicy, ScheduleConfig, TimerConfig, TimerCtx, +use my_timer::{ + EchoRequest, JobSpec, MyTimerCtx, RetryPolicy, ScheduleConfig, TimerConfig, }; #[tokio::main(flavor = "multi_thread", worker_threads = 2)] async fn main() -> Result<(), Box> { - let ctx = TimerCtx::new_async( + let ctx = MyTimerCtx::new_async( TimerConfig { name: "tokio-demo".into() }, Duration::from_secs(30), ).await?; diff --git a/examples/timer/timer.nim b/examples/timer/timer.nim index 5ecf789..098fd1a 100644 --- a/examples/timer/timer.nim +++ b/examples/timer/timer.nim @@ -3,10 +3,14 @@ import ffi, chronos, options type Maybe[T] = Option[T] # The library's main state type. The FFI context owns one instance. -type Timer = object +# Named `MyTimer` (not `Timer`) so the C-exported symbols are +# `my_timer_create` / `my_timer_destroy` / ... — `timer_create` would +# collide with POSIX ``'s `int timer_create(clockid_t, ...)` which +# `` transitively drags in on Linux. +type MyTimer = object name: string # set at creation time, read back in each response -declareLibrary("timer", Timer) +declareLibrary("my_timer", MyTimer) type TimerConfig {.ffi.} = object name: string @@ -31,17 +35,17 @@ type ComplexResponse {.ffi.} = object hasNote: bool # --- Constructor ----------------------------------------------------------- -# Called once from Rust. Creates the FFIContext + Timer. +# Called once from Rust. Creates the FFIContext + MyTimer. # Uses chronos (await sleepAsync) so the body is async. -proc timerCreate*(config: TimerConfig): Future[Result[Timer, string]] {.ffiCtor.} = +proc myTimerCreate*(config: TimerConfig): Future[Result[MyTimer, string]] {.ffiCtor.} = await sleepAsync(1.milliseconds) # proves chronos is live on the FFI thread - return ok(Timer(name: config.name)) + return ok(MyTimer(name: config.name)) # --- Async method ---------------------------------------------------------- # Waits `delayMs` milliseconds (non-blocking, on the chronos event loop) # then echoes the message back with a request counter. -proc timerEcho*( - timer: Timer, req: EchoRequest +proc myTimerEcho*( + timer: MyTimer, req: EchoRequest ): Future[Result[EchoResponse, string]] {.ffi.} = await sleepAsync(req.delayMs.milliseconds) return ok(EchoResponse(echoed: req.message, timerName: timer.name)) @@ -49,11 +53,11 @@ proc timerEcho*( # --- Sync method ----------------------------------------------------------- # No await — the macro detects this and fires the callback inline, # without going through the request channel. -proc timerVersion*(timer: Timer): Future[Result[string, string]] {.ffi.} = +proc myTimerVersion*(timer: MyTimer): Future[Result[string, string]] {.ffi.} = return ok("nim-timer v0.1.0") -proc timerComplex*( - timer: Timer, req: ComplexRequest +proc myTimerComplex*( + timer: MyTimer, req: ComplexRequest ): Future[Result[ComplexResponse, string]] {.ffi.} = let note = if req.note.isSome: req.note.get else: "" let retries = if req.retries.isSome: req.retries.get else: 0 @@ -67,8 +71,8 @@ proc timerComplex*( # Demonstrates how a {.ffi.} proc handles several object-typed parameters at # once. Each parameter is its own {.ffi.} type, so it lands in the generated # foreign-side bindings as a first-class struct/class, and the per-proc Req -# envelope (TimerScheduleReq on the wire) carries all three under field names -# that match the Nim params. +# envelope (MyTimerScheduleReq on the wire) carries all three under field +# names that match the Nim params. type JobSpec {.ffi.} = object name: string payload: seq[string] @@ -90,8 +94,8 @@ type ScheduleResult {.ffi.} = object firstRunAtMs: int effectiveBackoffMs: int -proc timerSchedule*( - timer: Timer, job: JobSpec, retry: RetryPolicy, schedule: ScheduleConfig +proc myTimerSchedule*( + timer: MyTimer, job: JobSpec, retry: RetryPolicy, schedule: ScheduleConfig ): Future[Result[ScheduleResult, string]] {.ffi.} = ## Composes three independent object-typed parameters (`job`, `retry`, ## `schedule`) into a single scheduling decision. The macro packs them into @@ -117,8 +121,8 @@ proc timerSchedule*( ) ) -proc timer_destroy*(timer: Timer) {.ffiDtor.} = - ## Tears down the FFI context created by timer_create. +proc my_timer_destroy*(timer: MyTimer) {.ffiDtor.} = + ## Tears down the FFI context created by my_timer_create. ## Blocks until the FFI thread and watchdog thread have joined. discard diff --git a/examples/timer/timer.nimble b/examples/timer/timer.nimble index 4f9c33c..02343b7 100644 --- a/examples/timer/timer.nimble +++ b/examples/timer/timer.nimble @@ -14,14 +14,14 @@ const nimFlags = "--mm:orc -d:chronicles_log_level=WARN" task build, "Compile the timer library": exec "nim c " & nimFlags & - " --app:lib --noMain --nimMainPrefix:libtimer timer.nim" + " --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:libtimer" & + exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libmy_timer" & " -d:ffiGenBindings -d:targetLang=rust" & " -d:ffiOutputDir=rust_bindings" & - " -d:ffiNimSrcRelPath=timer.nim" & " -o:/dev/null timer.nim" + " -d:ffiSrcPath=timer.nim" & " -o:/dev/null timer.nim" task genbindings_cpp, "Generate C++ bindings for the timer example": - exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libtimer" & + exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libmy_timer" & " -d:ffiGenBindings -d:targetLang=cpp" & " -d:ffiOutputDir=cpp_bindings" & - " -d:ffiNimSrcRelPath=timer.nim" & " -o:/dev/null timer.nim" + " -d:ffiSrcPath=timer.nim" & " -o:/dev/null timer.nim" diff --git a/ffi.nimble b/ffi.nimble index 753811f..47b1b8b 100644 --- a/ffi.nimble +++ b/ffi.nimble @@ -21,45 +21,52 @@ task buildffi, "Compile the library": task test, "Run all tests under --mm:orc and --mm:refc": for flags in [nimFlagsOrc, nimFlagsRefc]: - exec "nim c -r " & flags & " tests/test_alloc.nim" - exec "nim c -r " & flags & " tests/test_ffi_context.nim" - exec "nim c -r " & flags & " tests/test_gc_compat.nim" - exec "nim c -r " & flags & " tests/test_serial.nim" - exec "nim c -r " & flags & " tests/test_ctx_validation.nim" - exec "nim c -r " & flags & " tests/test_nim_native_api.nim" - exec "nim c -r " & flags & " tests/test_meta.nim" - exec "nim c -r " & flags & " tests/test_string_helpers.nim" - exec "nim c -r " & flags & " tests/test_wire_compat.nim" - exec "nim c -r " & flags & " tests/test_cddl_codegen.nim" + exec "nim c -r " & flags & " tests/unit/test_alloc.nim" + exec "nim c -r " & flags & " tests/unit/test_ffi_context.nim" + exec "nim c -r " & flags & " tests/unit/test_gc_compat.nim" + exec "nim c -r " & flags & " tests/unit/test_serial.nim" + exec "nim c -r " & flags & " tests/unit/test_ctx_validation.nim" + exec "nim c -r " & flags & " tests/unit/test_nim_native_api.nim" + exec "nim c -r " & flags & " tests/unit/test_meta.nim" + exec "nim c -r " & flags & " tests/unit/test_string_helpers.nim" + exec "nim c -r " & flags & " tests/unit/test_wire_compat.nim" + exec "nim c -r " & flags & " tests/unit/test_cddl_codegen.nim" task test_alloc, "Run alloc unit tests under --mm:orc and --mm:refc": - exec "nim c -r " & nimFlagsOrc & " tests/test_alloc.nim" - exec "nim c -r " & nimFlagsRefc & " tests/test_alloc.nim" + exec "nim c -r " & nimFlagsOrc & " tests/unit/test_alloc.nim" + exec "nim c -r " & nimFlagsRefc & " tests/unit/test_alloc.nim" task test_ffi, "Run FFI context integration tests under --mm:orc and --mm:refc": - exec "nim c -r " & nimFlagsOrc & " tests/test_ffi_context.nim" - exec "nim c -r " & nimFlagsRefc & " tests/test_ffi_context.nim" + exec "nim c -r " & nimFlagsOrc & " tests/unit/test_ffi_context.nim" + exec "nim c -r " & nimFlagsRefc & " tests/unit/test_ffi_context.nim" task test_serial, "Run CBOR codec unit tests": - exec "nim c -r " & nimFlagsOrc & " tests/test_serial.nim" - exec "nim c -r " & nimFlagsRefc & " tests/test_serial.nim" + exec "nim c -r " & nimFlagsOrc & " tests/unit/test_serial.nim" + exec "nim c -r " & nimFlagsRefc & " tests/unit/test_serial.nim" + +task test_cpp_e2e, "Build and run the C++ end-to-end tests for the timer example": + # Regenerate the C++ bindings so the suite always runs against fresh codegen. + exec "nimble genbindings_cpp" + exec "cmake -S tests/e2e/cpp -B tests/e2e/cpp/build" + exec "cmake --build tests/e2e/cpp/build" + exec "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:libtimer -d:ffiGenBindings -o:/dev/null examples/timer/timer.nim" - exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libtimer -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:libtimer" & + " --app:lib --noMain --nimMainPrefix:libmy_timer" & " -d:ffiGenBindings -d:targetLang=rust" & " -d:ffiOutputDir=examples/timer/rust_bindings" & - " -d:ffiNimSrcRelPath=../timer.nim" & + " -d:ffiSrcPath=../timer.nim" & " -o:/dev/null examples/timer/timer.nim" exec "nim c " & nimFlagsRefc & - " --app:lib --noMain --nimMainPrefix:libtimer" & + " --app:lib --noMain --nimMainPrefix:libmy_timer" & " -d:ffiGenBindings -d:targetLang=rust" & " -d:ffiOutputDir=examples/timer/rust_bindings" & - " -d:ffiNimSrcRelPath=../timer.nim" & + " -d:ffiSrcPath=../timer.nim" & " -o:/dev/null examples/timer/timer.nim" task genbindings_cddl, "Generate CDDL schema for the timer example": @@ -67,19 +74,19 @@ task genbindings_cddl, "Generate CDDL schema for the timer example": " --app:lib --noMain --nimMainPrefix:libtimer" & " -d:ffiGenBindings -d:targetLang=cddl" & " -d:ffiOutputDir=examples/timer/cddl_bindings" & - " -d:ffiNimSrcRelPath=../timer.nim" & + " -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:libtimer" & + " --app:lib --noMain --nimMainPrefix:libmy_timer" & " -d:ffiGenBindings -d:targetLang=cpp" & " -d:ffiOutputDir=examples/timer/cpp_bindings" & - " -d:ffiNimSrcRelPath=../timer.nim" & + " -d:ffiSrcPath=../timer.nim" & " -o:/dev/null examples/timer/timer.nim" exec "nim c " & nimFlagsRefc & - " --app:lib --noMain --nimMainPrefix:libtimer" & + " --app:lib --noMain --nimMainPrefix:libmy_timer" & " -d:ffiGenBindings -d:targetLang=cpp" & " -d:ffiOutputDir=examples/timer/cpp_bindings" & - " -d:ffiNimSrcRelPath=../timer.nim" & + " -d:ffiSrcPath=../timer.nim" & " -o:/dev/null examples/timer/timer.nim" diff --git a/tests/e2e/cpp/CMakeLists.txt b/tests/e2e/cpp/CMakeLists.txt new file mode 100644 index 0000000..41b2940 --- /dev/null +++ b/tests/e2e/cpp/CMakeLists.txt @@ -0,0 +1,42 @@ +cmake_minimum_required(VERSION 3.14) +project(nim_ffi_cpp_e2e CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# ── Reuse the timer cpp_bindings (compiles libmy_timer + exposes the +# my_timer_headers INTERFACE target) ── +get_filename_component(_cpp_bindings_dir + "${CMAKE_CURRENT_SOURCE_DIR}/../../../examples/timer/cpp_bindings" + ABSOLUTE) +add_subdirectory("${_cpp_bindings_dir}" cpp_bindings_build) + +# ── GoogleTest via FetchContent ─────────────────────────────────────────────── +include(FetchContent) +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.14.0 + GIT_SHALLOW TRUE +) +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) + +enable_testing() + +add_executable(timer_e2e_tests test_timer_e2e.cpp) +target_link_libraries(timer_e2e_tests PRIVATE my_timer_headers GTest::gtest_main) +add_dependencies(timer_e2e_tests nim_lib) + +# The Nim-built shared library has install_name `@rpath/libmy_timer.dylib` +# (set by `declareLibrary` on macOS for portability). The test binary must +# therefore know where to find that dylib at load time — embed the build-tree +# directory of the IMPORTED `my_timer` target as an rpath. +get_target_property(_my_timer_loc my_timer IMPORTED_LOCATION) +get_filename_component(_my_timer_dir "${_my_timer_loc}" DIRECTORY) +set_target_properties(timer_e2e_tests PROPERTIES + BUILD_RPATH "${_my_timer_dir}" + INSTALL_RPATH "${_my_timer_dir}") + +include(GoogleTest) +gtest_discover_tests(timer_e2e_tests) diff --git a/tests/e2e/cpp/README.md b/tests/e2e/cpp/README.md new file mode 100644 index 0000000..e26a656 --- /dev/null +++ b/tests/e2e/cpp/README.md @@ -0,0 +1,29 @@ +# C++ end-to-end tests + +These tests validate that a Nim FFI library exported with `nim-ffi`'s C++ +codegen is usable from a real C++ consumer. They drive the `my_timer` example +through its auto-generated `my_timer.hpp` bindings (constructor, sync method, +async methods, complex types with optional fields, multiple contexts) and +assert the round-tripped values. + +## Layout + +The suite reuses the generated bindings instead of duplicating the Nim build +glue: + +- `CMakeLists.txt` — `add_subdirectory`s `examples/timer/cpp_bindings`, which + compiles `libmy_timer` and exposes the `my_timer_headers` INTERFACE target. + Fetches GoogleTest and registers tests with CTest via `gtest_discover_tests`. +- `test_timer_e2e.cpp` — the test cases. + +## Running + +```sh +# 1. Generate the C++ bindings (writes examples/timer/cpp_bindings/) +nimble genbindings_cpp + +# 2. Configure + build + run the tests +cmake -S tests/e2e/cpp -B tests/e2e/cpp/build +cmake --build tests/e2e/cpp/build +ctest --test-dir tests/e2e/cpp/build --output-on-failure +``` diff --git a/tests/e2e/cpp/test_timer_e2e.cpp b/tests/e2e/cpp/test_timer_e2e.cpp new file mode 100644 index 0000000..79e9f6b --- /dev/null +++ b/tests/e2e/cpp/test_timer_e2e.cpp @@ -0,0 +1,130 @@ +// Basic C++ end-to-end tests for the auto-generated `timer` bindings. +// +// These tests link against the same `timer_headers` INTERFACE library and Nim +// shared object used by `examples/timer/cpp_bindings/main.cpp`. They exercise +// the full FFI round-trip — CBOR encode -> Nim FFI thread -> chronos -> CBOR +// decode -> C++ — to validate that a binding produced by `nimble +// genbindings_cpp` is callable end-to-end from C++. + +#include "my_timer.hpp" + +#include +#include +#include +#include + +#include + +namespace { + +MyTimerCtx makeCtx(const std::string& name = "e2e") { + return MyTimerCtx::create(TimerConfig{name}); +} + +} // namespace + +TEST(TimerE2E, CreateAndDestroy) { + auto ctx = makeCtx("create-destroy"); + // Destruction happens at scope exit via MyTimerCtx::~MyTimerCtx, + // which invokes timer_destroy on the underlying FFI context. + SUCCEED(); +} + +TEST(TimerE2E, VersionSync) { + auto ctx = makeCtx("version-sync"); + const auto v = ctx.version(); + EXPECT_EQ(v, "nim-timer v0.1.0"); +} + +TEST(TimerE2E, VersionAsync) { + auto ctx = makeCtx("version-async"); + auto fut = ctx.versionAsync(); + EXPECT_EQ(fut.get(), "nim-timer v0.1.0"); +} + +TEST(TimerE2E, EchoRoundTripsMessageAndTimerName) { + auto ctx = makeCtx("echo-ctx"); + const auto resp = ctx.echo(EchoRequest{"hello", 10}); + EXPECT_EQ(resp.echoed, "hello"); + EXPECT_EQ(resp.timerName, "echo-ctx"); +} + +TEST(TimerE2E, EchoHonoursDelay) { + auto ctx = makeCtx("echo-delay"); + constexpr int delayMs = 150; + + const auto start = std::chrono::steady_clock::now(); + const auto resp = ctx.echo(EchoRequest{"waited", delayMs}); + const auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start).count(); + + EXPECT_EQ(resp.echoed, "waited"); + EXPECT_GE(elapsed, delayMs - 20) // allow a tiny scheduler-precision slack + << "echo returned too early: " << elapsed << "ms < " << delayMs << "ms"; +} + +TEST(TimerE2E, ConcurrentAsyncCallsAreIndependent) { + auto ctx = makeCtx("concurrent"); + + auto f1 = ctx.echoAsync(EchoRequest{"one", 80}); + auto f2 = ctx.echoAsync(EchoRequest{"two", 40}); + auto f3 = ctx.echoAsync(EchoRequest{"three", 20}); + + const auto r3 = f3.get(); + const auto r2 = f2.get(); + const auto r1 = f1.get(); + + EXPECT_EQ(r1.echoed, "one"); + EXPECT_EQ(r2.echoed, "two"); + EXPECT_EQ(r3.echoed, "three"); + EXPECT_EQ(r1.timerName, "concurrent"); + EXPECT_EQ(r2.timerName, "concurrent"); + EXPECT_EQ(r3.timerName, "concurrent"); +} + +TEST(TimerE2E, ComplexWithOptionalNotePresent) { + auto ctx = makeCtx("complex-1"); + ComplexRequest req{ + std::vector{EchoRequest{"a", 1}, EchoRequest{"b", 2}}, + std::vector{"tag1", "tag2"}, + std::optional("a note"), + std::optional(2), + }; + + const auto resp = ctx.complex(req); + EXPECT_EQ(resp.itemCount, 2); + EXPECT_TRUE(resp.hasNote); + EXPECT_NE(resp.summary.find("note=a note"), std::string::npos) + << "summary missing note: " << resp.summary; + EXPECT_NE(resp.summary.find("retries=2"), std::string::npos) + << "summary missing retries: " << resp.summary; +} + +TEST(TimerE2E, ComplexWithOptionalNoteAbsent) { + auto ctx = makeCtx("complex-2"); + ComplexRequest req{ + std::vector{}, + std::vector{}, + std::nullopt, + std::nullopt, + }; + + const auto resp = ctx.complex(req); + EXPECT_EQ(resp.itemCount, 0); + EXPECT_FALSE(resp.hasNote); + EXPECT_NE(resp.summary.find("note="), std::string::npos) + << "summary should report : " << resp.summary; + EXPECT_NE(resp.summary.find("retries=0"), std::string::npos) + << "summary should report retries=0: " << resp.summary; +} + +TEST(TimerE2E, IndependentContextsKeepTheirOwnState) { + auto ctxA = makeCtx("alpha"); + auto ctxB = makeCtx("beta"); + + const auto rA = ctxA.echo(EchoRequest{"x", 5}); + const auto rB = ctxB.echo(EchoRequest{"x", 5}); + + EXPECT_EQ(rA.timerName, "alpha"); + EXPECT_EQ(rB.timerName, "beta"); +} diff --git a/tests/test_alloc.nim b/tests/unit/test_alloc.nim similarity index 98% rename from tests/test_alloc.nim rename to tests/unit/test_alloc.nim index f07448c..cd56e4c 100644 --- a/tests/test_alloc.nim +++ b/tests/unit/test_alloc.nim @@ -1,5 +1,5 @@ import unittest2 -import ../ffi/alloc +import ffi/alloc suite "alloc(cstring)": test "nil input returns empty cstring": diff --git a/tests/test_cddl_codegen.nim b/tests/unit/test_cddl_codegen.nim similarity index 100% rename from tests/test_cddl_codegen.nim rename to tests/unit/test_cddl_codegen.nim diff --git a/tests/test_ctx_validation.nim b/tests/unit/test_ctx_validation.nim similarity index 99% rename from tests/test_ctx_validation.nim rename to tests/unit/test_ctx_validation.nim index f14f71a..e7a4e10 100644 --- a/tests/test_ctx_validation.nim +++ b/tests/unit/test_ctx_validation.nim @@ -1,7 +1,7 @@ import std/[atomics, locks] import unittest2 import results -import ../ffi +import ffi type TestLib = object diff --git a/tests/test_ffi_context.nim b/tests/unit/test_ffi_context.nim similarity index 99% rename from tests/test_ffi_context.nim rename to tests/unit/test_ffi_context.nim index 9a68fbe..f33f760 100644 --- a/tests/test_ffi_context.nim +++ b/tests/unit/test_ffi_context.nim @@ -1,7 +1,7 @@ import std/[locks, options, strutils, os, atomics] import unittest2 import results -import ../ffi +import ffi type TestLib = object diff --git a/tests/test_gc_compat.nim b/tests/unit/test_gc_compat.nim similarity index 99% rename from tests/test_gc_compat.nim rename to tests/unit/test_gc_compat.nim index 970e11b..d04962d 100644 --- a/tests/test_gc_compat.nim +++ b/tests/unit/test_gc_compat.nim @@ -8,7 +8,7 @@ import std/locks import unittest2 import results -import ../ffi +import ffi type GcTestLib = object diff --git a/tests/test_meta.nim b/tests/unit/test_meta.nim similarity index 98% rename from tests/test_meta.nim rename to tests/unit/test_meta.nim index 5438922..8451416 100644 --- a/tests/test_meta.nim +++ b/tests/unit/test_meta.nim @@ -4,7 +4,7 @@ import unittest import std/[macros, strutils] -import ../ffi/internal/ffi_macro +import ffi/internal/ffi_macro suite "unpackReqField": ## `unpackReqField` builds AST via `std/macros` helpers (`ident`, `newDotExpr`, diff --git a/tests/test_nim_native_api.nim b/tests/unit/test_nim_native_api.nim similarity index 99% rename from tests/test_nim_native_api.nim rename to tests/unit/test_nim_native_api.nim index 184a55f..90c2906 100644 --- a/tests/test_nim_native_api.nim +++ b/tests/unit/test_nim_native_api.nim @@ -7,7 +7,7 @@ import std/options import unittest2 import results -import ../ffi +import ffi type Counter = object start: int diff --git a/tests/test_serial.nim b/tests/unit/test_serial.nim similarity index 99% rename from tests/test_serial.nim rename to tests/unit/test_serial.nim index e0b3d86..c2dd54d 100644 --- a/tests/test_serial.nim +++ b/tests/unit/test_serial.nim @@ -1,7 +1,7 @@ import std/options import unittest import results -import ../ffi +import ffi type Point {.ffi.} = object x: int diff --git a/tests/test_string_helpers.nim b/tests/unit/test_string_helpers.nim similarity index 98% rename from tests/test_string_helpers.nim rename to tests/unit/test_string_helpers.nim index a49be6b..00bb913 100644 --- a/tests/test_string_helpers.nim +++ b/tests/unit/test_string_helpers.nim @@ -4,7 +4,7 @@ ## for binding generation, so it's worth pinning their behaviour with tests. import unittest -import ../ffi/codegen/string_helpers +import ffi/codegen/string_helpers suite "camelToSnakeCase": test "empty string": diff --git a/tests/test_wire_compat.nim b/tests/unit/test_wire_compat.nim similarity index 99% rename from tests/test_wire_compat.nim rename to tests/unit/test_wire_compat.nim index 6d9856d..ddcccec 100644 --- a/tests/test_wire_compat.nim +++ b/tests/unit/test_wire_compat.nim @@ -17,7 +17,7 @@ import std/[options, strutils] import unittest import results -import ../ffi +import ffi type WireSimple {.ffi.} = object name: string