From 6596b5141210366dfd6619498c76e2890f6b3468 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 10 Dec 2025 18:52:14 -0600 Subject: [PATCH 1/2] change `Core`'s lifecycle instead of having `Core` be a static singleton with indeterminate construction and destruction sequencing, make Core's instance lifecycle explicit by tying it to `dfhooks_init` and` dfhooks_shutdown` --- library/Console-windows.cpp | 1 + library/Core.cpp | 20 ++++++++++++++++++-- library/Debug.cpp | 3 --- library/Hooks.cpp | 21 +++++++++++++++------ library/include/Core.h | 12 +++++++++--- library/include/Debug.h | 9 ++++++++- 6 files changed, 51 insertions(+), 15 deletions(-) diff --git a/library/Console-windows.cpp b/library/Console-windows.cpp index 12a3e0c2eb..2fc20bf79d 100644 --- a/library/Console-windows.cpp +++ b/library/Console-windows.cpp @@ -512,6 +512,7 @@ bool Console::init(bool) // FIXME: looks awfully empty, doesn't it? bool Console::shutdown(void) { + assert(inited); std::lock_guard lock{*wlock}; FreeConsole(); inited = false; diff --git a/library/Core.cpp b/library/Core.cpp index a5cd996667..7cc00f6401 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -127,6 +127,8 @@ namespace DFHack { return Filesystem::getInstallDir() / "hack" / "data" / "dfhack-config-defaults"; }; + Core* Core::active_instance = nullptr; + class MainThread { public: //! MainThread::suspend keeps the main DF thread suspended from Core::Init to @@ -1062,6 +1064,11 @@ df::viewscreen * Core::getTopViewscreen() { } bool Core::InitMainThread() { + assert(active_instance == nullptr); + + // set this instance as the active instance + active_instance = this; + // this hook is always called from DF's main (render) thread, so capture this thread id df_render_thread = std::this_thread::get_id(); @@ -1462,8 +1469,8 @@ bool Core::InitSimulationThread() } Core& Core::getInstance() { - static Core instance; - return instance; + assert(Core::active_instance != nullptr); + return *Core::active_instance; } bool Core::isSuspended(void) @@ -1893,6 +1900,7 @@ int Core::Shutdown ( void ) if (hotkey_mgr) { delete hotkey_mgr; + hotkey_mgr = nullptr; } if(plug_mgr) @@ -1900,13 +1908,21 @@ int Core::Shutdown ( void ) delete plug_mgr; plug_mgr = nullptr; } + // invalidate all modules allModules.clear(); Textures::cleanup(); DFSDL::cleanup(); + + // FIXME console has already been shut down at this point, so getConsole is returning a dead object DFSteam::cleanup(getConsole()); + memset(&(s_mods), 0, sizeof(s_mods)); d.reset(); + + // clear active instance + Core::active_instance = nullptr; + return -1; } diff --git a/library/Debug.cpp b/library/Debug.cpp index dafbeb5ce3..50bef6e538 100644 --- a/library/Debug.cpp +++ b/library/Debug.cpp @@ -66,9 +66,6 @@ void DebugManager::unregisterCategory(DebugCategory& cat) DebugRegisterBase::DebugRegisterBase(DebugCategory* cat) { - // Make sure Core lives at least as long any DebugCategory to - // allow debug prints until all Debugcategories has been destructed - Core::getInstance(); DebugManager::getInstance().registerCategory(*cat); } diff --git a/library/Hooks.cpp b/library/Hooks.cpp index 0f957fea06..4c2c4e76c8 100644 --- a/library/Hooks.cpp +++ b/library/Hooks.cpp @@ -7,6 +7,8 @@ static bool disabled = false; DFhackCExport const int32_t dfhooks_priority = 100; +static std::unique_ptr core_instance; + // called from the main thread before the simulation thread is started // and the main event loop is initiated DFhackCExport void dfhooks_init() { @@ -16,8 +18,11 @@ DFhackCExport void dfhooks_init() { return; } + // construct DFHack core instance + core_instance = std::make_unique(); + // we need to init DF globals before we can check the commandline - if (!DFHack::Core::getInstance().InitMainThread() || !df::global::game) { + if (!core_instance->InitMainThread() || !df::global::game) { // we don't set disabled to true here so symbol generation can work return; } @@ -26,6 +31,8 @@ DFhackCExport void dfhooks_init() { if (cmdline.find("--disable-dfhack") != std::string::npos) { fprintf(stderr, "dfhack: --disable-dfhack specified on commandline; disabling\n"); disabled = true; + core_instance->Shutdown(); + core_instance.reset(); return; } @@ -36,14 +43,16 @@ DFhackCExport void dfhooks_init() { DFhackCExport void dfhooks_shutdown() { if (disabled) return; - DFHack::Core::getInstance().Shutdown(); + core_instance->Shutdown(); + // release DFHack core instance + core_instance.reset(); } // called from the simulation thread in the main event loop DFhackCExport void dfhooks_update() { if (disabled) return; - DFHack::Core::getInstance().Update(); + core_instance->Update(); } // called from the simulation thread just before adding the macro @@ -59,7 +68,7 @@ DFhackCExport void dfhooks_prerender() { DFhackCExport bool dfhooks_sdl_event(SDL_Event* event) { if (disabled) return false; - return DFHack::Core::getInstance().DFH_SDL_Event(event); + return core_instance->DFH_SDL_Event(event); } // called from the main thread just after setting mouse state in gps and just @@ -68,7 +77,7 @@ DFhackCExport void dfhooks_sdl_loop() { if (disabled) return; // TODO: wire this up to the new SDL-based console once it is merged - DFHack::Core::getInstance().DFH_SDL_Loop(); + core_instance->DFH_SDL_Loop(); } // called from the main thread for each utf-8 char read from the ncurses input @@ -78,5 +87,5 @@ DFhackCExport void dfhooks_sdl_loop() { DFhackCExport bool dfhooks_ncurses_key(int key) { if (disabled) return false; - return DFHack::Core::getInstance().DFH_ncurses_key(key); + return core_instance->DFH_ncurses_key(key); } diff --git a/library/include/Core.h b/library/include/Core.h index 1f3cea5837..e76245fbd0 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -157,8 +157,11 @@ namespace DFHack friend void ::dfhooks_sdl_loop(); friend bool ::dfhooks_ncurses_key(int key); public: - /// Get the single Core instance or make one. + /// Get the current active Core instance. will assert if none exists + /// Use noInstance() to check first if unsure static Core& getInstance(); + static bool noInstance() { return active_instance == nullptr; } + /// check if the activity lock is owned by this thread bool isSuspended(void); /// Is everything OK? @@ -259,11 +262,14 @@ namespace DFHack return false; } + Core(); + ~Core(); + private: + static Core* active_instance; + DFHack::Console con; - Core(); - ~Core(); struct Private; std::unique_ptr d; diff --git a/library/include/Debug.h b/library/include/Debug.h index 48e661acc4..632d4910b6 100644 --- a/library/include/Debug.h +++ b/library/include/Debug.h @@ -183,7 +183,7 @@ class DFHACK_EXPORT DebugCategory final { }; /*! - * Fetch a steam object proxy object for output. It also adds standard + * Fetch a stream object proxy object for output. It also adds standard * message components like time and plugin and category names to the line. * * User must make sure that the line is terminated with a line end. @@ -194,6 +194,13 @@ class DFHACK_EXPORT DebugCategory final { */ ostream_proxy_prefix getStream(const level msgLevel) const { + // if the core instance is unavailable, use stderr as a fallback + if (Core::noInstance()) + { + static color_ostream_wrapper fallback{std::cerr}; + return {*this,fallback,msgLevel}; + } + return {*this,Core::getInstance().getConsole(),msgLevel}; } /*! From 867acf3dfb7ed67a4427270dae4229ecb8502e6e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 15 Apr 2026 21:05:47 -0500 Subject: [PATCH 2/2] DFSteam no longer needs a console to unload also replaced most of a macro with a template --- library/Core.cpp | 4 +--- library/include/modules/DFSteam.h | 2 +- library/modules/DFSteam.cpp | 35 +++++++++++++++++++++---------- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 1874d390fa..595488a6cc 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1941,9 +1941,7 @@ int Core::Shutdown ( void ) allModules.clear(); Textures::cleanup(); DFSDL::cleanup(); - - // FIXME console has already been shut down at this point, so getConsole is returning a dead object - DFSteam::cleanup(getConsole()); + DFSteam::cleanup(); memset(&(s_mods), 0, sizeof(s_mods)); d.reset(); diff --git a/library/include/modules/DFSteam.h b/library/include/modules/DFSteam.h index e604294f8a..7080a94e2e 100644 --- a/library/include/modules/DFSteam.h +++ b/library/include/modules/DFSteam.h @@ -24,7 +24,7 @@ bool init(DFHack::color_ostream& out); /** * Call this when DFHack is being unloaded. */ -void cleanup(DFHack::color_ostream& out); +void cleanup(); DFHACK_EXPORT void launchSteamDFHackIfNecessary(DFHack::color_ostream& out); diff --git a/library/modules/DFSteam.cpp b/library/modules/DFSteam.cpp index 61e50a61b8..12a33e206c 100644 --- a/library/modules/DFSteam.cpp +++ b/library/modules/DFSteam.cpp @@ -35,17 +35,20 @@ bool (*g_SteamAPI_RestartAppIfNecessary)(uint32_t unOwnAppID) = nullptr; void* (*g_SteamInternal_FindOrCreateUserInterface)(int, const char*) = nullptr; bool (*g_SteamAPI_ISteamApps_BIsAppInstalled)(void *iSteamApps, uint32_t appID) = nullptr; -static void bind_all(color_ostream& out, DFLibrary* handle) { -#define bind(name) \ - if (!handle) { \ - g_##name = nullptr; \ - } else { \ - g_##name = (decltype(g_##name))LookupPlugin(handle, #name); \ - if (!g_##name) { \ - WARN(dfsteam, out).print("steam library function not found: " #name "\n"); \ - } \ +template +static void bind_(color_ostream& out, DFLibrary* handle, const char* name, Ptr& func_ptr) { + if (!handle) { + func_ptr = nullptr; + } else { + func_ptr = (Ptr)LookupPlugin(handle, name); + if (!func_ptr) { + WARN(dfsteam, out).print("steam library function not found: {}\n", name); } + } +} +static void bind_all(color_ostream& out, DFLibrary* handle) { +#define bind(name) bind_(out, handle, #name, g_##name) bind(SteamAPI_Init); bind(SteamAPI_Shutdown); bind(SteamAPI_GetHSteamUser); @@ -56,6 +59,16 @@ static void bind_all(color_ostream& out, DFLibrary* handle) { #undef bind } +static void unbind_all() +{ + g_SteamAPI_Init = nullptr; + g_SteamAPI_Shutdown = nullptr; + g_SteamAPI_GetHSteamUser = nullptr; + g_SteamInternal_FindOrCreateUserInterface = nullptr; + g_SteamAPI_RestartAppIfNecessary = nullptr; + g_SteamAPI_ISteamApps_BIsAppInstalled = nullptr; +} + bool DFSteam::init(color_ostream& out) { char *steam_client_launch = getenv("SteamClientLaunch"); if (!steam_client_launch || strncmp(steam_client_launch, "1", 2) != 0) { @@ -84,7 +97,7 @@ bool DFSteam::init(color_ostream& out) { return true; } -void DFSteam::cleanup(color_ostream& out) { +void DFSteam::cleanup() { if (!g_steam_handle) return; @@ -94,7 +107,7 @@ void DFSteam::cleanup(color_ostream& out) { ClosePlugin(g_steam_handle); g_steam_handle = nullptr; - bind_all(out, nullptr); + unbind_all(); g_steam_initialized = false; }