From 1b4e3a43e202ed71fc83dd1aeb826190b49ba9e9 Mon Sep 17 00:00:00 2001 From: David Snopek Date: Fri, 29 May 2026 17:58:31 -0500 Subject: [PATCH] Use a different method to avoid `thread_local` on MacOS --- include/godot_cpp/classes/wrapped.hpp | 51 ++++++++++++++++++--------- include/godot_cpp/core/class_db.hpp | 5 +-- src/classes/wrapped.cpp | 35 ++++++------------ 3 files changed, 47 insertions(+), 44 deletions(-) diff --git a/include/godot_cpp/classes/wrapped.hpp b/include/godot_cpp/classes/wrapped.hpp index fbfc84254..2a3e7c45a 100644 --- a/include/godot_cpp/classes/wrapped.hpp +++ b/include/godot_cpp/classes/wrapped.hpp @@ -40,11 +40,9 @@ #include #if defined(MACOS_ENABLED) && defined(HOT_RELOAD_ENABLED) +#include #include -#define _GODOT_CPP_AVOID_THREAD_LOCAL -#define _GODOT_CPP_THREAD_LOCAL -#else -#define _GODOT_CPP_THREAD_LOCAL thread_local +#include #endif namespace godot { @@ -65,21 +63,45 @@ class Wrapped { template ::value, bool>> friend _ALWAYS_INLINE_ void _pre_initialize(); -#ifdef _GODOT_CPP_AVOID_THREAD_LOCAL - static std::recursive_mutex _constructing_mutex; + struct ConstructInfo { + const StringName *extension_class_name = nullptr; + const GDExtensionInstanceBindingCallbacks *class_binding_callbacks = nullptr; +#ifdef HOT_RELOAD_ENABLED + GDExtensionObjectPtr recreate_owner = nullptr; #endif + }; - _GODOT_CPP_THREAD_LOCAL static const StringName *_constructing_extension_class_name; - _GODOT_CPP_THREAD_LOCAL static const GDExtensionInstanceBindingCallbacks *_constructing_class_binding_callbacks; - -#ifdef HOT_RELOAD_ENABLED - _GODOT_CPP_THREAD_LOCAL static GDExtensionObjectPtr _constructing_recreate_owner; +#if defined(MACOS_ENABLED) && defined(HOT_RELOAD_ENABLED) + // On macOS, `thread_local` storage keeps the library from being unloaded, + // which breaks hot-reload. Instead we keep each thread's `ConstructInfo` in a + // map keyed by thread id. + class ConstructInfoStore { + std::mutex mutex; + std::map infos; + + public: + ConstructInfo &get() { + std::lock_guard lock(mutex); + return infos[std::this_thread::get_id()]; + } + }; + + static ConstructInfo &_get_construct_info() { + static ConstructInfoStore store; + return store.get(); + } +#else + static ConstructInfo &_get_construct_info() { + static thread_local ConstructInfo info; + return info; + } #endif template _ALWAYS_INLINE_ static void _set_construct_info() { - _constructing_extension_class_name = T::_get_extension_class_name(); - _constructing_class_binding_callbacks = &T::_gde_binding_callbacks; + ConstructInfo &info = _get_construct_info(); + info.extension_class_name = T::_get_extension_class_name(); + info.class_binding_callbacks = &T::_gde_binding_callbacks; } protected: @@ -124,9 +146,6 @@ class Wrapped { template ::value, bool>> _ALWAYS_INLINE_ void _pre_initialize() { -#ifdef _GODOT_CPP_AVOID_THREAD_LOCAL - Wrapped::_constructing_mutex.lock(); -#endif Wrapped::_set_construct_info(); } diff --git a/include/godot_cpp/core/class_db.hpp b/include/godot_cpp/core/class_db.hpp index c2fcb08cf..83b038e7b 100644 --- a/include/godot_cpp/core/class_db.hpp +++ b/include/godot_cpp/core/class_db.hpp @@ -136,10 +136,7 @@ class ClassDB { static GDExtensionClassInstancePtr _recreate_instance_func(void *data, GDExtensionObjectPtr obj) { if constexpr (!std::is_abstract_v) { #ifdef HOT_RELOAD_ENABLED -#ifdef _GODOT_CPP_AVOID_THREAD_LOCAL - std::lock_guard lk(Wrapped::_constructing_mutex); -#endif - Wrapped::_constructing_recreate_owner = obj; + Wrapped::_get_construct_info().recreate_owner = obj; T *new_instance = (T *)memalloc(sizeof(T)); memnew_placement(new_instance, T); return new_instance; diff --git a/src/classes/wrapped.cpp b/src/classes/wrapped.cpp index bc9c956a5..e2452cfef 100644 --- a/src/classes/wrapped.cpp +++ b/src/classes/wrapped.cpp @@ -40,26 +40,11 @@ namespace godot { -#ifdef _GODOT_CPP_AVOID_THREAD_LOCAL -std::recursive_mutex Wrapped::_constructing_mutex; -#endif - -_GODOT_CPP_THREAD_LOCAL const StringName *Wrapped::_constructing_extension_class_name = nullptr; -_GODOT_CPP_THREAD_LOCAL const GDExtensionInstanceBindingCallbacks *Wrapped::_constructing_class_binding_callbacks = nullptr; - -#ifdef HOT_RELOAD_ENABLED -_GODOT_CPP_THREAD_LOCAL GDExtensionObjectPtr Wrapped::_constructing_recreate_owner = nullptr; -#endif - const StringName *Wrapped::_get_extension_class_name() { return nullptr; } void Wrapped::_postinitialize() { -#ifdef _GODOT_CPP_AVOID_THREAD_LOCAL - Wrapped::_constructing_mutex.unlock(); -#endif - #if GODOT_VERSION_MINOR >= 4 Object *obj = dynamic_cast(this); if (obj) { @@ -74,10 +59,12 @@ void Wrapped::_postinitialize() { } Wrapped::Wrapped(const StringName &p_godot_class) { + ConstructInfo &info = _get_construct_info(); + #ifdef HOT_RELOAD_ENABLED - if (unlikely(Wrapped::_constructing_recreate_owner)) { - _owner = Wrapped::_constructing_recreate_owner; - Wrapped::_constructing_recreate_owner = nullptr; + if (unlikely(info.recreate_owner)) { + _owner = info.recreate_owner; + info.recreate_owner = nullptr; } else #endif { @@ -90,14 +77,14 @@ Wrapped::Wrapped(const StringName &p_godot_class) { #endif } - if (_constructing_extension_class_name) { - ::godot::gdextension_interface::object_set_instance(_owner, reinterpret_cast(_constructing_extension_class_name), this); - _constructing_extension_class_name = nullptr; + if (info.extension_class_name) { + ::godot::gdextension_interface::object_set_instance(_owner, reinterpret_cast(info.extension_class_name), this); + info.extension_class_name = nullptr; } - if (likely(_constructing_class_binding_callbacks)) { - ::godot::gdextension_interface::object_set_instance_binding(_owner, ::godot::gdextension_interface::token, this, _constructing_class_binding_callbacks); - _constructing_class_binding_callbacks = nullptr; + if (likely(info.class_binding_callbacks)) { + ::godot::gdextension_interface::object_set_instance_binding(_owner, ::godot::gdextension_interface::token, this, info.class_binding_callbacks); + info.class_binding_callbacks = nullptr; } else { CRASH_NOW_MSG("BUG: Godot Object created without binding callbacks. Did you forget to use memnew()?"); }