diff --git a/ffi.nimble b/ffi.nimble index 722beb6..7e94dfc 100644 --- a/ffi.nimble +++ b/ffi.nimble @@ -226,7 +226,7 @@ task genbindings_rust, "Generate Rust bindings for the timer example": " -o:/dev/null examples/timer/timer.nim" task genbindings_cddl, "Generate CDDL schema for the timer example": - exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libtimer" & + exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libmy_timer" & " -d:ffiGenBindings -d:targetLang=cddl" & " -d:ffiOutputDir=examples/timer/cddl_bindings" & " -d:ffiSrcPath=../timer.nim" & " -o:/dev/null examples/timer/timer.nim" diff --git a/ffi/internal/ffi_library.nim b/ffi/internal/ffi_library.nim index 5eadba6..a7c466e 100644 --- a/ffi/internal/ffi_library.nim +++ b/ffi/internal/ffi_library.nim @@ -1,10 +1,59 @@ -import std/[macros, atomics, sysatomics], strformat, chronicles, chronos +import + std/[macros, atomics, sysatomics, compilesettings], strformat, chronicles, chronos +import strutils import ../codegen/meta +func nimMainPrefixOnCmdLine(cmdLine: string): tuple[found: bool, value: string] = + ## Scan the compiler command line for `--nimMainPrefix:X` (last one wins) and + ## return its value. Switch names in Nim are style-insensitive, so the name + ## is matched lowercased and with underscores stripped; the separator may be + ## `:` or `=`. Returns `(false, "")` when the flag is absent — note config.nims + ## switches may not surface here, so absence is not proof it was never set. + var found = false + var value = "" + for tok in cmdLine.splitWhitespace(): + let body = tok.strip(trailing = false, chars = {'-'}) + let sep = body.find({':', '='}) + if sep < 0: + continue + if body[0 ..< sep].toLowerAscii().replace("_", "") == "nimmainprefix": + found = true + value = body[sep + 1 .. ^1] + (found, value) + +proc validateNimMainPrefix(libraryName: string) {.compileTime.} = + ## The Nim runtime init symbol is importc'd as `lib{libraryName}NimMain`, so + ## the build must pass `--nimMainPrefix:lib{libraryName}`; a mismatch otherwise + ## surfaces only at link time as an obscure undefined-symbol error. Absence + ## can't be an error — config.nims may set the prefix without it showing on + ## `commandLine` — so it only warrants a hint, and only for the `--app:lib` + ## build where the prefix actually matters. + let expectedPrefix = "lib" & libraryName + let (prefixFound, prefixValue) = + nimMainPrefixOnCmdLine(querySetting(SingleValueSetting.commandLine)) + if prefixFound and prefixValue != expectedPrefix: + error( + "declareLibrary(\"" & libraryName & + "\"): the Nim runtime init symbol is importc'd as " & expectedPrefix & + "NimMain, so the build needs --nimMainPrefix:" & expectedPrefix & + ", but the command line passes --nimMainPrefix:" & prefixValue & + ". Change the flag to --nimMainPrefix:" & expectedPrefix & + " (it must be \"lib\" followed by the declareLibrary name)." + ) + elif not prefixFound and compileOption("app", "lib"): + hint( + "declareLibrary(\"" & libraryName & "\"): pass --nimMainPrefix:" & expectedPrefix & + " so the Nim runtime init symbol " & expectedPrefix & + "NimMain resolves; without it the build may fail with an undefined-symbol" & + " link error (ignore this hint if the prefix is set in config.nims)." + ) + macro declareLibraryBase*(libraryName: static[string]): untyped = # Record the library name for binding generation currentLibName = libraryName + validateNimMainPrefix(libraryName) + var res = newStmtList() ## Generate {.pragma: exported, exportc, cdecl, raises: [].} diff --git a/tests/unit/mainprefix_fixture.nim b/tests/unit/mainprefix_fixture.nim new file mode 100644 index 0000000..6861e42 --- /dev/null +++ b/tests/unit/mainprefix_fixture.nim @@ -0,0 +1,5 @@ +import ffi + +type MpFixture = object + +declareLibrary("mpfixture", MpFixture) diff --git a/tests/unit/test_main_prefix_validation.nim b/tests/unit/test_main_prefix_validation.nim new file mode 100644 index 0000000..d8286f7 --- /dev/null +++ b/tests/unit/test_main_prefix_validation.nim @@ -0,0 +1,23 @@ +import std/[os, strutils] +import unittest2 + +# The validation only fires when --nimMainPrefix is on the command line, so we +# capture a real `nim check` of the fixture at this test's compile time. +# `mainprefix_fixture.nim` deliberately lacks the `test_` prefix so the nimble +# runner never compiles it standalone. +const + nimExe = getCurrentCompilerExe() + fixture = currentSourcePath.parentDir / "mainprefix_fixture.nim" + checkCmd = nimExe & " check --hints:off --colors:off " + wrongPrefixOutput = staticExec(checkCmd & "--nimMainPrefix:libWRONG " & fixture) + rightPrefixOutput = staticExec(checkCmd & "--nimMainPrefix:libmpfixture " & fixture) + +suite "compile-time --nimMainPrefix validation": + test "a mismatched prefix errors and names the expected flag": + check "Error:" in wrongPrefixOutput + # naming the expected flag is what distinguishes our error from any other + # compile failure, so assert on it rather than the bare "Error:". + check "needs --nimMainPrefix:libmpfixture" in wrongPrefixOutput + + test "the matching prefix compiles without error": + check "Error:" notin rightPrefixOutput