1
0
mirror of https://github.com/godotengine/godot.git synced 2025-11-05 12:10:55 +00:00

C#: Re-implement assembly reloading with ALCs

This commit is contained in:
Ignacio Roldán Etcheverry
2022-05-28 04:56:46 +02:00
parent d78e0a8426
commit e235cef09f
37 changed files with 1534 additions and 562 deletions

View File

@@ -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 assembly_path = gdmono->get_project_assembly_path();
String appname_safe = ProjectSettings::get_singleton()->get_safe_project_name();
appname_safe += ".dll";
if (proj_assembly) {
String proj_asm_path = proj_assembly->get_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();
ERR_CONTINUE(managed_callable->delegate_handle.value == nullptr);
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_TrySerializeDelegateWithGCHandle
.invoke(managed_callable->delegate_handle,
managed_serialized_data, &exc);
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();
bool success = GDMonoCache::managed_callbacks.ScriptManagerBridge_TryReloadRegisteredScriptWithClass(script.ptr());
// 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);
#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
if (!success) {
// Couldn't reload
script->pending_reload_instances.clear();
script->pending_reload_state.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
script->pending_reload_instances.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;
Dictionary properties;
HashMap<StringName, GDMonoField *>::Iterator match = script->event_signals.find(name);
if (!match) {
// The event or its signal attribute were removed
continue;
}
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);
const CSharpScript *top = this;
while (top != nullptr) {
List<PropertyInfo> props;
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
for (const PropertyInfo &prop : props) {
r_list->push_back(prop);
}
}
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");
}