diff options
author | Ignacio Roldán Etcheverry <ignalfonsore@gmail.com> | 2022-05-28 04:56:46 +0200 |
---|---|---|
committer | Ignacio Roldán Etcheverry <ignalfonsore@gmail.com> | 2022-08-22 03:36:51 +0200 |
commit | e235cef09f71d0cd752ba4931640be24dcb551ab (patch) | |
tree | bb347c5defc17beb54490d48a91edef9da2b0d1d /modules | |
parent | d78e0a842638df9c98a8f7637b125d36e488a367 (diff) |
C#: Re-implement assembly reloading with ALCs
Diffstat (limited to 'modules')
37 files changed, 1529 insertions, 557 deletions
diff --git a/modules/mono/SCsub b/modules/mono/SCsub index ab7a2bf761..7764ba0b45 100644 --- a/modules/mono/SCsub +++ b/modules/mono/SCsub @@ -15,7 +15,6 @@ mono_configure.configure(env, env_mono) env_mono.add_source_files(env.modules_sources, "*.cpp") env_mono.add_source_files(env.modules_sources, "glue/*.cpp") -env_mono.add_source_files(env.modules_sources, "glue/mono_glue.gen.cpp") env_mono.add_source_files(env.modules_sources, "mono_gd/*.cpp") env_mono.add_source_files(env.modules_sources, "utils/*.cpp") diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp index f74c32ba04..e54e5ac0bb 100644 --- a/modules/mono/csharp_script.cpp +++ b/modules/mono/csharp_script.cpp @@ -104,7 +104,7 @@ Error CSharpLanguage::execute_file(const String &p_path) { return OK; } -extern void *godotsharp_pinvoke_funcs[185]; +extern void *godotsharp_pinvoke_funcs[186]; [[maybe_unused]] volatile void **do_not_strip_godotsharp_pinvoke_funcs; #ifdef TOOLS_ENABLED extern void *godotsharp_editor_pinvoke_funcs[30]; @@ -646,6 +646,28 @@ void CSharpLanguage::frame() { } } +struct CSharpScriptDepSort { + // Must support sorting so inheritance works properly (parent must be reloaded first) + bool operator()(const Ref<CSharpScript> &A, const Ref<CSharpScript> &B) const { + if (A == B) { + // Shouldn't happen but just in case... + return false; + } + const Script *I = B->get_base_script().ptr(); + while (I) { + if (I == A.ptr()) { + // A is a base of B + return true; + } + + I = I->get_base_script().ptr(); + } + + // A isn't a base of B + return false; + } +}; + void CSharpLanguage::reload_all_scripts() { #ifdef GD_MONO_HOT_RELOAD if (is_assembly_reloading_needed()) { @@ -676,38 +698,29 @@ bool CSharpLanguage::is_assembly_reloading_needed() { return false; } -#warning TODO -#if 0 - GDMonoAssembly *proj_assembly = gdmono->get_project_assembly(); - - String appname_safe = ProjectSettings::get_singleton()->get_safe_project_name(); - - appname_safe += ".dll"; - - if (proj_assembly) { - String proj_asm_path = proj_assembly->get_path(); + String assembly_path = gdmono->get_project_assembly_path(); - if (!FileAccess::exists(proj_asm_path)) { - // Maybe it wasn't loaded from the default path, so check this as well - proj_asm_path = GodotSharpDirs::get_res_temp_assemblies_dir().plus_file(appname_safe); - if (!FileAccess::exists(proj_asm_path)) { - return false; // No assembly to load - } + if (!assembly_path.is_empty()) { + if (!FileAccess::exists(assembly_path)) { + return false; // No assembly to load } - if (FileAccess::get_modified_time(proj_asm_path) <= proj_assembly->get_modified_time()) { + if (FileAccess::get_modified_time(assembly_path) <= gdmono->get_project_assembly_modified_time()) { return false; // Already up to date } } else { - if (!FileAccess::exists(GodotSharpDirs::get_res_temp_assemblies_dir().plus_file(appname_safe))) { + String appname_safe = ProjectSettings::get_singleton()->get_safe_project_name(); + + assembly_path = GodotSharpDirs::get_res_temp_assemblies_dir() + .plus_file(appname_safe + ".dll"); + assembly_path = ProjectSettings::get_singleton()->globalize_path(assembly_path); + + if (!FileAccess::exists(assembly_path)) { return false; // No assembly to load } } return true; -#else - return false; -#endif } void CSharpLanguage::reload_assemblies(bool p_soft_reload) { @@ -715,27 +728,12 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { return; } -#warning TODO ALCs after switching to .NET 6 + // TODO: + // Currently, this reloads all scripts, including those whose class is not part of the + // assembly load context being unloaded. As such, we unnecessarily reload GodotTools. - // Try to load the project assembly if it was not yet loaded - // (while hot-reload is not yet implemented) - gdmono->initialize_load_assemblies(); + print_verbose(".NET: Reloading assemblies..."); - { - MutexLock lock(script_instances_mutex); - - for (SelfList<CSharpScript> *elem = script_list.first(); elem; elem = elem->next()) { - Ref<CSharpScript> script(elem->self()); - - script->exports_invalidated = true; - - if (!script->get_path().is_empty()) { - script->reload(p_soft_reload); - } - } - } - -#if 0 // There is no soft reloading with Mono. It's always hard reloading. List<Ref<CSharpScript>> scripts; @@ -758,18 +756,12 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { for (SelfList<ManagedCallable> *elem = ManagedCallable::instances.first(); elem; elem = elem->next()) { ManagedCallable *managed_callable = elem->self(); - Array serialized_data; - MonoObject *managed_serialized_data = GDMonoMarshal::variant_to_mono_object(serialized_data); + ERR_CONTINUE(managed_callable->delegate_handle.value == nullptr); - MonoException *exc = nullptr; - bool success = (bool)GDMonoCache::managed_callbacks.methodthunk_DelegateUtils_TrySerializeDelegateWithGCHandle - .invoke(managed_callable->delegate_handle, - managed_serialized_data, &exc); + Array serialized_data; - if (exc) { - GDMonoUtils::debug_print_unhandled_exception(exc); - continue; - } + bool success = GDMonoCache::managed_callbacks.DelegateUtils_TrySerializeDelegateWithGCHandle( + managed_callable->delegate_handle, &serialized_data); if (success) { ManagedCallable::instances_pending_reload.insert(managed_callable, serialized_data); @@ -798,17 +790,12 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { // If someone removes a script from a node, deletes the script, builds, adds a script to the // same node, then builds again, the script might have no path and also no script_class. In // that case, we can't (and don't need to) reload it. - if (script->get_path().is_empty() && !script->script_class) { + if (script->get_path().is_empty() && !script->valid) { continue; } to_reload.push_back(script); - if (script->get_path().is_empty()) { - script->tied_class_name_for_reload = script->script_class->get_name_for_lookup(); - script->tied_class_namespace_for_reload = script->script_class->get_namespace(); - } - // Script::instances are deleted during managed object disposal, which happens on domain finalize. // Only placeholders are kept. Therefore we need to keep a copy before that happens. @@ -841,17 +828,20 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { CSharpInstance *csi = static_cast<CSharpInstance *>(obj->get_script_instance()); - // Call OnBeforeSerialize - if (csi->script->script_class->implements_interface(GDMonoCache::cached_data.class_ISerializationListener)) { - obj->get_script_instance()->call(string_names.on_before_serialize); - } + // Call OnBeforeSerialize and save instance info - // Save instance info CSharpScript::StateBackup state; - // TODO: Proper state backup (Not only variants, serialize managed state of scripts) - csi->get_properties_state_for_reloading(state.properties); - csi->get_event_signals_state_for_reloading(state.event_signals); + Dictionary properties; + + GDMonoCache::managed_callbacks.CSharpInstanceBridge_SerializeState( + csi->get_gchandle_intptr(), &properties, &state.event_signals); + + for (const Variant *s = properties.next(nullptr); s != nullptr; s = properties.next(s)) { + StringName name = *s; + Variant value = properties[*s]; + state.properties.push_back(Pair<StringName, Variant>(name, value)); + } owners_map[obj->get_instance_id()] = state; } @@ -868,7 +858,7 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { } // Do domain reload - if (gdmono->reload_scripts_domain() != OK) { + if (gdmono->reload_project_assemblies() != OK) { // Failed to reload the scripts domain // Make sure to add the scripts back to their owners before returning for (Ref<CSharpScript> &scr : to_reload) { @@ -899,6 +889,9 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { scr->pending_reload_state.erase(obj_id); } + + scr->pending_reload_instances.clear(); + scr->pending_reload_state.clear(); } return; @@ -916,46 +909,21 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { if (!script->valid) { script->pending_reload_instances.clear(); + script->pending_reload_state.clear(); continue; } } else { - const StringName &class_namespace = script->tied_class_namespace_for_reload; - const StringName &class_name = script->tied_class_name_for_reload; - GDMonoAssembly *project_assembly = gdmono->get_project_assembly(); - - // Search in project and tools assemblies first as those are the most likely to have the class - GDMonoClass *script_class = (project_assembly ? project_assembly->get_class(class_namespace, class_name) : nullptr); + bool success = GDMonoCache::managed_callbacks.ScriptManagerBridge_TryReloadRegisteredScriptWithClass(script.ptr()); -#ifdef TOOLS_ENABLED - if (!script_class) { - GDMonoAssembly *tools_assembly = gdmono->get_tools_assembly(); - script_class = (tools_assembly ? tools_assembly->get_class(class_namespace, class_name) : nullptr); - } -#endif - - if (!script_class) { - script_class = gdmono->get_class(class_namespace, class_name); - } - - if (!script_class) { - // The class was removed, can't reload - script->pending_reload_instances.clear(); - continue; - } - - bool obj_type = GDMonoCache::cached_data.class_GodotObject->is_assignable_from(script_class); - if (!obj_type) { - // The class no longer inherits Godot.Object, can't reload + if (!success) { + // Couldn't reload script->pending_reload_instances.clear(); + script->pending_reload_state.clear(); continue; } - - GDMonoClass *native = GDMonoUtils::get_class_native_base(script_class); - - CSharpScript::reload_registered_script(script, script_class, native); } - StringName native_name = NATIVE_GDMONOCLASS_NAME(script->native); + StringName native_name = script->get_instance_base_type(); { for (const ObjectID &obj_id : script->pending_reload_instances) { @@ -1020,57 +988,25 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { ERR_CONTINUE(!obj->get_script_instance()); - // TODO: Restore serialized state - CSharpScript::StateBackup &state_backup = script->pending_reload_state[obj_id]; - for (const Pair<StringName, Variant> &G : state_backup.properties) { - obj->get_script_instance()->set(G.first, G.second); - } - CSharpInstance *csi = CAST_CSHARP_INSTANCE(obj->get_script_instance()); if (csi) { - for (const Pair<StringName, Array> &G : state_backup.event_signals) { - const StringName &name = G.first; - const Array &serialized_data = G.second; - - HashMap<StringName, GDMonoField *>::Iterator match = script->event_signals.find(name); - - if (!match) { - // The event or its signal attribute were removed - continue; - } + Dictionary properties; - GDMonoField *event_signal_field = match->value; - - MonoObject *managed_serialized_data = GDMonoMarshal::variant_to_mono_object(serialized_data); - MonoDelegate *delegate = nullptr; - - MonoException *exc = nullptr; - bool success = (bool)GDMonoCache::managed_callbacks.methodthunk_DelegateUtils_TryDeserializeDelegate.invoke(managed_serialized_data, &delegate, &exc); - - if (exc) { - GDMonoUtils::debug_print_unhandled_exception(exc); - continue; - } - - if (success) { - ERR_CONTINUE(delegate == nullptr); - event_signal_field->set_value(csi->get_mono_object(), (MonoObject *)delegate); - } else if (OS::get_singleton()->is_stdout_verbose()) { - OS::get_singleton()->print("Failed to deserialize event signal delegate\n"); - } + for (const Pair<StringName, Variant> &G : state_backup.properties) { + properties[G.first] = G.second; } - // Call OnAfterDeserialization - if (csi->script->script_class->implements_interface(GDMonoCache::cached_data.class_ISerializationListener)) { - obj->get_script_instance()->call(string_names.on_after_deserialize); - } + // Restore serialized state and call OnAfterDeserialization + GDMonoCache::managed_callbacks.CSharpInstanceBridge_DeserializeState( + csi->get_gchandle_intptr(), &properties, &state_backup.event_signals); } } script->pending_reload_instances.clear(); + script->pending_reload_state.clear(); } // Deserialize managed callables @@ -1081,20 +1017,13 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { ManagedCallable *managed_callable = elem.key; const Array &serialized_data = elem.value; - MonoObject *managed_serialized_data = GDMonoMarshal::variant_to_mono_object(serialized_data); - void *delegate = nullptr; + GCHandleIntPtr delegate = { nullptr }; - MonoException *exc = nullptr; - bool success = (bool)GDMonoCache::managed_callbacks.methodthunk_DelegateUtils_TryDeserializeDelegateWithGCHandle - .invoke(managed_serialized_data, &delegate, &exc); - - if (exc) { - GDMonoUtils::debug_print_unhandled_exception(exc); - continue; - } + bool success = GDMonoCache::managed_callbacks.DelegateUtils_TryDeserializeDelegateWithGCHandle( + &serialized_data, &delegate); if (success) { - ERR_CONTINUE(delegate == nullptr); + ERR_CONTINUE(delegate.value == nullptr); managed_callable->delegate_handle = delegate; } else if (OS::get_singleton()->is_stdout_verbose()) { OS::get_singleton()->print("Failed to deserialize delegate\n"); @@ -1111,7 +1040,6 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { NodeDock::get_singleton()->update_lists(); } #endif -#endif } #endif @@ -1155,12 +1083,6 @@ bool CSharpLanguage::debug_break(const String &p_error, bool p_allow_continue) { } void CSharpLanguage::_on_scripts_domain_about_to_unload() { - for (KeyValue<Object *, CSharpScriptBinding> &E : script_bindings) { - CSharpScriptBinding &script_binding = E.value; - script_binding.gchandle.release(); - script_binding.inited = false; - } - #ifdef GD_MONO_HOT_RELOAD { MutexLock lock(ManagedCallable::instances_mutex); @@ -1263,7 +1185,8 @@ bool CSharpLanguage::setup_csharp_script_binding(CSharpScriptBinding &r_script_b #endif GCHandleIntPtr strong_gchandle = - GDMonoCache::managed_callbacks.ScriptManagerBridge_CreateManagedForGodotObjectBinding(&type_name, p_object); + GDMonoCache::managed_callbacks.ScriptManagerBridge_CreateManagedForGodotObjectBinding( + &type_name, p_object); ERR_FAIL_NULL_V(strong_gchandle.value, false); @@ -1604,75 +1527,6 @@ bool CSharpInstance::get(const StringName &p_name, Variant &r_ret) const { return false; } -#warning TODO -#if 0 -void CSharpInstance::get_properties_state_for_reloading(List<Pair<StringName, Variant>> &r_state) { - List<PropertyInfo> property_list; - get_property_list(&property_list); - - for (const PropertyInfo &prop_info : property_list) { - Pair<StringName, Variant> state_pair; - state_pair.first = prop_info.name; - - ManagedType managedType; - - GDMonoField *field = nullptr; - GDMonoClass *top = script->script_class; - while (top && top != script->native) { - field = top->get_field(state_pair.first); - if (field) { - break; - } - - top = top->get_parent_class(); - } - if (!field) { - continue; // Properties ignored. We get the property baking fields instead. - } - - managedType = field->get_type(); - - if (GDMonoMarshal::managed_to_variant_type(managedType) != Variant::NIL) { // If we can marshal it - if (get(state_pair.first, state_pair.second)) { - r_state.push_back(state_pair); - } - } - } -} - -void CSharpInstance::get_event_signals_state_for_reloading(List<Pair<StringName, Array>> &r_state) { - MonoObject *owner_managed = get_mono_object(); - ERR_FAIL_NULL(owner_managed); - - for (const KeyValue<StringName, GDMonoField *> &E : script->event_signals) { - GDMonoField *event_signal_field = E.value; - - MonoDelegate *delegate_field_value = (MonoDelegate *)event_signal_field->get_value(owner_managed); - if (!delegate_field_value) { - continue; // Empty - } - - Array serialized_data; - MonoObject *managed_serialized_data = GDMonoMarshal::variant_to_mono_object(serialized_data); - - MonoException *exc = nullptr; - bool success = (bool)GDMonoCache::managed_callbacks.methodthunk_DelegateUtils_TrySerializeDelegate - .invoke(delegate_field_value, managed_serialized_data, &exc); - - if (exc) { - GDMonoUtils::debug_print_unhandled_exception(exc); - continue; - } - - if (success) { - r_state.push_back(Pair<StringName, Array>(event_signal_field->get_name(), serialized_data)); - } else if (OS::get_singleton()->is_stdout_verbose()) { - OS::get_singleton()->print("Failed to serialize event signal delegate\n"); - } - } -} -#endif - void CSharpInstance::get_property_list(List<PropertyInfo> *p_properties) const { List<PropertyInfo> props; script->get_script_property_list(&props); @@ -1906,6 +1760,7 @@ void CSharpInstance::mono_object_disposed_baseref(GCHandleIntPtr p_gchandle_to_f // If the native instance is still alive and Dispose() was called // (instead of the finalizer), then we remove the script instance. r_remove_script_instance = true; + // TODO: Last usage of 'is_finalizing_scripts_domain'. It should be replaced with a check to determine if the load context is being unloaded. } else if (!GDMono::get_singleton()->is_finalizing_scripts_domain()) { // If the native instance is still alive and this is called from the finalizer, // then it was referenced from another thread before the finalizer could @@ -2156,8 +2011,8 @@ void CSharpScript::_update_exports_values(HashMap<StringName, Variant> &values, propnames.push_back(prop_info); } - if (base_cache.is_valid()) { - base_cache->_update_exports_values(values, propnames); + if (base_script.is_valid()) { + base_script->_update_exports_values(values, propnames); } } #endif @@ -2319,13 +2174,16 @@ void CSharpScript::update_script_class_info(Ref<CSharpScript> p_script) { // only for this, so need to call the destructor manually before passing this to C#. rpc_functions_dict.~Dictionary(); + Ref<CSharpScript> base_script; GDMonoCache::managed_callbacks.ScriptManagerBridge_UpdateScriptClassInfo( - p_script.ptr(), &tool, &rpc_functions_dict); + p_script.ptr(), &tool, &rpc_functions_dict, &base_script); p_script->tool = tool; p_script->rpc_config.clear(); p_script->rpc_config = rpc_functions_dict; + + p_script->base_script = base_script; } bool CSharpScript::can_instantiate() const { @@ -2586,8 +2444,8 @@ bool CSharpScript::get_property_default_value(const StringName &p_property, Vari return true; } - if (base_cache.is_valid()) { - return base_cache->get_property_default_value(p_property, r_value); + if (base_script.is_valid()) { + return base_script->get_property_default_value(p_property, r_value); } #endif @@ -2671,26 +2529,35 @@ bool CSharpScript::inherits_script(const Ref<Script> &p_script) const { } Ref<Script> CSharpScript::get_base_script() const { - // TODO search in metadata file once we have it, not important any way? - return Ref<Script>(); + return base_script; } void CSharpScript::get_script_property_list(List<PropertyInfo> *r_list) const { - List<PropertyInfo> props; - #ifdef TOOLS_ENABLED - for (const PropertyInfo &E : exported_members_cache) { - props.push_back(E); + const CSharpScript *top = this; + while (top != nullptr) { + for (const PropertyInfo &E : top->exported_members_cache) { + r_list->push_back(E); + } + + top = top->base_script.ptr(); } #else - for (const KeyValue<StringName, PropertyInfo> &E : member_info) { - props.push_front(E.value); - } -#endif + const CSharpScript *top = this; + while (top != nullptr) { + List<PropertyInfo> props; - for (const PropertyInfo &prop : props) { - r_list->push_back(prop); + for (const KeyValue<StringName, PropertyInfo> &E : top->member_info) { + props.push_front(E.value); + } + + for (const PropertyInfo &prop : props) { + r_list->push_back(prop); + } + + top = top->base_script.ptr(); } +#endif } int CSharpScript::get_member_line(const StringName &p_member) const { @@ -2852,6 +2719,4 @@ CSharpLanguage::StringNameCache::StringNameCache() { _property_can_revert = StaticCString::create("_property_can_revert"); _property_get_revert = StaticCString::create("_property_get_revert"); _script_source = StaticCString::create("script/source"); - on_before_serialize = StaticCString::create("OnBeforeSerialize"); - on_after_deserialize = StaticCString::create("OnAfterDeserialize"); } diff --git a/modules/mono/csharp_script.h b/modules/mono/csharp_script.h index 1ca4e20047..29a36bcc1e 100644 --- a/modules/mono/csharp_script.h +++ b/modules/mono/csharp_script.h @@ -68,14 +68,6 @@ TScriptInstance *cast_script_instance(ScriptInstance *p_inst) { class CSharpScript : public Script { GDCLASS(CSharpScript, Script); -public: - struct SignalParameter { - String name; - Variant::Type type; - bool nil_is_variant = false; - }; - -private: friend class CSharpInstance; friend class CSharpLanguage; @@ -83,7 +75,7 @@ private: bool valid = false; bool reload_invalidated = false; - Ref<CSharpScript> base_cache; // TODO what's this for? + Ref<CSharpScript> base_script; HashSet<Object *> instances; @@ -93,13 +85,11 @@ private: // Replace with buffer containing the serialized state of managed scripts. // Keep variant state backup to use only with script instance placeholders. List<Pair<StringName, Variant>> properties; - List<Pair<StringName, Array>> event_signals; + Dictionary event_signals; }; HashSet<ObjectID> pending_reload_instances; RBMap<ObjectID, StateBackup> pending_reload_state; - StringName tied_class_name_for_reload; - StringName tied_class_namespace_for_reload; #endif String source; @@ -174,8 +164,12 @@ public: void get_members(HashSet<StringName> *p_members) override; - bool is_tool() const override { return tool; } - bool is_valid() const override { return valid; } + bool is_tool() const override { + return tool; + } + bool is_valid() const override { + return valid; + } bool inherits_script(const Ref<Script> &p_script) const override; @@ -191,7 +185,9 @@ public: const Variant get_rpc_config() const override; #ifdef TOOLS_ENABLED - bool is_placeholder_fallback_enabled() const override { return placeholder_fallback_enabled; } + bool is_placeholder_fallback_enabled() const override { + return placeholder_fallback_enabled; + } #endif Error load_source_code(const String &p_path); @@ -231,9 +227,6 @@ class CSharpInstance : public ScriptInstance { // Do not use unless you know what you are doing static CSharpInstance *create_for_managed_type(Object *p_owner, CSharpScript *p_script, const MonoGCHandleData &p_gchandle); - void get_properties_state_for_reloading(List<Pair<StringName, Variant>> &r_state); - void get_event_signals_state_for_reloading(List<Pair<StringName, Array>> &r_state); - public: _FORCE_INLINE_ bool is_destructing_script_instance() { return destructing_script_instance; } @@ -325,8 +318,6 @@ class CSharpLanguage : public ScriptLanguage { StringName _property_can_revert; StringName _property_get_revert; StringName _script_source; - StringName on_before_serialize; // OnBeforeSerialize - StringName on_after_deserialize; // OnAfterDeserialize StringNameCache(); }; @@ -361,18 +352,30 @@ public: StringNameCache string_names; - const Mutex &get_language_bind_mutex() { return language_bind_mutex; } - const Mutex &get_script_instances_mutex() { return script_instances_mutex; } + const Mutex &get_language_bind_mutex() { + return language_bind_mutex; + } + const Mutex &get_script_instances_mutex() { + return script_instances_mutex; + } - _FORCE_INLINE_ int get_language_index() { return lang_idx; } + _FORCE_INLINE_ int get_language_index() { + return lang_idx; + } void set_language_index(int p_idx); - _FORCE_INLINE_ const StringNameCache &get_string_names() { return string_names; } + _FORCE_INLINE_ const StringNameCache &get_string_names() { + return string_names; + } - _FORCE_INLINE_ static CSharpLanguage *get_singleton() { return singleton; } + _FORCE_INLINE_ static CSharpLanguage *get_singleton() { + return singleton; + } #ifdef TOOLS_ENABLED - _FORCE_INLINE_ EditorPlugin *get_godotsharp_editor() const { return godotsharp_editor; } + _FORCE_INLINE_ EditorPlugin *get_godotsharp_editor() const { + return godotsharp_editor; + } #endif static void release_script_gchandle(MonoGCHandleData &p_gchandle); @@ -387,7 +390,9 @@ public: void reload_assemblies(bool p_soft_reload); #endif - _FORCE_INLINE_ ManagedCallableMiddleman *get_managed_callable_middleman() const { return managed_callable_middleman; } + _FORCE_INLINE_ ManagedCallableMiddleman *get_managed_callable_middleman() const { + return managed_callable_middleman; + } String get_name() const override; @@ -416,7 +421,9 @@ public: Script *create_script() const override; bool has_named_classes() const override; bool supports_builtin_mode() const override; - /* TODO? */ int find_function(const String &p_function, const String &p_code) const override { return -1; } + /* TODO? */ int find_function(const String &p_function, const String &p_code) const override { + return -1; + } String make_function(const String &p_class, const String &p_name, const PackedStringArray &p_args) const override; virtual String _get_indentation() const; /* TODO? */ void auto_indent_code(String &p_code, int p_from_line, int p_to_line) const override {} @@ -431,14 +438,20 @@ public: /* TODO */ void debug_get_stack_level_locals(int p_level, List<String> *p_locals, List<Variant> *p_values, int p_max_subitems, int p_max_depth) override {} /* TODO */ void debug_get_stack_level_members(int p_level, List<String> *p_members, List<Variant> *p_values, int p_max_subitems, int p_max_depth) override {} /* TODO */ void debug_get_globals(List<String> *p_locals, List<Variant> *p_values, int p_max_subitems, int p_max_depth) override {} - /* TODO */ String debug_parse_stack_level_expression(int p_level, const String &p_expression, int p_max_subitems, int p_max_depth) override { return ""; } + /* TODO */ String debug_parse_stack_level_expression(int p_level, const String &p_expression, int p_max_subitems, int p_max_depth) override { + return ""; + } Vector<StackInfo> debug_get_current_stack_info() override; /* PROFILING FUNCTIONS */ /* TODO */ void profiling_start() override {} /* TODO */ void profiling_stop() override {} - /* TODO */ int profiling_get_accumulated_data(ProfilingInfo *p_info_arr, int p_info_max) override { return 0; } - /* TODO */ int profiling_get_frame_data(ProfilingInfo *p_info_arr, int p_info_max) override { return 0; } + /* TODO */ int profiling_get_accumulated_data(ProfilingInfo *p_info_arr, int p_info_max) override { + return 0; + } + /* TODO */ int profiling_get_frame_data(ProfilingInfo *p_info_arr, int p_info_max) override { + return 0; + } void frame() override; diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs index 4ac7274e41..c218212f04 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs @@ -224,8 +224,9 @@ namespace Godot.SourceGenerators { foreach (var property in properties) { - // Ignore properties without a getter. Godot properties must be readable. - if (property.IsWriteOnly) + // TODO: We should still restore read-only properties after reloading assembly. Two possible ways: reflection or turn RestoreGodotObjectData into a constructor overload. + // Ignore properties without a getter or without a setter. Godot properties must be both readable and writable. + if (property.IsWriteOnly || property.IsReadOnly) continue; var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(property.Type, typeCache); @@ -244,6 +245,11 @@ namespace Godot.SourceGenerators { foreach (var field in fields) { + // TODO: We should still restore read-only fields after reloading assembly. Two possible ways: reflection or turn RestoreGodotObjectData into a constructor overload. + // Ignore properties without a getter or without a setter. Godot properties must be both readable and writable. + if (field.IsReadOnly) + continue; + var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(field.Type, typeCache); if (marshalType == null) diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSerializationGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSerializationGenerator.cs new file mode 100644 index 0000000000..3a7086a2be --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSerializationGenerator.cs @@ -0,0 +1,217 @@ +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Godot.SourceGenerators +{ + [Generator] + public class ScriptSerializationGenerator : ISourceGenerator + { + public void Initialize(GeneratorInitializationContext context) + { + } + + public void Execute(GeneratorExecutionContext context) + { + if (context.AreGodotSourceGeneratorsDisabled()) + return; + + INamedTypeSymbol[] godotClasses = context + .Compilation.SyntaxTrees + .SelectMany(tree => + tree.GetRoot().DescendantNodes() + .OfType<ClassDeclarationSyntax>() + .SelectGodotScriptClasses(context.Compilation) + // Report and skip non-partial classes + .Where(x => + { + if (x.cds.IsPartial()) + { + if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out var typeMissingPartial)) + { + Common.ReportNonPartialGodotScriptOuterClass(context, typeMissingPartial!); + return false; + } + + return true; + } + + Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol); + return false; + }) + .Select(x => x.symbol) + ) + .Distinct<INamedTypeSymbol>(SymbolEqualityComparer.Default) + .ToArray(); + + if (godotClasses.Length > 0) + { + var typeCache = new MarshalUtils.TypeCache(context); + + foreach (var godotClass in godotClasses) + { + VisitGodotScriptClass(context, typeCache, godotClass); + } + } + } + + private static void VisitGodotScriptClass( + GeneratorExecutionContext context, + MarshalUtils.TypeCache typeCache, + INamedTypeSymbol symbol + ) + { + INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace; + string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ? + namespaceSymbol.FullQualifiedName() : + string.Empty; + bool hasNamespace = classNs.Length != 0; + + bool isInnerClass = symbol.ContainingType != null; + + string uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint() + + "_ScriptSerialization_Generated"; + + var source = new StringBuilder(); + + source.Append("using Godot;\n"); + source.Append("using Godot.NativeInterop;\n"); + source.Append("\n"); + + if (hasNamespace) + { + source.Append("namespace "); + source.Append(classNs); + source.Append(" {\n\n"); + } + + if (isInnerClass) + { + var containingType = symbol.ContainingType; + + while (containingType != null) + { + source.Append("partial "); + source.Append(containingType.GetDeclarationKeyword()); + source.Append(" "); + source.Append(containingType.NameWithTypeParameters()); + source.Append("\n{\n"); + + containingType = containingType.ContainingType; + } + } + + source.Append("partial class "); + source.Append(symbol.NameWithTypeParameters()); + source.Append("\n{\n"); + + var members = symbol.GetMembers(); + + var propertySymbols = members + .Where(s => !s.IsStatic && s.Kind == SymbolKind.Property) + .Cast<IPropertySymbol>(); + + var fieldSymbols = members + .Where(s => !s.IsStatic && s.Kind == SymbolKind.Field && !s.IsImplicitlyDeclared) + .Cast<IFieldSymbol>(); + + var godotClassProperties = propertySymbols.WhereIsGodotCompatibleType(typeCache).ToArray(); + var godotClassFields = fieldSymbols.WhereIsGodotCompatibleType(typeCache).ToArray(); + + source.Append( + " protected override void SaveGodotObjectData(global::Godot.Bridge.GodotSerializationInfo info)\n {\n"); + source.Append(" base.SaveGodotObjectData(info);\n"); + + foreach (var property in godotClassProperties) + { + string propertyName = property.PropertySymbol.Name; + + source.Append(" info.AddProperty(GodotInternal.PropName_") + .Append(propertyName) + .Append(", this.") + .Append(propertyName) + .Append(");\n"); + } + + foreach (var field in godotClassFields) + { + string fieldName = field.FieldSymbol.Name; + + source.Append(" info.AddProperty(GodotInternal.PropName_") + .Append(fieldName) + .Append(", this.") + .Append(fieldName) + .Append(");\n"); + } + + source.Append(" }\n"); + + source.Append( + " protected override void RestoreGodotObjectData(global::Godot.Bridge.GodotSerializationInfo info)\n {\n"); + source.Append(" base.RestoreGodotObjectData(info);\n"); + + foreach (var property in godotClassProperties) + { + string propertyName = property.PropertySymbol.Name; + string propertyTypeQualifiedName = property.PropertySymbol.Type.FullQualifiedName(); + + source.Append(" if (info.TryGetProperty<") + .Append(propertyTypeQualifiedName) + .Append(">(GodotInternal.PropName_") + .Append(propertyName) + .Append(", out var _value_") + .Append(propertyName) + .Append("))\n") + .Append(" this.") + .Append(propertyName) + .Append(" = _value_") + .Append(propertyName) + .Append(";\n"); + } + + foreach (var field in godotClassFields) + { + string fieldName = field.FieldSymbol.Name; + string fieldTypeQualifiedName = field.FieldSymbol.Type.FullQualifiedName(); + + source.Append(" if (info.TryGetProperty<") + .Append(fieldTypeQualifiedName) + .Append(">(GodotInternal.PropName_") + .Append(fieldName) + .Append(", out var _value_") + .Append(fieldName) + .Append("))\n") + .Append(" this.") + .Append(fieldName) + .Append(" = _value_") + .Append(fieldName) + .Append(";\n"); + } + + source.Append(" }\n"); + + source.Append("}\n"); // partial class + + if (isInnerClass) + { + var containingType = symbol.ContainingType; + + while (containingType != null) + { + source.Append("}\n"); // outer class + + containingType = containingType.ContainingType; + } + } + + if (hasNamespace) + { + source.Append("\n}\n"); + } + + context.AddSource(uniqueHint, SourceText.From(source.ToString(), Encoding.UTF8)); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs index 18073d6492..f8a810fd44 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs @@ -11,16 +11,16 @@ namespace GodotTools.Build [Serializable] public sealed partial class BuildInfo : RefCounted // TODO Remove RefCounted once we have proper serialization { - public string Solution { get; } - public string Configuration { get; } - public string? RuntimeIdentifier { get; } - public string? PublishOutputDir { get; } - public bool Restore { get; } - public bool Rebuild { get; } - public bool OnlyClean { get; } + public string Solution { get; private set; } + public string Configuration { get; private set; } + public string? RuntimeIdentifier { get; private set; } + public string? PublishOutputDir { get; private set; } + public bool Restore { get; private set; } + public bool Rebuild { get; private set; } + public bool OnlyClean { get; private set; } // TODO Use List once we have proper serialization - public Array<string> CustomProperties { get; } = new Array<string>(); + public Array<string> CustomProperties { get; private set; } = new Array<string>(); public string LogsDirPath => Path.Combine(GodotSharpDirs.BuildLogsDirs, $"{Solution.MD5Text()}_{Configuration}"); @@ -56,6 +56,13 @@ namespace GodotTools.Build } } + // Needed for instantiation from Godot, after reloading assemblies + private BuildInfo() + { + Solution = string.Empty; + Configuration = string.Empty; + } + public BuildInfo(string solution, string configuration, bool restore, bool rebuild, bool onlyClean) { Solution = solution; diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs index f7b8c6bffd..bac5464fd6 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs @@ -58,7 +58,7 @@ namespace GodotTools.Build } // TODO Use List once we have proper serialization. - private readonly Array<BuildIssue> _issues = new Array<BuildIssue>(); + private Array<BuildIssue> _issues = new Array<BuildIssue>(); private ItemList _issuesList; private PopupMenu _issuesListContextMenu; private TextEdit _buildLog; @@ -133,7 +133,9 @@ namespace GodotTools.Build if (string.IsNullOrEmpty(issue.ProjectFile) && string.IsNullOrEmpty(issue.File)) return; - string projectDir = issue.ProjectFile.Length > 0 ? issue.ProjectFile.GetBaseDir() : _buildInfo.Solution.GetBaseDir(); + string projectDir = issue.ProjectFile.Length > 0 ? + issue.ProjectFile.GetBaseDir() : + _buildInfo.Solution.GetBaseDir(); string file = Path.Combine(projectDir.SimplifyGodotPath(), issue.File.SimplifyGodotPath()); @@ -412,6 +414,16 @@ namespace GodotTools.Build { // In case it didn't update yet. We don't want to have to serialize any pending output. UpdateBuildLogText(); + + // NOTE: + // Currently, GodotTools is loaded in its own load context. This load context is not reloaded, but the script still are. + // Until that changes, we need workarounds like this one because events keep strong references to disposed objects. + BuildManager.BuildLaunchFailed -= BuildLaunchFailed; + BuildManager.BuildStarted -= BuildStarted; + BuildManager.BuildFinished -= BuildFinished; + // StdOutput/Error can be received from different threads, so we need to use CallDeferred + BuildManager.StdOutputReceived -= StdOutputReceived; + BuildManager.StdErrorReceived -= StdErrorReceived; } public void OnAfterDeserialize() diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj index 2765391a27..f5734e6e69 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj +++ b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj @@ -9,6 +9,7 @@ <GodotSourceRootPath>$(SolutionDir)/../../../../</GodotSourceRootPath> <GodotOutputDataDir>$(GodotSourceRootPath)/bin/GodotSharp</GodotOutputDataDir> <GodotApiAssembliesDir>$(GodotOutputDataDir)/Api/$(GodotApiConfiguration)</GodotApiAssembliesDir> + <ProduceReferenceAssembly>false</ProduceReferenceAssembly> <AllowUnsafeBlocks>true</AllowUnsafeBlocks> </PropertyGroup> <!-- Needed for our source generators to work despite this not being a Godot game project --> diff --git a/modules/mono/glue/GodotSharp/ExternalAnnotations/System.Runtime.InteropServices.xml b/modules/mono/glue/GodotSharp/ExternalAnnotations/System.Runtime.InteropServices.xml new file mode 100644 index 0000000000..2dc350d4f2 --- /dev/null +++ b/modules/mono/glue/GodotSharp/ExternalAnnotations/System.Runtime.InteropServices.xml @@ -0,0 +1,5 @@ +<assembly name="System.Runtime.InteropServices"> + <member name="T:System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute"> + <attribute ctor="M:JetBrains.Annotations.MeansImplicitUseAttribute.#ctor" /> + </member> +</assembly> diff --git a/modules/mono/glue/GodotSharp/GodotPlugins/Main.cs b/modules/mono/glue/GodotSharp/GodotPlugins/Main.cs index 2a2e147eaa..395cc9bf66 100644 --- a/modules/mono/glue/GodotSharp/GodotPlugins/Main.cs +++ b/modules/mono/glue/GodotSharp/GodotPlugins/Main.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Loader; using Godot.Bridge; @@ -11,10 +12,55 @@ namespace GodotPlugins { public static class Main { + // IMPORTANT: + // Keeping strong references to the AssemblyLoadContext (our PluginLoadContext) prevents + // it from being unloaded. To avoid issues, we wrap the reference in this class, and mark + // all the methods that access it as non-inlineable. This way we prevent local references + // (either real or introduced by the JIT) to escape the scope of these methods due to + // inlining, which could keep the AssemblyLoadContext alive while trying to unload. + private sealed class PluginLoadContextWrapper + { + private PluginLoadContext? _pluginLoadContext; + + public string? AssemblyLoadedPath + { + [MethodImpl(MethodImplOptions.NoInlining)] + get => _pluginLoadContext?.AssemblyLoadedPath; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static (Assembly, PluginLoadContextWrapper) CreateAndLoadFromAssemblyName( + AssemblyName assemblyName, + string pluginPath, + ICollection<string> sharedAssemblies, + AssemblyLoadContext mainLoadContext + ) + { + var wrapper = new PluginLoadContextWrapper(); + wrapper._pluginLoadContext = new PluginLoadContext( + pluginPath, sharedAssemblies, mainLoadContext); + var assembly = wrapper._pluginLoadContext.LoadFromAssemblyName(assemblyName); + return (assembly, wrapper); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public WeakReference CreateWeakReference() + { + return new WeakReference(_pluginLoadContext, trackResurrection: true); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + internal void Unload() + { + _pluginLoadContext?.Unload(); + _pluginLoadContext = null; + } + } + private static readonly List<AssemblyName> SharedAssemblies = new(); private static readonly Assembly CoreApiAssembly = typeof(Godot.Object).Assembly; private static Assembly? _editorApiAssembly; - private static Assembly? _projectAssembly; + private static PluginLoadContextWrapper? _projectLoadContext; private static readonly AssemblyLoadContext MainLoadContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? @@ -35,6 +81,8 @@ namespace GodotPlugins SharedAssemblies.Add(CoreApiAssembly.GetName()); NativeLibrary.SetDllImportResolver(CoreApiAssembly, _dllImportResolver); + AlcReloadCfg.Configure(alcReloadEnabled: editorHint.ToBool()); + if (editorHint.ToBool()) { _editorApiAssembly = Assembly.Load("GodotSharpEditor"); @@ -46,6 +94,7 @@ namespace GodotPlugins { LoadProjectAssemblyCallback = &LoadProjectAssembly, LoadToolsAssemblyCallback = &LoadToolsAssembly, + UnloadProjectPluginCallback = &UnloadProjectPlugin, }; *managedCallbacks = ManagedCallbacks.Create(); @@ -55,37 +104,41 @@ namespace GodotPlugins catch (Exception e) { Console.Error.WriteLine(e); - return false.ToGodotBool(); + return godot_bool.False; } } [StructLayout(LayoutKind.Sequential)] private struct PluginsCallbacks { - public unsafe delegate* unmanaged<char*, godot_bool> LoadProjectAssemblyCallback; + public unsafe delegate* unmanaged<char*, godot_string*, godot_bool> LoadProjectAssemblyCallback; public unsafe delegate* unmanaged<char*, IntPtr> LoadToolsAssemblyCallback; + public unsafe delegate* unmanaged<godot_bool> UnloadProjectPluginCallback; } [UnmanagedCallersOnly] - private static unsafe godot_bool LoadProjectAssembly(char* nAssemblyPath) + private static unsafe godot_bool LoadProjectAssembly(char* nAssemblyPath, godot_string* outLoadedAssemblyPath) { try { - if (_projectAssembly != null) + if (_projectLoadContext != null) return godot_bool.True; // Already loaded string assemblyPath = new(nAssemblyPath); - _projectAssembly = LoadPlugin(assemblyPath); + (var projectAssembly, _projectLoadContext) = LoadPlugin(assemblyPath); + + string loadedAssemblyPath = _projectLoadContext.AssemblyLoadedPath ?? assemblyPath; + *outLoadedAssemblyPath = Marshaling.ConvertStringToNative(loadedAssemblyPath); - ScriptManagerBridge.LookupScriptsInAssembly(_projectAssembly); + ScriptManagerBridge.LookupScriptsInAssembly(projectAssembly); return godot_bool.True; } catch (Exception e) { Console.Error.WriteLine(e); - return false.ToGodotBool(); + return godot_bool.False; } } @@ -99,7 +152,7 @@ namespace GodotPlugins if (_editorApiAssembly == null) throw new InvalidOperationException("The Godot editor API assembly is not loaded"); - var assembly = LoadPlugin(assemblyPath); + var (assembly, _) = LoadPlugin(assemblyPath); NativeLibrary.SetDllImportResolver(assembly, _dllImportResolver!); @@ -122,7 +175,7 @@ namespace GodotPlugins } } - private static Assembly LoadPlugin(string assemblyPath) + private static (Assembly, PluginLoadContextWrapper) LoadPlugin(string assemblyPath) { string assemblyName = Path.GetFileNameWithoutExtension(assemblyPath); @@ -135,8 +188,78 @@ namespace GodotPlugins sharedAssemblies.Add(sharedAssemblyName); } - var loadContext = new PluginLoadContext(assemblyPath, sharedAssemblies, MainLoadContext); - return loadContext.LoadFromAssemblyName(new AssemblyName(assemblyName)); + return PluginLoadContextWrapper.CreateAndLoadFromAssemblyName( + new AssemblyName(assemblyName), assemblyPath, sharedAssemblies, MainLoadContext); + } + + [UnmanagedCallersOnly] + private static godot_bool UnloadProjectPlugin() + { + try + { + return UnloadPlugin(ref _projectLoadContext).ToGodotBool(); + } + catch (Exception e) + { + Console.Error.WriteLine(e); + return godot_bool.False; + } + } + + private static bool UnloadPlugin(ref PluginLoadContextWrapper? pluginLoadContext) + { + try + { + if (pluginLoadContext == null) + return true; + + Console.WriteLine("Unloading assembly load context..."); + + var alcWeakReference = pluginLoadContext.CreateWeakReference(); + + pluginLoadContext.Unload(); + pluginLoadContext = null; + + int startTimeMs = Environment.TickCount; + bool takingTooLong = false; + + while (alcWeakReference.IsAlive) + { + GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); + GC.WaitForPendingFinalizers(); + + if (!alcWeakReference.IsAlive) + break; + + int elapsedTimeMs = Environment.TickCount - startTimeMs; + + if (!takingTooLong && elapsedTimeMs >= 2000) + { + takingTooLong = true; + + // TODO: How to log from GodotPlugins? (delegate pointer?) + Console.Error.WriteLine("Assembly unloading is taking longer than expected..."); + } + else if (elapsedTimeMs >= 5000) + { + // TODO: How to log from GodotPlugins? (delegate pointer?) + Console.Error.WriteLine( + "Failed to unload assemblies. Possible causes: Strong GC handles, running threads, etc."); + + return false; + } + } + + Console.WriteLine("Assembly load context unloaded successfully."); + + return true; + } + catch (Exception e) + { + // TODO: How to log exceptions from GodotPlugins? (delegate pointer?) + Console.Error.WriteLine(e); + return false; + } } } } diff --git a/modules/mono/glue/GodotSharp/GodotPlugins/PluginLoadContext.cs b/modules/mono/glue/GodotSharp/GodotPlugins/PluginLoadContext.cs index 982549fff7..dcd572c65e 100644 --- a/modules/mono/glue/GodotSharp/GodotPlugins/PluginLoadContext.cs +++ b/modules/mono/glue/GodotSharp/GodotPlugins/PluginLoadContext.cs @@ -12,8 +12,11 @@ namespace GodotPlugins private readonly ICollection<string> _sharedAssemblies; private readonly AssemblyLoadContext _mainLoadContext; + public string? AssemblyLoadedPath { get; private set; } + public PluginLoadContext(string pluginPath, ICollection<string> sharedAssemblies, AssemblyLoadContext mainLoadContext) + : base(isCollectible: true) { _resolver = new AssemblyDependencyResolver(pluginPath); _sharedAssemblies = sharedAssemblies; @@ -31,6 +34,8 @@ namespace GodotPlugins string? assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); if (assemblyPath != null) { + AssemblyLoadedPath = assemblyPath; + // Load in memory to prevent locking the file using var assemblyFile = File.Open(assemblyPath, FileMode.Open, FileAccess.Read, FileShare.Read); string pdbPath = Path.ChangeExtension(assemblyPath, ".pdb"); diff --git a/modules/mono/glue/GodotSharp/GodotSharp.sln.DotSettings b/modules/mono/glue/GodotSharp/GodotSharp.sln.DotSettings index 3103fa78c7..ba65b61e95 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp.sln.DotSettings +++ b/modules/mono/glue/GodotSharp/GodotSharp.sln.DotSettings @@ -1,5 +1,6 @@ <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GC/@EntryIndexedValue">GC</s:String> + <s:Boolean x:Key="/Default/UserDictionary/Words/=alcs/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=gdnative/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=godotsharp/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=icall/@EntryIndexedValue">True</s:Boolean> diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/AlcReloadCfg.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/AlcReloadCfg.cs new file mode 100644 index 0000000000..ac2e2fae3c --- /dev/null +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/AlcReloadCfg.cs @@ -0,0 +1,18 @@ +namespace Godot.Bridge; + +public static class AlcReloadCfg +{ + private static bool _configured = false; + + public static void Configure(bool alcReloadEnabled) + { + if (_configured) + return; + + _configured = true; + + IsAlcReloadingEnabled = alcReloadEnabled; + } + + internal static bool IsAlcReloadingEnabled = false; +} diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs index db53a508ef..9ede67b285 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs @@ -18,7 +18,7 @@ namespace Godot.Bridge { *ret = default; (*refCallError).Error = godot_variant_call_error_error.GODOT_CALL_ERROR_CALL_ERROR_INSTANCE_IS_NULL; - return false.ToGodotBool(); + return godot_bool.False; } bool methodInvoked = godotObject.InvokeGodotClassMethod(CustomUnsafe.AsRef(method), @@ -31,17 +31,17 @@ namespace Godot.Bridge // This is important, as it tells Object::call that no method was called. // Otherwise, it would prevent Object::call from calling native methods. (*refCallError).Error = godot_variant_call_error_error.GODOT_CALL_ERROR_CALL_ERROR_INVALID_METHOD; - return false.ToGodotBool(); + return godot_bool.False; } *ret = retValue; - return true.ToGodotBool(); + return godot_bool.True; } catch (Exception e) { ExceptionUtils.LogException(e); *ret = default; - return false.ToGodotBool(); + return godot_bool.False; } } @@ -57,7 +57,7 @@ namespace Godot.Bridge if (godotObject.SetGodotClassPropertyValue(CustomUnsafe.AsRef(name), CustomUnsafe.AsRef(value))) { - return true.ToGodotBool(); + return godot_bool.True; } var nameManaged = StringName.CreateTakingOwnershipOfDisposableValue( @@ -70,7 +70,7 @@ namespace Godot.Bridge catch (Exception e) { ExceptionUtils.LogException(e); - return false.ToGodotBool(); + return godot_bool.False; } } @@ -88,7 +88,7 @@ namespace Godot.Bridge if (godotObject.GetGodotClassPropertyValue(CustomUnsafe.AsRef(name), out godot_variant outRetValue)) { *outRet = outRetValue; - return true.ToGodotBool(); + return godot_bool.True; } var nameManaged = StringName.CreateTakingOwnershipOfDisposableValue( @@ -99,17 +99,17 @@ namespace Godot.Bridge if (ret == null) { *outRet = default; - return false.ToGodotBool(); + return godot_bool.False; } *outRet = Marshaling.ConvertManagedObjectToVariant(ret); - return true.ToGodotBool(); + return godot_bool.True; } catch (Exception e) { ExceptionUtils.LogException(e); *outRet = default; - return false.ToGodotBool(); + return godot_bool.False; } } @@ -141,7 +141,7 @@ namespace Godot.Bridge if (self == null) { *outRes = default; - *outValid = false.ToGodotBool(); + *outValid = godot_bool.False; return; } @@ -150,18 +150,18 @@ namespace Godot.Bridge if (resultStr == null) { *outRes = default; - *outValid = false.ToGodotBool(); + *outValid = godot_bool.False; return; } *outRes = Marshaling.ConvertStringToNative(resultStr); - *outValid = true.ToGodotBool(); + *outValid = godot_bool.True; } catch (Exception e) { ExceptionUtils.LogException(e); *outRes = default; - *outValid = false.ToGodotBool(); + *outValid = godot_bool.False; } } @@ -173,14 +173,86 @@ namespace Godot.Bridge var godotObject = (Object)GCHandle.FromIntPtr(godotObjectGCHandle).Target; if (godotObject == null) - return false.ToGodotBool(); + return godot_bool.False; return godotObject.HasGodotClassMethod(CustomUnsafe.AsRef(method)).ToGodotBool(); } catch (Exception e) { ExceptionUtils.LogException(e); - return false.ToGodotBool(); + return godot_bool.False; + } + } + + [UnmanagedCallersOnly] + internal static unsafe void SerializeState( + IntPtr godotObjectGCHandle, + godot_dictionary* propertiesState, + godot_dictionary* signalEventsState + ) + { + try + { + var godotObject = (Object)GCHandle.FromIntPtr(godotObjectGCHandle).Target; + + if (godotObject == null) + return; + + // Call OnBeforeSerialize + + // ReSharper disable once SuspiciousTypeConversion.Global + if (godotObject is ISerializationListener serializationListener) + serializationListener.OnBeforeSerialize(); + + // Save instance state + + var info = new GodotSerializationInfo( + Collections.Dictionary<StringName, object>.CreateTakingOwnershipOfDisposableValue( + NativeFuncs.godotsharp_dictionary_new_copy(*propertiesState)), + Collections.Dictionary<StringName, Collections.Array>.CreateTakingOwnershipOfDisposableValue( + NativeFuncs.godotsharp_dictionary_new_copy(*signalEventsState))); + + godotObject.SaveGodotObjectData(info); + } + catch (Exception e) + { + ExceptionUtils.LogException(e); + } + } + + [UnmanagedCallersOnly] + internal static unsafe void DeserializeState( + IntPtr godotObjectGCHandle, + godot_dictionary* propertiesState, + godot_dictionary* signalEventsState + ) + { + try + { + var godotObject = (Object)GCHandle.FromIntPtr(godotObjectGCHandle).Target; + + if (godotObject == null) + return; + + // Restore instance state + + var info = new GodotSerializationInfo( + Collections.Dictionary<StringName, object>.CreateTakingOwnershipOfDisposableValue( + NativeFuncs.godotsharp_dictionary_new_copy(*propertiesState)), + Collections.Dictionary<StringName, Collections.Array>.CreateTakingOwnershipOfDisposableValue( + NativeFuncs.godotsharp_dictionary_new_copy(*signalEventsState))); + + godotObject.RestoreGodotObjectData(info); + + // Call OnAfterDeserialize + + // ReSharper disable once SuspiciousTypeConversion.Global + if (godotObject is ISerializationListener serializationListener) + serializationListener.OnAfterDeserialize(); + } + catch (Exception e) + { + ExceptionUtils.LogException(e); } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/GCHandleBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/GCHandleBridge.cs index bd11811c7d..456a118b90 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/GCHandleBridge.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/GCHandleBridge.cs @@ -11,7 +11,7 @@ namespace Godot.Bridge { try { - GCHandle.FromIntPtr(gcHandlePtr).Free(); + CustomGCHandle.Free(GCHandle.FromIntPtr(gcHandlePtr)); } catch (Exception e) { diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/GodotSerializationInfo.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/GodotSerializationInfo.cs new file mode 100644 index 0000000000..26fbed8cac --- /dev/null +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/GodotSerializationInfo.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Godot.Bridge; + +public class GodotSerializationInfo +{ + private readonly Collections.Dictionary<StringName, object> _properties = new(); + private readonly Collections.Dictionary<StringName, Collections.Array> _signalEvents = new(); + + internal GodotSerializationInfo() + { + } + + internal GodotSerializationInfo( + Collections.Dictionary<StringName, object> properties, + Collections.Dictionary<StringName, Collections.Array> signalEvents + ) + { + _properties = properties; + _signalEvents = signalEvents; + } + + public void AddProperty(StringName name, object value) + { + _properties[name] = value; + } + + public bool TryGetProperty<T>(StringName name, [MaybeNullWhen(false)] out T value) + { + return _properties.TryGetValueAsType(name, out value); + } + + public void AddSignalEventDelegate(StringName name, Delegate eventDelegate) + { + var serializedData = new Collections.Array(); + + if (DelegateUtils.TrySerializeDelegate(eventDelegate, serializedData)) + { + _signalEvents[name] = serializedData; + } + else if (OS.IsStdoutVerbose()) + { + Console.WriteLine($"Failed to serialize event signal delegate: {name}"); + } + } + + public Delegate GetSignalEventDelegate(StringName name) + { + if (DelegateUtils.TryDeserializeDelegate(_signalEvents[name], out var eventDelegate)) + { + return eventDelegate; + } + else if (OS.IsStdoutVerbose()) + { + Console.WriteLine($"Failed to deserialize event signal delegate: {name}"); + } + + return null; + } + + public IEnumerable<StringName> GetSignalEventsList() + { + return _signalEvents.Keys; + } +} diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs index 9f9d740659..a6e5f6da1a 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs @@ -11,6 +11,8 @@ namespace Godot.Bridge public delegate* unmanaged<IntPtr, godot_variant**, int, godot_bool*, void> SignalAwaiter_SignalCallback; public delegate* unmanaged<IntPtr, godot_variant**, uint, godot_variant*, void> DelegateUtils_InvokeWithVariantArgs; public delegate* unmanaged<IntPtr, IntPtr, godot_bool> DelegateUtils_DelegateEquals; + public delegate* unmanaged<IntPtr, godot_array*, godot_bool> DelegateUtils_TrySerializeDelegateWithGCHandle; + public delegate* unmanaged<godot_array*, IntPtr*, godot_bool> DelegateUtils_TryDeserializeDelegateWithGCHandle; public delegate* unmanaged<void> ScriptManagerBridge_FrameCallback; public delegate* unmanaged<godot_string_name*, IntPtr, IntPtr> ScriptManagerBridge_CreateManagedForGodotObjectBinding; public delegate* unmanaged<IntPtr, IntPtr, godot_variant**, int, godot_bool> ScriptManagerBridge_CreateManagedForGodotObjectScriptInstance; @@ -23,7 +25,8 @@ namespace Godot.Bridge public delegate* unmanaged<IntPtr, godot_string*, godot_bool> ScriptManagerBridge_AddScriptBridge; public delegate* unmanaged<godot_string*, godot_ref*, void> ScriptManagerBridge_GetOrCreateScriptBridgeForPath; public delegate* unmanaged<IntPtr, void> ScriptManagerBridge_RemoveScriptBridge; - public delegate* unmanaged<IntPtr, godot_bool*, godot_dictionary*, void> ScriptManagerBridge_UpdateScriptClassInfo; + public delegate* unmanaged<IntPtr, godot_bool> ScriptManagerBridge_TryReloadRegisteredScriptWithClass; + public delegate* unmanaged<IntPtr, godot_bool*, godot_dictionary*, godot_ref*, void> ScriptManagerBridge_UpdateScriptClassInfo; public delegate* unmanaged<IntPtr, IntPtr*, godot_bool, godot_bool> ScriptManagerBridge_SwapGCHandleForType; public delegate* unmanaged<IntPtr, delegate* unmanaged<IntPtr, godot_string*, void*, int, void>, void> ScriptManagerBridge_GetPropertyInfoList; public delegate* unmanaged<IntPtr, delegate* unmanaged<IntPtr, void*, int, void>, void> ScriptManagerBridge_GetPropertyDefaultValues; @@ -33,6 +36,8 @@ namespace Godot.Bridge public delegate* unmanaged<IntPtr, godot_bool, void> CSharpInstanceBridge_CallDispose; public delegate* unmanaged<IntPtr, godot_string*, godot_bool*, void> CSharpInstanceBridge_CallToString; public delegate* unmanaged<IntPtr, godot_string_name*, godot_bool> CSharpInstanceBridge_HasMethodUnknownParams; + public delegate* unmanaged<IntPtr, godot_dictionary*, godot_dictionary*, void> CSharpInstanceBridge_SerializeState; + public delegate* unmanaged<IntPtr, godot_dictionary*, godot_dictionary*, void> CSharpInstanceBridge_DeserializeState; public delegate* unmanaged<IntPtr, void> GCHandleBridge_FreeGCHandle; public delegate* unmanaged<void*, void> DebuggingUtils_GetCurrentStackInfo; public delegate* unmanaged<void> DisposablesTracker_OnGodotShuttingDown; @@ -47,6 +52,8 @@ namespace Godot.Bridge SignalAwaiter_SignalCallback = &SignalAwaiter.SignalCallback, DelegateUtils_InvokeWithVariantArgs = &DelegateUtils.InvokeWithVariantArgs, DelegateUtils_DelegateEquals = &DelegateUtils.DelegateEquals, + DelegateUtils_TrySerializeDelegateWithGCHandle = &DelegateUtils.TrySerializeDelegateWithGCHandle, + DelegateUtils_TryDeserializeDelegateWithGCHandle = &DelegateUtils.TryDeserializeDelegateWithGCHandle, ScriptManagerBridge_FrameCallback = &ScriptManagerBridge.FrameCallback, ScriptManagerBridge_CreateManagedForGodotObjectBinding = &ScriptManagerBridge.CreateManagedForGodotObjectBinding, ScriptManagerBridge_CreateManagedForGodotObjectScriptInstance = &ScriptManagerBridge.CreateManagedForGodotObjectScriptInstance, @@ -59,6 +66,7 @@ namespace Godot.Bridge ScriptManagerBridge_AddScriptBridge = &ScriptManagerBridge.AddScriptBridge, ScriptManagerBridge_GetOrCreateScriptBridgeForPath = &ScriptManagerBridge.GetOrCreateScriptBridgeForPath, ScriptManagerBridge_RemoveScriptBridge = &ScriptManagerBridge.RemoveScriptBridge, + ScriptManagerBridge_TryReloadRegisteredScriptWithClass = &ScriptManagerBridge.TryReloadRegisteredScriptWithClass, ScriptManagerBridge_UpdateScriptClassInfo = &ScriptManagerBridge.UpdateScriptClassInfo, ScriptManagerBridge_SwapGCHandleForType = &ScriptManagerBridge.SwapGCHandleForType, ScriptManagerBridge_GetPropertyInfoList = &ScriptManagerBridge.GetPropertyInfoList, @@ -69,6 +77,8 @@ namespace Godot.Bridge CSharpInstanceBridge_CallDispose = &CSharpInstanceBridge.CallDispose, CSharpInstanceBridge_CallToString = &CSharpInstanceBridge.CallToString, CSharpInstanceBridge_HasMethodUnknownParams = &CSharpInstanceBridge.HasMethodUnknownParams, + CSharpInstanceBridge_SerializeState = &CSharpInstanceBridge.SerializeState, + CSharpInstanceBridge_DeserializeState = &CSharpInstanceBridge.DeserializeState, GCHandleBridge_FreeGCHandle = &GCHandleBridge.FreeGCHandle, DebuggingUtils_GetCurrentStackInfo = &DebuggingUtils.GetCurrentStackInfo, DisposablesTracker_OnGodotShuttingDown = &DisposablesTracker.OnGodotShuttingDown, diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs index 61987c6466..40f1235e7e 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs @@ -1,22 +1,77 @@ +#nullable enable + using System; +using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Loader; using System.Runtime.Serialization; using Godot.Collections; using Godot.NativeInterop; namespace Godot.Bridge { - public static class ScriptManagerBridge + // TODO: Make class internal once we replace LookupScriptsInAssembly (the only public member) with source generators + public static partial class ScriptManagerBridge { - private static System.Collections.Generic.Dictionary<string, Type> _pathScriptMap = new(); + private static ConcurrentDictionary<AssemblyLoadContext, ConcurrentDictionary<Type, byte>> + _alcData = new(); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void OnAlcUnloading(AssemblyLoadContext alc) + { + if (_alcData.TryRemove(alc, out var typesInAlc)) + { + foreach (var type in typesInAlc.Keys) + { + if (_scriptTypeBiMap.RemoveByScriptType(type, out IntPtr scriptPtr) && + !_pathTypeBiMap.TryGetScriptPath(type, out _)) + { + // For scripts without a path, we need to keep the class qualified name for reloading + _scriptDataForReload.TryAdd(scriptPtr, + (type.Assembly.GetName().Name, type.FullName ?? type.ToString())); + } + + _pathTypeBiMap.RemoveByScriptType(type); + } + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void AddTypeForAlcReloading(Type type) + { + var alc = AssemblyLoadContext.GetLoadContext(type.Assembly); + if (alc == null) + return; + + var typesInAlc = _alcData.GetOrAdd(alc, + static alc => + { + alc.Unloading += OnAlcUnloading; + return new(); + }); + typesInAlc.TryAdd(type, 0); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void TrackAlcForUnloading(AssemblyLoadContext alc) + { + _ = _alcData.GetOrAdd(alc, + static alc => + { + alc.Unloading += OnAlcUnloading; + return new(); + }); + } + + private static ScriptTypeBiMap _scriptTypeBiMap = new(); + private static PathScriptTypeBiMap _pathTypeBiMap = new(); - private static readonly object ScriptBridgeLock = new(); - private static System.Collections.Generic.Dictionary<IntPtr, Type> _scriptTypeMap = new(); - private static System.Collections.Generic.Dictionary<Type, IntPtr> _typeScriptMap = new(); + private static ConcurrentDictionary<IntPtr, (string? assemblyName, string classFullName)> + _scriptDataForReload = new(); [UnmanagedCallersOnly] internal static void FrameCallback() @@ -55,7 +110,7 @@ namespace Godot.Bridge _ = ctor!.Invoke(obj, null); - return GCHandle.ToIntPtr(GCHandle.Alloc(obj)); + return GCHandle.ToIntPtr(CustomGCHandle.AllocStrong(obj)); } catch (Exception e) { @@ -74,7 +129,7 @@ namespace Godot.Bridge try { // Performance is not critical here as this will be replaced with source generators. - Type scriptType = _scriptTypeMap[scriptPtr]; + Type scriptType = _scriptTypeBiMap.GetScriptType(scriptPtr); var obj = (Object)FormatterServices.GetUninitializedObject(scriptType); var ctor = scriptType @@ -99,7 +154,7 @@ namespace Godot.Bridge var parameters = ctor.GetParameters(); int paramCount = parameters.Length; - object[] invokeParams = new object[paramCount]; + var invokeParams = new object?[paramCount]; for (int i = 0; i < paramCount; i++) { @@ -112,12 +167,12 @@ namespace Godot.Bridge _ = ctor.Invoke(obj, invokeParams); - return true.ToGodotBool(); + return godot_bool.True; } catch (Exception e) { ExceptionUtils.LogException(e); - return false.ToGodotBool(); + return godot_bool.False; } } @@ -127,7 +182,7 @@ namespace Godot.Bridge try { // Performance is not critical here as this will be replaced with source generators. - if (!_scriptTypeMap.TryGetValue(scriptPtr, out var scriptType)) + if (!_scriptTypeBiMap.TryGetScriptType(scriptPtr, out Type? scriptType)) { *outRes = default; return; @@ -144,7 +199,7 @@ namespace Godot.Bridge return; } - var nativeName = (StringName)field.GetValue(null); + var nativeName = (StringName?)field.GetValue(null); if (nativeName == null) { @@ -166,7 +221,7 @@ namespace Godot.Bridge { try { - var target = (Object)GCHandle.FromIntPtr(gcHandlePtr).Target; + var target = (Object?)GCHandle.FromIntPtr(gcHandlePtr).Target; if (target != null) target.NativePtr = newPtr; } @@ -176,14 +231,14 @@ namespace Godot.Bridge } } - private static Type TypeGetProxyClass(string nativeTypeNameStr) + private static Type? TypeGetProxyClass(string nativeTypeNameStr) { // Performance is not critical here as this will be replaced with a generated dictionary. if (nativeTypeNameStr[0] == '_') nativeTypeNameStr = nativeTypeNameStr.Substring(1); - Type wrapperType = typeof(Object).Assembly.GetType("Godot." + nativeTypeNameStr); + Type? wrapperType = typeof(Object).Assembly.GetType("Godot." + nativeTypeNameStr); if (wrapperType == null) { @@ -216,7 +271,12 @@ namespace Godot.Bridge if (scriptPathAttr == null) return; - _pathScriptMap[scriptPathAttr.Path] = type; + _pathTypeBiMap.Add(scriptPathAttr.Path, type); + + if (AlcReloadCfg.IsAlcReloadingEnabled) + { + AddTypeForAlcReloading(type); + } } var assemblyHasScriptsAttr = assembly.GetCustomAttributes(inherit: false) @@ -267,15 +327,15 @@ namespace Godot.Bridge { try { - var owner = (Object)GCHandle.FromIntPtr(ownerGCHandlePtr).Target; + var owner = (Object?)GCHandle.FromIntPtr(ownerGCHandlePtr).Target; if (owner == null) { - *outOwnerIsNull = true.ToGodotBool(); + *outOwnerIsNull = godot_bool.True; return; } - *outOwnerIsNull = false.ToGodotBool(); + *outOwnerIsNull = godot_bool.False; owner.InternalRaiseEventSignal(CustomUnsafe.AsRef(eventSignalName), new NativeVariantPtrArgs(args), argCount); @@ -283,7 +343,7 @@ namespace Godot.Bridge catch (Exception e) { ExceptionUtils.LogException(e); - *outOwnerIsNull = false.ToGodotBool(); + *outOwnerIsNull = godot_bool.False; } } @@ -295,7 +355,7 @@ namespace Godot.Bridge // Performance is not critical here as this will be replaced with source generators. using var signals = new Dictionary(); - Type top = _scriptTypeMap[scriptPtr]; + Type? top = _scriptTypeBiMap.GetScriptType(scriptPtr); Type native = Object.InternalGetClassNativeBase(top); while (top != null && top != native) @@ -391,7 +451,7 @@ namespace Godot.Bridge string signalNameStr = Marshaling.ConvertStringToManaged(*signalName); - Type top = _scriptTypeMap[scriptPtr]; + Type? top = _scriptTypeBiMap.GetScriptType(scriptPtr); Type native = Object.InternalGetClassNativeBase(top); while (top != null && top != native) @@ -405,7 +465,7 @@ namespace Godot.Bridge .Any(signalDelegate => signalDelegate.Name == signalNameStr) ) { - return true.ToGodotBool(); + return godot_bool.True; } // Event signals @@ -417,18 +477,18 @@ namespace Godot.Bridge .Any(eventSignal => eventSignal.Name == signalNameStr) ) { - return true.ToGodotBool(); + return godot_bool.True; } top = top.BaseType; } - return false.ToGodotBool(); + return godot_bool.False; } catch (Exception e) { ExceptionUtils.LogException(e); - return false.ToGodotBool(); + return godot_bool.False; } } @@ -437,18 +497,18 @@ namespace Godot.Bridge { try { - if (!_scriptTypeMap.TryGetValue(scriptPtr, out var scriptType)) - return false.ToGodotBool(); + if (!_scriptTypeBiMap.TryGetScriptType(scriptPtr, out Type? scriptType)) + return godot_bool.False; - if (!_scriptTypeMap.TryGetValue(scriptPtrMaybeBase, out var maybeBaseType)) - return false.ToGodotBool(); + if (!_scriptTypeBiMap.TryGetScriptType(scriptPtrMaybeBase, out Type? maybeBaseType)) + return godot_bool.False; return (scriptType == maybeBaseType || maybeBaseType.IsAssignableFrom(scriptType)).ToGodotBool(); } catch (Exception e) { ExceptionUtils.LogException(e); - return false.ToGodotBool(); + return godot_bool.False; } } @@ -457,26 +517,25 @@ namespace Godot.Bridge { try { - lock (ScriptBridgeLock) + lock (_scriptTypeBiMap.ReadWriteLock) { - if (!_scriptTypeMap.ContainsKey(scriptPtr)) + if (!_scriptTypeBiMap.IsScriptRegistered(scriptPtr)) { string scriptPathStr = Marshaling.ConvertStringToManaged(*scriptPath); - if (!_pathScriptMap.TryGetValue(scriptPathStr, out Type scriptType)) - return false.ToGodotBool(); + if (!_pathTypeBiMap.TryGetScriptType(scriptPathStr, out Type? scriptType)) + return godot_bool.False; - _scriptTypeMap.Add(scriptPtr, scriptType); - _typeScriptMap.Add(scriptType, scriptPtr); + _scriptTypeBiMap.Add(scriptPtr, scriptType); } } - return true.ToGodotBool(); + return godot_bool.True; } catch (Exception e) { ExceptionUtils.LogException(e); - return false.ToGodotBool(); + return godot_bool.False; } } @@ -485,7 +544,7 @@ namespace Godot.Bridge { string scriptPathStr = Marshaling.ConvertStringToManaged(*scriptPath); - if (!_pathScriptMap.TryGetValue(scriptPathStr, out Type scriptType)) + if (!_pathTypeBiMap.TryGetScriptType(scriptPathStr, out Type? scriptType)) { NativeFuncs.godotsharp_internal_new_csharp_script(outScript); return; @@ -494,34 +553,84 @@ namespace Godot.Bridge GetOrCreateScriptBridgeForType(scriptType, outScript); } - internal static unsafe void GetOrCreateScriptBridgeForType(Type scriptType, godot_ref* outScript) + private static unsafe void GetOrCreateScriptBridgeForType(Type scriptType, godot_ref* outScript) { - lock (ScriptBridgeLock) + lock (_scriptTypeBiMap.ReadWriteLock) { - if (_typeScriptMap.TryGetValue(scriptType, out IntPtr scriptPtr)) + if (_scriptTypeBiMap.TryGetScriptPtr(scriptType, out IntPtr scriptPtr)) { + // Use existing NativeFuncs.godotsharp_ref_new_from_ref_counted_ptr(out *outScript, scriptPtr); return; } - NativeFuncs.godotsharp_internal_new_csharp_script(outScript); - scriptPtr = outScript->Reference; + // This path is slower, but it's only executed for the first instantiation of the type + CreateScriptBridgeForType(scriptType, outScript); + } + } - _scriptTypeMap.Add(scriptPtr, scriptType); - _typeScriptMap.Add(scriptType, scriptPtr); + internal static unsafe void GetOrLoadOrCreateScriptForType(Type scriptType, godot_ref* outScript) + { + static bool GetPathOtherwiseGetOrCreateScript(Type scriptType, godot_ref* outScript, + [MaybeNullWhen(false)] out string scriptPath) + { + lock (_scriptTypeBiMap.ReadWriteLock) + { + if (_scriptTypeBiMap.TryGetScriptPtr(scriptType, out IntPtr scriptPtr)) + { + // Use existing + NativeFuncs.godotsharp_ref_new_from_ref_counted_ptr(out *outScript, scriptPtr); + scriptPath = null; + return false; + } - NativeFuncs.godotsharp_internal_reload_registered_script(scriptPtr); + // This path is slower, but it's only executed for the first instantiation of the type + + if (_pathTypeBiMap.TryGetScriptPath(scriptType, out scriptPath)) + return true; + + CreateScriptBridgeForType(scriptType, outScript); + scriptPath = null; + return false; + } + } + + if (GetPathOtherwiseGetOrCreateScript(scriptType, outScript, out string? scriptPath)) + { + // This path is slower, but it's only executed for the first instantiation of the type + + // This must be done outside the read-write lock, as the script resource loading can lock it + using godot_string scriptPathIn = Marshaling.ConvertStringToNative(scriptPath); + if (!NativeFuncs.godotsharp_internal_script_load(scriptPathIn, outScript).ToBool()) + { + GD.PushError($"Cannot load script for type '{scriptType.FullName}'. Path: '{scriptPath}'."); + + // If loading of the script fails, best we can do create a new script + // with no path, as we do for types without an associated script file. + GetOrCreateScriptBridgeForType(scriptType, outScript); + } } } + private static unsafe void CreateScriptBridgeForType(Type scriptType, godot_ref* outScript) + { + NativeFuncs.godotsharp_internal_new_csharp_script(outScript); + IntPtr scriptPtr = outScript->Reference; + + // Caller takes care of locking + _scriptTypeBiMap.Add(scriptPtr, scriptType); + + NativeFuncs.godotsharp_internal_reload_registered_script(scriptPtr); + } + [UnmanagedCallersOnly] internal static void RemoveScriptBridge(IntPtr scriptPtr) { try { - lock (ScriptBridgeLock) + lock (_scriptTypeBiMap.ReadWriteLock) { - _ = _scriptTypeMap.Remove(scriptPtr); + _scriptTypeBiMap.Remove(scriptPtr); } } catch (Exception e) @@ -531,13 +640,74 @@ namespace Godot.Bridge } [UnmanagedCallersOnly] + internal static godot_bool TryReloadRegisteredScriptWithClass(IntPtr scriptPtr) + { + try + { + lock (_scriptTypeBiMap.ReadWriteLock) + { + if (_scriptTypeBiMap.TryGetScriptType(scriptPtr, out _)) + { + // NOTE: + // Currently, we reload all scripts, not only the ones from the unloaded ALC. + // As such, we need to handle this case instead of treating it as an error. + NativeFuncs.godotsharp_internal_reload_registered_script(scriptPtr); + return godot_bool.True; + } + + if (!_scriptDataForReload.TryGetValue(scriptPtr, out var dataForReload)) + { + GD.PushError("Missing class qualified name for reloading script"); + return godot_bool.False; + } + + _ = _scriptDataForReload.TryRemove(scriptPtr, out _); + + if (dataForReload.assemblyName == null) + { + GD.PushError( + $"Missing assembly name of class '{dataForReload.classFullName}' for reloading script"); + return godot_bool.False; + } + + var scriptType = ReflectionUtils.FindTypeInLoadedAssemblies(dataForReload.assemblyName, + dataForReload.classFullName); + + if (scriptType == null) + { + // The class was removed, can't reload + return godot_bool.False; + } + + // ReSharper disable once RedundantNameQualifier + if (!typeof(Godot.Object).IsAssignableFrom(scriptType)) + { + // The class no longer inherits Godot.Object, can't reload + return godot_bool.False; + } + + _scriptTypeBiMap.Add(scriptPtr, scriptType); + + NativeFuncs.godotsharp_internal_reload_registered_script(scriptPtr); + + return godot_bool.True; + } + } + catch (Exception e) + { + ExceptionUtils.LogException(e); + return godot_bool.False; + } + } + + [UnmanagedCallersOnly] internal static unsafe void UpdateScriptClassInfo(IntPtr scriptPtr, godot_bool* outTool, - godot_dictionary* outRpcFunctionsDest) + godot_dictionary* outRpcFunctionsDest, godot_ref* outBaseScript) { try { // Performance is not critical here as this will be replaced with source generators. - var scriptType = _scriptTypeMap[scriptPtr]; + var scriptType = _scriptTypeBiMap.GetScriptType(scriptPtr); *outTool = scriptType.GetCustomAttributes(inherit: false) .OfType<ToolAttribute>() @@ -551,13 +721,13 @@ namespace Godot.Bridge } if (!(*outTool).ToBool() && scriptType.Assembly.GetName().Name == "GodotTools") - *outTool = true.ToGodotBool(); + *outTool = godot_bool.True; // RPC functions Dictionary<string, Dictionary> rpcFunctions = new(); - Type top = scriptType; + Type? top = scriptType; Type native = Object.InternalGetClassNativeBase(top); while (top != null && top != native) @@ -595,11 +765,21 @@ namespace Godot.Bridge *outRpcFunctionsDest = NativeFuncs.godotsharp_dictionary_new_copy( (godot_dictionary)((Dictionary)rpcFunctions).NativeValue); + + var baseType = scriptType.BaseType; + if (baseType != null && baseType != native) + { + GetOrLoadOrCreateScriptForType(baseType, outBaseScript); + } + else + { + *outBaseScript = default; + } } catch (Exception e) { ExceptionUtils.LogException(e); - *outTool = false.ToGodotBool(); + *outTool = godot_bool.False; *outRpcFunctionsDest = NativeFuncs.godotsharp_dictionary_new(); } } @@ -612,28 +792,29 @@ namespace Godot.Bridge { var oldGCHandle = GCHandle.FromIntPtr(oldGCHandlePtr); - object target = oldGCHandle.Target; + object? target = oldGCHandle.Target; if (target == null) { - oldGCHandle.Free(); + CustomGCHandle.Free(oldGCHandle); *outNewGCHandlePtr = IntPtr.Zero; - return false.ToGodotBool(); // Called after the managed side was collected, so nothing to do here + return godot_bool.False; // Called after the managed side was collected, so nothing to do here } // Release the current weak handle and replace it with a strong handle. - var newGCHandle = GCHandle.Alloc(target, - createWeak.ToBool() ? GCHandleType.Weak : GCHandleType.Normal); + var newGCHandle = createWeak.ToBool() ? + CustomGCHandle.AllocWeak(target) : + CustomGCHandle.AllocStrong(target); - oldGCHandle.Free(); + CustomGCHandle.Free(oldGCHandle); *outNewGCHandlePtr = GCHandle.ToIntPtr(newGCHandle); - return true.ToGodotBool(); + return godot_bool.True; } catch (Exception e) { ExceptionUtils.LogException(e); *outNewGCHandlePtr = IntPtr.Zero; - return false.ToGodotBool(); + return godot_bool.False; } } @@ -662,7 +843,7 @@ namespace Godot.Bridge { try { - Type scriptType = _scriptTypeMap[scriptPtr]; + Type scriptType = _scriptTypeBiMap.GetScriptType(scriptPtr); GetPropertyInfoListForType(scriptType, scriptPtr, addPropInfoFunc); } catch (Exception e) @@ -684,7 +865,7 @@ namespace Godot.Bridge if (getGodotPropertiesMetadataMethod == null) return; - var properties = (System.Collections.Generic.List<PropertyInfo>) + var properties = (System.Collections.Generic.List<PropertyInfo>?) getGodotPropertiesMetadataMethod.Invoke(null, null); if (properties == null || properties.Count <= 0) @@ -774,7 +955,7 @@ namespace Godot.Bridge { try { - Type top = _scriptTypeMap[scriptPtr]; + Type? top = _scriptTypeBiMap.GetScriptType(scriptPtr); Type native = Object.InternalGetClassNativeBase(top); while (top != null && top != native) @@ -804,7 +985,7 @@ namespace Godot.Bridge if (getGodotPropertyDefaultValuesMethod == null) return; - var defaultValues = (System.Collections.Generic.Dictionary<StringName, object>) + var defaultValues = (System.Collections.Generic.Dictionary<StringName, object>?) getGodotPropertyDefaultValuesMethod.Invoke(null, null); if (defaultValues == null || defaultValues.Count <= 0) diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs new file mode 100644 index 0000000000..a58f6849ad --- /dev/null +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs @@ -0,0 +1,92 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Godot.Bridge; + +#nullable enable + +public static partial class ScriptManagerBridge +{ + private class ScriptTypeBiMap + { + public readonly object ReadWriteLock = new(); + private System.Collections.Generic.Dictionary<IntPtr, Type> _scriptTypeMap = new(); + private System.Collections.Generic.Dictionary<Type, IntPtr> _typeScriptMap = new(); + + public void Add(IntPtr scriptPtr, Type scriptType) + { + // TODO: What if this is called while unloading a load context, but after we already did cleanup in preparation for unloading? + + _scriptTypeMap.Add(scriptPtr, scriptType); + _typeScriptMap.Add(scriptType, scriptPtr); + + if (AlcReloadCfg.IsAlcReloadingEnabled) + { + AddTypeForAlcReloading(scriptType); + } + } + + public void Remove(IntPtr scriptPtr) + { + if (_scriptTypeMap.Remove(scriptPtr, out Type? scriptType)) + _ = _typeScriptMap.Remove(scriptType); + } + + public bool RemoveByScriptType(Type scriptType, out IntPtr scriptPtr) + { + if (_typeScriptMap.Remove(scriptType, out scriptPtr)) + return _scriptTypeMap.Remove(scriptPtr); + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Type GetScriptType(IntPtr scriptPtr) => _scriptTypeMap[scriptPtr]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetScriptType(IntPtr scriptPtr, [MaybeNullWhen(false)] out Type scriptType) => + _scriptTypeMap.TryGetValue(scriptPtr, out scriptType); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetScriptPtr(Type scriptType, out IntPtr scriptPtr) => + _typeScriptMap.TryGetValue(scriptType, out scriptPtr); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsScriptRegistered(IntPtr scriptPtr) => _scriptTypeMap.ContainsKey(scriptPtr); + } + + private class PathScriptTypeBiMap + { + private System.Collections.Generic.Dictionary<string, Type> _pathTypeMap = new(); + private System.Collections.Generic.Dictionary<Type, string> _typePathMap = new(); + + public void Add(string scriptPath, Type scriptType) + { + _pathTypeMap.Add(scriptPath, scriptType); + + // Due to partial classes, more than one file can point to the same type, so + // there could be duplicate keys in this case. We only add a type as key once. + _typePathMap.TryAdd(scriptType, scriptPath); + } + + public void RemoveByScriptType(Type scriptType) + { + foreach (var pair in _pathTypeMap + .Where(p => p.Value == scriptType).ToArray()) + { + _pathTypeMap.Remove(pair.Key); + } + + _typePathMap.Remove(scriptType); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetScriptType(string scriptPath, [MaybeNullWhen(false)] out Type scriptType) => + _pathTypeMap.TryGetValue(scriptPath, out scriptType); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetScriptPath(Type scriptType, [MaybeNullWhen(false)] out string scriptPath) => + _typePathMap.TryGetValue(scriptType, out scriptPath); + } +} diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/CustomGCHandle.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/CustomGCHandle.cs new file mode 100644 index 0000000000..42f19ace1a --- /dev/null +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/CustomGCHandle.cs @@ -0,0 +1,98 @@ +#nullable enable + +using System; +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Loader; +using Godot.Bridge; + +namespace Godot; + +/// <summary> +/// Provides a GCHandle that becomes weak when unloading the assembly load context, without having +/// to manually replace the GCHandle. This hides all the complexity of releasing strong GC handles +/// to allow the assembly load context to unload properly. +/// +/// Internally, a strong CustomGCHandle actually contains a weak GCHandle, while the actual strong +/// reference is stored in a static table. +/// </summary> +public static class CustomGCHandle +{ + // ConditionalWeakTable uses DependentHandle, so it stores weak references. + // Having the assembly load context as key won't prevent it from unloading. + private static ConditionalWeakTable<AssemblyLoadContext, object?> _alcsBeingUnloaded = new(); + + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool IsAlcBeingUnloaded(AssemblyLoadContext alc) => _alcsBeingUnloaded.TryGetValue(alc, out _); + + // ReSharper disable once RedundantNameQualifier + private static ConcurrentDictionary< + AssemblyLoadContext, + ConcurrentDictionary<GCHandle, object> + > _strongReferencesByAlc = new(); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void OnAlcUnloading(AssemblyLoadContext alc) + { + _alcsBeingUnloaded.Add(alc, null); + + if (_strongReferencesByAlc.TryRemove(alc, out var strongReferences)) + { + strongReferences.Clear(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GCHandle AllocStrong(object value) + => AllocStrong(value, value.GetType()); + + public static GCHandle AllocStrong(object value, Type valueType) + { + if (AlcReloadCfg.IsAlcReloadingEnabled) + { + var alc = AssemblyLoadContext.GetLoadContext(valueType.Assembly); + + if (alc != null) + { + var weakHandle = GCHandle.Alloc(value, GCHandleType.Weak); + + if (!IsAlcBeingUnloaded(alc)) + { + var strongReferences = _strongReferencesByAlc.GetOrAdd(alc, + static alc => + { + alc.Unloading += OnAlcUnloading; + return new(); + }); + strongReferences.TryAdd(weakHandle, value); + } + + return weakHandle; + } + } + + return GCHandle.Alloc(value, GCHandleType.Normal); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GCHandle AllocWeak(object value) => GCHandle.Alloc(value, GCHandleType.Weak); + + public static void Free(GCHandle handle) + { + if (AlcReloadCfg.IsAlcReloadingEnabled) + { + var target = handle.Target; + + if (target != null) + { + var alc = AssemblyLoadContext.GetLoadContext(target.GetType().Assembly); + + if (alc != null && _strongReferencesByAlc.TryGetValue(alc, out var strongReferences)) + _ = strongReferences.TryRemove(handle, out _); + } + } + + handle.Free(); + } +} diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs index fb5e3c6dda..48eec66182 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs @@ -1,6 +1,8 @@ +#nullable enable + using System; using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; @@ -16,14 +18,14 @@ namespace Godot { try { - var @delegateA = (Delegate)GCHandle.FromIntPtr(delegateGCHandleA).Target; - var @delegateB = (Delegate)GCHandle.FromIntPtr(delegateGCHandleB).Target; - return (@delegateA == @delegateB).ToGodotBool(); + var @delegateA = (Delegate?)GCHandle.FromIntPtr(delegateGCHandleA).Target; + var @delegateB = (Delegate?)GCHandle.FromIntPtr(delegateGCHandleB).Target; + return (@delegateA! == @delegateB!).ToGodotBool(); } catch (Exception e) { ExceptionUtils.LogException(e); - return false.ToGodotBool(); + return godot_bool.False; } } @@ -34,10 +36,10 @@ namespace Godot try { // TODO: Optimize - var @delegate = (Delegate)GCHandle.FromIntPtr(delegateGCHandle).Target; - var managedArgs = new object[argc]; + var @delegate = (Delegate)GCHandle.FromIntPtr(delegateGCHandle).Target!; + var managedArgs = new object?[argc]; - var parameterInfos = @delegate!.Method.GetParameters(); + var parameterInfos = @delegate.Method.GetParameters(); var paramsLength = parameterInfos.Length; if (argc != paramsLength) @@ -52,7 +54,7 @@ namespace Godot *args[i], parameterInfos[i].ParameterType); } - object invokeRet = @delegate.DynamicInvoke(managedArgs); + object? invokeRet = @delegate.DynamicInvoke(managedArgs); *outRet = Marshaling.ConvertManagedObjectToVariant(invokeRet); } @@ -72,10 +74,7 @@ namespace Godot CompilerGenerated } - internal static bool TrySerializeDelegateWithGCHandle(IntPtr delegateGCHandle, Collections.Array serializedData) - => TrySerializeDelegate((Delegate)GCHandle.FromIntPtr(delegateGCHandle).Target, serializedData); - - private static bool TrySerializeDelegate(Delegate @delegate, Collections.Array serializedData) + internal static bool TrySerializeDelegate(Delegate @delegate, Collections.Array serializedData) { if (@delegate is MulticastDelegate multicastDelegate) { @@ -98,7 +97,7 @@ namespace Godot } } - if (TrySerializeSingleDelegate(@delegate, out byte[] buffer)) + if (TrySerializeSingleDelegate(@delegate, out byte[]? buffer)) { serializedData.Add(buffer); return true; @@ -107,11 +106,11 @@ namespace Godot return false; } - private static bool TrySerializeSingleDelegate(Delegate @delegate, out byte[] buffer) + private static bool TrySerializeSingleDelegate(Delegate @delegate, [MaybeNullWhen(false)] out byte[] buffer) { buffer = null; - object target = @delegate.Target; + object? target = @delegate.Target; switch (target) { @@ -200,9 +199,6 @@ namespace Godot private static bool TrySerializeMethodInfo(BinaryWriter writer, MethodInfo methodInfo) { - if (methodInfo == null) - return false; - SerializeType(writer, methodInfo.DeclaringType); writer.Write(methodInfo.Name); @@ -241,7 +237,7 @@ namespace Godot return true; } - private static void SerializeType(BinaryWriter writer, Type type) + private static void SerializeType(BinaryWriter writer, Type? type) { if (type == null) { @@ -256,9 +252,8 @@ namespace Godot int genericArgumentsCount = genericArgs.Length; writer.Write(genericArgumentsCount); - string assemblyQualifiedName = genericTypeDef.AssemblyQualifiedName; - Debug.Assert(assemblyQualifiedName != null); - writer.Write(assemblyQualifiedName); + writer.Write(genericTypeDef.Assembly.GetName().Name ?? ""); + writer.Write(genericTypeDef.FullName ?? genericTypeDef.ToString()); for (int i = 0; i < genericArgs.Length; i++) SerializeType(writer, genericArgs[i]); @@ -268,21 +263,62 @@ namespace Godot int genericArgumentsCount = 0; writer.Write(genericArgumentsCount); - string assemblyQualifiedName = type.AssemblyQualifiedName; - Debug.Assert(assemblyQualifiedName != null); - writer.Write(assemblyQualifiedName); + writer.Write(type.Assembly.GetName().Name ?? ""); + writer.Write(type.FullName ?? type.ToString()); } } - private static bool TryDeserializeDelegateWithGCHandle(Collections.Array serializedData, - out IntPtr delegateGCHandle) + [UnmanagedCallersOnly] + internal static unsafe godot_bool TrySerializeDelegateWithGCHandle(IntPtr delegateGCHandle, + godot_array* nSerializedData) { - bool res = TryDeserializeDelegate(serializedData, out Delegate @delegate); - delegateGCHandle = GCHandle.ToIntPtr(GCHandle.Alloc(@delegate)); - return res; + try + { + var serializedData = Collections.Array.CreateTakingOwnershipOfDisposableValue( + NativeFuncs.godotsharp_array_new_copy(*nSerializedData)); + + var @delegate = (Delegate)GCHandle.FromIntPtr(delegateGCHandle).Target!; + + return TrySerializeDelegate(@delegate, serializedData) + .ToGodotBool(); + } + catch (Exception e) + { + ExceptionUtils.LogException(e); + return godot_bool.False; + } } - private static bool TryDeserializeDelegate(Collections.Array serializedData, out Delegate @delegate) + [UnmanagedCallersOnly] + internal static unsafe godot_bool TryDeserializeDelegateWithGCHandle(godot_array* nSerializedData, + IntPtr* delegateGCHandle) + { + try + { + var serializedData = Collections.Array.CreateTakingOwnershipOfDisposableValue( + NativeFuncs.godotsharp_array_new_copy(*nSerializedData)); + + if (TryDeserializeDelegate(serializedData, out Delegate? @delegate)) + { + *delegateGCHandle = GCHandle.ToIntPtr(CustomGCHandle.AllocStrong(@delegate)); + return godot_bool.True; + } + else + { + *delegateGCHandle = IntPtr.Zero; + return godot_bool.False; + } + } + catch (Exception e) + { + ExceptionUtils.LogException(e); + *delegateGCHandle = default; + return godot_bool.False; + } + } + + internal static bool TryDeserializeDelegate(Collections.Array serializedData, + [MaybeNullWhen(false)] out Delegate @delegate) { if (serializedData.Count == 1) { @@ -302,12 +338,12 @@ namespace Godot { if (elem is Collections.Array multiCastData) { - if (TryDeserializeDelegate(multiCastData, out Delegate oneDelegate)) + if (TryDeserializeDelegate(multiCastData, out Delegate? oneDelegate)) delegates.Add(oneDelegate); } else { - if (TryDeserializeSingleDelegate((byte[])elem, out Delegate oneDelegate)) + if (TryDeserializeSingleDelegate((byte[])elem, out Delegate? oneDelegate)) delegates.Add(oneDelegate); } } @@ -315,11 +351,11 @@ namespace Godot if (delegates.Count <= 0) return false; - @delegate = delegates.Count == 1 ? delegates[0] : Delegate.Combine(delegates.ToArray()); + @delegate = delegates.Count == 1 ? delegates[0] : Delegate.Combine(delegates.ToArray())!; return true; } - private static bool TryDeserializeSingleDelegate(byte[] buffer, out Delegate @delegate) + private static bool TryDeserializeSingleDelegate(byte[] buffer, [MaybeNullWhen(false)] out Delegate @delegate) { @delegate = null; @@ -332,14 +368,18 @@ namespace Godot { case TargetKind.Static: { - Type delegateType = DeserializeType(reader); + Type? delegateType = DeserializeType(reader); if (delegateType == null) return false; - if (!TryDeserializeMethodInfo(reader, out MethodInfo methodInfo)) + if (!TryDeserializeMethodInfo(reader, out MethodInfo? methodInfo)) + return false; + + @delegate = Delegate.CreateDelegate(delegateType, null, methodInfo, throwOnBindFailure: false); + + if (@delegate == null) return false; - @delegate = Delegate.CreateDelegate(delegateType, null, methodInfo); return true; } case TargetKind.GodotObject: @@ -350,32 +390,37 @@ namespace Godot if (godotObject == null) return false; - Type delegateType = DeserializeType(reader); + Type? delegateType = DeserializeType(reader); if (delegateType == null) return false; - if (!TryDeserializeMethodInfo(reader, out MethodInfo methodInfo)) + if (!TryDeserializeMethodInfo(reader, out MethodInfo? methodInfo)) + return false; + + @delegate = Delegate.CreateDelegate(delegateType, godotObject, methodInfo, + throwOnBindFailure: false); + + if (@delegate == null) return false; - @delegate = Delegate.CreateDelegate(delegateType, godotObject, methodInfo); return true; } case TargetKind.CompilerGenerated: { - Type targetType = DeserializeType(reader); + Type? targetType = DeserializeType(reader); if (targetType == null) return false; - Type delegateType = DeserializeType(reader); + Type? delegateType = DeserializeType(reader); if (delegateType == null) return false; - if (!TryDeserializeMethodInfo(reader, out MethodInfo methodInfo)) + if (!TryDeserializeMethodInfo(reader, out MethodInfo? methodInfo)) return false; int fieldCount = reader.ReadInt32(); - object recreatedTarget = Activator.CreateInstance(targetType); + object recreatedTarget = Activator.CreateInstance(targetType)!; for (int i = 0; i < fieldCount; i++) { @@ -383,12 +428,17 @@ namespace Godot int valueBufferLength = reader.ReadInt32(); byte[] valueBuffer = reader.ReadBytes(valueBufferLength); - FieldInfo fieldInfo = - targetType.GetField(name, BindingFlags.Instance | BindingFlags.Public); + FieldInfo? fieldInfo = targetType.GetField(name, + BindingFlags.Instance | BindingFlags.Public); fieldInfo?.SetValue(recreatedTarget, GD.Bytes2Var(valueBuffer)); } - @delegate = Delegate.CreateDelegate(delegateType, recreatedTarget, methodInfo); + @delegate = Delegate.CreateDelegate(delegateType, recreatedTarget, methodInfo, + throwOnBindFailure: false); + + if (@delegate == null) + return false; + return true; } default: @@ -397,18 +447,22 @@ namespace Godot } } - private static bool TryDeserializeMethodInfo(BinaryReader reader, out MethodInfo methodInfo) + private static bool TryDeserializeMethodInfo(BinaryReader reader, + [MaybeNullWhen(false)] out MethodInfo methodInfo) { methodInfo = null; - Type declaringType = DeserializeType(reader); + Type? declaringType = DeserializeType(reader); + + if (declaringType == null) + return false; string methodName = reader.ReadString(); int flags = reader.ReadInt32(); bool hasReturn = reader.ReadBoolean(); - Type returnType = hasReturn ? DeserializeType(reader) : typeof(void); + Type? returnType = hasReturn ? DeserializeType(reader) : typeof(void); int parametersCount = reader.ReadInt32(); @@ -418,7 +472,7 @@ namespace Godot for (int i = 0; i < parametersCount; i++) { - Type parameterType = DeserializeType(reader); + Type? parameterType = DeserializeType(reader); if (parameterType == null) return false; parameterTypes[i] = parameterType; @@ -432,15 +486,23 @@ namespace Godot return methodInfo != null && methodInfo.ReturnType == returnType; } - private static Type DeserializeType(BinaryReader reader) + private static Type? DeserializeType(BinaryReader reader) { int genericArgumentsCount = reader.ReadInt32(); if (genericArgumentsCount == -1) return null; - string assemblyQualifiedName = reader.ReadString(); - var type = Type.GetType(assemblyQualifiedName); + string assemblyName = reader.ReadString(); + + if (assemblyName.Length == 0) + { + GD.PushError($"Missing assembly name of type when attempting to deserialize delegate"); + return null; + } + + string typeFullName = reader.ReadString(); + var type = ReflectionUtils.FindTypeInLoadedAssemblies(assemblyName, typeFullName); if (type == null) return null; // Type not found @@ -451,7 +513,7 @@ namespace Godot for (int i = 0; i < genericArgumentsCount; i++) { - Type genericArgumentType = DeserializeType(reader); + Type? genericArgumentType = DeserializeType(reader); if (genericArgumentType == null) return null; genericArgumentTypes[i] = genericArgumentType; diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs index 2bea2f3b4f..2523728c8b 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs @@ -578,6 +578,24 @@ namespace Godot.Collections return found; } + // TODO: This is temporary. It's needed for the serialization generator. It won't be needed once we replace Sysme.Object with a Variant type. + internal bool TryGetValueAsType<TValueCustom>(TKey key, [MaybeNullWhen(false)] out TValueCustom value) + { + using godot_variant variantKey = Marshaling.ConvertManagedObjectToVariant(key); + var self = (godot_dictionary)_underlyingDict.NativeValue; + bool found = NativeFuncs.godotsharp_dictionary_try_get_value(ref self, + variantKey, out godot_variant retValue).ToBool(); + + using (retValue) + { + value = found ? + (TValueCustom)Marshaling.ConvertVariantToManagedObjectOfType(retValue, typeof(TValueCustom)) : + default; + } + + return found; + } + // ICollection<KeyValuePair<TKey, TValue>> /// <summary> diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/DisposablesTracker.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/DisposablesTracker.cs index 4e15b37e44..75793ea446 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/DisposablesTracker.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/DisposablesTracker.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using System.Runtime.InteropServices; -using System.Runtime.Loader; using Godot.NativeInterop; #nullable enable @@ -10,17 +9,12 @@ namespace Godot { internal static class DisposablesTracker { - static DisposablesTracker() - { - AssemblyLoadContext.Default.Unloading += _ => OnUnloading(); - } - [UnmanagedCallersOnly] internal static void OnGodotShuttingDown() { try { - OnUnloading(); + OnGodotShuttingDownImpl(); } catch (Exception e) { @@ -28,7 +22,7 @@ namespace Godot } } - private static void OnUnloading() + private static void OnGodotShuttingDownImpl() { bool isStdoutVerbose; @@ -66,30 +60,30 @@ namespace Godot } // ReSharper disable once RedundantNameQualifier - private static ConcurrentDictionary<WeakReference<Godot.Object>, object?> GodotObjectInstances { get; } = + private static ConcurrentDictionary<WeakReference<Godot.Object>, byte> GodotObjectInstances { get; } = new(); - private static ConcurrentDictionary<WeakReference<IDisposable>, object?> OtherInstances { get; } = + private static ConcurrentDictionary<WeakReference<IDisposable>, byte> OtherInstances { get; } = new(); public static WeakReference<Object> RegisterGodotObject(Object godotObject) { var weakReferenceToSelf = new WeakReference<Object>(godotObject); - GodotObjectInstances.TryAdd(weakReferenceToSelf, null); + GodotObjectInstances.TryAdd(weakReferenceToSelf, 0); return weakReferenceToSelf; } public static WeakReference<IDisposable> RegisterDisposable(IDisposable disposable) { var weakReferenceToSelf = new WeakReference<IDisposable>(disposable); - OtherInstances.TryAdd(weakReferenceToSelf, null); + OtherInstances.TryAdd(weakReferenceToSelf, 0); return weakReferenceToSelf; } - public static void UnregisterGodotObject(WeakReference<Object> weakReference) + public static void UnregisterGodotObject(Object godotObject, WeakReference<Object> weakReferenceToSelf) { - if (!GodotObjectInstances.TryRemove(weakReference, out _)) - throw new ArgumentException("Godot Object not registered", nameof(weakReference)); + if (!GodotObjectInstances.TryRemove(weakReferenceToSelf, out _)) + throw new ArgumentException("Godot Object not registered", nameof(weakReferenceToSelf)); } public static void UnregisterDisposable(WeakReference<IDisposable> weakReference) diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/ExceptionUtils.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/ExceptionUtils.cs index 7a2f205632..5a0ea2ba13 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/ExceptionUtils.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/ExceptionUtils.cs @@ -95,7 +95,7 @@ namespace Godot.NativeInterop } NativeFuncs.godotsharp_internal_script_debugger_send_error(nFunc, nFile, line, - nErrorMsg, nExcMsg, p_warning: false.ToGodotBool(), stackInfoVector); + nErrorMsg, nExcMsg, p_warning: godot_bool.False, stackInfoVector); } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropUtils.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropUtils.cs index f6f0186016..82f1c04d40 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropUtils.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropUtils.cs @@ -50,7 +50,9 @@ namespace Godot.NativeInterop public static void TieManagedToUnmanaged(Object managed, IntPtr unmanaged, StringName nativeName, bool refCounted, Type type, Type nativeType) { - var gcHandle = GCHandle.Alloc(managed, refCounted ? GCHandleType.Weak : GCHandleType.Normal); + var gcHandle = refCounted ? + CustomGCHandle.AllocWeak(managed) : + CustomGCHandle.AllocStrong(managed, type); if (type == nativeType) { @@ -65,7 +67,7 @@ namespace Godot.NativeInterop // We don't dispose `script` ourselves here. // `tie_user_managed_to_unmanaged` does it for us to avoid another P/Invoke call. godot_ref script; - ScriptManagerBridge.GetOrCreateScriptBridgeForType(type, &script); + ScriptManagerBridge.GetOrLoadOrCreateScriptForType(type, &script); // IMPORTANT: This must be called after GetOrCreateScriptBridgeForType NativeFuncs.godotsharp_internal_tie_user_managed_to_unmanaged( @@ -80,7 +82,7 @@ namespace Godot.NativeInterop if (type == nativeType) return; - var strongGCHandle = GCHandle.Alloc(managed, GCHandleType.Normal); + var strongGCHandle = CustomGCHandle.AllocStrong(managed); NativeFuncs.godotsharp_internal_tie_managed_to_unmanaged_with_pre_setup( GCHandle.ToIntPtr(strongGCHandle), unmanaged); } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs index fc11f56680..1a0d9946d2 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs @@ -577,7 +577,29 @@ namespace Godot.NativeInterop { if (typeof(Godot.Object).IsAssignableFrom(type)) { - var godotObject = VariantUtils.ConvertToGodotObject(p_var); + if (p_var.Type == Variant.Type.Nil) + { + res = null; + return true; + } + + if (p_var.Type != Variant.Type.Object) + { + GD.PushError("Invalid cast when marshaling Godot.Object type." + + $" Variant type is `{p_var.Type}`; expected `{p_var.Object}`."); + res = null; + return true; + } + + var godotObjectPtr = VariantUtils.ConvertToGodotObjectPtr(p_var); + + if (godotObjectPtr == IntPtr.Zero) + { + res = null; + return true; + } + + var godotObject = InteropUtils.UnmanagedGetManaged(godotObjectPtr); if (!type.IsInstanceOfType(godotObject)) { @@ -864,9 +886,9 @@ namespace Godot.NativeInterop { if (p_managed_callable.Delegate != null) { + var gcHandle = CustomGCHandle.AllocStrong(p_managed_callable.Delegate); NativeFuncs.godotsharp_callable_new_with_delegate( - GCHandle.ToIntPtr(GCHandle.Alloc(p_managed_callable.Delegate)), - out godot_callable callable); + GCHandle.ToIntPtr(gcHandle), out godot_callable callable); return callable; } else diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs index d7c57fa260..1ab2b4c0bf 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs @@ -95,7 +95,10 @@ namespace Godot.NativeInterop IntPtr oldGCHandlePtr); [DllImport(GodotDllName)] - internal static extern void godotsharp_internal_new_csharp_script(godot_ref* r_script); + internal static extern void godotsharp_internal_new_csharp_script(godot_ref* r_dest); + + [DllImport(GodotDllName)] + internal static extern godot_bool godotsharp_internal_script_load(in godot_string p_path, godot_ref* r_dest); [DllImport(GodotDllName)] internal static extern void godotsharp_internal_reload_registered_script(IntPtr scriptPtr); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs index 71a620716f..8d683c6d1e 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; +using Godot.Bridge; using Godot.NativeInterop; namespace Godot @@ -150,7 +151,7 @@ namespace Godot NativePtr = IntPtr.Zero; } - DisposablesTracker.UnregisterGodotObject(_weakReferenceToSelf); + DisposablesTracker.UnregisterGodotObject(this, _weakReferenceToSelf); } /// <summary> @@ -328,5 +329,77 @@ namespace Godot return nativeConstructor; } + + protected internal virtual void SaveGodotObjectData(GodotSerializationInfo info) + { + // Temporary solution via reflection until we add a signals events source generator + + Type top = GetType(); + Type native = InternalGetClassNativeBase(top); + + while (top != null && top != native) + { + var foundEventSignals = top.GetEvents( + BindingFlags.DeclaredOnly | BindingFlags.Instance | + BindingFlags.NonPublic | BindingFlags.Public) + .Where(ev => ev.GetCustomAttributes().OfType<SignalAttribute>().Any()) + .Select(ev => ev.Name); + + var fields = top.GetFields( + BindingFlags.DeclaredOnly | BindingFlags.Instance | + BindingFlags.NonPublic | BindingFlags.Public); + + foreach (var eventSignalField in fields + .Where(f => typeof(Delegate).IsAssignableFrom(f.FieldType)) + .Where(f => foundEventSignals.Contains(f.Name))) + { + var eventSignalDelegate = (Delegate)eventSignalField.GetValue(this); + info.AddSignalEventDelegate(eventSignalField.Name, eventSignalDelegate); + } + + top = top.BaseType; + } + } + + // TODO: Should this be a constructor overload? + protected internal virtual void RestoreGodotObjectData(GodotSerializationInfo info) + { + // Temporary solution via reflection until we add a signals events source generator + + void RestoreSignalEvent(StringName signalEventName) + { + Type top = GetType(); + Type native = InternalGetClassNativeBase(top); + + while (top != null && top != native) + { + var foundEventSignal = top.GetEvent(signalEventName, + BindingFlags.DeclaredOnly | BindingFlags.Instance | + BindingFlags.NonPublic | BindingFlags.Public); + + if (foundEventSignal != null && + foundEventSignal.GetCustomAttributes().OfType<SignalAttribute>().Any()) + { + var field = top.GetField(foundEventSignal.Name, + BindingFlags.DeclaredOnly | BindingFlags.Instance | + BindingFlags.NonPublic | BindingFlags.Public); + + if (field != null && typeof(Delegate).IsAssignableFrom(field.FieldType)) + { + var eventSignalDelegate = info.GetSignalEventDelegate(signalEventName); + field.SetValue(this, eventSignalDelegate); + return; + } + } + + top = top.BaseType; + } + } + + foreach (var signalEventName in info.GetSignalEventsList()) + { + RestoreSignalEvent(signalEventName); + } + } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/ReflectionUtils.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/ReflectionUtils.cs new file mode 100644 index 0000000000..ee605f8d8f --- /dev/null +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/ReflectionUtils.cs @@ -0,0 +1,16 @@ +using System; +using System.Linq; + +#nullable enable + +namespace Godot; + +internal class ReflectionUtils +{ + public static Type? FindTypeInLoadedAssemblies(string assemblyName, string typeFullName) + { + return AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name == assemblyName)? + .GetType(typeFullName); + } +} diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/SignalAwaiter.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/SignalAwaiter.cs index fb72d706c7..8ba3c403fa 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/SignalAwaiter.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/SignalAwaiter.cs @@ -12,10 +12,11 @@ namespace Godot public SignalAwaiter(Object source, StringName signal, Object target) { + var awaiterGcHandle = CustomGCHandle.AllocStrong(this); using godot_string_name signalSrc = NativeFuncs.godotsharp_string_name_new_copy( (godot_string_name)(signal?.NativeValue ?? default)); NativeFuncs.godotsharp_internal_signal_awaiter_connect(Object.GetPtr(source), in signalSrc, - Object.GetPtr(target), GCHandle.ToIntPtr(GCHandle.Alloc(this))); + Object.GetPtr(target), GCHandle.ToIntPtr(awaiterGcHandle)); } public bool IsCompleted => _completed; @@ -39,11 +40,11 @@ namespace Godot if (awaiter == null) { - *outAwaiterIsNull = true.ToGodotBool(); + *outAwaiterIsNull = godot_bool.True; return; } - *outAwaiterIsNull = false.ToGodotBool(); + *outAwaiterIsNull = godot_bool.False; awaiter._completed = true; @@ -59,7 +60,7 @@ namespace Godot catch (Exception e) { ExceptionUtils.LogException(e); - *outAwaiterIsNull = false.ToGodotBool(); + *outAwaiterIsNull = godot_bool.False; } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj index 2e121bb789..d0897fe85e 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj @@ -49,6 +49,8 @@ <!-- Sources --> <ItemGroup> <Compile Include="Core\AABB.cs" /> + <Compile Include="Core\Bridge\GodotSerializationInfo.cs" /> + <Compile Include="Core\CustomGCHandle.cs" /> <Compile Include="Core\Array.cs" /> <Compile Include="Core\Attributes\AssemblyHasScriptsAttribute.cs" /> <Compile Include="Core\Attributes\ExportAttribute.cs" /> @@ -59,9 +61,11 @@ <Compile Include="Core\Basis.cs" /> <Compile Include="Core\Bridge\CSharpInstanceBridge.cs" /> <Compile Include="Core\Bridge\GCHandleBridge.cs" /> + <Compile Include="Core\Bridge\AlcReloadCfg.cs" /> <Compile Include="Core\Bridge\ManagedCallbacks.cs" /> <Compile Include="Core\Bridge\PropertyInfo.cs" /> <Compile Include="Core\Bridge\ScriptManagerBridge.cs" /> + <Compile Include="Core\Bridge\ScriptManagerBridge.types.cs" /> <Compile Include="Core\Callable.cs" /> <Compile Include="Core\Color.cs" /> <Compile Include="Core\Colors.cs" /> @@ -100,6 +104,7 @@ <Compile Include="Core\Quaternion.cs" /> <Compile Include="Core\Rect2.cs" /> <Compile Include="Core\Rect2i.cs" /> + <Compile Include="Core\ReflectionUtils.cs" /> <Compile Include="Core\RID.cs" /> <Compile Include="Core\NativeInterop\NativeFuncs.cs" /> <Compile Include="Core\NativeInterop\InteropStructs.cs" /> diff --git a/modules/mono/glue/runtime_interop.cpp b/modules/mono/glue/runtime_interop.cpp index df72413fcc..637db00706 100644 --- a/modules/mono/glue/runtime_interop.cpp +++ b/modules/mono/glue/runtime_interop.cpp @@ -320,6 +320,17 @@ GD_PINVOKE_EXPORT void godotsharp_internal_new_csharp_script(Ref<CSharpScript> * memnew_placement(r_dest, Ref<CSharpScript>(memnew(CSharpScript))); } +GD_PINVOKE_EXPORT bool godotsharp_internal_script_load(const String *p_path, Ref<CSharpScript> *r_dest) { + Ref<Resource> res = ResourceLoader::load(*p_path); + if (res.is_valid()) { + memnew_placement(r_dest, Ref<CSharpScript>(res)); + return true; + } else { + memnew_placement(r_dest, Ref<CSharpScript>()); + return false; + } +} + GD_PINVOKE_EXPORT void godotsharp_internal_reload_registered_script(CSharpScript *p_script) { CRASH_COND(!p_script); CSharpScript::reload_registered_script(Ref<CSharpScript>(p_script)); @@ -1311,7 +1322,7 @@ GD_PINVOKE_EXPORT void godotsharp_object_to_string(Object *p_ptr, godot_string * #endif // We need this to prevent the functions from being stripped. -void *godotsharp_pinvoke_funcs[185] = { +void *godotsharp_pinvoke_funcs[186] = { (void *)godotsharp_method_bind_get_method, (void *)godotsharp_get_class_constructor, (void *)godotsharp_engine_get_singleton, @@ -1331,6 +1342,7 @@ void *godotsharp_pinvoke_funcs[185] = { (void *)godotsharp_internal_tie_user_managed_to_unmanaged, (void *)godotsharp_internal_tie_managed_to_unmanaged_with_pre_setup, (void *)godotsharp_internal_new_csharp_script, + (void *)godotsharp_internal_script_load, (void *)godotsharp_internal_reload_registered_script, (void *)godotsharp_array_filter_godot_objects_by_native, (void *)godotsharp_array_filter_godot_objects_by_non_native, diff --git a/modules/mono/managed_callable.cpp b/modules/mono/managed_callable.cpp index 334de14a6b..9305dc645a 100644 --- a/modules/mono/managed_callable.cpp +++ b/modules/mono/managed_callable.cpp @@ -87,6 +87,8 @@ void ManagedCallable::call(const Variant **p_arguments, int p_argcount, Variant r_call_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD; // Can't find anything better r_return_value = Variant(); + ERR_FAIL_COND(delegate_handle.value == nullptr); + GDMonoCache::managed_callbacks.DelegateUtils_InvokeWithVariantArgs( delegate_handle, p_arguments, p_argcount, &r_return_value); diff --git a/modules/mono/mono_gd/gd_mono.cpp b/modules/mono/mono_gd/gd_mono.cpp index d57ad2831a..f5e38c2e61 100644 --- a/modules/mono/mono_gd/gd_mono.cpp +++ b/modules/mono/mono_gd/gd_mono.cpp @@ -501,106 +501,48 @@ void GDMono::_init_godot_api_hashes() { #ifdef TOOLS_ENABLED bool GDMono::_load_project_assembly() { - String appname = ProjectSettings::get_singleton()->get("application/config/name"); - String appname_safe = OS::get_singleton()->get_safe_dir_name(appname); - if (appname_safe.is_empty()) { - appname_safe = "UnnamedProject"; - } + String appname_safe = ProjectSettings::get_singleton()->get_safe_project_name(); String assembly_path = GodotSharpDirs::get_res_temp_assemblies_dir() .plus_file(appname_safe + ".dll"); assembly_path = ProjectSettings::get_singleton()->globalize_path(assembly_path); - return plugin_callbacks.LoadProjectAssemblyCallback(assembly_path.utf16()); -} -#endif - -#warning TODO hot-reload -#if 0 -Error GDMono::_unload_scripts_domain() { - ERR_FAIL_NULL_V(scripts_domain, ERR_BUG); - - CSharpLanguage::get_singleton()->_on_scripts_domain_about_to_unload(); - - print_verbose("Mono: Finalizing scripts domain..."); - - if (mono_domain_get() != root_domain) { - mono_domain_set(root_domain, true); - } - - finalizing_scripts_domain = true; + String loaded_assembly_path; + bool success = plugin_callbacks.LoadProjectAssemblyCallback(assembly_path.utf16(), &loaded_assembly_path); - if (!mono_domain_finalize(scripts_domain, 2000)) { - ERR_PRINT("Mono: Domain finalization timeout."); + if (success) { + project_assembly_path = loaded_assembly_path.simplify_path(); + project_assembly_modified_time = FileAccess::get_modified_time(loaded_assembly_path); } - finalizing_scripts_domain = false; - - mono_gc_collect(mono_gc_max_generation()); - - core_api_assembly = nullptr; -#ifdef TOOLS_ENABLED - editor_api_assembly = nullptr; -#endif - - project_assembly = nullptr; -#ifdef TOOLS_ENABLED - tools_assembly = nullptr; -#endif - - MonoDomain *domain = scripts_domain; - scripts_domain = nullptr; - - print_verbose("Mono: Unloading scripts domain..."); - - MonoException *exc = nullptr; - mono_domain_try_unload(domain, (MonoObject **)&exc); - - if (exc) { - ERR_PRINT("Exception thrown when unloading scripts domain."); - GDMonoUtils::debug_unhandled_exception(exc); - return FAILED; - } - - return OK; + return success; } +#endif #ifdef GD_MONO_HOT_RELOAD -Error GDMono::reload_scripts_domain() { +Error GDMono::reload_project_assemblies() { ERR_FAIL_COND_V(!runtime_initialized, ERR_BUG); - if (scripts_domain) { - Error domain_unload_err = _unload_scripts_domain(); - ERR_FAIL_COND_V_MSG(domain_unload_err != OK, domain_unload_err, "Mono: Failed to unload scripts domain."); - } - - Error domain_load_err = _load_scripts_domain(); - ERR_FAIL_COND_V_MSG(domain_load_err != OK, domain_load_err, "Mono: Failed to load scripts domain."); + finalizing_scripts_domain = true; - // Load assemblies. The API and tools assemblies are required, - // the application is aborted if these assemblies cannot be loaded. + CSharpLanguage::get_singleton()->_on_scripts_domain_about_to_unload(); - if (!_try_load_api_assemblies()) { - CRASH_NOW_MSG("Failed to load one of the API assemblies."); + if (!get_plugin_callbacks().UnloadProjectPluginCallback()) { + ERR_FAIL_V_MSG(Error::FAILED, ".NET: Failed to unload assemblies."); } -#if defined(TOOLS_ENABLED) - bool tools_assemblies_loaded = _load_tools_assemblies(); - CRASH_COND_MSG(!tools_assemblies_loaded, "Mono: Failed to load '" TOOLS_ASM_NAME "' assemblies."); -#endif + finalizing_scripts_domain = false; // Load the project's main assembly. Here, during hot-reloading, we do // consider failing to load the project's main assembly to be an error. - // However, unlike the API and tools assemblies, the application can continue working. if (!_load_project_assembly()) { - print_error("Mono: Failed to load project assembly"); + print_error(".NET: Failed to load project assembly."); return ERR_CANT_OPEN; } return OK; } #endif -#endif GDMono::GDMono() { singleton = this; diff --git a/modules/mono/mono_gd/gd_mono.h b/modules/mono/mono_gd/gd_mono.h index 301782575c..776399a544 100644 --- a/modules/mono/mono_gd/gd_mono.h +++ b/modules/mono/mono_gd/gd_mono.h @@ -45,10 +45,12 @@ namespace gdmono { #ifdef TOOLS_ENABLED struct PluginCallbacks { - using FuncLoadProjectAssemblyCallback = bool(GD_CLR_STDCALL *)(const char16_t *); + using FuncLoadProjectAssemblyCallback = bool(GD_CLR_STDCALL *)(const char16_t *, String *); using FuncLoadToolsAssemblyCallback = Object *(GD_CLR_STDCALL *)(const char16_t *); + using FuncUnloadProjectPluginCallback = bool(GD_CLR_STDCALL *)(); FuncLoadProjectAssemblyCallback LoadProjectAssemblyCallback = nullptr; FuncLoadToolsAssemblyCallback LoadToolsAssemblyCallback = nullptr; + FuncUnloadProjectPluginCallback UnloadProjectPluginCallback = nullptr; }; #endif @@ -63,14 +65,13 @@ class GDMono { void *hostfxr_dll_handle = nullptr; bool is_native_aot = false; + String project_assembly_path; + uint64_t project_assembly_modified_time = 0; + #ifdef TOOLS_ENABLED bool _load_project_assembly(); #endif - bool _try_load_api_assemblies(); - - Error _unload_scripts_domain(); - uint64_t api_core_hash; #ifdef TOOLS_ENABLED uint64_t api_editor_hash; @@ -114,17 +115,32 @@ public: #endif } - static GDMono *get_singleton() { return singleton; } + static GDMono *get_singleton() { + return singleton; + } + + _FORCE_INLINE_ bool is_runtime_initialized() const { + return runtime_initialized; + } + _FORCE_INLINE_ bool is_finalizing_scripts_domain() { + return finalizing_scripts_domain; + } - _FORCE_INLINE_ bool is_runtime_initialized() const { return runtime_initialized; } - _FORCE_INLINE_ bool is_finalizing_scripts_domain() { return finalizing_scripts_domain; } + _FORCE_INLINE_ const String &get_project_assembly_path() const { + return project_assembly_path; + } + _FORCE_INLINE_ uint64_t get_project_assembly_modified_time() const { + return project_assembly_modified_time; + } #ifdef TOOLS_ENABLED - const gdmono::PluginCallbacks &get_plugin_callbacks() { return plugin_callbacks; } + const gdmono::PluginCallbacks &get_plugin_callbacks() { + return plugin_callbacks; + } #endif #ifdef GD_MONO_HOT_RELOAD - Error reload_scripts_domain(); + Error reload_project_assemblies(); #endif void initialize(); diff --git a/modules/mono/mono_gd/gd_mono_cache.cpp b/modules/mono/mono_gd/gd_mono_cache.cpp index fc47a0e09b..37433c59ee 100644 --- a/modules/mono/mono_gd/gd_mono_cache.cpp +++ b/modules/mono/mono_gd/gd_mono_cache.cpp @@ -52,6 +52,8 @@ void update_godot_api_cache(const ManagedCallbacks &p_managed_callbacks) { CHECK_CALLBACK_NOT_NULL(SignalAwaiter, SignalCallback); CHECK_CALLBACK_NOT_NULL(DelegateUtils, InvokeWithVariantArgs); CHECK_CALLBACK_NOT_NULL(DelegateUtils, DelegateEquals); + CHECK_CALLBACK_NOT_NULL(DelegateUtils, TrySerializeDelegateWithGCHandle); + CHECK_CALLBACK_NOT_NULL(DelegateUtils, TryDeserializeDelegateWithGCHandle); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, FrameCallback); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, CreateManagedForGodotObjectBinding); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, CreateManagedForGodotObjectScriptInstance); @@ -64,6 +66,7 @@ void update_godot_api_cache(const ManagedCallbacks &p_managed_callbacks) { CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, AddScriptBridge); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, GetOrCreateScriptBridgeForPath); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, RemoveScriptBridge); + CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, TryReloadRegisteredScriptWithClass); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, UpdateScriptClassInfo); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, SwapGCHandleForType); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, GetPropertyInfoList); @@ -74,6 +77,8 @@ void update_godot_api_cache(const ManagedCallbacks &p_managed_callbacks) { CHECK_CALLBACK_NOT_NULL(CSharpInstanceBridge, CallDispose); CHECK_CALLBACK_NOT_NULL(CSharpInstanceBridge, CallToString); CHECK_CALLBACK_NOT_NULL(CSharpInstanceBridge, HasMethodUnknownParams); + CHECK_CALLBACK_NOT_NULL(CSharpInstanceBridge, SerializeState); + CHECK_CALLBACK_NOT_NULL(CSharpInstanceBridge, DeserializeState); CHECK_CALLBACK_NOT_NULL(GCHandleBridge, FreeGCHandle); CHECK_CALLBACK_NOT_NULL(DebuggingUtils, GetCurrentStackInfo); CHECK_CALLBACK_NOT_NULL(DisposablesTracker, OnGodotShuttingDown); diff --git a/modules/mono/mono_gd/gd_mono_cache.h b/modules/mono/mono_gd/gd_mono_cache.h index 763a7f3e5e..92778cc2c9 100644 --- a/modules/mono/mono_gd/gd_mono_cache.h +++ b/modules/mono/mono_gd/gd_mono_cache.h @@ -74,6 +74,8 @@ struct ManagedCallbacks { using FuncSignalAwaiter_SignalCallback = void(GD_CLR_STDCALL *)(GCHandleIntPtr, const Variant **, int32_t, bool *); using FuncDelegateUtils_InvokeWithVariantArgs = void(GD_CLR_STDCALL *)(GCHandleIntPtr, const Variant **, uint32_t, const Variant *); using FuncDelegateUtils_DelegateEquals = bool(GD_CLR_STDCALL *)(GCHandleIntPtr, GCHandleIntPtr); + using FuncDelegateUtils_TrySerializeDelegateWithGCHandle = bool(GD_CLR_STDCALL *)(GCHandleIntPtr, const Array *); + using FuncDelegateUtils_TryDeserializeDelegateWithGCHandle = bool(GD_CLR_STDCALL *)(const Array *, GCHandleIntPtr *); using FuncScriptManagerBridge_FrameCallback = void(GD_CLR_STDCALL *)(); using FuncScriptManagerBridge_CreateManagedForGodotObjectBinding = GCHandleIntPtr(GD_CLR_STDCALL *)(const StringName *, Object *); using FuncScriptManagerBridge_CreateManagedForGodotObjectScriptInstance = bool(GD_CLR_STDCALL *)(const CSharpScript *, Object *, const Variant **, int32_t); @@ -86,7 +88,8 @@ struct ManagedCallbacks { using FuncScriptManagerBridge_AddScriptBridge = bool(GD_CLR_STDCALL *)(const CSharpScript *, const String *); using FuncScriptManagerBridge_GetOrCreateScriptBridgeForPath = void(GD_CLR_STDCALL *)(const String *, Ref<CSharpScript> *); using FuncScriptManagerBridge_RemoveScriptBridge = void(GD_CLR_STDCALL *)(const CSharpScript *); - using FuncScriptManagerBridge_UpdateScriptClassInfo = void(GD_CLR_STDCALL *)(const CSharpScript *, bool *, Dictionary *); + using FuncScriptManagerBridge_TryReloadRegisteredScriptWithClass = bool(GD_CLR_STDCALL *)(const CSharpScript *); + using FuncScriptManagerBridge_UpdateScriptClassInfo = void(GD_CLR_STDCALL *)(const CSharpScript *, bool *, Dictionary *, Ref<CSharpScript> *); using FuncScriptManagerBridge_SwapGCHandleForType = bool(GD_CLR_STDCALL *)(GCHandleIntPtr, GCHandleIntPtr *, bool); using FuncScriptManagerBridge_GetPropertyInfoList = void(GD_CLR_STDCALL *)(CSharpScript *, Callback_ScriptManagerBridge_GetPropertyInfoList_Add); using FuncScriptManagerBridge_GetPropertyDefaultValues = void(GD_CLR_STDCALL *)(CSharpScript *, Callback_ScriptManagerBridge_GetPropertyDefaultValues_Add); @@ -96,6 +99,8 @@ struct ManagedCallbacks { using FuncCSharpInstanceBridge_CallDispose = void(GD_CLR_STDCALL *)(GCHandleIntPtr, bool); using FuncCSharpInstanceBridge_CallToString = void(GD_CLR_STDCALL *)(GCHandleIntPtr, String *, bool *); using FuncCSharpInstanceBridge_HasMethodUnknownParams = bool(GD_CLR_STDCALL *)(GCHandleIntPtr, const StringName *); + using FuncCSharpInstanceBridge_SerializeState = void(GD_CLR_STDCALL *)(GCHandleIntPtr, const Dictionary *, const Dictionary *); + using FuncCSharpInstanceBridge_DeserializeState = void(GD_CLR_STDCALL *)(GCHandleIntPtr, const Dictionary *, const Dictionary *); using FuncGCHandleBridge_FreeGCHandle = void(GD_CLR_STDCALL *)(GCHandleIntPtr); using FuncDebuggingUtils_GetCurrentStackInfo = void(GD_CLR_STDCALL *)(Vector<ScriptLanguage::StackInfo> *); using FuncDisposablesTracker_OnGodotShuttingDown = void(GD_CLR_STDCALL *)(); @@ -104,6 +109,8 @@ struct ManagedCallbacks { FuncSignalAwaiter_SignalCallback SignalAwaiter_SignalCallback; FuncDelegateUtils_InvokeWithVariantArgs DelegateUtils_InvokeWithVariantArgs; FuncDelegateUtils_DelegateEquals DelegateUtils_DelegateEquals; + FuncDelegateUtils_TrySerializeDelegateWithGCHandle DelegateUtils_TrySerializeDelegateWithGCHandle; + FuncDelegateUtils_TryDeserializeDelegateWithGCHandle DelegateUtils_TryDeserializeDelegateWithGCHandle; FuncScriptManagerBridge_FrameCallback ScriptManagerBridge_FrameCallback; FuncScriptManagerBridge_CreateManagedForGodotObjectBinding ScriptManagerBridge_CreateManagedForGodotObjectBinding; FuncScriptManagerBridge_CreateManagedForGodotObjectScriptInstance ScriptManagerBridge_CreateManagedForGodotObjectScriptInstance; @@ -116,6 +123,7 @@ struct ManagedCallbacks { FuncScriptManagerBridge_AddScriptBridge ScriptManagerBridge_AddScriptBridge; FuncScriptManagerBridge_GetOrCreateScriptBridgeForPath ScriptManagerBridge_GetOrCreateScriptBridgeForPath; FuncScriptManagerBridge_RemoveScriptBridge ScriptManagerBridge_RemoveScriptBridge; + FuncScriptManagerBridge_TryReloadRegisteredScriptWithClass ScriptManagerBridge_TryReloadRegisteredScriptWithClass; FuncScriptManagerBridge_UpdateScriptClassInfo ScriptManagerBridge_UpdateScriptClassInfo; FuncScriptManagerBridge_SwapGCHandleForType ScriptManagerBridge_SwapGCHandleForType; FuncScriptManagerBridge_GetPropertyInfoList ScriptManagerBridge_GetPropertyInfoList; @@ -126,6 +134,8 @@ struct ManagedCallbacks { FuncCSharpInstanceBridge_CallDispose CSharpInstanceBridge_CallDispose; FuncCSharpInstanceBridge_CallToString CSharpInstanceBridge_CallToString; FuncCSharpInstanceBridge_HasMethodUnknownParams CSharpInstanceBridge_HasMethodUnknownParams; + FuncCSharpInstanceBridge_SerializeState CSharpInstanceBridge_SerializeState; + FuncCSharpInstanceBridge_DeserializeState CSharpInstanceBridge_DeserializeState; FuncGCHandleBridge_FreeGCHandle GCHandleBridge_FreeGCHandle; FuncDebuggingUtils_GetCurrentStackInfo DebuggingUtils_GetCurrentStackInfo; FuncDisposablesTracker_OnGodotShuttingDown DisposablesTracker_OnGodotShuttingDown; @@ -137,9 +147,6 @@ extern bool godot_api_cache_updated; void update_godot_api_cache(const ManagedCallbacks &p_managed_callbacks); -inline void clear_godot_api_cache() { - managed_callbacks = ManagedCallbacks(); -} } // namespace GDMonoCache #undef GD_CLR_STDCALL |