import std/[macros, atomics, sysatomics, compilesettings], strformat, chronicles, chronos import strutils import ../codegen/meta func nimMainPrefixOnCmdLine(cmdLine: string): tuple[found: bool, value: string] = ## Last `--nimMainPrefix:X` on the command line (style-insensitive match, `:` ## or `=`); absence isn't proof it was never set (config.nims may not surface). 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 init symbol is importc'd as `lib{libraryName}NimMain`, so the build must ## pass `--nimMainPrefix:lib{libraryName}`; a mismatch errors, absence only ## hints (config.nims may set it) and only under `--app:lib`. 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 = currentLibName = libraryName validateNimMainPrefix(libraryName) var res = newStmtList() # {.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()), ) # {.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", ) # {.passc: "-fPIC".} res.add nnkPragma.newTree(nnkExprColonExpr.newTree(ident"passc", newLit("-fPIC"))) # soname / install_name only apply to a shared library and break an executable link (fatally on macOS), so emit them only under `--app:lib`. if compileOption("app", "lib"): when defined(linux): let soName = fmt"-Wl,-soname,lib{libraryName}.so" res.add( newNimNode(nnkPragma).add( nnkExprColonExpr.newTree(ident"passl", newStrLitNode(soName)) ) ) elif defined(macosx): 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) # initState: 0=not started, 1=in progress, 2=done. Atomic (not a bool) so a racing caller can't skip past the gate mid-init (else Windows WSAStartup fails). let atomicType = nnkBracketExpr.newTree(ident("Atomic"), ident("int")) let varStmt = nnkVarSection.newTree( nnkIdentDefs.newTree(ident("initState"), 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.} = ## Calls `NimMain` exactly once to init the Nim runtime. Concurrent ## callers must block until it returns (its chronos globalInit runs ## WSAStartup on Windows; racing past yields "WSAStartup failed" later). var expected: int = 0 if initState.compareExchange(expected, 1): `nimMainName`() initState.store(2) else: while initState.load() != 2: cpuRelax() 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, defaultABIFormat: static[string] = "cbor", ): untyped = ## Declares a library and emits the C-exported event ABI (`_add_event_listener` / ## `_remove_event_listener`) on its `FFIContext`. `defaultABIFormat` (`"cbor"`/`"c"`) ## is inherited unless an annotation overrides via `"abi = ..."`. currentLibType = $libType # so handle-receiver `.ffi.` procs can resolve the pool let (abiOk, abiFmt) = parseABIFormatName(defaultABIFormat) if not abiOk: error( "declareLibrary: unknown defaultABIFormat '" & defaultABIFormat & "'; valid values are \"c\" and \"cbor\"" ) currentDefaultABIFormat = abiFmt libraryDeclared = true var stmts = newStmtList() stmts.add(newCall(ident("declareLibraryBase"), newStrLitNode(libraryName))) # The pool the generated wrappers validate against. let poolIdent = ident($libType & "FFIPool") stmts.add quote do: when not declared(`poolIdent`): var `poolIdent`*: FFIContextPool[`libType`] let ctxType = nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libType)) let cdeclExportPragma = newTree( nnkPragma, ident("dynlib"), ident("exportc"), ident("cdecl"), newTree(nnkExprColonExpr, ident("raises"), newTree(nnkBracket)), ) # {libraryName}_add_event_listener let addName = libraryName & "_add_event_listener" let addErr = "error: invalid context in " & addName let addBody = quote: var ret: uint64 = 0 if isNil(ctx): echo `addErr` return ret let evtName = if eventName.isNil(): "" else: $eventName ret = addEventListener(ctx[].eventRegistry, evtName, callback, userData) return ret stmts.add( newProc( name = ident(addName), params = @[ ident("uint64"), newIdentDefs(ident("ctx"), ctxType), newIdentDefs(ident("eventName"), ident("cstring")), newIdentDefs(ident("callback"), ident("FFICallBack")), newIdentDefs(ident("userData"), ident("pointer")), ], body = addBody, pragmas = cdeclExportPragma, ) ) # Param is `listenerId`, not `id`: `id` collides with chronos's `futures.id` template under quote injection and the captured symbol wins. let removeName = libraryName & "_remove_event_listener" let removeErr = "error: invalid context in " & removeName let removeBody = quote: var ret: cint = 1 if isNil(ctx): echo `removeErr` return ret if removeEventListener(ctx[].eventRegistry, listenerId): ret = 0 return ret stmts.add( newProc( name = ident(removeName), params = @[ ident("cint"), newIdentDefs(ident("ctx"), ctxType), newIdentDefs(ident("listenerId"), ident("uint64")), ], body = removeBody, pragmas = cdeclExportPragma, ) ) return stmts