nim-ffi/ffi/internal/ffi_library.nim
Ivan FB e22b887d7c
fix(library): block first-time callers until NimMain completes
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 <noreply@anthropic.com>
2026-06-18 20:11:39 +02:00

151 lines
5.2 KiB
Nim

import std/[macros, atomics], strformat, chronicles, chronos
import ../codegen/meta
macro declareLibraryBase*(libraryName: static[string]): untyped =
# Record the library name for binding generation
currentLibName = libraryName
var res = newStmtList()
## Generate {.pragma: exported, exportc, cdecl, raises: [].}
res.add nnkPragma.newTree(
nnkExprColonExpr.newTree(ident"pragma", ident"exported"),
ident"exportc",
ident"cdecl",
nnkExprColonExpr.newTree(ident"raises", nnkBracket.newTree()),
)
## Generate {.pragma: callback, cdecl, raises: [], gcsafe.}
res.add nnkPragma.newTree(
nnkExprColonExpr.newTree(ident"pragma", ident"callback"),
ident"cdecl",
nnkExprColonExpr.newTree(ident"raises", nnkBracket.newTree()),
ident"gcsafe",
)
## Generate {.passc: "-fPIC".}
res.add nnkPragma.newTree(nnkExprColonExpr.newTree(ident"passc", newLit("-fPIC")))
when defined(linux):
## Generates {.passl: "-Wl,-soname,libwaku.so".} (considering libraryName=="waku", for example)
let soName = fmt"-Wl,-soname,lib{libraryName}.so"
res.add(
newNimNode(nnkPragma).add(
nnkExprColonExpr.newTree(ident"passl", newStrLitNode(soName))
)
)
elif defined(macosx):
## Generates {.passl: "-install_name @rpath/libwaku.dylib".}
let installName = fmt"-install_name @rpath/lib{libraryName}.dylib"
res.add(
newNimNode(nnkPragma).add(
nnkExprColonExpr.newTree(ident"passl", newStrLitNode(installName))
)
)
## proc lib{libraryName}NimMain() {.importc.}
let libNimMainName = ident(fmt"lib{libraryName}NimMain")
let importcPragma = nnkPragma.newTree(ident"importc")
let procDef = newProc(
name = libNimMainName,
params = @[ident"void"],
pragmas = importcPragma,
body = newEmptyNode(),
)
res.add(procDef)
# 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("initDone"), atomicType, newEmptyNode()),
)
res.add(varStmt)
## Android chronicles redirection
let chroniclesBlock = quote:
when defined(android) and compiles(defaultChroniclesStream.outputs[0].writer):
defaultChroniclesStream.outputs[0].writer = proc(
logLevel: LogLevel, msg: LogOutputStr
) {.raises: [].} =
echo logLevel, msg
result.add(chroniclesBlock)
let procName = ident("initializeLibrary")
let nimMainName = ident("lib" & libraryName & "NimMain")
let initializeLibraryProc = quote:
proc `procName`*() {.exported.} =
if not initialized.exchange(true):
## Every Nim library needs to call `<yourprefix>NimMain` once exactly,
## to initialize the Nim runtime.
## Being `<yourprefix>` 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):
var locals {.volatile, noinit.}: pointer
locals = addr(locals)
nimGC_setStackBottom(locals)
res.add(initializeLibraryProc)
return res
macro declareLibrary*(libraryName: static[string], libType: untyped): untyped =
## Declares a library with the given name and automatically generates
## `{libraryName}_set_event_callback`, a C-exported function that stores the
## caller's event callback on the FFIContext.
##
## `libType` is the Nim type of the main library object (e.g. `Waku`). It is used
## to type the `ctx: ptr FFIContext[libType]` parameter of the generated
## `{libraryName}_set_event_callback` proc.
result = newStmtList()
# Emit the base bootstrap (pragmas, linker flags, NimMain, initializeLibrary)
result.add(newCall(ident("declareLibraryBase"), newStrLitNode(libraryName)))
let funcName = libraryName & "_set_event_callback"
let funcIdent = ident(funcName)
let errorMsg = "error: invalid context in " & funcName
let ctxType = nnkPtrTy.newTree(
nnkBracketExpr.newTree(ident("FFIContext"), libType)
)
let procBody = quote do:
if isNil(ctx):
echo `errorMsg`
return
ctx[].callbackState.callback = cast[pointer](callback)
ctx[].callbackState.userData = userData
let procNode = newProc(
name = funcIdent,
params = @[
newEmptyNode(),
newIdentDefs(ident("ctx"), ctxType),
newIdentDefs(ident("callback"), ident("FFICallBack")),
newIdentDefs(ident("userData"), ident("pointer")),
],
body = procBody,
pragmas = newTree(
nnkPragma,
ident("dynlib"),
ident("exportc"),
ident("cdecl"),
newTree(nnkExprColonExpr, ident("raises"), newTree(nnkBracket)),
),
)
result.add(procNode)