## C++ binding generator for the nim-ffi framework. ## Generates a header-only C++ binding and CMakeLists.txt from compile-time FFI metadata. import std/[os, strutils] import ./meta proc nimTypeToCpp*(typeName: string): string = case typeName of "string", "cstring": "std::string" of "int", "int64": "int64_t" of "int32": "int32_t" of "bool": "bool" of "float", "float64": "double" of "pointer": "void*" else: typeName proc stripLibPrefixCpp(procName, libName: string): string = let prefix = libName & "_" if procName.startsWith(prefix): return procName[prefix.len .. ^1] return procName proc generateCppHeader*( procs: seq[FFIProcMeta], types: seq[FFITypeMeta], libName: string, ): string = var lines: seq[string] = @[] lines.add("#pragma once") lines.add("#include ") lines.add("#include ") lines.add("#include ") lines.add("#include ") lines.add("#include ") lines.add("#include ") lines.add("#include ") lines.add("") # Types if types.len > 0: lines.add("// ============================================================") lines.add("// Types") lines.add("// ============================================================") lines.add("") for t in types: lines.add("struct $1 {" % [t.name]) for f in t.fields: let cppType = nimTypeToCpp(f.typeName) lines.add(" $1 $2;" % [cppType, f.name]) lines.add("};") var fieldNames: seq[string] = @[] for f in t.fields: fieldNames.add(f.name) lines.add("NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE($1, $2)" % [t.name, fieldNames.join(", ")]) lines.add("") # C extern declarations lines.add("// ============================================================") lines.add("// C FFI declarations") lines.add("// ============================================================") lines.add("") lines.add("extern \"C\" {") lines.add("typedef void (*FfiCallback)(int ret, const char* msg, size_t len, void* user_data);") lines.add("") for p in procs: var params: seq[string] = @[] if p.kind == ffiFfiKind: params.add("void* ctx") params.add("FfiCallback callback") params.add("void* user_data") for ep in p.extraParams: params.add("const char* $1_json" % [ep.name]) else: # ffiCtorKind for ep in p.extraParams: params.add("const char* $1_json" % [ep.name]) params.add("FfiCallback callback") params.add("void* user_data") lines.add("int $1($2);" % [p.procName, params.join(", ")]) lines.add("} // extern \"C\"") lines.add("") # Anonymous namespace with synchronous call helper lines.add("// ============================================================") lines.add("// Synchronous call helper (anonymous namespace, header-only)") lines.add("// ============================================================") lines.add("") lines.add("namespace {") lines.add("") lines.add("struct FfiCallState_ {") lines.add(" std::mutex mtx;") lines.add(" std::condition_variable cv;") lines.add(" bool done{false};") lines.add(" bool ok{false};") lines.add(" std::string msg;") lines.add("};") lines.add("") lines.add("inline void ffi_cb_(int ret, const char* msg, size_t /*len*/, void* ud) {") lines.add(" auto* s = static_cast(ud);") lines.add(" std::lock_guard lock(s->mtx);") lines.add(" s->ok = (ret == 0);") lines.add(" s->msg = msg ? std::string(msg) : std::string{};") lines.add(" s->done = true;") lines.add(" s->cv.notify_one();") lines.add("}") lines.add("") lines.add("inline std::string ffi_call_(std::function f) {") lines.add(" FfiCallState_ state;") lines.add(" const int ret = f(ffi_cb_, &state);") lines.add(" if (ret == 2)") lines.add(" throw std::runtime_error(\"RET_MISSING_CALLBACK (internal error)\");") lines.add(" std::unique_lock lock(state.mtx);") lines.add(" state.cv.wait(lock, [&state]{ return state.done; });") lines.add(" if (!state.ok)") lines.add(" throw std::runtime_error(state.msg);") lines.add(" return state.msg;") lines.add("}") lines.add("") lines.add("} // anonymous namespace") lines.add("") # Derive context type name and separate ctors / methods var ctors: seq[FFIProcMeta] = @[] var methods: seq[FFIProcMeta] = @[] for p in procs: if p.kind == ffiCtorKind: ctors.add(p) else: methods.add(p) let libTypeName = if ctors.len > 0: ctors[0].libTypeName else: libName[0 .. 0].toUpperAscii() & libName[1 .. ^1] let ctxTypeName = libTypeName & "Ctx" lines.add("// ============================================================") lines.add("// High-level C++ context class") lines.add("// ============================================================") lines.add("") lines.add("class $1 {" % [ctxTypeName]) lines.add("public:") # Static create() factory for ctor in ctors: var ctorParams: seq[string] = @[] for ep in ctor.extraParams: let cppType = nimTypeToCpp(ep.typeName) ctorParams.add("const $1& $2" % [cppType, ep.name]) lines.add(" static $1 create($2) {" % [ctxTypeName, ctorParams.join(", ")]) for ep in ctor.extraParams: lines.add(" const auto $1_json = nlohmann::json($1).dump();" % [ep.name]) var callArgs: seq[string] = @[] for ep in ctor.extraParams: callArgs.add("$1_json.c_str()" % [ep.name]) callArgs.add("cb") callArgs.add("ud") lines.add(" const auto raw = ffi_call_([&](FfiCallback cb, void* ud) {") lines.add(" return $1($2);" % [ctor.procName, callArgs.join(", ")]) lines.add(" });") lines.add(" // ctor returns the context address as a plain decimal string") lines.add(" const auto addr = std::stoull(raw);") lines.add(" return $1(reinterpret_cast(static_cast(addr)));" % [ctxTypeName]) lines.add(" }") lines.add("") # Instance methods for m in methods: let methodName = stripLibPrefixCpp(m.procName, libName) let retCppType = nimTypeToCpp(m.returnTypeName) var methParams: seq[string] = @[] for ep in m.extraParams: let cppType = nimTypeToCpp(ep.typeName) methParams.add("const $1& $2" % [cppType, ep.name]) let methParamsStr = methParams.join(", ") lines.add(" $1 $2($3) const {" % [retCppType, methodName, methParamsStr]) for ep in m.extraParams: lines.add(" const auto $1_json = nlohmann::json($1).dump();" % [ep.name]) var callArgs = @["ptr_", "cb", "ud"] for ep in m.extraParams: callArgs.add("$1_json.c_str()" % [ep.name]) lines.add(" const auto raw = ffi_call_([&](FfiCallback cb, void* ud) {") lines.add(" return $1($2);" % [m.procName, callArgs.join(", ")]) lines.add(" });") if retCppType == "std::string": lines.add(" return nlohmann::json::parse(raw).get();") else: lines.add(" return nlohmann::json::parse(raw).get<$1>();" % [retCppType]) lines.add(" }") lines.add("") lines.add("private:") lines.add(" void* ptr_;") lines.add(" explicit $1(void* p) : ptr_(p) {}" % [ctxTypeName]) lines.add("};") lines.add("") result = lines.join("\n") proc generateCppCMakeLists*(libName: string, nimSrcRelPath: string): string = ## Generates CMakeLists.txt for the C++ bindings directory. ## CMake uses ${...} which would clash with Nim's % format operator, ## so we build the file line by line using string concatenation. let src = nimSrcRelPath.replace("\\", "/") let cv = "${CMAKE_CURRENT_SOURCE_DIR}" # CMake variable shorthand let rv = "${REPO_ROOT}" let lf = "${NIM_LIB_FILE}" let nm = "${NIM_EXECUTABLE}" let ns = "${NIM_SRC}" let sd = "${_search_dir}" var L: seq[string] = @[] L.add("cmake_minimum_required(VERSION 3.14)") L.add("project(" & libName & "_cpp_bindings CXX)") L.add("") L.add("set(CMAKE_CXX_STANDARD 17)") L.add("set(CMAKE_CXX_STANDARD_REQUIRED ON)") L.add("") L.add("# ── nlohmann/json ─────────────────────────────────────────────────────────────") L.add("include(FetchContent)") L.add("FetchContent_Declare(") L.add(" nlohmann_json") L.add(" GIT_REPOSITORY https://github.com/nlohmann/json.git") L.add(" GIT_TAG v3.11.3") L.add(" GIT_SHALLOW TRUE") L.add(")") L.add("FetchContent_MakeAvailable(nlohmann_json)") L.add("") L.add("# ── Locate the repository root (contains ffi.nimble) ─────────────────────────") L.add("set(_search_dir \"" & cv & "\")") L.add("set(REPO_ROOT \"\")") L.add("foreach(_i RANGE 10)") L.add(" if(EXISTS \"" & sd & "/ffi.nimble\")") L.add(" set(REPO_ROOT \"" & sd & "\")") L.add(" break()") L.add(" endif()") L.add(" get_filename_component(_search_dir \"" & sd & "\" DIRECTORY)") L.add("endforeach()") L.add("if(\"${REPO_ROOT}\" STREQUAL \"\")") L.add(" message(FATAL_ERROR \"Cannot find repo root (no ffi.nimble in any ancestor)\")") L.add("endif()") L.add("") L.add("# ── Nim source path ───────────────────────────────────────────────────────────") L.add("get_filename_component(NIM_SRC") L.add(" \"" & cv & "/" & src & "\"") L.add(" ABSOLUTE)") L.add("") L.add("# ── Compile the Nim shared library ───────────────────────────────────────────") L.add("find_program(NIM_EXECUTABLE nim REQUIRED)") L.add("") L.add("if(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")") L.add(" set(NIM_LIB_FILE \"" & rv & "/lib" & libName & ".dylib\")") L.add("elseif(CMAKE_SYSTEM_NAME STREQUAL \"Windows\")") L.add(" set(NIM_LIB_FILE \"" & rv & "/" & libName & ".dll\")") L.add("else()") L.add(" set(NIM_LIB_FILE \"" & rv & "/lib" & libName & ".so\")") L.add("endif()") L.add("") L.add("add_custom_command(") L.add(" OUTPUT \"" & lf & "\"") L.add(" COMMAND \"" & nm & "\" c") L.add(" --mm:orc") L.add(" -d:chronicles_log_level=WARN") L.add(" --app:lib") L.add(" --noMain") L.add(" \"--nimMainPrefix:lib" & libName & "\"") L.add(" \"-o:" & lf & "\"") L.add(" \"" & ns & "\"") L.add(" WORKING_DIRECTORY \"" & rv & "\"") L.add(" DEPENDS \"" & ns & "\"") L.add(" COMMENT \"Compiling Nim library lib" & libName & "\"") L.add(" VERBATIM") L.add(")") L.add("add_custom_target(nim_lib ALL DEPENDS \"" & lf & "\")") L.add("") L.add("add_library(" & libName & " SHARED IMPORTED GLOBAL)") L.add("set_target_properties(" & libName & " PROPERTIES IMPORTED_LOCATION \"" & lf & "\")") L.add("add_dependencies(" & libName & " nim_lib)") L.add("") L.add("# ── Interface target exposing the generated header ────────────────────────────") L.add("add_library(" & libName & "_headers INTERFACE)") L.add("target_include_directories(" & libName & "_headers INTERFACE \"" & cv & "\")") L.add("target_link_libraries(" & libName & "_headers INTERFACE " & libName & " nlohmann_json::nlohmann_json)") L.add("") L.add("# ── Optional example executable ───────────────────────────────────────────────") L.add("if(EXISTS \"" & cv & "/main.cpp\")") L.add(" add_executable(example main.cpp)") L.add(" target_link_libraries(example PRIVATE " & libName & "_headers)") L.add(" add_dependencies(example nim_lib)") L.add("endif()") L.add("") result = L.join("\n") proc generateCppBindings*( procs: seq[FFIProcMeta], types: seq[FFITypeMeta], libName: string, outputDir: string, nimSrcRelPath: string, ) = createDir(outputDir) writeFile(outputDir / (libName & ".hpp"), generateCppHeader(procs, types, libName)) writeFile(outputDir / "CMakeLists.txt", generateCppCMakeLists(libName, nimSrcRelPath))