diff --git a/examples/nim_timer/nim_timer.nimble b/examples/nim_timer/nim_timer.nimble index a21b837..fc98471 100644 --- a/examples/nim_timer/nim_timer.nimble +++ b/examples/nim_timer/nim_timer.nimble @@ -11,6 +11,8 @@ requires "taskpools" requires "https://github.com/logos-messaging/nim-ffi >= 0.1.3" const nimFlags = "--mm:orc -d:chronicles_log_level=WARN" +# Flags baked into the generated build.rs / CMakeLists.txt -- see ffi.nimble. +const genBakedFlags = "--mm:orc -d:chronicles_log_level=WARN" task build, "Compile the nimtimer library": exec "nim c " & nimFlags & @@ -19,9 +21,13 @@ task build, "Compile the nimtimer library": task genbindings_rust, "Generate Rust bindings for the nimtimer example": exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libnimtimer" & " -d:ffiGenBindings -d:targetLang=rust" & " -d:ffiOutputDir=rust_bindings" & - " -d:ffiNimSrcRelPath=nim_timer.nim" & " -o:/dev/null nim_timer.nim" + " -d:ffiNimSrcRelPath=nim_timer.nim" & + " -d:ffiNimBuildFlags=\"" & genBakedFlags & "\"" & + " -o:/dev/null nim_timer.nim" task genbindings_cpp, "Generate C++ bindings for the nimtimer example": exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libnimtimer" & " -d:ffiGenBindings -d:targetLang=cpp" & " -d:ffiOutputDir=cpp_bindings" & - " -d:ffiNimSrcRelPath=nim_timer.nim" & " -o:/dev/null nim_timer.nim" + " -d:ffiNimSrcRelPath=nim_timer.nim" & + " -d:ffiNimBuildFlags=\"" & genBakedFlags & "\"" & + " -o:/dev/null nim_timer.nim" diff --git a/ffi.nimble b/ffi.nimble index 8dc08f4..e27be71 100644 --- a/ffi.nimble +++ b/ffi.nimble @@ -41,18 +41,26 @@ task genbindings_example, "Generate Rust bindings for the nim_timer example": exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -o:/dev/null examples/nim_timer/nim_timer.nim" exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -o:/dev/null examples/nim_timer/nim_timer.nim" +# Flags baked into the generated build.rs / CMakeLists.txt so downstream +# consumers compile the Nim library with the same conventions this example +# uses. Anything that should NOT be hardcoded into the published bindings +# goes here, not in the generator. +const genBakedFlags = "--mm:orc -d:chronicles_log_level=WARN" + task genbindings_rust, "Generate Rust bindings for the nim_timer example": exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libnimtimer" & " -d:ffiGenBindings -d:targetLang=rust" & " -d:ffiOutputDir=examples/nim_timer/rust_bindings" & " -d:ffiNimSrcRelPath=../nim_timer.nim" & + " -d:ffiNimBuildFlags=\"" & genBakedFlags & "\"" & " -o:/dev/null examples/nim_timer/nim_timer.nim" exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libnimtimer" & " -d:ffiGenBindings -d:targetLang=rust" & " -d:ffiOutputDir=examples/nim_timer/rust_bindings" & " -d:ffiNimSrcRelPath=../nim_timer.nim" & + " -d:ffiNimBuildFlags=\"" & genBakedFlags & "\"" & " -o:/dev/null examples/nim_timer/nim_timer.nim" task genbindings_cpp, "Generate C++ bindings for the nim_timer example": @@ -61,10 +69,12 @@ task genbindings_cpp, "Generate C++ bindings for the nim_timer example": " -d:ffiGenBindings -d:targetLang=cpp" & " -d:ffiOutputDir=examples/nim_timer/cpp_bindings" & " -d:ffiNimSrcRelPath=../nim_timer.nim" & + " -d:ffiNimBuildFlags=\"" & genBakedFlags & "\"" & " -o:/dev/null examples/nim_timer/nim_timer.nim" exec "nim c " & nimFlagsRefc & " --app:lib --noMain --nimMainPrefix:libnimtimer" & " -d:ffiGenBindings -d:targetLang=cpp" & " -d:ffiOutputDir=examples/nim_timer/cpp_bindings" & " -d:ffiNimSrcRelPath=../nim_timer.nim" & + " -d:ffiNimBuildFlags=\"" & genBakedFlags & "\"" & " -o:/dev/null examples/nim_timer/nim_timer.nim" diff --git a/ffi/codegen/cpp.nim b/ffi/codegen/cpp.nim index 988c500..996fa6a 100644 --- a/ffi/codegen/cpp.nim +++ b/ffi/codegen/cpp.nim @@ -483,10 +483,15 @@ proc generateCppHeader*( result = lines.join("\n") -proc generateCppCMakeLists*(libName: string, nimSrcRelPath: string): string = +proc generateCppCMakeLists*( + libName: string, nimSrcRelPath: string, nimBuildFlags: 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. + ## nimBuildFlags is a whitespace-separated string of extra `nim c` flags + ## injected after `nim c` and before the required --app:lib / --noMain / + ## --nimMainPrefix / -o: flags. let src = nimSrcRelPath.replace("\\", "/") let cv = "${CMAKE_CURRENT_SOURCE_DIR}" # CMake variable shorthand let rv = "${REPO_ROOT}" @@ -554,8 +559,8 @@ proc generateCppCMakeLists*(libName: string, nimSrcRelPath: string): string = 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") + for tok in nimBuildFlags.splitWhitespace(): + L.add(" " & tok) L.add(" --app:lib") L.add(" --noMain") L.add(" \"--nimMainPrefix:lib" & libName & "\"") @@ -601,7 +606,11 @@ proc generateCppBindings*( libName: string, outputDir: string, nimSrcRelPath: string, + nimBuildFlags: string = "", ) = createDir(outputDir) writeFile(outputDir / (libName & ".hpp"), generateCppHeader(procs, types, libName)) - writeFile(outputDir / "CMakeLists.txt", generateCppCMakeLists(libName, nimSrcRelPath)) + writeFile( + outputDir / "CMakeLists.txt", + generateCppCMakeLists(libName, nimSrcRelPath, nimBuildFlags), + ) diff --git a/ffi/codegen/meta.nim b/ffi/codegen/meta.nim index 7cb6bb7..d949f11 100644 --- a/ffi/codegen/meta.nim +++ b/ffi/codegen/meta.nim @@ -44,3 +44,11 @@ const ffiOutputDir* {.strdefine.} = "" # Nim source path (relative to outputDir) embedded in generated build files; # set with -d:ffiNimSrcRelPath=../relative/path.nim const ffiNimSrcRelPath* {.strdefine.} = "" + +# Whitespace-separated extra Nim compiler flags to bake into the generated +# build.rs / CMakeLists.txt for the project's own `nim c` invocation. The +# generator emits only the strictly-required flags (--app:lib, --noMain, +# --nimMainPrefix, -o:) by default; everything else (gc choice, log level, +# project-specific -d defines, etc.) is up to the caller. Example: +# -d:ffiNimBuildFlags="--mm:orc -d:chronicles_log_level=WARN" +const ffiNimBuildFlags* {.strdefine.} = "" diff --git a/ffi/codegen/rust.nim b/ffi/codegen/rust.nim index 0d59327..0e17b77 100644 --- a/ffi/codegen/rust.nim +++ b/ffi/codegen/rust.nim @@ -81,10 +81,27 @@ tokio = { version = "1", features = ["sync"] } """ % [libName] -proc generateBuildRs*(libName: string, nimSrcRelPath: string): string = +proc rustEscapeStrLit(s: string): string = + ## Escapes a string for safe embedding in a Rust string literal. + s.replace("\\", "\\\\").replace("\"", "\\\"") + +proc generateBuildRs*( + libName: string, nimSrcRelPath: string, nimBuildFlags: string = "" +): string = ## Generates build.rs that compiles the Nim library. ## nimSrcRelPath is relative to the output (crate) directory. + ## nimBuildFlags is a whitespace-separated string of extra `nim c` flags + ## (e.g. "--mm:orc -d:chronicles_log_level=WARN") to inject into the + ## generated `nim c` invocation, after `nim c` and before the required + ## --app:lib / --noMain / --nimMainPrefix / -o: flags. let escapedSrc = nimSrcRelPath.replace("\\", "\\\\") + # Build the leading section of the cmd.arg(...) chain: the mandatory + # `.arg("c")` plus one `.arg("...")` per caller-supplied flag, each on its + # own line. The template then reads as `cmd$3` -- $3 is the whole leading + # chain, so no placeholder ever sits inside a method-call expression. + var argChain = ".arg(\"c\")" + for tok in nimBuildFlags.splitWhitespace(): + argChain.add("\n .arg(\"" & rustEscapeStrLit(tok) & "\")") result = """use std::path::PathBuf; use std::process::Command; @@ -123,9 +140,7 @@ fn main() { compile_error!("nim-ffi build.rs: unsupported target OS (expected macos, linux, or windows)"); let mut cmd = Command::new("nim"); - cmd.arg("c") - .arg("--mm:orc") - .arg("-d:chronicles_log_level=WARN") + cmd$3 .arg("--app:lib") .arg("--noMain") .arg(format!("--nimMainPrefix:lib$2")) @@ -140,7 +155,7 @@ fn main() { println!("cargo:rerun-if-changed={}", nim_src.display()); } """ % - [escapedSrc, libName] + [escapedSrc, libName, argChain] proc generateLibRs*(): string = result = """mod ffi; @@ -502,13 +517,17 @@ proc generateRustCrate*( libName: string, outputDir: string, nimSrcRelPath: string, + nimBuildFlags: string = "", ) = ## Generates a complete Rust crate in outputDir. createDir(outputDir) createDir(outputDir / "src") writeFile(outputDir / "Cargo.toml", generateCargoToml(libName)) - writeFile(outputDir / "build.rs", generateBuildRs(libName, nimSrcRelPath)) + writeFile( + outputDir / "build.rs", + generateBuildRs(libName, nimSrcRelPath, nimBuildFlags), + ) writeFile(outputDir / "src" / "lib.rs", generateLibRs()) writeFile(outputDir / "src" / "ffi.rs", generateFfiRs(procs)) writeFile(outputDir / "src" / "types.rs", generateTypesRs(types)) diff --git a/ffi/internal/ffi_macro.nim b/ffi/internal/ffi_macro.nim index 9a75f93..efa4c5b 100644 --- a/ffi/internal/ffi_macro.nim +++ b/ffi/internal/ffi_macro.nim @@ -1604,6 +1604,7 @@ macro ffiDtor*(prc: untyped): untyped = macro genBindings*( outputDir: static[string] = ffiOutputDir, nimSrcRelPath: static[string] = ffiNimSrcRelPath, + nimBuildFlags: static[string] = ffiNimBuildFlags, ): untyped = ## Emits C++ or Rust binding files from the compile-time FFI registries. ## @@ -1638,11 +1639,13 @@ macro genBindings*( case lang of "rust": generateRustCrate( - ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath + ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath, + nimBuildFlags, ) of "cpp", "c++": generateCppBindings( - ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath + ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath, + nimBuildFlags, ) else: error("genBindings: unknown targetLang '" & lang & "'. Use 'rust' or 'cpp'.")