mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-24 10:19:53 +00:00
feat(codegen): native (non-CBOR) C++ generator — core
A native C++ binding generator (`cpp_native.nim`), the C++ counterpart of the C
and Go native paths and companion to the CBOR `cpp.nim`. It emits
`<lib>_native.hpp`: an idiomatic C++ struct + `toC`/`fromC` per `{.ffi.}` type,
and a `<Lib>Node` class whose methods marshal typed args into / read typed
struct returns out of the native ABI (`<name>` entry points + flat C structs in
`<lib>.h`) — zero serialization. Wired into genBindings under
`targetLang=cpp` + `-d:ffiMode=native`; emits the native C header alongside so
the binding is self-contained. Task: `genbindings_cpp_native`.
First cut covers scalar/string/bool/nested-struct fields (create/version/echo);
seq/Option params are `// SKIPPED`, and native typed events are next. Filename
is `_native.hpp` for now to coexist with the CBOR `.hpp` (rename is a follow-up).
Verified end-to-end: the generated example builds and round-trips a typed
EchoResponse (`make run`).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f08cb7971d
commit
725a7b6551
3
examples/timer/cpp_native_bindings/.gitignore
vendored
Normal file
3
examples/timer/cpp_native_bindings/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/example
|
||||
/libmy_timer.dylib
|
||||
/libmy_timer.so
|
||||
41
examples/timer/cpp_native_bindings/Makefile
Normal file
41
examples/timer/cpp_native_bindings/Makefile
Normal file
@ -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)
|
||||
37
examples/timer/cpp_native_bindings/README.md
Normal file
37
examples/timer/cpp_native_bindings/README.md
Normal file
@ -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`.
|
||||
22
examples/timer/cpp_native_bindings/main.cpp
Normal file
22
examples/timer/cpp_native_bindings/main.cpp
Normal file
@ -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 <iostream>
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
123
examples/timer/cpp_native_bindings/my_timer.h
Normal file
123
examples/timer/cpp_native_bindings/my_timer.h
Normal file
@ -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 <Type>*` (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 `<name>_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 <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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 */
|
||||
209
examples/timer/cpp_native_bindings/my_timer_native.hpp
Normal file
209
examples/timer/cpp_native_bindings/my_timer_native.hpp
Normal file
@ -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 <cstdint>
|
||||
#include <future>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
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 <typename T> struct Capture {
|
||||
int ret = RET_ERR;
|
||||
T value{};
|
||||
std::string err;
|
||||
std::promise<void> done;
|
||||
};
|
||||
struct AckCapture {
|
||||
int ret = RET_ERR;
|
||||
std::string err;
|
||||
std::promise<void> 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<detail::AckCapture*>(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<detail::Capture<std::string>*>(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<detail::Capture<EchoResponse>*>(ud);
|
||||
c->ret = ret;
|
||||
if (ret == RET_OK) c->value = fromC(*reinterpret_cast<const ::EchoResponse*>(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<EchoResponse> 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<std::string> 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
|
||||
@ -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" &
|
||||
|
||||
333
ffi/codegen/cpp_native.nim
Normal file
333
ffi/codegen/cpp_native.nim
Normal file
@ -0,0 +1,333 @@
|
||||
## Native (zero-serialization) C++ binding generator.
|
||||
##
|
||||
## Emits `<lib>.hpp`: an idiomatic C++ wrapper over the *native* C ABI (the
|
||||
## `<name>` entry points + flat C structs declared in `<lib>.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` (`<lib>_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 <cstdint>")
|
||||
L.add("#include <future>")
|
||||
L.add("#include <stdexcept>")
|
||||
L.add("#include <string>")
|
||||
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 <typename T> struct Capture {")
|
||||
L.add(" int ret = RET_ERR;")
|
||||
L.add(" T value{};")
|
||||
L.add(" std::string err;")
|
||||
L.add(" std::promise<void> done;")
|
||||
L.add("};")
|
||||
L.add("struct AckCapture {")
|
||||
L.add(" int ret = RET_ERR;")
|
||||
L.add(" std::string err;")
|
||||
L.add(" std::promise<void> 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<detail::AckCapture*>(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<detail::Capture<std::string>*>(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<detail::Capture<" & rt & ">*>(ud);")
|
||||
L.add(" c->ret = ret;")
|
||||
L.add(" if (ret == RET_OK) c->value = fromC(*reinterpret_cast<const ::" &
|
||||
rt & "*>(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] = @[],
|
||||
) =
|
||||
# `<lib>_native.hpp` for now so it coexists with the CBOR `<lib>.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),
|
||||
)
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user