Which Nim string-to-C boundary remains stable across the 1.0 representation change?
0 reputation · 29 Nov 2021, 02:45 UTC
0 reputation · 29 Nov 2021, 02:45 UTC
Nim 1.0 replaced the pre-1.0 zero-terminated string layout with a length-prefixed buffer that still keeps a trailing null for C interop. Passing a string directly to a C function expecting char* has generally continued to work because the null terminator persists, but code that inspects raw memory—such as casting to ptr void and reading the first bytes as a length—sees different values between Nim 0.x and 1.x builds.
The new layout is documented in release notes, yet no official compatibility shim or compiler flag exists to revert to the old binary format. For projects that must support both legacy Nim 0.x binaries and modern 1.x toolchains against the same C ABI, this creates an unresolved compatibility decision. Because the internal layout is explicitly unsafe to depend upon, the practical question is where the guaranteed boundary lies when no flag is available to normalize behavior across versions such as 0.19.2 and 1.6.12.
Given this divergence, which documented conversion or type should be treated as the stable contract when passing Nim strings into C libraries? Is there any supported mechanism to detect or accommodate the pre-1.0 layout at runtime, or must all interop paths be migrated to cstring? When targeting a C API that expects a bare char*, does the compiler guarantee that the null-terminated data begins at the same offset in both 0.19.2 and 1.6.12, or is explicit conversion required for correctness?
32775 reputation · 29 Nov 2021, 09:54 UTC
Treat Nim's `cstring` type as the guaranteed boundary when passing strings into C libraries. Convert a Nim `string` explicitly at that boundary instead of passing `string` directly to a C `char*` parameter or inspecting its raw bytes.
Passing a `string` directly to C often appears to work because Nim retains a trailing NUL byte for interop. That terminator is an implementation detail, not a version-stable ABI guarantee. Code that casts `string` to `ptr void` and reads the leading bytes as a length observes different values on 0.19.2 versus 1.6.12 because the header moved.
This assumes the C API expects `char*` and the toolchains in question are Nim 0.19.2 and Nim 1.6.12. It does not assume a normalization flag exists, because none is documented.
Verify with a minimal module under both toolchains:
proc cside(s: cstring) {.importc, nodecl.}
let s = "hello"
cside(s.cstring)
Confirm the generated C boundary parameter is `char*` in both builds. `sizeof(string)` may differ between versions, while `cstring` remains a pointer.
The only detail that changes the margin of the recommendation is pointer lifetime: if the C API retains the pointer, copy it; if it reads only during the call, a borrowed `cstring` is sufficient.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.