diff --git a/examples/nim_timer/nim_timer.nim b/examples/nim_timer/nim_timer.nim index ca645a4..b9e25a9 100644 --- a/examples/nim_timer/nim_timer.nim +++ b/examples/nim_timer/nim_timer.nim @@ -47,4 +47,4 @@ proc nimtimer_version*( return ok("nim-timer v0.1.0") when defined(ffiGenBindings): - genBindings("rust", "examples/nim_timer/nim_bindings", "../nim_timer.nim") + genBindings("examples/nim_timer/nim_bindings", "../nim_timer.nim") diff --git a/ffi.nimble b/ffi.nimble index ba84437..c94c3dc 100644 --- a/ffi.nimble +++ b/ffi.nimble @@ -31,5 +31,8 @@ task test_ffi, "Run FFI context integration tests": task test_serial, "Run serial unit tests": exec "nim c -r " & nimFlags & " tests/test_serial.nim" -task genbindings_example, "Generate Rust bindings for the nim_timer example": - exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -o:/dev/null examples/nim_timer/nim_timer.nim" +task genbindings_rust, "Generate Rust bindings for the nim_timer example": + exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -d:ffiTargetLang=rust -o:/dev/null examples/nim_timer/nim_timer.nim" + +task genbindings_cpp, "Generate C++ bindings for the nim_timer example": + exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -d:ffiTargetLang=cpp -o:/dev/null examples/nim_timer/nim_timer.nim" diff --git a/ffi/codegen/cpp.nim b/ffi/codegen/cpp.nim new file mode 100644 index 0000000..1cdd708 --- /dev/null +++ b/ffi/codegen/cpp.nim @@ -0,0 +1,310 @@ +## 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)) diff --git a/ffi/codegen/meta.nim b/ffi/codegen/meta.nim index 2f3d2dc..39bf1b9 100644 --- a/ffi/codegen/meta.nim +++ b/ffi/codegen/meta.nim @@ -33,3 +33,6 @@ type var ffiProcRegistry* {.compileTime.}: seq[FFIProcMeta] var ffiTypeRegistry* {.compileTime.}: seq[FFITypeMeta] var currentLibName* {.compileTime.}: string + +# Target language for binding generation; override with -d:ffiTargetLang=cpp +const ffiTargetLang* {.strdefine.} = "rust" diff --git a/ffi/internal/ffi_macro.nim b/ffi/internal/ffi_macro.nim index 1cb8cea..3f3ad6d 100644 --- a/ffi/internal/ffi_macro.nim +++ b/ffi/internal/ffi_macro.nim @@ -4,6 +4,7 @@ import ../ffi_types import ../codegen/meta when defined(ffiGenBindings): import ../codegen/rust + import ../codegen/cpp # --------------------------------------------------------------------------- # String helpers used by multiple macros @@ -1383,21 +1384,28 @@ macro ffiCtor*(prc: untyped): untyped = # --------------------------------------------------------------------------- macro genBindings*( - lang: static[string], outputDir: static[string], nimSrcRelPath: static[string] = "", ): untyped = - ## Generates binding files for the specified target language. + ## Generates binding files for the target language set by -d:ffiTargetLang=. + ## Supported values: "rust" (default), "cpp" (case-insensitive). ## Call at the END of your library file, after all {.ffiCtor.} and {.ffi.} procs. ## ## Example: - ## genBindings("rust", "examples/nim_timer/nim_bindings", "examples/nim_timer/nim_timer.nim") + ## genBindings("examples/nim_timer/nim_bindings", "../nim_timer.nim") ## - ## Activate with: nim c -d:ffiGenBindings mylib.nim + ## Activate with: nim c -d:ffiGenBindings -d:ffiTargetLang=rust mylib.nim + ## or: nim c -d:ffiGenBindings -d:ffiTargetLang=cpp mylib.nim when defined(ffiGenBindings): - if lang == "rust": - let libName = deriveLibName(ffiProcRegistry) + let lang = ffiTargetLang.toLowerAscii() + let libName = deriveLibName(ffiProcRegistry) + case lang + of "rust": generateRustCrate(ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath) + of "cpp", "c++": + generateCppBindings(ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath) + else: + error("genBindings: unknown ffiTargetLang '" & lang & "'. Use 'rust' or 'cpp'.") result = newEmptyNode()