Skip to content

Commit 0eed91c

Browse files
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>
1 parent 1060340 commit 0eed91c

4 files changed

Lines changed: 215 additions & 0 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/example
2+
/libmy_timer.dylib
3+
/libmy_timer.so

examples/timer/cpp_native/Makefile

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Build the native (same-process) C++ example for the timer library.
2+
#
3+
# make run # build the Nim dylib + the C++ driver, then run it
4+
# make clean
5+
#
6+
# Links the library directly and uses the native ABI (../c_bindings/my_timer.h)
7+
# — no CBOR, no tinycbor. The Nim library is compiled from the repo root so its
8+
# vendored Nimble dependencies resolve.
9+
10+
REPO_ROOT := $(abspath ../../..)
11+
NIM_SRC := $(REPO_ROOT)/examples/timer/timer.nim
12+
HDR_DIR := ../c_bindings
13+
14+
UNAME_S := $(shell uname -s)
15+
ifeq ($(UNAME_S),Darwin)
16+
LIBNAME := libmy_timer.dylib
17+
RPATH := -Wl,-rpath,.
18+
else
19+
LIBNAME := libmy_timer.so
20+
RPATH := -Wl,-rpath,'$$ORIGIN'
21+
endif
22+
23+
CXX ?= c++
24+
CXXFLAGS ?= -std=c++17 -Wall -Wextra -O2 -I$(HDR_DIR)
25+
NIMFLAGS := --mm:orc -d:chronicles_log_level=WARN --app:lib --noMain \
26+
--nimMainPrefix:libmy_timer
27+
28+
.PHONY: all run clean
29+
30+
all: example
31+
32+
$(LIBNAME):
33+
cd $(REPO_ROOT) && nim c $(NIMFLAGS) -o:$(CURDIR)/$(LIBNAME) $(NIM_SRC)
34+
35+
example: main.cpp $(HDR_DIR)/my_timer.h $(LIBNAME)
36+
$(CXX) $(CXXFLAGS) main.cpp -L. -lmy_timer $(RPATH) -o example
37+
38+
run: example
39+
./example
40+
41+
clean:
42+
rm -f example $(LIBNAME)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# C++ example — native (same-process)
2+
3+
A C++ program that links the timer library directly and calls its **native**
4+
(zero-serialization) C ABI, with an idiomatic RAII wrapper. Struct returns come
5+
back as typed C structs read in the callback.
6+
7+
```cpp
8+
mytimer::TimerNode node("my-app");
9+
std::cout << node.version(); // "nim-timer v0.1.0"
10+
auto r = node.echo("hello", /*delayMs=*/5);
11+
std::cout << r.echoed << " / " << r.timerName;
12+
```
13+
14+
## Native vs CBOR C++
15+
16+
This repository ships **two** C++ examples, matching the two ABIs:
17+
18+
| Example | ABI | Use it for |
19+
|---------|-----|------------|
20+
| **`cpp_native/`** (this one) | native `<name>` | **Same-process / local**. Passes flat C structs by value, zero serialization. |
21+
| [`../cpp_bindings/`](../cpp_bindings) | CBOR `<name>_cbor` (tinycbor) | **Inter-process communication**, where the request must be serialized to cross the boundary anyway. |
22+
23+
In one address space the CBOR round-trip is pure overhead, so prefer this native
24+
path locally; reach for the CBOR bindings only when you actually cross a
25+
process/machine boundary (see also [`../ipc`](../ipc)).
26+
27+
## Build & run
28+
29+
```sh
30+
cd examples/timer/cpp_native
31+
make run
32+
```
33+
34+
This compiles `libmy_timer.{dylib,so}` and runs `./example`. Each call is
35+
dispatched on the library's background FFI thread; the wrapper blocks on a
36+
`std::future` until the result callback fires. A struct return (`EchoResponse`)
37+
is delivered as a `const EchoResponse*` in the callback — valid only for the
38+
callback's lifetime, so the wrapper copies its fields out before returning.
39+
40+
The native header comes from [`../c_bindings/my_timer.h`](../c_bindings) (the C
41+
and C++ native examples share it); regenerate it with `nimble genbindings_c`.

examples/timer/cpp_native/main.cpp

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Native (zero-serialization, same-process) C++ example for the timer library.
2+
//
3+
// This is the in-process path: the program links libmy_timer and calls the
4+
// native `<name>` entry points, passing `{.ffi.}` types as plain C structs by
5+
// value and receiving struct returns as a typed `const <Type>*` on the
6+
// callback. No CBOR — the CBOR ABI (see ../cpp_bindings) is for crossing a
7+
// process/machine boundary, where serialization is unavoidable.
8+
//
9+
// Each call is dispatched on the library's FFI thread; an idiomatic RAII
10+
// wrapper blocks on a std::future until the result callback fires.
11+
#include "my_timer.h" // native C ABI (../c_bindings)
12+
13+
#include <cstdint>
14+
#include <future>
15+
#include <iostream>
16+
#include <stdexcept>
17+
#include <string>
18+
19+
namespace mytimer {
20+
21+
struct EchoResult {
22+
std::string echoed;
23+
std::string timerName;
24+
};
25+
26+
namespace detail {
27+
// One-shot capture shared with a C callback via `userData`.
28+
struct Capture {
29+
int ret = RET_ERR;
30+
std::string text; // string return / error text
31+
EchoResult echo; // typed EchoResponse return
32+
std::promise<void> done;
33+
};
34+
35+
inline std::string rawText(const char *msg, std::size_t len) {
36+
return (msg && len) ? std::string(msg, len) : std::string();
37+
}
38+
39+
// `{.ffi.}`-callback shaped free functions (non-capturing => usable as C fn ptrs)
40+
extern "C" inline void ackCb(int ret, const char *msg, std::size_t len, void *ud) {
41+
auto *c = static_cast<Capture *>(ud);
42+
c->ret = ret;
43+
if (ret == RET_ERR) c->text = rawText(msg, len);
44+
c->done.set_value();
45+
}
46+
extern "C" inline void stringCb(int ret, const char *msg, std::size_t len, void *ud) {
47+
auto *c = static_cast<Capture *>(ud);
48+
c->ret = ret;
49+
c->text = rawText(msg, len);
50+
c->done.set_value();
51+
}
52+
extern "C" inline void echoCb(int ret, const char *msg, std::size_t len, void *ud) {
53+
auto *c = static_cast<Capture *>(ud);
54+
c->ret = ret;
55+
if (ret == RET_OK) {
56+
const auto *r = reinterpret_cast<const EchoResponse *>(msg); // typed return
57+
c->echo.echoed = r->echoed ? r->echoed : "";
58+
c->echo.timerName = r->timerName ? r->timerName : "";
59+
} else {
60+
c->text = rawText(msg, len);
61+
}
62+
c->done.set_value();
63+
}
64+
} // namespace detail
65+
66+
class TimerNode {
67+
public:
68+
explicit TimerNode(const std::string &name) {
69+
detail::Capture cap;
70+
auto fut = cap.done.get_future();
71+
TimerConfig cfg{};
72+
cfg.name = name.c_str();
73+
ctx_ = my_timer_create(cfg, detail::ackCb, &cap);
74+
if (!ctx_) throw std::runtime_error("my_timer_create returned null");
75+
fut.wait();
76+
if (cap.ret != RET_OK) throw std::runtime_error("create failed: " + cap.text);
77+
}
78+
79+
std::string version() {
80+
detail::Capture cap;
81+
auto fut = cap.done.get_future();
82+
if (my_timer_version(ctx_, detail::stringCb, &cap) != RET_OK)
83+
throw std::runtime_error("version dispatch failed");
84+
fut.wait();
85+
if (cap.ret != RET_OK) throw std::runtime_error(cap.text);
86+
return cap.text;
87+
}
88+
89+
EchoResult echo(const std::string &message, std::int64_t delayMs = 0) {
90+
detail::Capture cap;
91+
auto fut = cap.done.get_future();
92+
EchoRequest req{};
93+
req.message = message.c_str();
94+
req.delayMs = delayMs;
95+
if (my_timer_echo(ctx_, detail::echoCb, &cap, req) != RET_OK)
96+
throw std::runtime_error("echo dispatch failed");
97+
fut.wait();
98+
if (cap.ret != RET_OK) throw std::runtime_error(cap.text);
99+
return cap.echo;
100+
}
101+
102+
~TimerNode() {
103+
if (ctx_) my_timer_destroy(ctx_);
104+
}
105+
106+
TimerNode(const TimerNode &) = delete;
107+
TimerNode &operator=(const TimerNode &) = delete;
108+
109+
private:
110+
void *ctx_ = nullptr;
111+
};
112+
113+
} // namespace mytimer
114+
115+
int main() {
116+
try {
117+
mytimer::TimerNode node("cpp-native-demo");
118+
std::cout << "version: " << node.version() << "\n";
119+
120+
auto r = node.echo("hello from C++", /*delayMs=*/5);
121+
std::cout << "echo: echoed=" << r.echoed << " timerName=" << r.timerName << "\n";
122+
123+
std::cout << "done.\n";
124+
return 0;
125+
} catch (const std::exception &e) {
126+
std::cerr << "error: " << e.what() << "\n";
127+
return 1;
128+
}
129+
}

0 commit comments

Comments
 (0)