mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-20 08:19:55 +00:00
Add basic cpp e2e tests (#27)
This commit is contained in:
parent
e12745e85c
commit
584e818ac9
47
.github/workflows/ci.yml
vendored
47
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -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
4
.gitignore
vendored
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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) {}
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "timer"
|
||||
name = "my_timer"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"] }
|
||||
|
||||
|
||||
@ -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.)");
|
||||
}
|
||||
|
||||
@ -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?;
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
61
ffi.nimble
61
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"
|
||||
|
||||
42
tests/e2e/cpp/CMakeLists.txt
Normal file
42
tests/e2e/cpp/CMakeLists.txt
Normal 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
29
tests/e2e/cpp/README.md
Normal 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
|
||||
```
|
||||
130
tests/e2e/cpp/test_timer_e2e.cpp
Normal file
130
tests/e2e/cpp/test_timer_e2e.cpp
Normal 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");
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import unittest2
|
||||
import ../ffi/alloc
|
||||
import ffi/alloc
|
||||
|
||||
suite "alloc(cstring)":
|
||||
test "nil input returns empty cstring":
|
||||
@ -1,7 +1,7 @@
|
||||
import std/[atomics, locks]
|
||||
import unittest2
|
||||
import results
|
||||
import ../ffi
|
||||
import ffi
|
||||
|
||||
type TestLib = object
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import std/[locks, options, strutils, os, atomics]
|
||||
import unittest2
|
||||
import results
|
||||
import ../ffi
|
||||
import ffi
|
||||
|
||||
type TestLib = object
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
import std/locks
|
||||
import unittest2
|
||||
import results
|
||||
import ../ffi
|
||||
import ffi
|
||||
|
||||
type GcTestLib = object
|
||||
|
||||
@ -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`,
|
||||
@ -7,7 +7,7 @@
|
||||
import std/options
|
||||
import unittest2
|
||||
import results
|
||||
import ../ffi
|
||||
import ffi
|
||||
|
||||
type Counter = object
|
||||
start: int
|
||||
@ -1,7 +1,7 @@
|
||||
import std/options
|
||||
import unittest
|
||||
import results
|
||||
import ../ffi
|
||||
import ffi
|
||||
|
||||
type Point {.ffi.} = object
|
||||
x: int
|
||||
@ -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":
|
||||
@ -17,7 +17,7 @@
|
||||
import std/[options, strutils]
|
||||
import unittest
|
||||
import results
|
||||
import ../ffi
|
||||
import ffi
|
||||
|
||||
type WireSimple {.ffi.} = object
|
||||
name: string
|
||||
Loading…
x
Reference in New Issue
Block a user