diff options
Diffstat (limited to 'lib/nimhcr.nim')
-rw-r--r-- | lib/nimhcr.nim | 671 |
1 files changed, 671 insertions, 0 deletions
diff --git a/lib/nimhcr.nim b/lib/nimhcr.nim new file mode 100644 index 000000000..e87bb2413 --- /dev/null +++ b/lib/nimhcr.nim @@ -0,0 +1,671 @@ +discard """ +batchable: false +""" + +# +# +# Nim's Runtime Library +# (c) Copyright 2018 Nim Contributors +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# + +# This is the Nim hot code reloading run-time for the native targets. +# +# This minimal dynamic library is not subject to reloading when the +# `hotCodeReloading` build mode is enabled. It's responsible for providing +# a permanent memory location for all globals and procs within a program +# and orchestrating the reloading. For globals, this is easily achieved +# by storing them on the heap. For procs, we produce on the fly simple +# trampolines that can be dynamically overwritten to jump to a different +# target. In the host program, all globals and procs are first registered +# here with `hcrRegisterGlobal` and `hcrRegisterProc` and then the +# returned permanent locations are used in every reference to these symbols +# onwards. +# +# Detailed description: +# +# When code is compiled with the hotCodeReloading option for native targets +# a couple of things happen for all modules in a project: +# - the useNimRtl option is forced (including when building the HCR runtime too) +# - all modules of a target get built into separate shared libraries +# - the smallest granularity of reloads is modules +# - for each .c (or .cpp) in the corresponding nimcache folder of the project +# a shared object is built with the name of the source file + DLL extension +# - only the main module produces whatever the original project type intends +# (again in nimcache) and is then copied to its original destination +# - linking is done in parallel - just like compilation +# - function calls to functions from the same project go through function pointers: +# - with a few exceptions - see the nonReloadable pragma +# - the forward declarations of the original functions become function +# pointers as static globals with the same names +# - the original function definitions get suffixed with <name>_actual +# - the function pointers get initialized with the address of the corresponding +# function in the DatInit of their module through a call to either hcrRegisterProc +# or hcrGetProc. When being registered, the <name>_actual address is passed to +# hcrRegisterProc and a permanent location is returned and assigned to the pointer. +# This way the implementation (<name>_actual) can change but the address for it +# will be the same - this works by just updating a jump instruction (trampoline). +# For functions from other modules hcrGetProc is used (after they are registered). +# - globals are initialized only once and their state is preserved +# - including locals with the {.global.} pragma +# - their definitions are changed into pointer definitions which are initialized +# in the DatInit() of their module with calls to hcrRegisterGlobal (supplying the +# size of the type that this HCR runtime should allocate) and a bool is returned +# which when true triggers the initialization code for the global (only once). +# Globals from other modules: a global pointer coupled with a hcrGetGlobal call. +# - globals which have already been initialized cannot have their values changed +# by changing their initialization - use a handler or some other mechanism +# - new globals can be introduced when reloading +# - top-level code (global scope) is executed only once - at the first module load +# - the runtime knows every symbol's module owner (globals and procs) +# - both the RTL and HCR shared libraries need to be near the program for execution +# - same folder, in the PATH or LD_LIBRARY_PATH env var, etc (depending on OS) +# - the main module is responsible for initializing the HCR runtime +# - the main module loads the RTL and HCR shared objects +# - after that a call to hcrInit() is done in the main module which triggers +# the loading of all modules the main one imports, and doing that for the +# dependencies of each module recursively. Basically a DFS traversal. +# - then initialization takes place with several passes over all modules: +# - HcrInit - initializes the pointers for HCR procs such as hcrRegisterProc +# - HcrCreateTypeInfos - creates globals which will be referenced in the next pass +# - DatInit - usual dat init + register/get procs and get globals +# - Init - it does the following multiplexed operations: +# - register globals (if already registered - then just retrieve pointer) +# - execute top level scope (only if loaded for the first time) +# - when modules are loaded the originally built shared libraries get copied in +# the same folder and the copies are loaded instead of the original files +# - a module import tree is built in the runtime (and maintained when reloading) +# - hcrPerformCodeReload +# - named `performCodeReload`, requires the hotcodereloading module +# - explicitly called by the user - the current active callstack shouldn't contain +# any functions which are defined in modules that will be reloaded (or crash!). +# The reason is that old dynamic libraries get unloaded. +# Example: +# if A is the main module and it imports B, then only B is reloadable and only +# if when calling hcrPerformCodeReload there is no function defined in B in the +# current active callstack at the point of the call (it has to be done from A) +# - for reloading to take place the user has to have rebuilt parts of the application +# without changes affecting the main module in any way - it shouldn't be rebuilt. +# - to determine what needs to be reloaded the runtime starts traversing the import +# tree from the root and checks the timestamps of the loaded shared objects +# - modules that are no longer referenced are unloaded and cleaned up properly +# - symbols (procs/globals) that have been removed in the code are also cleaned up +# - so changing the init of a global does nothing, but removing it, reloading, +# and then re-introducing it with a new initializer works +# - new modules can be imported, and imports can also be reodereded/removed +# - hcrReloadNeeded() can be used to determine if any module needs reloading +# - named `hasAnyModuleChanged`, requires the hotcodereloading module +# - code in the beforeCodeReload/afterCodeReload handlers is executed on each reload +# - require the hotcodereloading module +# - such handlers can be added and removed +# - before each reload all "beforeCodeReload" handlers are executed and after +# that all handlers (including "after") from the particular module are deleted +# - the order of execution is the same as the order of top-level code execution. +# Example: if A imports B which imports C, then all handlers in C will be executed +# first (from top to bottom) followed by all from B and lastly all from A +# - after the reload all "after" handlers are executed the same way as "before" +# - the handlers for a reloaded module are always removed when reloading and then +# registered when the top-level scope is executed (thanks to `executeOnReload`) +# +# TODO next: +# +# - implement the before/after handlers and hasModuleChanged for the javascript target +# - ARM support for the trampolines +# - investigate: +# - soon the system module might be importing other modules - the init order...? +# (revert https://github.com/nim-lang/Nim/pull/11971 when working on this) +# - rethink the closure iterators +# - ability to keep old versions of dynamic libraries alive +# - because of async server code +# - perhaps with refcounting of .dlls for unfinished closures +# - linking with static libs +# - all shared objects for each module will (probably) have to link to them +# - state in static libs gets duplicated +# - linking is slow and therefore iteration time suffers +# - have just a single .dll for all .nim files and bulk reload? +# - think about the compile/link/passc/passl/emit/injectStmt pragmas +# - if a passc pragma is introduced (either written or dragged in by a new +# import) the whole command line for compilation changes - for example: +# winlean.nim: {.passc: "-DWIN32_LEAN_AND_MEAN".} +# - play with plugins/dlls/lfIndirect/lfDynamicLib/lfExportLib - shouldn't add an extra '*' +# - everything thread-local related +# - tests +# - add a new travis build matrix entry which builds everything with HCR enabled +# - currently building with useNimRtl is problematic - lots of problems... +# - how to supply the nimrtl/nimhcr shared objects to all test binaries...? +# - think about building to C++ instead of only to C - added type safety +# - run tests through valgrind and the sanitizers! +# +# TODO - nice to have cool stuff: +# +# - separate handling of global state for much faster reloading and manipulation +# - imagine sliders in an IDE for tweaking variables +# - perhaps using shared memory +# - multi-dll projects - how everything can be reloaded..? +# - a single HCR instance shared across multiple .dlls +# - instead of having to call hcrPerformCodeReload from a function in each dll +# - which currently renders the main module of each dll not reloadable +# - ability to check with the current callstack if a reload is "legal" +# - if it is in any function which is in a module about to be reloaded ==> error +# - pragma annotations for files - to be excluded from dll shenanigans +# - for such file-global pragmas look at codeReordering or injectStmt +# - how would the initialization order be kept? messy... +# - C code calling stable exportc interface of nim code (for bindings) +# - generate proxy functions with the stable names +# - in a non-reloadable part (the main binary) that call the function pointers +# - parameter passing/forwarding - how? use the same trampoline jumping? +# - extracting the dependencies for these stubs/proxies will be hard... +# - changing memory layout of types - detecting this..? +# - implement with registerType() call to HCR runtime...? +# - and checking if a previously registered type matches +# - issue an error +# - or let the user handle this by transferring the state properly +# - perhaps in the before/afterCodeReload handlers +# - implement executeOnReload for global vars too - not just statements (and document!) +# - cleanup at shutdown - freeing all globals +# - fallback mechanism if the program crashes (the program should detect crashes +# by itself using SEH/signals on Windows/Unix) - should be able to revert to +# previous versions of the .dlls by calling some function from HCR +# - improve runtime performance - possibilities +# - implement a way for multiple .nim files to be bundled into the same dll +# and have all calls within that domain to use the "_actual" versions of +# procs so there are no indirections (or the ability to just bundle everything +# except for a few unreloadable modules into a single mega reloadable dll) +# - try to load the .dlls at specific addresses of memory (close to each other) +# allocated with execution flags - check this: https://github.com/fancycode/MemoryModule +# +# TODO - unimportant: +# +# - have a "bad call" trampoline that all no-longer-present functions are routed to call there +# - so the user gets some error msg if he calls a dangling pointer instead of a crash +# - before/afterCodeReload and hasModuleChanged should be accessible only where appropriate +# - nim_program_result is inaccessible in HCR mode from external C code (see nimbase.h) +# - proper .json build file - but the format is different... multiple link commands... +# - avoid registering globals on each loop when using an iterator in global scope +# +# TODO - REPL: +# - proper way (as proposed by Zahary): +# - parse the input code and put everything in global scope except for +# statements with side effects only - those go in afterCodeReload blocks +# - my very hacky idea: just append to a closure iterator the new statements +# followed by a yield statement. So far I can think of 2 problems: +# - import and some other code cannot be written inside of a proc - +# has to be parsed and extracted in the outer scope +# - when new variables are created they are actually locals to the closure +# so the struct for the closure state grows in memory, but it has already +# been allocated when the closure was created with the previous smaller size. +# That would lead to working with memory outside of the initially allocated +# block. Perhaps something can be done about this - some way of re-allocating +# the state and transferring the old... + +when defined(nimPreviewSlimSystem): + import std/assertions + +when not defined(js) and (defined(hotcodereloading) or + defined(createNimHcr) or + defined(testNimHcr)): + const + dllExt = when defined(windows): "dll" + elif defined(macosx): "dylib" + else: "so" + type + HcrProcGetter* = proc (libHandle: pointer, procName: cstring): pointer {.nimcall.} + HcrGcMarkerProc = proc () {.nimcall, raises: [].} + HcrModuleInitializer* = proc () {.nimcall.} + +when defined(createNimHcr): + when system.appType != "lib": + {.error: "This file has to be compiled as a library!".} + + import std/[os, tables, sets, times, strutils, reservedmem, dynlib] + + template trace(args: varargs[untyped]) = + when defined(testNimHcr) or defined(traceHcr): + echo args + + proc sanitize(arg: Time): string = + when defined(testNimHcr): return "<time>" + else: return $arg + + proc sanitize(arg: string|cstring): string = + when defined(testNimHcr): return ($arg).splitFile.name.splitFile.name + else: return $arg + + {.pragma: nimhcr, compilerproc, exportc, dynlib.} + + # XXX these types are CPU specific and need ARM etc support + type + ShortJumpInstruction {.packed.} = object + opcode: byte + offset: int32 + + LongJumpInstruction {.packed.} = object + opcode1: byte + opcode2: byte + offset: int32 + absoluteAddr: pointer + + proc writeJump(jumpTableEntry: ptr LongJumpInstruction, targetFn: pointer) = + let + jumpFrom = jumpTableEntry.shift(sizeof(ShortJumpInstruction)) + jumpDistance = distance(jumpFrom, targetFn) + + if abs(jumpDistance) < 0x7fff0000: + let shortJump = cast[ptr ShortJumpInstruction](jumpTableEntry) + shortJump.opcode = 0xE9 # relative jump + shortJump.offset = int32(jumpDistance) + else: + jumpTableEntry.opcode1 = 0xff # indirect absolute jump + jumpTableEntry.opcode2 = 0x25 + when hostCPU == "i386": + # on x86 we write the absolute address of the following pointer + jumpTableEntry.offset = cast[int32](addr jumpTableEntry.absoluteAddr) + else: + # on x64, we use a relative address for the same location + jumpTableEntry.offset = 0 + jumpTableEntry.absoluteAddr = targetFn + + if hostCPU == "arm": + const jumpSize = 8 + elif hostCPU == "arm64": + const jumpSize = 16 + + const defaultJumpTableSize = case hostCPU + of "i386": 50 + of "amd64": 500 + else: 50 + + let jumpTableSizeStr = getEnv("HOT_CODE_RELOADING_JUMP_TABLE_SIZE") + let jumpTableSize = if jumpTableSizeStr.len > 0: parseInt(jumpTableSizeStr) + else: defaultJumpTableSize + + # TODO: perhaps keep track of free slots due to removed procs using a free list + var jumpTable = ReservedMemSeq[LongJumpInstruction].init( + memStart = cast[pointer](0x10000000), + maxLen = jumpTableSize * 1024 * 1024 div sizeof(LongJumpInstruction), + accessFlags = memExecReadWrite) + + type + ProcSym = object + jump: ptr LongJumpInstruction + gen: int + + GlobalVarSym = object + p: pointer + markerProc: HcrGcMarkerProc + gen: int + + ModuleDesc = object + procs: Table[string, ProcSym] + globals: Table[string, GlobalVarSym] + imports: seq[string] + handle: LibHandle + hash: string + gen: int + lastModification: Time + handlers: seq[tuple[isBefore: bool, cb: proc () {.nimcall.}]] + + proc newModuleDesc(): ModuleDesc = + result.procs = initTable[string, ProcSym]() + result.globals = initTable[string, GlobalVarSym]() + result.handle = nil + result.gen = -1 + result.lastModification = low(Time) + + # the global state necessary for traversing and reloading the module import tree + var modules = initTable[string, ModuleDesc]() + var root: string + var system: string + var mainDatInit: HcrModuleInitializer + var generation = 0 + + # necessary for queries such as "has module X changed" - contains all but the main module + var hashToModuleMap = initTable[string, string]() + + # necessary for registering handlers and keeping them up-to-date + var currentModule: string + + # supplied from the main module - used by others to initialize pointers to this runtime + var hcrDynlibHandle: pointer + var getProcAddr: HcrProcGetter + + proc hcrRegisterProc*(module: cstring, name: cstring, fn: pointer): pointer {.nimhcr.} = + trace " register proc: ", module.sanitize, " ", name + # Please note: We must allocate a local copy of the strings, because the supplied + # `cstring` will reside in the data segment of a DLL that will be later unloaded. + let name = $name + let module = $module + + var jumpTableEntryAddr: ptr LongJumpInstruction + + modules[module].procs.withValue(name, p): + trace " update proc: ", name + jumpTableEntryAddr = p.jump + p.gen = generation + do: + let len = jumpTable.len + jumpTable.setLen(len + 1) + jumpTableEntryAddr = addr jumpTable[len] + modules[module].procs[name] = ProcSym(jump: jumpTableEntryAddr, gen: generation) + + writeJump jumpTableEntryAddr, fn + return jumpTableEntryAddr + + proc hcrGetProc*(module: cstring, name: cstring): pointer {.nimhcr.} = + trace " get proc: ", module.sanitize, " ", name + return modules[$module].procs.getOrDefault($name, ProcSym()).jump + + proc hcrRegisterGlobal*(module: cstring, + name: cstring, + size: Natural, + gcMarker: HcrGcMarkerProc, + outPtr: ptr pointer): bool {.nimhcr.} = + trace " register global: ", module.sanitize, " ", name + # Please note: We must allocate local copies of the strings, because the supplied + # `cstring` will reside in the data segment of a DLL that will be later unloaded. + # Also using a ptr pointer instead of a var pointer (an output parameter) + # because for the C++ backend var parameters use references and in this use case + # it is not possible to cast an int* (for example) to a void* and then pass it + # to void*& since the casting yields an rvalue and references bind only to lvalues. + let name = $name + let module = $module + + modules[module].globals.withValue(name, global): + trace " update global: ", name + outPtr[] = global.p + global.gen = generation + global.markerProc = gcMarker + return false + do: + outPtr[] = alloc0(size) + modules[module].globals[name] = GlobalVarSym(p: outPtr[], + gen: generation, + markerProc: gcMarker) + return true + + proc hcrGetGlobal*(module: cstring, name: cstring): pointer {.nimhcr.} = + trace " get global: ", module.sanitize, " ", name + return modules[$module].globals[$name].p + + proc getListOfModules(cstringArray: ptr pointer): seq[string] = + var curr = cast[ptr cstring](cstringArray) + while len(curr[]) > 0: + result.add($curr[]) + curr = cast[ptr cstring](cast[int64](curr) + sizeof(ptr cstring)) + + template cleanup(collection, body) = + var toDelete: seq[string] + for name, data in collection.pairs: + if data.gen < generation: + toDelete.add(name) + trace "HCR Cleaning ", astToStr(collection), " :: ", name, " ", data.gen + for name {.inject.} in toDelete: + body + + proc cleanupGlobal(module: string, name: string) = + var g: GlobalVarSym + if modules[module].globals.take(name, g): + dealloc g.p + + proc cleanupSymbols(module: string) = + cleanup modules[module].globals: + cleanupGlobal(module, name) + + cleanup modules[module].procs: + modules[module].procs.del(name) + + proc unloadDll(name: string) = + if modules[name].handle != nil: + unloadLib(modules[name].handle) + + proc loadDll(name: cstring) {.nimhcr.} = + let name = $name + trace "HCR LOADING: ", name.sanitize + if modules.contains(name): + unloadDll(name) + else: + modules[name] = newModuleDesc() + + let copiedName = name & ".copy." & dllExt + copyFileWithPermissions(name, copiedName) + + let lib = loadLib(copiedName) + assert lib != nil + modules[name].handle = lib + modules[name].gen = generation + modules[name].lastModification = getLastModificationTime(name) + + # update the list of imports by the module + let getImportsProc = cast[proc (): ptr pointer {.nimcall.}]( + checkedSymAddr(lib, "HcrGetImportedModules")) + modules[name].imports = getListOfModules(getImportsProc()) + # get the hash of the module + let getHashProc = cast[proc (): cstring {.nimcall.}]( + checkedSymAddr(lib, "HcrGetSigHash")) + modules[name].hash = $getHashProc() + hashToModuleMap[modules[name].hash] = name + + # Remove handlers for this module if reloading - they will be re-registered. + # In order for them to be re-registered we need to de-register all globals + # that trigger the registering of handlers through calls to hcrAddEventHandler + modules[name].handlers.setLen(0) + + proc initHcrData(name: cstring) {.nimhcr.} = + trace "HCR Hcr init: ", name.sanitize + cast[proc (h: pointer, gpa: HcrProcGetter) {.nimcall.}]( + checkedSymAddr(modules[$name].handle, "HcrInit000"))(hcrDynlibHandle, getProcAddr) + + proc initTypeInfoGlobals(name: cstring) {.nimhcr.} = + trace "HCR TypeInfo globals init: ", name.sanitize + cast[HcrModuleInitializer](checkedSymAddr(modules[$name].handle, "HcrCreateTypeInfos"))() + + proc initPointerData(name: cstring) {.nimhcr.} = + trace "HCR Dat init: ", name.sanitize + cast[HcrModuleInitializer](checkedSymAddr(modules[$name].handle, "DatInit000"))() + + proc initGlobalScope(name: cstring) {.nimhcr.} = + trace "HCR Init000: ", name.sanitize + # set the currently inited module - necessary for registering the before/after HCR handlers + currentModule = $name + cast[HcrModuleInitializer](checkedSymAddr(modules[$name].handle, "Init000"))() + + var modulesToInit: seq[string] = @[] + var allModulesOrderedByDFS: seq[string] = @[] + + proc recursiveDiscovery(dlls: seq[string]) = + for curr in dlls: + if modules.contains(curr): + # skip updating modules that have already been updated to the latest generation + if modules[curr].gen >= generation: + trace "HCR SKIP: ", curr.sanitize, " gen is already: ", modules[curr].gen + continue + # skip updating an unmodified module but continue traversing its dependencies + if modules[curr].lastModification >= getLastModificationTime(curr): + trace "HCR SKIP (not modified): ", curr.sanitize, " ", modules[curr].lastModification.sanitize + # update generation so module doesn't get collected + modules[curr].gen = generation + # recurse to imported modules - they might be changed + recursiveDiscovery(modules[curr].imports) + allModulesOrderedByDFS.add(curr) + continue + loadDll(curr.cstring) + # first load all dependencies of the current module and init it after that + recursiveDiscovery(modules[curr].imports) + + allModulesOrderedByDFS.add(curr) + modulesToInit.add(curr) + + proc initModules() = + # first init the pointers to hcr functions and also do the registering of typeinfo globals + for curr in modulesToInit: + initHcrData(curr.cstring) + initTypeInfoGlobals(curr.cstring) + # for now system always gets fully inited before any other module (including when reloading) + initPointerData(system.cstring) + initGlobalScope(system.cstring) + # proceed with the DatInit calls - for all modules - including the main one! + for curr in allModulesOrderedByDFS: + if curr != system: + initPointerData(curr.cstring) + mainDatInit() + # execute top-level code (in global scope) + for curr in modulesToInit: + if curr != system: + initGlobalScope(curr.cstring) + # cleanup old symbols which are gone now + for curr in modulesToInit: + cleanupSymbols(curr) + + proc hcrInit*(moduleList: ptr pointer, main, sys: cstring, + datInit: HcrModuleInitializer, handle: pointer, gpa: HcrProcGetter) {.nimhcr.} = + trace "HCR INITING: ", main.sanitize, " gen: ", generation + # initialize globals + root = $main + system = $sys + mainDatInit = datInit + hcrDynlibHandle = handle + getProcAddr = gpa + # the root is already added and we need it because symbols from it will also be registered in the HCR system + modules[root].imports = getListOfModules(moduleList) + modules[root].gen = high(int) # something huge so it doesn't get collected + # recursively initialize all modules + recursiveDiscovery(modules[root].imports) + initModules() + # the next module to be inited will be the root + currentModule = root + + proc hcrHasModuleChanged*(moduleHash: string): bool {.nimhcr.} = + let module = hashToModuleMap[moduleHash] + return modules[module].lastModification < getLastModificationTime(module) + + proc hcrReloadNeeded*(): bool {.nimhcr.} = + for hash, _ in hashToModuleMap: + if hcrHasModuleChanged(hash): + return true + return false + + proc hcrPerformCodeReload*() {.nimhcr.} = + if not hcrReloadNeeded(): + trace "HCR - no changes" + return + + # We disable the GC during the reload, because the reloading procedures + # will replace type info objects and GC marker procs. This seems to create + # problems when the GC is executed while the reload is underway. + # Future versions of NIMHCR won't use the GC, because all globals and the + # metadata needed to access them will be placed in shared memory, so they + # can be manipulated from external programs without reloading. + when declared(GC_disable): + GC_disable() + defer: GC_enable() + elif declared(GC_disableOrc): + GC_disableOrc() + defer: GC_enableOrc() + + inc(generation) + trace "HCR RELOADING: ", generation + + var traversedHandlerModules = initHashSet[string]() + + proc recursiveExecuteHandlers(isBefore: bool, module: string) = + # do not process an already traversed module + if traversedHandlerModules.containsOrIncl(module): return + traversedHandlerModules.incl module + # first recurse to do a DFS traversal + for curr in modules[module].imports: + recursiveExecuteHandlers(isBefore, curr) + # and then execute the handlers - from leaf modules all the way up to the root module + for curr in modules[module].handlers: + if curr.isBefore == isBefore: + curr.cb() + + # first execute the before reload handlers + traversedHandlerModules.clear() + recursiveExecuteHandlers(true, root) + + # do the reloading + modulesToInit = @[] + allModulesOrderedByDFS = @[] + recursiveDiscovery(modules[root].imports) + initModules() + + # execute the after reload handlers + traversedHandlerModules.clear() + recursiveExecuteHandlers(false, root) + + # collecting no longer referenced modules - based on their generation + cleanup modules: + cleanupSymbols(name) + unloadDll(name) + hashToModuleMap.del(modules[name].hash) + modules.del(name) + + proc hcrAddEventHandler*(isBefore: bool, cb: proc () {.nimcall.}) {.nimhcr.} = + modules[currentModule].handlers.add( + (isBefore: isBefore, cb: cb)) + + proc hcrAddModule*(module: cstring) {.nimhcr.} = + if not modules.contains($module): + modules[$module] = newModuleDesc() + + proc hcrGeneration*(): int {.nimhcr.} = + generation + + proc hcrMarkGlobals*() {.compilerproc, exportc, dynlib, nimcall, gcsafe.} = + # This is gcsafe, because it will be registered + # only in the GC of the main thread. + {.gcsafe.}: + for _, module in modules: + for _, global in module.globals: + if global.markerProc != nil: + global.markerProc() + +elif defined(hotcodereloading) or defined(testNimHcr): + when not defined(js): + const + nimhcrLibname = when defined(windows): "nimhcr." & dllExt + elif defined(macosx): "libnimhcr." & dllExt + else: "libnimhcr." & dllExt + + {.pragma: nimhcr, compilerproc, importc, dynlib: nimhcrLibname.} + + proc hcrRegisterProc*(module: cstring, name: cstring, fn: pointer): pointer {.nimhcr.} + + proc hcrGetProc*(module: cstring, name: cstring): pointer {.nimhcr.} + + proc hcrRegisterGlobal*(module: cstring, name: cstring, size: Natural, + gcMarker: HcrGcMarkerProc, outPtr: ptr pointer): bool {.nimhcr.} + proc hcrGetGlobal*(module: cstring, name: cstring): pointer {.nimhcr.} + + proc hcrInit*(moduleList: ptr pointer, + main, sys: cstring, + datInit: HcrModuleInitializer, + handle: pointer, + gpa: HcrProcGetter) {.nimhcr.} + + proc hcrAddModule*(module: cstring) {.nimhcr.} + + proc hcrHasModuleChanged*(moduleHash: string): bool {.nimhcr.} + + proc hcrReloadNeeded*(): bool {.nimhcr.} + + proc hcrPerformCodeReload*() {.nimhcr.} + + proc hcrAddEventHandler*(isBefore: bool, cb: proc () {.nimcall.}) {.nimhcr.} + + proc hcrMarkGlobals*() {.raises: [], nimhcr, nimcall, gcsafe.} + + when declared(nimRegisterGlobalMarker): + nimRegisterGlobalMarker(cast[GlobalMarkerProc](hcrMarkGlobals)) + + else: + proc hcrHasModuleChanged*(moduleHash: string): bool = + # TODO + false + + proc hcrAddEventHandler*(isBefore: bool, cb: proc () {.nimcall.}) = + # TODO + discard + |