diff --git a/examples/timer/cpp_native_bindings/.gitignore b/examples/timer/cpp_native_bindings/.gitignore new file mode 100644 index 0000000..15097d5 --- /dev/null +++ b/examples/timer/cpp_native_bindings/.gitignore @@ -0,0 +1,3 @@ +/example +/libmy_timer.dylib +/libmy_timer.so diff --git a/examples/timer/cpp_native_bindings/Makefile b/examples/timer/cpp_native_bindings/Makefile new file mode 100644 index 0000000..f583211 --- /dev/null +++ b/examples/timer/cpp_native_bindings/Makefile @@ -0,0 +1,41 @@ +# Build + run the GENERATED native C++ example. +# +# make run # build the Nim dylib + the C++ driver against the generated hpp +# make clean +# +# Regenerate the bindings with `nimble genbindings_cpp_native` (from the repo +# root). 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 + +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. +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 my_timer_native.hpp 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_bindings/README.md b/examples/timer/cpp_native_bindings/README.md new file mode 100644 index 0000000..40c0dc3 --- /dev/null +++ b/examples/timer/cpp_native_bindings/README.md @@ -0,0 +1,37 @@ +# C++ bindings — native (generated) + +**Generated** native (zero-serialization) C++ bindings for the timer library — +the C++ counterpart of `c_bindings` / `go_bindings`. The CBOR C++ bindings live +in [`../cpp_bindings`](../cpp_bindings). + +| File | Description | +|------|-------------| +| `my_timer_native.hpp` | Generated wrapper: a C++ struct + `toC`/`fromC` per `{.ffi.}` type, and a `My_timerNode` class whose methods marshal typed args into / read typed struct returns out of the native ABI — no CBOR. | +| `my_timer.h` | Native C header (structs + entry points) the `.hpp` includes. | +| `main.cpp`, `Makefile` | A driver + build. | + +```cpp +my_timer::My_timerNode node(my_timer::TimerConfig{"my-app"}); +std::cout << node.Version(); +auto r = node.Echo(my_timer::EchoRequest{"hello", 5}); // -> EchoResponse +std::cout << r.echoed << " / " << r.timerName; +``` + +Regenerate with `nimble genbindings_cpp_native` (from the repo root). + +## Build & run + +```sh +cd examples/timer/cpp_native_bindings +make run +``` + +## Status + +First cut. Methods whose params/returns use only scalar / string / bool / +nested-struct fields are generated (create, version, echo). Methods using +**sequences or optionals** are emitted as `// SKIPPED` for now (complex, +schedule) — those plus **native typed events** are the next increments. The +native-bare / `_cbor` filename reconciliation (matching the C headers) is also a +follow-up; today this emits `my_timer_native.hpp` so it coexists with the CBOR +`my_timer.hpp`. diff --git a/examples/timer/cpp_native_bindings/main.cpp b/examples/timer/cpp_native_bindings/main.cpp new file mode 100644 index 0000000..7555e22 --- /dev/null +++ b/examples/timer/cpp_native_bindings/main.cpp @@ -0,0 +1,22 @@ +// Driver for the GENERATED native C++ bindings (my_timer_native.hpp). +// Uses the native (zero-serialization) ABI: typed structs in, typed structs out. +#include "my_timer_native.hpp" + +#include + +int main() { + try { + my_timer::My_timerNode node(my_timer::TimerConfig{"cpp-native-gen"}); + std::cout << "version: " << node.Version() << "\n"; + + auto r = node.Echo(my_timer::EchoRequest{"hello from generated C++", 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; + } +} diff --git a/examples/timer/cpp_native_bindings/my_timer.h b/examples/timer/cpp_native_bindings/my_timer.h new file mode 100644 index 0000000..d5270b0 --- /dev/null +++ b/examples/timer/cpp_native_bindings/my_timer.h @@ -0,0 +1,123 @@ +// Generated by nim-ffi C codegen. Do not edit by hand. +// +// Native (zero-serialization) C ABI. Each call delivers its result to the +// callback. On RET_OK: +// - string-returning procs: (msg, len) is the raw string bytes (not +// NUL-terminated; use len). +// - struct-returning procs: msg is a pointer to the returned C struct — cast +// it to `const *` (len is sizeof). It is valid ONLY for the duration +// of the callback; copy out anything you need before returning. The library +// deep-frees it right after the callback (you free nothing). +// On RET_ERR, (msg, len) is the raw error text. A `_cbor` variant of each +// proc also exists for generic/cross-language callers that prefer CBOR. +#ifndef NIM_FFI_GEN_MY_TIMER_H +#define NIM_FFI_GEN_MY_TIMER_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef NIM_FFI_RET_CODES +#define NIM_FFI_RET_CODES +#define RET_OK 0 +#define RET_ERR 1 +#define RET_MISSING_CALLBACK 2 +#endif + +#ifndef NIM_FFI_CALLBACK_T +#define NIM_FFI_CALLBACK_T +typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData); +#endif + + +// --- {.ffi.}-annotated types, exposed as C structs ---------- +typedef struct { + const char* name; +} TimerConfig; + +typedef struct { + const char* message; + int64_t delayMs; +} EchoRequest; + +typedef struct { + const char* echoed; + const char* timerName; +} EchoResponse; + +typedef struct { + EchoRequest *messages; + size_t messages_len; + const char* *tags; + size_t tags_len; + int note_present; + const char* note; + int retries_present; + int64_t retries; +} ComplexRequest; + +typedef struct { + const char* summary; + int64_t itemCount; + int hasNote; +} ComplexResponse; + +typedef struct { + const char* message; + int64_t echoCount; +} EchoEvent; + +typedef struct { + const char* name; + const char* *payload; + size_t payload_len; + int64_t priority; +} JobSpec; + +typedef struct { + int64_t maxAttempts; + int64_t backoffMs; + const char* *retryOn; + size_t retryOn_len; +} RetryPolicy; + +typedef struct { + int64_t startAtMs; + int64_t intervalMs; + int jitter_present; + int64_t jitter; +} ScheduleConfig; + +typedef struct { + const char* jobId; + int64_t willRunCount; + int64_t firstRunAtMs; + int64_t effectiveBackoffMs; +} ScheduleResult; + + +void *my_timer_create(TimerConfig config, FFICallBack callback, void *userData); + +int my_timer_echo(void *ctx, FFICallBack callback, void *userData, EchoRequest req); + +int my_timer_version(void *ctx, FFICallBack callback, void *userData); + +int my_timer_complex(void *ctx, FFICallBack callback, void *userData, ComplexRequest req); + +int my_timer_schedule(void *ctx, FFICallBack callback, void *userData, JobSpec job, RetryPolicy retry, ScheduleConfig schedule); + +int my_timer_destroy(void *ctx); + +// Native event payloads — cast the callback's msg accordingly: +// "on_echo_fired" -> const EchoEvent * +uint64_t my_timer_add_event_listener(void *ctx, const char *eventName, FFICallBack callback, void *userData); +int my_timer_remove_event_listener(void *ctx, uint64_t listenerId); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif /* NIM_FFI_GEN_MY_TIMER_H */ \ No newline at end of file diff --git a/examples/timer/cpp_native_bindings/my_timer_native.hpp b/examples/timer/cpp_native_bindings/my_timer_native.hpp new file mode 100644 index 0000000..56b741b --- /dev/null +++ b/examples/timer/cpp_native_bindings/my_timer_native.hpp @@ -0,0 +1,209 @@ +// Generated by nim-ffi native C++ codegen. Do not edit by hand. +// +// Native (zero-serialization) wrapper over the C ABI in "my_timer.h". Struct params/returns cross as flat C-POD structs — no CBOR. For the +// inter-process path use the CBOR header (my_timer_cbor.hpp). +#ifndef NIM_FFI_GEN_MY_TIMER_NATIVE_HPP +#define NIM_FFI_GEN_MY_TIMER_NATIVE_HPP + +#include "my_timer.h" +#include +#include +#include +#include + +namespace my_timer { + +struct TimerConfig { + std::string name{}; +}; +inline ::TimerConfig toC(const TimerConfig& v) { + ::TimerConfig c{}; + c.name = v.name.c_str(); + return c; +} +inline TimerConfig fromC(const ::TimerConfig& c) { + TimerConfig v{}; + v.name = c.name ? std::string(c.name) : std::string(); + return v; +} + +struct EchoRequest { + std::string message{}; + int64_t delayMs{}; +}; +inline ::EchoRequest toC(const EchoRequest& v) { + ::EchoRequest c{}; + c.message = v.message.c_str(); + c.delayMs = v.delayMs; + return c; +} +inline EchoRequest fromC(const ::EchoRequest& c) { + EchoRequest v{}; + v.message = c.message ? std::string(c.message) : std::string(); + v.delayMs = c.delayMs; + return v; +} + +struct EchoResponse { + std::string echoed{}; + std::string timerName{}; +}; +inline ::EchoResponse toC(const EchoResponse& v) { + ::EchoResponse c{}; + c.echoed = v.echoed.c_str(); + c.timerName = v.timerName.c_str(); + return c; +} +inline EchoResponse fromC(const ::EchoResponse& c) { + EchoResponse v{}; + v.echoed = c.echoed ? std::string(c.echoed) : std::string(); + v.timerName = c.timerName ? std::string(c.timerName) : std::string(); + return v; +} + +struct ComplexResponse { + std::string summary{}; + int64_t itemCount{}; + bool hasNote{}; +}; +inline ::ComplexResponse toC(const ComplexResponse& v) { + ::ComplexResponse c{}; + c.summary = v.summary.c_str(); + c.itemCount = v.itemCount; + c.hasNote = (v.hasNote ? 1 : 0); + return c; +} +inline ComplexResponse fromC(const ::ComplexResponse& c) { + ComplexResponse v{}; + v.summary = c.summary ? std::string(c.summary) : std::string(); + v.itemCount = c.itemCount; + v.hasNote = c.hasNote != 0; + return v; +} + +struct EchoEvent { + std::string message{}; + int64_t echoCount{}; +}; +inline ::EchoEvent toC(const EchoEvent& v) { + ::EchoEvent c{}; + c.message = v.message.c_str(); + c.echoCount = v.echoCount; + return c; +} +inline EchoEvent fromC(const ::EchoEvent& c) { + EchoEvent v{}; + v.message = c.message ? std::string(c.message) : std::string(); + v.echoCount = c.echoCount; + return v; +} + +struct ScheduleResult { + std::string jobId{}; + int64_t willRunCount{}; + int64_t firstRunAtMs{}; + int64_t effectiveBackoffMs{}; +}; +inline ::ScheduleResult toC(const ScheduleResult& v) { + ::ScheduleResult c{}; + c.jobId = v.jobId.c_str(); + c.willRunCount = v.willRunCount; + c.firstRunAtMs = v.firstRunAtMs; + c.effectiveBackoffMs = v.effectiveBackoffMs; + return c; +} +inline ScheduleResult fromC(const ::ScheduleResult& c) { + ScheduleResult v{}; + v.jobId = c.jobId ? std::string(c.jobId) : std::string(); + v.willRunCount = c.willRunCount; + v.firstRunAtMs = c.firstRunAtMs; + v.effectiveBackoffMs = c.effectiveBackoffMs; + return v; +} + +namespace detail { +template struct Capture { + int ret = RET_ERR; + T value{}; + std::string err; + std::promise done; +}; +struct AckCapture { + int ret = RET_ERR; + std::string err; + std::promise done; +}; +inline std::string rawText(const char* msg, std::size_t len) { + return (msg && len) ? std::string(msg, len) : std::string(); +} +} // namespace detail + +extern "C" { +inline void my_timer_native_ack(int ret, const char* msg, std::size_t len, void* ud) { + auto* c = static_cast(ud); + c->ret = ret; + if (ret == RET_ERR) c->err = detail::rawText(msg, len); + c->done.set_value(); +} +inline void my_timer_native_str(int ret, const char* msg, std::size_t len, void* ud) { + auto* c = static_cast*>(ud); + c->ret = ret; + if (ret == RET_OK) c->value = detail::rawText(msg, len); + else c->err = detail::rawText(msg, len); + c->done.set_value(); +} +inline void my_timer_native_my_timer_echo(int ret, const char* msg, std::size_t len, void* ud) { + auto* c = static_cast*>(ud); + c->ret = ret; + if (ret == RET_OK) c->value = fromC(*reinterpret_cast(msg)); + else c->err = detail::rawText(msg, len); + c->done.set_value(); +} +} // extern "C" + +class My_timerNode { + public: + explicit My_timerNode(const TimerConfig& config) { + detail::AckCapture cap; + auto fut = cap.done.get_future(); + auto c_config = toC(config); + ctx_ = my_timer_create(c_config, my_timer_native_ack, &cap); + if (!ctx_) throw std::runtime_error("my_timer_create returned null"); + fut.wait(); + if (cap.ret != RET_OK) throw std::runtime_error(cap.err); + } + + EchoResponse Echo(const EchoRequest& req) { + detail::Capture cap; + auto fut = cap.done.get_future(); + auto c_req = toC(req); + if (my_timer_echo(ctx_, my_timer_native_my_timer_echo, &cap, c_req) != RET_OK) + throw std::runtime_error("my_timer_echo dispatch failed"); + fut.wait(); + if (cap.ret != RET_OK) throw std::runtime_error(cap.err); + return cap.value; + } + + std::string Version() { + detail::Capture cap; + auto fut = cap.done.get_future(); + if (my_timer_version(ctx_, my_timer_native_str, &cap) != RET_OK) + throw std::runtime_error("my_timer_version dispatch failed"); + fut.wait(); + if (cap.ret != RET_OK) throw std::runtime_error(cap.err); + return cap.value; + } + + // SKIPPED my_timer_complex: seq/Option/multi-struct params not yet supported by native C++ codegen + // SKIPPED my_timer_schedule: seq/Option/multi-struct params not yet supported by native C++ codegen + ~My_timerNode() { if (ctx_) my_timer_destroy(ctx_); } + My_timerNode(const My_timerNode&) = delete; + My_timerNode& operator=(const My_timerNode&) = delete; + + private: + void* ctx_ = nullptr; +}; + +} // namespace my_timer + +#endif // NIM_FFI_GEN_MY_TIMER_NATIVE_HPP \ No newline at end of file diff --git a/ffi.nimble b/ffi.nimble index effec1d..cc6ca57 100644 --- a/ffi.nimble +++ b/ffi.nimble @@ -197,6 +197,14 @@ task genbindings_cpp, "Generate C++ bindings for the timer example": " -d:ffiSrcPath=../timer.nim" & " -o:/dev/null examples/timer/timer.nim" +task genbindings_cpp_native, "Generate native (non-CBOR) C++ bindings for the timer example": + exec "nim c " & nimFlagsOrc & + " --app:lib --noMain --nimMainPrefix:libmy_timer" & + " -d:ffiGenBindings -d:targetLang=cpp -d:ffiMode=native" & + " -d:ffiOutputDir=examples/timer/cpp_native_bindings" & + " -d:ffiSrcPath=../timer.nim" & + " -o:/dev/null examples/timer/timer.nim" + task genbindings_cpp_echo, "Generate C++ bindings for the echo example": exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libecho" & diff --git a/ffi/codegen/cpp_native.nim b/ffi/codegen/cpp_native.nim new file mode 100644 index 0000000..1b01d48 --- /dev/null +++ b/ffi/codegen/cpp_native.nim @@ -0,0 +1,333 @@ +## Native (zero-serialization) C++ binding generator. +## +## Emits `.hpp`: an idiomatic C++ wrapper over the *native* C ABI (the +## `` entry points + flat C structs declared in `.h`). Each `{.ffi.}` +## type is mirrored as a C++ struct with `toC` / `fromC` converters to the C-POD +## layout, and methods marshal typed args in / read typed struct returns out — +## no CBOR. Companion to the CBOR generator in `cpp.nim` (`_cbor.hpp`). +## +## Commit 1 covers scalar / string / bool / nested-struct fields and the procs +## that use only those (the timer's create / version / echo). Sequences, +## optionals and native events are layered on next. + +import std/[os, strutils] +import ./meta, ./string_helpers +import ./c as cgen + +proc cppType(t: string): string = + ## Idiomatic C++ type for an `{.ffi.}` field / scalar. + case t.strip() + of "string", "cstring": "std::string" + of "int", "int64", "clong": "int64_t" + of "int32", "cint": "int32_t" + of "int16": "int16_t" + of "int8": "int8_t" + of "uint", "uint64", "csize_t": "uint64_t" + of "uint32", "cuint": "uint32_t" + of "uint16": "uint16_t" + of "uint8", "byte": "uint8_t" + of "bool": "bool" + of "float", "float32": "float" + of "float64": "double" + else: t.strip() # nested {.ffi.} struct -> its C++ name + +proc isSeqT(t: string): bool = + t.strip().startsWith("seq[") and t.strip().endsWith("]") + +proc isOptT(t: string): bool = + let s = t.strip() + (s.startsWith("Option[") or s.startsWith("Maybe[")) and s.endsWith("]") + +proc isStringT(t: string): bool = + t.strip() in ["string", "cstring"] + +proc isStructT(t: string, types: seq[FFITypeMeta]): bool = + for ty in types: + if ty.name == t.strip(): + return true + false + +proc isSimpleType(t: FFITypeMeta, types: seq[FFITypeMeta]): bool = + ## True if every field is scalar / string / bool / nested *simple* struct + ## (no sequences or optionals yet). + for f in t.fields: + let ft = f.typeName.strip() + if isSeqT(ft) or isOptT(ft): + return false + if isStructT(ft, types): + for inner in types: + if inner.name == ft and not isSimpleType(inner, types): + return false + true + +proc paramSimple(typeName: string, types: seq[FFITypeMeta]): bool = + let t = typeName.strip() + if isStringT(t) or t == "bool": + return true + if cppType(t) != t: # mapped scalar + return true + if isStructT(t, types): + for ty in types: + if ty.name == t: + return isSimpleType(ty, types) + false + +# --- toC / fromC field converters ------------------------------------------- +proc toCField(f: FFIFieldMeta, types: seq[FFITypeMeta]): string = + let t = f.typeName.strip() + if isStringT(t): + "v." & f.name & ".c_str()" + elif t == "bool": + "(v." & f.name & " ? 1 : 0)" + elif isStructT(t, types): + "toC(v." & f.name & ")" + else: + "v." & f.name + +proc fromCField(f: FFIFieldMeta, types: seq[FFITypeMeta]): string = + let t = f.typeName.strip() + if isStringT(t): + "c." & f.name & " ? std::string(c." & f.name & ") : std::string()" + elif t == "bool": + "c." & f.name & " != 0" + elif isStructT(t, types): + "fromC(c." & f.name & ")" + else: + "c." & f.name + +proc emitTypes(types: seq[FFITypeMeta]): seq[string] = + var L: seq[string] = @[] + for t in types: + if not isSimpleType(t, types): + continue + L.add("struct " & t.name & " {") + for f in t.fields: + L.add(" " & cppType(f.typeName) & " " & f.name & "{};") + L.add("};") + # toC: build the C-POD struct (strings borrow the C++ object's storage, + # valid for the duration of the call). + L.add("inline ::" & t.name & " toC(const " & t.name & "& v) {") + L.add(" ::" & t.name & " c{};") + for f in t.fields: + L.add(" c." & f.name & " = " & toCField(f, types) & ";") + L.add(" return c;") + L.add("}") + # fromC: copy out of a C-POD struct into C++-owned values. + L.add("inline " & t.name & " fromC(const ::" & t.name & "& c) {") + L.add(" " & t.name & " v{};") + for f in t.fields: + L.add(" v." & f.name & " = " & fromCField(f, types) & ";") + L.add(" return v;") + L.add("}") + L.add("") + return L + +proc methodName(procName, libName: string): string = + let prefix = libName & "_" + let bare = + if procName.startsWith(prefix): + procName[prefix.len .. ^1] + else: + procName + capitalizeFirstLetter(snakeToPascalCase(bare)) + +proc procSupported(p: FFIProcMeta, types: seq[FFITypeMeta]): bool = + for ep in p.extraParams: + if ep.isPtr or not paramSimple(ep.typeName, types): + return false + true + +proc generateCppNativeHeader*( + procs: seq[FFIProcMeta], + types: seq[FFITypeMeta], + libName: string, + events: seq[FFIEventMeta] = @[], +): string = + let guard = "NIM_FFI_GEN_" & libName.toUpper() & "_NATIVE_HPP" + let nodeT = capitalizeFirstLetter(libName) & "Node" + var L: seq[string] = @[] + L.add("// Generated by nim-ffi native C++ codegen. Do not edit by hand.") + L.add("//") + L.add("// Native (zero-serialization) wrapper over the C ABI in \"" & libName & + ".h\". Struct params/returns cross as flat C-POD structs — no CBOR. For the") + L.add("// inter-process path use the CBOR header (" & libName & "_cbor.hpp).") + L.add("#ifndef " & guard) + L.add("#define " & guard) + L.add("") + L.add("#include \"" & libName & ".h\"") + L.add("#include ") + L.add("#include ") + L.add("#include ") + L.add("#include ") + L.add("") + L.add("namespace " & libName & " {") + L.add("") + + for line in emitTypes(types): + L.add(line) + + # Per-call blocking capture, parameterised by the C++ return type. + L.add("namespace detail {") + L.add("template struct Capture {") + L.add(" int ret = RET_ERR;") + L.add(" T value{};") + L.add(" std::string err;") + L.add(" std::promise done;") + L.add("};") + L.add("struct AckCapture {") + L.add(" int ret = RET_ERR;") + L.add(" std::string err;") + L.add(" std::promise done;") + L.add("};") + L.add("inline std::string rawText(const char* msg, std::size_t len) {") + L.add(" return (msg && len) ? std::string(msg, len) : std::string();") + L.add("}") + L.add("} // namespace detail") + L.add("") + + # Find ctor / dtor. + var ctor, dtor: FFIProcMeta + var haveCtor, haveDtor = false + for p in procs: + if p.kind == FFIKind.CTOR: + (ctor, haveCtor) = (p, true) + elif p.kind == FFIKind.DTOR: + (dtor, haveDtor) = (p, true) + + # Exported C callbacks (one per struct-returning method + shared ack/string). + L.add("extern \"C\" {") + L.add("inline void " & libName & + "_native_ack(int ret, const char* msg, std::size_t len, void* ud) {") + L.add(" auto* c = static_cast(ud);") + L.add(" c->ret = ret;") + L.add(" if (ret == RET_ERR) c->err = detail::rawText(msg, len);") + L.add(" c->done.set_value();") + L.add("}") + L.add("inline void " & libName & + "_native_str(int ret, const char* msg, std::size_t len, void* ud) {") + L.add(" auto* c = static_cast*>(ud);") + L.add(" c->ret = ret;") + L.add(" if (ret == RET_OK) c->value = detail::rawText(msg, len);") + L.add(" else c->err = detail::rawText(msg, len);") + L.add(" c->done.set_value();") + L.add("}") + for p in procs: + if p.kind != FFIKind.FFI or not procSupported(p, types): + continue + if not isStructT(p.returnTypeName, types): + continue + let rt = p.returnTypeName + L.add("inline void " & libName & "_native_" & p.procName & + "(int ret, const char* msg, std::size_t len, void* ud) {") + L.add(" auto* c = static_cast*>(ud);") + L.add(" c->ret = ret;") + L.add(" if (ret == RET_OK) c->value = fromC(*reinterpret_cast(msg));") + L.add(" else c->err = detail::rawText(msg, len);") + L.add(" c->done.set_value();") + L.add("}") + L.add("} // extern \"C\"") + L.add("") + + # The node class. + L.add("class " & nodeT & " {") + L.add(" public:") + if haveCtor: + var params: seq[string] = @[] + var conv: seq[string] = @[] + var args: seq[string] = @[] + for ep in ctor.extraParams: + params.add("const " & cppType(ep.typeName) & "& " & ep.name) + if isStructT(ep.typeName, types): + conv.add(" auto c_" & ep.name & " = toC(" & ep.name & ");") + args.add("c_" & ep.name) + else: + args.add(ep.name) + let argsStr = if args.len > 0: args.join(", ") & ", " else: "" + L.add(" explicit " & nodeT & "(" & params.join(", ") & ") {") + L.add(" detail::AckCapture cap;") + L.add(" auto fut = cap.done.get_future();") + for c in conv: + L.add(c) + L.add(" ctx_ = " & ctor.procName & "(" & argsStr & + libName & "_native_ack, &cap);") + L.add(" if (!ctx_) throw std::runtime_error(\"" & ctor.procName & + " returned null\");") + L.add(" fut.wait();") + L.add(" if (cap.ret != RET_OK) throw std::runtime_error(cap.err);") + L.add(" }") + L.add("") + + for p in procs: + if p.kind != FFIKind.FFI: + continue + if not procSupported(p, types): + L.add(" // SKIPPED " & p.procName & + ": seq/Option/multi-struct params not yet supported by native C++ codegen") + continue + let mName = methodName(p.procName, libName) + var params: seq[string] = @[] + var conv: seq[string] = @[] + var args: seq[string] = @[] + for ep in p.extraParams: + params.add("const " & cppType(ep.typeName) & "& " & ep.name) + if isStructT(ep.typeName, types): + conv.add(" auto c_" & ep.name & " = toC(" & ep.name & ");") + args.add("c_" & ep.name) + else: + args.add(ep.name) + let argsStr = if args.len > 0: ", " & args.join(", ") else: "" + let structRet = isStructT(p.returnTypeName, types) + let retT = if structRet: p.returnTypeName else: "std::string" + let capT = if structRet: p.returnTypeName else: "std::string" + let cbName = + if structRet: libName & "_native_" & p.procName else: libName & "_native_str" + L.add(" " & retT & " " & mName & "(" & params.join(", ") & ") {") + L.add(" detail::Capture<" & capT & "> cap;") + L.add(" auto fut = cap.done.get_future();") + for c in conv: + L.add(c) + L.add(" if (" & p.procName & "(ctx_, " & cbName & ", &cap" & argsStr & + ") != RET_OK)") + L.add(" throw std::runtime_error(\"" & p.procName & + " dispatch failed\");") + L.add(" fut.wait();") + L.add(" if (cap.ret != RET_OK) throw std::runtime_error(cap.err);") + L.add(" return cap.value;") + L.add(" }") + L.add("") + + if haveDtor: + L.add(" ~" & nodeT & "() { if (ctx_) " & dtor.procName & "(ctx_); }") + L.add(" " & nodeT & "(const " & nodeT & "&) = delete;") + L.add(" " & nodeT & "& operator=(const " & nodeT & "&) = delete;") + L.add("") + L.add(" private:") + L.add(" void* ctx_ = nullptr;") + L.add("};") + L.add("") + L.add("} // namespace " & libName) + L.add("") + L.add("#endif // " & guard) + return L.join("\n") + +proc generateCppNativeBindings*( + procs: seq[FFIProcMeta], + types: seq[FFITypeMeta], + libName: string, + outputDir: string, + nimSrcRelPath: string, + events: seq[FFIEventMeta] = @[], +) = + # `_native.hpp` for now so it coexists with the CBOR `.hpp`; the + # native-bare / `_cbor` rename (matching C) is a follow-up. Emit the native C + # header too (the structs + entry points the .hpp includes), so the binding is + # self-contained. + writeFile( + outputDir / (libName & ".h"), + cgen.generateCHeader(procs, types, libName, events), + ) + writeFile( + outputDir / (libName & "_native.hpp"), + generateCppNativeHeader(procs, types, libName, events), + ) diff --git a/ffi/internal/ffi_macro.nim b/ffi/internal/ffi_macro.nim index 576a35a..389d6c2 100644 --- a/ffi/internal/ffi_macro.nim +++ b/ffi/internal/ffi_macro.nim @@ -6,6 +6,7 @@ import ./native_pod when defined(ffiGenBindings): import ../codegen/rust import ../codegen/cpp + import ../codegen/cpp_native import ../codegen/cddl import ../codegen/c import ../codegen/go @@ -1971,10 +1972,16 @@ macro genBindings*( ffiEventRegistry, ) of "cpp", "c++": - generateCppBindings( - ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath, - ffiEventRegistry, - ) + if ffiEmitCbor(): + generateCppBindings( + ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath, + ffiEventRegistry, + ) + if ffiEmitNative(): + generateCppNativeBindings( + ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath, + ffiEventRegistry, + ) of "cddl": generateCddlBindings( ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath