mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-20 08:19:55 +00:00
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:
parent
c000a8467d
commit
03c9cbea36
3
examples/timer/cpp_native/.gitignore
vendored
Normal file
3
examples/timer/cpp_native/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/example
|
||||
/libmy_timer.dylib
|
||||
/libmy_timer.so
|
||||
42
examples/timer/cpp_native/Makefile
Normal file
42
examples/timer/cpp_native/Makefile
Normal 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)
|
||||
41
examples/timer/cpp_native/README.md
Normal file
41
examples/timer/cpp_native/README.md
Normal 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`.
|
||||
129
examples/timer/cpp_native/main.cpp
Normal file
129
examples/timer/cpp_native/main.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user