Start using CBOR (#23)

Co-authored-by: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com>
Co-authored-by: Gabriel Cruz <8129788+gmelodie@users.noreply.github.com>
This commit is contained in:
Ivan FB
2026-05-16 01:08:42 +02:00
committed by GitHub
co-authored by NagyZoltanPeter Gabriel Cruz
parent 159c9287d8
commit ac303a707e
68 changed files with 8594 additions and 2665 deletions
@@ -0,0 +1,80 @@
cmake_minimum_required(VERSION 3.14)
project(timer_cpp_bindings CXX C)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ── Locate the repository root (contains ffi.nimble) ─────────────────────────
set(_search_dir "${CMAKE_CURRENT_SOURCE_DIR}")
set(REPO_ROOT "")
foreach(_i RANGE 10)
if(EXISTS "${_search_dir}/ffi.nimble")
set(REPO_ROOT "${_search_dir}")
break()
endif()
get_filename_component(_search_dir "${_search_dir}" DIRECTORY)
endforeach()
if("${REPO_ROOT}" STREQUAL "")
message(FATAL_ERROR "Cannot find repo root (no ffi.nimble in any ancestor)")
endif()
get_filename_component(NIM_SRC
"${CMAKE_CURRENT_SOURCE_DIR}/../timer.nim"
ABSOLUTE)
find_program(NIM_EXECUTABLE nim REQUIRED)
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
set(NIM_LIB_FILE "${REPO_ROOT}/libtimer.dylib")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
set(NIM_LIB_FILE "${REPO_ROOT}/timer.dll")
else()
set(NIM_LIB_FILE "${REPO_ROOT}/libtimer.so")
endif()
add_custom_command(
OUTPUT "${NIM_LIB_FILE}"
COMMAND "${NIM_EXECUTABLE}" c
--mm:orc
-d:chronicles_log_level=WARN
--app:lib
--noMain
"--nimMainPrefix:libtimer"
"-o:${NIM_LIB_FILE}"
"${NIM_SRC}"
WORKING_DIRECTORY "${REPO_ROOT}"
DEPENDS "${NIM_SRC}"
COMMENT "Compiling Nim library libtimer"
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)
# ── TinyCBOR (vendored at ffi/codegen/templates/cpp/vendor/tinycbor) ─────────
set(TINYCBOR_SRC_DIR "${REPO_ROOT}/ffi/codegen/templates/cpp/vendor")
add_library(tinycbor STATIC
"${TINYCBOR_SRC_DIR}/tinycbor/cborencoder.c"
"${TINYCBOR_SRC_DIR}/tinycbor/cborencoder_close_container_checked.c"
"${TINYCBOR_SRC_DIR}/tinycbor/cborparser.c"
"${TINYCBOR_SRC_DIR}/tinycbor/cborparser_dup_string.c"
"${TINYCBOR_SRC_DIR}/tinycbor/cborerrorstrings.c"
)
target_include_directories(tinycbor PUBLIC
"${TINYCBOR_SRC_DIR}" # consumer uses #include <tinycbor/cbor.h>
"${TINYCBOR_SRC_DIR}/tinycbor" # internal _p.h includes resolve here
)
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)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp")
add_executable(example main.cpp)
target_link_libraries(example PRIVATE timer_headers)
add_dependencies(example nim_lib)
endif()
+36
View File
@@ -0,0 +1,36 @@
# C++ Bindings for nim-timer
## Purpose
This folder contains **auto-generated C++ bindings** for the `timer` Nim library. It is generated from `../timer.nim` and provides:
- `timer.hpp`: High-level C++ class (`TimerCtx`) 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
## How It's Generated
Generate or regenerate these bindings by running from the parent directory:
```sh
cd examples/timer
nimble genbindings_cpp
```
This command:
1. Invokes the Nim compiler with `-d:targetLang:cpp` flag
2. Triggers `genBindings("examples/timer/cpp_bindings", "../timer.nim")` in `timer.nim`
3. Creates/updates the generated binding files
## Building the Example
```sh
cd examples/timer/cpp_bindings
cmake -S . -B build
cmake --build build
./build/example
```
## Do Not Edit
The generated files in this folder are overwritten each time `nimble genbindings_cpp` runs. Any manual changes will be lost.
+72
View File
@@ -0,0 +1,72 @@
#include "timer.hpp"
#include <iostream>
#include <future>
int main() {
try {
auto ctx = TimerCtx::create(TimerConfig{"cpp-demo"});
std::cout << "[1] Context created\n";
auto versionFuture = ctx.versionAsync();
auto echo1Future = ctx.echoAsync(EchoRequest{"hello from C++", 200});
auto echo2Future = ctx.echoAsync(EchoRequest{"second C++ request", 50});
auto version = versionFuture.get();
std::cout << "[2] Version: " << version << "\n";
auto echo = echo1Future.get();
std::cout << "[3] Echo 1: echoed=" << echo.echoed
<< ", timerName=" << echo.timerName << "\n";
auto echo2 = echo2Future.get();
std::cout << "[4] Echo 2: echoed=" << echo2.echoed
<< ", timerName=" << echo2.timerName << "\n";
auto complexReq = ComplexRequest{
std::vector<EchoRequest>{EchoRequest{"one", 10}, EchoRequest{"two", 20}},
std::vector<std::string>{"fast", "async"},
std::optional<std::string>("extra note"),
std::optional<int64_t>(3)
};
auto complexFuture = ctx.complexAsync(complexReq);
auto complex = complexFuture.get();
std::cout << "[5] Complex: summary=" << complex.summary
<< ", itemCount=" << complex.itemCount
<< ", hasNote=" << complex.hasNote << "\n";
// ── 6. Call with three complex parameters ─────────────────────
// Each parameter is its own generated C++ struct. The nim-ffi
// macro packs all three into one CBOR envelope on the wire — at
// the call site, this is just a typed method invocation.
auto job = JobSpec{
/*name*/ "nightly-rollup",
/*payload*/ std::vector<std::string>{"rollup", "v2"},
/*priority*/ 10,
};
auto retry = RetryPolicy{
/*maxAttempts*/ 3,
/*backoffMs*/ 500,
/*retryOn*/ std::vector<std::string>{"timeout", "5xx"},
};
auto schedule = ScheduleConfig{
/*startAtMs*/ 1000,
/*intervalMs*/ 15000,
/*jitter*/ std::optional<int64_t>(250),
};
auto scheduleFuture = ctx.scheduleAsync(job, retry, schedule);
auto scheduleRes = scheduleFuture.get();
std::cout << "[6] Schedule (3 complex params): jobId=" << scheduleRes.jobId
<< ", willRunCount=" << scheduleRes.willRunCount
<< ", firstRunAtMs=" << scheduleRes.firstRunAtMs
<< ", effectiveBackoffMs=" << scheduleRes.effectiveBackoffMs
<< "\n";
std::cout << "\nDone.\n";
} catch (const std::exception& ex) {
std::cerr << "Error: " << ex.what() << "\n";
return 1;
}
return 0;
}
+775
View File
@@ -0,0 +1,775 @@
#pragma once
#include <string>
#include <cstdint>
#include <chrono>
#include <stdexcept>
#include <mutex>
#include <condition_variable>
#include <memory>
#include <functional>
#include <future>
#include <vector>
#include <optional>
#include <type_traits>
#include <cstring>
extern "C" {
#include <tinycbor/cbor.h>
}
// ── encode_cbor overloads (primitives + containers) ─────────────────────
// Per-struct encode_cbor / decode_cbor are emitted by cpp.nim next to each
// generated struct. These helpers cover the leaf types and container shapes
// the struct emitters defer into.
inline CborError encode_cbor(CborEncoder& e, bool v) {
return cbor_encode_boolean(&e, v);
}
inline CborError encode_cbor(CborEncoder& e, int64_t v) {
return cbor_encode_int(&e, v);
}
inline CborError encode_cbor(CborEncoder& e, int32_t v) {
return cbor_encode_int(&e, static_cast<int64_t>(v));
}
inline CborError encode_cbor(CborEncoder& e, uint64_t v) {
return cbor_encode_uint(&e, v);
}
inline CborError encode_cbor(CborEncoder& e, double v) {
return cbor_encode_double(&e, v);
}
inline CborError encode_cbor(CborEncoder& e, const std::string& v) {
return cbor_encode_text_string(&e, v.data(), v.size());
}
template<typename T>
inline CborError encode_cbor(CborEncoder& e, const std::vector<T>& v) {
CborEncoder arr;
CborError err = cbor_encoder_create_array(&e, &arr, v.size());
if (err) return err;
for (const auto& item : v) {
err = encode_cbor(arr, item);
if (err) return err;
}
return cbor_encoder_close_container(&e, &arr);
}
template<typename T>
inline CborError encode_cbor(CborEncoder& e, const std::optional<T>& v) {
if (!v) return cbor_encode_null(&e);
return encode_cbor(e, *v);
}
// ── decode_cbor overloads ───────────────────────────────────────────────
inline CborError decode_cbor(CborValue& it, bool& out) {
if (!cbor_value_is_boolean(&it)) return CborErrorImproperValue;
CborError err = cbor_value_get_boolean(&it, &out);
if (err) return err;
return cbor_value_advance(&it);
}
inline CborError decode_cbor(CborValue& it, int64_t& out) {
if (!cbor_value_is_integer(&it)) return CborErrorImproperValue;
CborError err = cbor_value_get_int64_checked(&it, &out);
if (err) return err;
return cbor_value_advance(&it);
}
inline CborError decode_cbor(CborValue& it, int32_t& out) {
int64_t tmp = 0;
CborError err = decode_cbor(it, tmp);
if (err) return err;
out = static_cast<int32_t>(tmp);
return CborNoError;
}
inline CborError decode_cbor(CborValue& it, uint64_t& out) {
if (!cbor_value_is_unsigned_integer(&it)) return CborErrorImproperValue;
CborError err = cbor_value_get_uint64(&it, &out);
if (err) return err;
return cbor_value_advance(&it);
}
inline CborError decode_cbor(CborValue& it, double& out) {
if (cbor_value_is_double(&it)) {
CborError err = cbor_value_get_double(&it, &out);
if (err) return err;
return cbor_value_advance(&it);
}
if (cbor_value_is_float(&it)) {
float f = 0.0f;
CborError err = cbor_value_get_float(&it, &f);
if (err) return err;
out = static_cast<double>(f);
return cbor_value_advance(&it);
}
return CborErrorImproperValue;
}
inline CborError decode_cbor(CborValue& it, std::string& out) {
if (!cbor_value_is_text_string(&it)) return CborErrorImproperValue;
size_t len = 0;
CborError err = cbor_value_get_string_length(&it, &len);
if (err) return err;
out.resize(len);
err = cbor_value_copy_text_string(&it, out.empty() ? nullptr : &out[0], &len, nullptr);
if (err) return err;
return cbor_value_advance(&it);
}
template<typename T>
inline CborError decode_cbor(CborValue& it, std::vector<T>& out) {
if (!cbor_value_is_array(&it)) return CborErrorImproperValue;
size_t len = 0;
CborError err = cbor_value_get_array_length(&it, &len);
if (err) return err;
out.clear();
out.resize(len);
CborValue inner;
err = cbor_value_enter_container(&it, &inner);
if (err) return err;
for (size_t i = 0; i < len; ++i) {
err = decode_cbor(inner, out[i]);
if (err) return err;
}
return cbor_value_leave_container(&it, &inner);
}
template<typename T>
inline CborError decode_cbor(CborValue& it, std::optional<T>& out) {
if (cbor_value_is_null(&it)) {
out = std::nullopt;
return cbor_value_advance(&it);
}
T tmp{};
CborError err = decode_cbor(it, tmp);
if (err) return err;
out = std::move(tmp);
return CborNoError;
}
// ── Public entry points ─────────────────────────────────────────────────
template<typename T>
inline std::vector<std::uint8_t> encodeCborFFI(const T& value) {
// Start with a generous 4 KiB buffer; double on overflow until it fits.
std::vector<std::uint8_t> buf(4096);
while (true) {
CborEncoder enc;
cbor_encoder_init(&enc, buf.data(), buf.size(), 0);
CborError err = encode_cbor(enc, value);
if (err == CborNoError) {
const size_t used = cbor_encoder_get_buffer_size(&enc, buf.data());
buf.resize(used);
return buf;
}
if (err == CborErrorOutOfMemory) {
const size_t extra = cbor_encoder_get_extra_bytes_needed(&enc);
buf.resize(buf.size() + (extra > 0 ? extra : buf.size()));
continue;
}
throw std::runtime_error(std::string("FFI CBOR encode failed: ") +
cbor_error_string(err));
}
}
template<typename T>
inline T decodeCborFFI(const std::vector<std::uint8_t>& bytes) {
CborParser parser;
CborValue it;
CborError err = cbor_parser_init(bytes.data(), bytes.size(), 0, &parser, &it);
if (err != CborNoError) {
throw std::runtime_error(std::string("FFI CBOR parse init failed: ") +
cbor_error_string(err));
}
T out{};
err = decode_cbor(it, out);
if (err != CborNoError) {
throw std::runtime_error(std::string("FFI CBOR decode failed: ") +
cbor_error_string(err));
}
return out;
}
// ============================================================
// User-declared FFI types
// ============================================================
struct TimerConfig {
std::string name;
};
inline CborError encode_cbor(CborEncoder& e, const TimerConfig& v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(&e, &m, 1);
if (err) return err;
err = cbor_encode_text_stringz(&m, "name"); if (err) return err;
err = encode_cbor(m, v.name); if (err) return err;
return cbor_encoder_close_container(&e, &m);
}
inline CborError decode_cbor(CborValue& it, TimerConfig& v) {
if (!cbor_value_is_map(&it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(&it, "name", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.name); if (err) return err;
return cbor_value_advance(&it);
}
struct EchoRequest {
std::string message;
int64_t delayMs;
};
inline CborError encode_cbor(CborEncoder& e, const EchoRequest& v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(&e, &m, 2);
if (err) return err;
err = cbor_encode_text_stringz(&m, "message"); if (err) return err;
err = encode_cbor(m, v.message); if (err) return err;
err = cbor_encode_text_stringz(&m, "delayMs"); if (err) return err;
err = encode_cbor(m, v.delayMs); if (err) return err;
return cbor_encoder_close_container(&e, &m);
}
inline CborError decode_cbor(CborValue& it, EchoRequest& v) {
if (!cbor_value_is_map(&it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(&it, "message", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.message); if (err) return err;
err = cbor_value_map_find_value(&it, "delayMs", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.delayMs); if (err) return err;
return cbor_value_advance(&it);
}
struct EchoResponse {
std::string echoed;
std::string timerName;
};
inline CborError encode_cbor(CborEncoder& e, const EchoResponse& v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(&e, &m, 2);
if (err) return err;
err = cbor_encode_text_stringz(&m, "echoed"); if (err) return err;
err = encode_cbor(m, v.echoed); if (err) return err;
err = cbor_encode_text_stringz(&m, "timerName"); if (err) return err;
err = encode_cbor(m, v.timerName); if (err) return err;
return cbor_encoder_close_container(&e, &m);
}
inline CborError decode_cbor(CborValue& it, EchoResponse& v) {
if (!cbor_value_is_map(&it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(&it, "echoed", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.echoed); if (err) return err;
err = cbor_value_map_find_value(&it, "timerName", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.timerName); if (err) return err;
return cbor_value_advance(&it);
}
struct ComplexRequest {
std::vector<EchoRequest> messages;
std::vector<std::string> tags;
std::optional<std::string> note;
std::optional<int64_t> retries;
};
inline CborError encode_cbor(CborEncoder& e, const ComplexRequest& v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(&e, &m, 4);
if (err) return err;
err = cbor_encode_text_stringz(&m, "messages"); if (err) return err;
err = encode_cbor(m, v.messages); if (err) return err;
err = cbor_encode_text_stringz(&m, "tags"); if (err) return err;
err = encode_cbor(m, v.tags); if (err) return err;
err = cbor_encode_text_stringz(&m, "note"); if (err) return err;
err = encode_cbor(m, v.note); if (err) return err;
err = cbor_encode_text_stringz(&m, "retries"); if (err) return err;
err = encode_cbor(m, v.retries); if (err) return err;
return cbor_encoder_close_container(&e, &m);
}
inline CborError decode_cbor(CborValue& it, ComplexRequest& v) {
if (!cbor_value_is_map(&it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(&it, "messages", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.messages); if (err) return err;
err = cbor_value_map_find_value(&it, "tags", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.tags); if (err) return err;
err = cbor_value_map_find_value(&it, "note", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.note); if (err) return err;
err = cbor_value_map_find_value(&it, "retries", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.retries); if (err) return err;
return cbor_value_advance(&it);
}
struct ComplexResponse {
std::string summary;
int64_t itemCount;
bool hasNote;
};
inline CborError encode_cbor(CborEncoder& e, const ComplexResponse& v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(&e, &m, 3);
if (err) return err;
err = cbor_encode_text_stringz(&m, "summary"); if (err) return err;
err = encode_cbor(m, v.summary); if (err) return err;
err = cbor_encode_text_stringz(&m, "itemCount"); if (err) return err;
err = encode_cbor(m, v.itemCount); if (err) return err;
err = cbor_encode_text_stringz(&m, "hasNote"); if (err) return err;
err = encode_cbor(m, v.hasNote); if (err) return err;
return cbor_encoder_close_container(&e, &m);
}
inline CborError decode_cbor(CborValue& it, ComplexResponse& v) {
if (!cbor_value_is_map(&it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(&it, "summary", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.summary); if (err) return err;
err = cbor_value_map_find_value(&it, "itemCount", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.itemCount); if (err) return err;
err = cbor_value_map_find_value(&it, "hasNote", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.hasNote); if (err) return err;
return cbor_value_advance(&it);
}
struct JobSpec {
std::string name;
std::vector<std::string> payload;
int64_t priority;
};
inline CborError encode_cbor(CborEncoder& e, const JobSpec& v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(&e, &m, 3);
if (err) return err;
err = cbor_encode_text_stringz(&m, "name"); if (err) return err;
err = encode_cbor(m, v.name); if (err) return err;
err = cbor_encode_text_stringz(&m, "payload"); if (err) return err;
err = encode_cbor(m, v.payload); if (err) return err;
err = cbor_encode_text_stringz(&m, "priority"); if (err) return err;
err = encode_cbor(m, v.priority); if (err) return err;
return cbor_encoder_close_container(&e, &m);
}
inline CborError decode_cbor(CborValue& it, JobSpec& v) {
if (!cbor_value_is_map(&it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(&it, "name", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.name); if (err) return err;
err = cbor_value_map_find_value(&it, "payload", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.payload); if (err) return err;
err = cbor_value_map_find_value(&it, "priority", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.priority); if (err) return err;
return cbor_value_advance(&it);
}
struct RetryPolicy {
int64_t maxAttempts;
int64_t backoffMs;
std::vector<std::string> retryOn;
};
inline CborError encode_cbor(CborEncoder& e, const RetryPolicy& v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(&e, &m, 3);
if (err) return err;
err = cbor_encode_text_stringz(&m, "maxAttempts"); if (err) return err;
err = encode_cbor(m, v.maxAttempts); if (err) return err;
err = cbor_encode_text_stringz(&m, "backoffMs"); if (err) return err;
err = encode_cbor(m, v.backoffMs); if (err) return err;
err = cbor_encode_text_stringz(&m, "retryOn"); if (err) return err;
err = encode_cbor(m, v.retryOn); if (err) return err;
return cbor_encoder_close_container(&e, &m);
}
inline CborError decode_cbor(CborValue& it, RetryPolicy& v) {
if (!cbor_value_is_map(&it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(&it, "maxAttempts", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.maxAttempts); if (err) return err;
err = cbor_value_map_find_value(&it, "backoffMs", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.backoffMs); if (err) return err;
err = cbor_value_map_find_value(&it, "retryOn", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.retryOn); if (err) return err;
return cbor_value_advance(&it);
}
struct ScheduleConfig {
int64_t startAtMs;
int64_t intervalMs;
std::optional<int64_t> jitter;
};
inline CborError encode_cbor(CborEncoder& e, const ScheduleConfig& v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(&e, &m, 3);
if (err) return err;
err = cbor_encode_text_stringz(&m, "startAtMs"); if (err) return err;
err = encode_cbor(m, v.startAtMs); if (err) return err;
err = cbor_encode_text_stringz(&m, "intervalMs"); if (err) return err;
err = encode_cbor(m, v.intervalMs); if (err) return err;
err = cbor_encode_text_stringz(&m, "jitter"); if (err) return err;
err = encode_cbor(m, v.jitter); if (err) return err;
return cbor_encoder_close_container(&e, &m);
}
inline CborError decode_cbor(CborValue& it, ScheduleConfig& v) {
if (!cbor_value_is_map(&it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(&it, "startAtMs", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.startAtMs); if (err) return err;
err = cbor_value_map_find_value(&it, "intervalMs", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.intervalMs); if (err) return err;
err = cbor_value_map_find_value(&it, "jitter", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.jitter); if (err) return err;
return cbor_value_advance(&it);
}
struct ScheduleResult {
std::string jobId;
int64_t willRunCount;
int64_t firstRunAtMs;
int64_t effectiveBackoffMs;
};
inline CborError encode_cbor(CborEncoder& e, const ScheduleResult& v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(&e, &m, 4);
if (err) return err;
err = cbor_encode_text_stringz(&m, "jobId"); if (err) return err;
err = encode_cbor(m, v.jobId); if (err) return err;
err = cbor_encode_text_stringz(&m, "willRunCount"); if (err) return err;
err = encode_cbor(m, v.willRunCount); if (err) return err;
err = cbor_encode_text_stringz(&m, "firstRunAtMs"); if (err) return err;
err = encode_cbor(m, v.firstRunAtMs); if (err) return err;
err = cbor_encode_text_stringz(&m, "effectiveBackoffMs"); if (err) return err;
err = encode_cbor(m, v.effectiveBackoffMs); if (err) return err;
return cbor_encoder_close_container(&e, &m);
}
inline CborError decode_cbor(CborValue& it, ScheduleResult& v) {
if (!cbor_value_is_map(&it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(&it, "jobId", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.jobId); if (err) return err;
err = cbor_value_map_find_value(&it, "willRunCount", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.willRunCount); if (err) return err;
err = cbor_value_map_find_value(&it, "firstRunAtMs", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.firstRunAtMs); if (err) return err;
err = cbor_value_map_find_value(&it, "effectiveBackoffMs", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.effectiveBackoffMs); if (err) return err;
return cbor_value_advance(&it);
}
// ============================================================
// Per-proc request envelopes (CBOR encoded on the wire)
// ============================================================
struct TimerCreateCtorReq {
TimerConfig config;
};
inline CborError encode_cbor(CborEncoder& e, const TimerCreateCtorReq& v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(&e, &m, 1);
if (err) return err;
err = cbor_encode_text_stringz(&m, "config"); if (err) return err;
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) {
if (!cbor_value_is_map(&it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(&it, "config", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.config); if (err) return err;
return cbor_value_advance(&it);
}
struct TimerEchoReq {
EchoRequest req;
};
inline CborError encode_cbor(CborEncoder& e, const TimerEchoReq& v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(&e, &m, 1);
if (err) return err;
err = cbor_encode_text_stringz(&m, "req"); if (err) return err;
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) {
if (!cbor_value_is_map(&it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(&it, "req", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.req); if (err) return err;
return cbor_value_advance(&it);
}
struct TimerVersionReq {
};
inline CborError encode_cbor(CborEncoder& e, const TimerVersionReq&) {
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&) {
if (!cbor_value_is_map(&it)) return CborErrorImproperValue;
return cbor_value_advance(&it);
}
struct TimerComplexReq {
ComplexRequest req;
};
inline CborError encode_cbor(CborEncoder& e, const TimerComplexReq& v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(&e, &m, 1);
if (err) return err;
err = cbor_encode_text_stringz(&m, "req"); if (err) return err;
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) {
if (!cbor_value_is_map(&it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(&it, "req", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.req); if (err) return err;
return cbor_value_advance(&it);
}
struct TimerScheduleReq {
JobSpec job;
RetryPolicy retry;
ScheduleConfig schedule;
};
inline CborError encode_cbor(CborEncoder& e, const TimerScheduleReq& v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(&e, &m, 3);
if (err) return err;
err = cbor_encode_text_stringz(&m, "job"); if (err) return err;
err = encode_cbor(m, v.job); if (err) return err;
err = cbor_encode_text_stringz(&m, "retry"); if (err) return err;
err = encode_cbor(m, v.retry); if (err) return err;
err = cbor_encode_text_stringz(&m, "schedule"); if (err) return err;
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) {
if (!cbor_value_is_map(&it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(&it, "job", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.job); if (err) return err;
err = cbor_value_map_find_value(&it, "retry", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.retry); if (err) return err;
err = cbor_value_map_find_value(&it, "schedule", &field); if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = decode_cbor(field, v.schedule); if (err) return err;
return cbor_value_advance(&it);
}
// ============================================================
// C FFI declarations
// ============================================================
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);
} // extern "C"
// ============================================================
// Synchronous call helper
// ============================================================
namespace {
struct FFICallState_ {
std::mutex mtx;
std::condition_variable cv;
bool done{false};
bool ok{false};
std::vector<std::uint8_t> bytes;
std::string err;
};
inline void ffi_cb_(int ret, const char* msg, size_t len, void* ud) {
// ffi_call_ heap-allocated a shared_ptr and passed its address as ud;
// take ownership here so it's freed on every exit path.
std::unique_ptr<std::shared_ptr<FFICallState_>> handle(
static_cast<std::shared_ptr<FFICallState_>*>(ud));
FFICallState_& s = **handle;
std::lock_guard<std::mutex> lock(s.mtx);
s.ok = (ret == 0);
if (msg && len > 0) {
const auto* p = reinterpret_cast<const std::uint8_t*>(msg);
if (s.ok) s.bytes.assign(p, p + len);
else s.err.assign(msg, len);
}
s.done = true;
s.cv.notify_one();
}
inline std::vector<std::uint8_t> ffi_call_(std::function<int(FFICallback, void*)> f,
std::chrono::milliseconds timeout) {
auto state = std::make_shared<FFICallState_>();
auto* cb_ref = new std::shared_ptr<FFICallState_>(state);
const int ret = f(ffi_cb_, cb_ref);
if (ret == 2) {
delete cb_ref;
throw std::runtime_error("RET_MISSING_CALLBACK (internal error)");
}
std::unique_lock<std::mutex> lock(state->mtx);
const bool fired = state->cv.wait_for(lock, timeout, [&]{ return state->done; });
if (!fired)
throw std::runtime_error("FFI call timed out after " + std::to_string(timeout.count()) + "ms");
if (!state->ok)
throw std::runtime_error(state->err);
return state->bytes;
}
} // anonymous namespace
// ============================================================
// High-level C++ context class
// ============================================================
class TimerCtx {
public:
static TimerCtx create(const TimerConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
const auto ffi_req_ = TimerCreateCtorReq{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);
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);
} 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}) {
return std::async(std::launch::async, [config, timeout]() { return create(config, timeout); });
}
// Rule of Five: because this class owns a raw resource (the 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
// the same context. So we define all five special members explicitly:
// 1. destructor — releases the context.
// 2. copy constructor — deleted; contexts are not copyable.
// 3. copy assignment — deleted; same reason.
// 4. move constructor — transfers ownership, nulls the source.
// 5. move assignment — destroys the current context, then
// transfers ownership from `other`.
// See: https://en.cppreference.com/w/cpp/language/rule_of_three
~TimerCtx() {
if (ptr_) {
timer_destroy(ptr_);
ptr_ = nullptr;
}
}
TimerCtx(const TimerCtx&) = delete;
TimerCtx& operator=(const TimerCtx&) = delete;
TimerCtx(TimerCtx&& other) noexcept : ptr_(other.ptr_), timeout_(other.timeout_) {
other.ptr_ = nullptr;
}
TimerCtx& operator=(TimerCtx&& other) noexcept {
if (this != &other) {
if (ptr_) timer_destroy(ptr_);
ptr_ = other.ptr_;
timeout_ = other.timeout_;
other.ptr_ = nullptr;
}
return *this;
}
EchoResponse echo(const EchoRequest& req) const {
const auto ffi_req_ = TimerEchoReq{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());
}, timeout_);
return decodeCborFFI<EchoResponse>(ffi_raw_);
}
std::future<EchoResponse> echoAsync(const EchoRequest& req) const {
return std::async(std::launch::async, [this, req]() { return this->echo(req); });
}
std::string version() const {
const auto ffi_req_ = TimerVersionReq{};
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());
}, timeout_);
return decodeCborFFI<std::string>(ffi_raw_);
}
std::future<std::string> versionAsync() const {
return std::async(std::launch::async, [this]() { return this->version(); });
}
ComplexResponse complex(const ComplexRequest& req) const {
const auto ffi_req_ = TimerComplexReq{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());
}, timeout_);
return decodeCborFFI<ComplexResponse>(ffi_raw_);
}
std::future<ComplexResponse> complexAsync(const ComplexRequest& req) const {
return std::async(std::launch::async, [this, req]() { return this->complex(req); });
}
ScheduleResult schedule(const JobSpec& job, const RetryPolicy& retry, const ScheduleConfig& schedule) const {
const auto ffi_req_ = TimerScheduleReq{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());
}, timeout_);
return decodeCborFFI<ScheduleResult>(ffi_raw_);
}
std::future<ScheduleResult> scheduleAsync(const JobSpec& job, const RetryPolicy& retry, const ScheduleConfig& schedule) const {
return std::async(std::launch::async, [this, job, retry, schedule]() { return this->schedule(job, retry, schedule); });
}
private:
void* ptr_;
std::chrono::milliseconds timeout_;
explicit TimerCtx(void* p, std::chrono::milliseconds t) : ptr_(p), timeout_(t) {}
};