docs(examples): add native (same-process) C++ example

Complements the CBOR C++ bindings (../cpp_bindings) with the native path: a C++
program that links the library and calls the native `<name>` 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 <noreply@anthropic.com>
This commit is contained in:
Ivan FB 2026-05-31 16:41:28 +02:00
parent c000a8467d
commit 03c9cbea36
No known key found for this signature in database
GPG Key ID: DF0C67A04C543270
4 changed files with 215 additions and 0 deletions

3
examples/timer/cpp_native/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/example
/libmy_timer.dylib
/libmy_timer.so

View File

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

View File

@ -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 `<name>` | **Same-process / local**. Passes flat C structs by value, zero serialization. |
| [`../cpp_bindings/`](../cpp_bindings) | CBOR `<name>_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`.

View File

@ -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 `<name>` entry points, passing `{.ffi.}` types as plain C structs by
// value and receiving struct returns as a typed `const <Type>*` 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 <cstdint>
#include <future>
#include <iostream>
#include <stdexcept>
#include <string>
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<void> 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<Capture *>(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<Capture *>(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<Capture *>(ud);
c->ret = ret;
if (ret == RET_OK) {
const auto *r = reinterpret_cast<const EchoResponse *>(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;
}
}