Add basic cpp e2e tests (#27)

This commit is contained in:
Ivan FB 2026-05-19 12:43:34 +02:00 committed by GitHub
parent e12745e85c
commit 584e818ac9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 449 additions and 183 deletions

View File

@ -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

View File

@ -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

4
.gitignore vendored
View File

@ -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

View File

@ -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"

View File

@ -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()

View File

@ -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

View File

@ -1,10 +1,10 @@
#include "timer.hpp"
#include "my_timer.hpp"
#include <iostream>
#include <future>
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();

View File

@ -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<std::uint8_t> ffi_call_(std::function<int(FFICallback, void*)
// High-level C++ context class
// ============================================================
class TimerCtx {
class MyTimerCtx {
public:
static TimerCtx create(const TimerConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
const auto ffi_req_ = TimerCreateCtorReq{config};
static MyTimerCtx create(const TimerConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
const auto ffi_req_ = MyTimerCreateCtorReq{config};
const auto ffi_req_bytes_ = encodeCborFFI(ffi_req_);
const auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {
(void)timer_create(ffi_req_bytes_.data(), ffi_req_bytes_.size(), cb, ud);
(void)my_timer_create(ffi_req_bytes_.data(), ffi_req_bytes_.size(), cb, ud);
return 0;
}, timeout);
const auto addr_str = decodeCborFFI<std::string>(ffi_raw_);
try {
const auto addr = std::stoull(addr_str);
return TimerCtx(reinterpret_cast<void*>(static_cast<uintptr_t>(addr)), timeout);
return MyTimerCtx(reinterpret_cast<void*>(static_cast<uintptr_t>(addr)), timeout);
} catch (const std::exception&) {
throw std::runtime_error("FFI create returned non-numeric address: " + addr_str);
}
}
static std::future<TimerCtx> createAsync(const TimerConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
static std::future<MyTimerCtx> 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<EchoResponse>(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<std::string>(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<ComplexResponse>(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<ScheduleResult>(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) {}
};

View File

@ -1,5 +1,5 @@
[package]
name = "timer"
name = "my_timer"
version = "0.1.0"
edition = "2021"

View File

@ -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

View File

@ -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());
}

View File

@ -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<Self, String> {
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<Self, String> {
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<EchoResponse, String> {
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::<EchoResponse>(&raw_bytes)
}
pub async fn echo_async(&self, req: EchoRequest) -> Result<EchoResponse, String> {
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::<EchoResponse>(&raw_bytes)
}
pub fn version(&self) -> Result<String, String> {
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::<String>(&raw_bytes)
}
pub async fn version_async(&self) -> Result<String, String> {
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::<String>(&raw_bytes)
}
pub fn complex(&self, req: ComplexRequest) -> Result<ComplexResponse, String> {
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::<ComplexResponse>(&raw_bytes)
}
pub async fn complex_async(&self, req: ComplexRequest) -> Result<ComplexResponse, String> {
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::<ComplexResponse>(&raw_bytes)
}
pub fn schedule(&self, job: JobSpec, retry: RetryPolicy, schedule: ScheduleConfig) -> Result<ScheduleResult, String> {
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::<ScheduleResult>(&raw_bytes)
}
pub async fn schedule_async(&self, job: JobSpec, retry: RetryPolicy, schedule: ScheduleConfig) -> Result<ScheduleResult, String> {
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::<ScheduleResult>(&raw_bytes)
}

View File

@ -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;
}

View File

@ -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,

View File

@ -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"] }

View File

@ -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.)");
}

View File

@ -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<dyn std::error::Error>> {
let ctx = TimerCtx::new_async(
let ctx = MyTimerCtx::new_async(
TimerConfig { name: "tokio-demo".into() },
Duration::from_secs(30),
).await?;

View File

@ -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 `<time.h>`'s `int timer_create(clockid_t, ...)` which
# `<pthread.h>` 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: "<none>"
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

View File

@ -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"

View File

@ -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"

View File

@ -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)

29
tests/e2e/cpp/README.md Normal file
View File

@ -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
```

View File

@ -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 <chrono>
#include <future>
#include <string>
#include <vector>
#include <gtest/gtest.h>
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::milliseconds>(
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>{EchoRequest{"a", 1}, EchoRequest{"b", 2}},
std::vector<std::string>{"tag1", "tag2"},
std::optional<std::string>("a note"),
std::optional<int64_t>(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<EchoRequest>{},
std::vector<std::string>{},
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=<none>"), std::string::npos)
<< "summary should report <none>: " << 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");
}

View File

@ -1,5 +1,5 @@
import unittest2
import ../ffi/alloc
import ffi/alloc
suite "alloc(cstring)":
test "nil input returns empty cstring":

View File

@ -1,7 +1,7 @@
import std/[atomics, locks]
import unittest2
import results
import ../ffi
import ffi
type TestLib = object

View File

@ -1,7 +1,7 @@
import std/[locks, options, strutils, os, atomics]
import unittest2
import results
import ../ffi
import ffi
type TestLib = object

View File

@ -8,7 +8,7 @@
import std/locks
import unittest2
import results
import ../ffi
import ffi
type GcTestLib = object

View File

@ -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`,

View File

@ -7,7 +7,7 @@
import std/options
import unittest2
import results
import ../ffi
import ffi
type Counter = object
start: int

View File

@ -1,7 +1,7 @@
import std/options
import unittest
import results
import ../ffi
import ffi
type Point {.ffi.} = object
x: int

View File

@ -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":

View File

@ -17,7 +17,7 @@
import std/[options, strutils]
import unittest
import results
import ../ffi
import ffi
type WireSimple {.ffi.} = object
name: string