From e22b887d7ca4ca69cfc463b20ea37b06bcdf6433 Mon Sep 17 00:00:00 2001 From: Ivan FB Date: Thu, 18 Jun 2026 20:11:39 +0200 Subject: [PATCH] fix(library): block first-time callers until NimMain completes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit initializeLibrary ran NimMain only on the thread that won `initialized.exchange(true)`; concurrent first-time callers fell straight through and used a half-initialized Nim runtime — a heap-corrupting race when multiple foreign (e.g. Go) threads create the first context at once. Add an `initDone` flag the winner sets after NimMain; the others spin until it is set before proceeding. Co-Authored-By: Claude Opus 4.8 --- ffi/internal/ffi_library.nim | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/ffi/internal/ffi_library.nim b/ffi/internal/ffi_library.nim index 7d7d11d..356ac2d 100644 --- a/ffi/internal/ffi_library.nim +++ b/ffi/internal/ffi_library.nim @@ -53,10 +53,13 @@ macro declareLibraryBase*(libraryName: static[string]): untyped = ) res.add(procDef) - # Create: var initialized: Atomic[bool] + # Create: var initialized, initDone: Atomic[bool] + # `initialized` elects the single thread that runs NimMain; `initDone` signals + # that NimMain has finished so concurrent first-time callers can safely proceed. let atomicType = nnkBracketExpr.newTree(ident("Atomic"), ident("bool")) let varStmt = nnkVarSection.newTree( - nnkIdentDefs.newTree(ident("initialized"), atomicType, newEmptyNode()) + nnkIdentDefs.newTree(ident("initialized"), atomicType, newEmptyNode()), + nnkIdentDefs.newTree(ident("initDone"), atomicType, newEmptyNode()), ) res.add(varStmt) @@ -80,6 +83,13 @@ macro declareLibraryBase*(libraryName: static[string]): untyped = ## Being `` the value given in the optional ## compilation flag --nimMainPrefix:yourprefix `nimMainName`() + initDone.store(true) + else: + ## Another thread won the election and is running (or has run) NimMain. + ## Block until it finishes: proceeding now would race a half-initialized + ## Nim runtime/GC and corrupt the heap on concurrent first-time calls. + while not initDone.load(): + discard when declared(setupForeignThreadGc): setupForeignThreadGc() when declared(nimGC_setStackBottom):