From 03c9cbea36d3daf24f329cb6832299cde67a7a9b Mon Sep 17 00:00:00 2001 From: Ivan FB Date: Sun, 31 May 2026 16:41:28 +0200 Subject: [PATCH] docs(examples): add native (same-process) C++ example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complements the CBOR C++ bindings (../cpp_bindings) with the native path: a C++ program that links the library and calls the native `` ABI directly, passing {.ffi.} structs by value and reading typed struct returns (EchoResponse) from the callback — no CBOR, no tinycbor. An RAII TimerNode wrapper bridges the async FFI-thread callback to a synchronous API via std::future. README spells out native (same-process) vs CBOR (IPC). Verified end-to-end with `make run`. Co-Authored-By: Claude Opus 4.8 --- examples/timer/cpp_native/.gitignore | 3 + examples/timer/cpp_native/Makefile | 42 +++++++++ examples/timer/cpp_native/README.md | 41 +++++++++ examples/timer/cpp_native/main.cpp | 129 +++++++++++++++++++++++++++ 4 files changed, 215 insertions(+) create mode 100644 examples/timer/cpp_native/.gitignore create mode 100644 examples/timer/cpp_native/Makefile create mode 100644 examples/timer/cpp_native/README.md create mode 100644 examples/timer/cpp_native/main.cpp diff --git a/examples/timer/cpp_native/.gitignore b/examples/timer/cpp_native/.gitignore new file mode 100644 index 0000000..15097d5 --- /dev/null +++ b/examples/timer/cpp_native/.gitignore @@ -0,0 +1,3 @@ +/example +/libmy_timer.dylib +/libmy_timer.so diff --git a/examples/timer/cpp_native/Makefile b/examples/timer/cpp_native/Makefile new file mode 100644 index 0000000..7a4277f --- /dev/null +++ b/examples/timer/cpp_native/Makefile @@ -0,0 +1,42 @@ +# Build the native (same-process) C++ example for the timer library. +# +# make run # build the Nim dylib + the C++ driver, then run it +# make clean +# +# Links the library directly and uses the native ABI (../c_bindings/my_timer.h) +# — no CBOR, no tinycbor. The Nim library is compiled from the repo root so its +# vendored Nimble dependencies resolve. + +REPO_ROOT := $(abspath ../../..) +NIM_SRC := $(REPO_ROOT)/examples/timer/timer.nim +HDR_DIR := ../c_bindings + +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Darwin) + LIBNAME := libmy_timer.dylib + RPATH := -Wl,-rpath,. +else + LIBNAME := libmy_timer.so + RPATH := -Wl,-rpath,'$$ORIGIN' +endif + +CXX ?= c++ +CXXFLAGS ?= -std=c++17 -Wall -Wextra -O2 -I$(HDR_DIR) +NIMFLAGS := --mm:orc -d:chronicles_log_level=WARN --app:lib --noMain \ + --nimMainPrefix:libmy_timer + +.PHONY: all run clean + +all: example + +$(LIBNAME): + cd $(REPO_ROOT) && nim c $(NIMFLAGS) -o:$(CURDIR)/$(LIBNAME) $(NIM_SRC) + +example: main.cpp $(HDR_DIR)/my_timer.h $(LIBNAME) + $(CXX) $(CXXFLAGS) main.cpp -L. -lmy_timer $(RPATH) -o example + +run: example + ./example + +clean: + rm -f example $(LIBNAME) diff --git a/examples/timer/cpp_native/README.md b/examples/timer/cpp_native/README.md new file mode 100644 index 0000000..21691f6 --- /dev/null +++ b/examples/timer/cpp_native/README.md @@ -0,0 +1,41 @@ +# C++ example — native (same-process) + +A C++ program that links the timer library directly and calls its **native** +(zero-serialization) C ABI, with an idiomatic RAII wrapper. Struct returns come +back as typed C structs read in the callback. + +```cpp +mytimer::TimerNode node("my-app"); +std::cout << node.version(); // "nim-timer v0.1.0" +auto r = node.echo("hello", /*delayMs=*/5); +std::cout << r.echoed << " / " << r.timerName; +``` + +## Native vs CBOR C++ + +This repository ships **two** C++ examples, matching the two ABIs: + +| Example | ABI | Use it for | +|---------|-----|------------| +| **`cpp_native/`** (this one) | native `` | **Same-process / local**. Passes flat C structs by value, zero serialization. | +| [`../cpp_bindings/`](../cpp_bindings) | CBOR `_cbor` (tinycbor) | **Inter-process communication**, where the request must be serialized to cross the boundary anyway. | + +In one address space the CBOR round-trip is pure overhead, so prefer this native +path locally; reach for the CBOR bindings only when you actually cross a +process/machine boundary (see also [`../ipc`](../ipc)). + +## Build & run + +```sh +cd examples/timer/cpp_native +make run +``` + +This compiles `libmy_timer.{dylib,so}` and runs `./example`. Each call is +dispatched on the library's background FFI thread; the wrapper blocks on a +`std::future` until the result callback fires. A struct return (`EchoResponse`) +is delivered as a `const EchoResponse*` in the callback — valid only for the +callback's lifetime, so the wrapper copies its fields out before returning. + +The native header comes from [`../c_bindings/my_timer.h`](../c_bindings) (the C +and C++ native examples share it); regenerate it with `nimble genbindings_c`. diff --git a/examples/timer/cpp_native/main.cpp b/examples/timer/cpp_native/main.cpp new file mode 100644 index 0000000..3567948 --- /dev/null +++ b/examples/timer/cpp_native/main.cpp @@ -0,0 +1,129 @@ +// Native (zero-serialization, same-process) C++ example for the timer library. +// +// This is the in-process path: the program links libmy_timer and calls the +// native `` entry points, passing `{.ffi.}` types as plain C structs by +// value and receiving struct returns as a typed `const *` on the +// callback. No CBOR — the CBOR ABI (see ../cpp_bindings) is for crossing a +// process/machine boundary, where serialization is unavoidable. +// +// Each call is dispatched on the library's FFI thread; an idiomatic RAII +// wrapper blocks on a std::future until the result callback fires. +#include "my_timer.h" // native C ABI (../c_bindings) + +#include +#include +#include +#include +#include + +namespace mytimer { + +struct EchoResult { + std::string echoed; + std::string timerName; +}; + +namespace detail { +// One-shot capture shared with a C callback via `userData`. +struct Capture { + int ret = RET_ERR; + std::string text; // string return / error text + EchoResult echo; // typed EchoResponse return + std::promise done; +}; + +inline std::string rawText(const char *msg, std::size_t len) { + return (msg && len) ? std::string(msg, len) : std::string(); +} + +// `{.ffi.}`-callback shaped free functions (non-capturing => usable as C fn ptrs) +extern "C" inline void ackCb(int ret, const char *msg, std::size_t len, void *ud) { + auto *c = static_cast(ud); + c->ret = ret; + if (ret == RET_ERR) c->text = rawText(msg, len); + c->done.set_value(); +} +extern "C" inline void stringCb(int ret, const char *msg, std::size_t len, void *ud) { + auto *c = static_cast(ud); + c->ret = ret; + c->text = rawText(msg, len); + c->done.set_value(); +} +extern "C" inline void echoCb(int ret, const char *msg, std::size_t len, void *ud) { + auto *c = static_cast(ud); + c->ret = ret; + if (ret == RET_OK) { + const auto *r = reinterpret_cast(msg); // typed return + c->echo.echoed = r->echoed ? r->echoed : ""; + c->echo.timerName = r->timerName ? r->timerName : ""; + } else { + c->text = rawText(msg, len); + } + c->done.set_value(); +} +} // namespace detail + +class TimerNode { +public: + explicit TimerNode(const std::string &name) { + detail::Capture cap; + auto fut = cap.done.get_future(); + TimerConfig cfg{}; + cfg.name = name.c_str(); + ctx_ = my_timer_create(cfg, detail::ackCb, &cap); + if (!ctx_) throw std::runtime_error("my_timer_create returned null"); + fut.wait(); + if (cap.ret != RET_OK) throw std::runtime_error("create failed: " + cap.text); + } + + std::string version() { + detail::Capture cap; + auto fut = cap.done.get_future(); + if (my_timer_version(ctx_, detail::stringCb, &cap) != RET_OK) + throw std::runtime_error("version dispatch failed"); + fut.wait(); + if (cap.ret != RET_OK) throw std::runtime_error(cap.text); + return cap.text; + } + + EchoResult echo(const std::string &message, std::int64_t delayMs = 0) { + detail::Capture cap; + auto fut = cap.done.get_future(); + EchoRequest req{}; + req.message = message.c_str(); + req.delayMs = delayMs; + if (my_timer_echo(ctx_, detail::echoCb, &cap, req) != RET_OK) + throw std::runtime_error("echo dispatch failed"); + fut.wait(); + if (cap.ret != RET_OK) throw std::runtime_error(cap.text); + return cap.echo; + } + + ~TimerNode() { + if (ctx_) my_timer_destroy(ctx_); + } + + TimerNode(const TimerNode &) = delete; + TimerNode &operator=(const TimerNode &) = delete; + +private: + void *ctx_ = nullptr; +}; + +} // namespace mytimer + +int main() { + try { + mytimer::TimerNode node("cpp-native-demo"); + std::cout << "version: " << node.version() << "\n"; + + auto r = node.echo("hello from C++", /*delayMs=*/5); + std::cout << "echo: echoed=" << r.echoed << " timerName=" << r.timerName << "\n"; + + std::cout << "done.\n"; + return 0; + } catch (const std::exception &e) { + std::cerr << "error: " << e.what() << "\n"; + return 1; + } +}