diff --git a/modules/mono/SdkPackageVersions.props b/modules/mono/SdkPackageVersions.props index 2bd60bb7195..9947335b2ff 100644 --- a/modules/mono/SdkPackageVersions.props +++ b/modules/mono/SdkPackageVersions.props @@ -2,6 +2,6 @@ 4.0.*-* 4.0.0-dev7 - 4.0.0-dev6 + 4.0.0-dev7 diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp index ade436d3e08..99bd3edb9e3 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[178]; +extern void *godotsharp_pinvoke_funcs[179]; [[maybe_unused]] volatile void **do_not_strip_godotsharp_pinvoke_funcs; #ifdef TOOLS_ENABLED extern void *godotsharp_editor_pinvoke_funcs[30]; @@ -1263,17 +1263,24 @@ void CSharpLanguage::release_script_gchandle(MonoGCHandleData &p_gchandle) { } } -void CSharpLanguage::release_script_gchandle(void *p_expected_mono_obj_unused, MonoGCHandleData &p_gchandle) { -#warning KNOWN BUG. DO NOT USE THIS IN PRODUCTION - // KNOWN BUG: - // I removed the patch from commit e558e1ec09aa27852426bbd24dfa21e9b60cfbfc. - // This may cause data races. Re-implementing it without the Mono embedding API would be - // too painful and would make the code even more of a mess than it already was. - // We will switch from scripts to the new extension system before a release with .NET 6 support. - // The problem the old patch was working around won't be present at all with the new extension system. +void CSharpLanguage::release_script_gchandle_thread_safe(GCHandleIntPtr p_gchandle_to_free, MonoGCHandleData &r_gchandle) { + if (!r_gchandle.is_released() && r_gchandle.get_intptr() == p_gchandle_to_free) { // Do not lock unnecessarily + MutexLock lock(get_singleton()->script_gchandle_release_mutex); + if (!r_gchandle.is_released() && r_gchandle.get_intptr() == p_gchandle_to_free) { + r_gchandle.release(); + } + } +} - (void)p_expected_mono_obj_unused; - return release_script_gchandle(p_gchandle); +void CSharpLanguage::release_binding_gchandle_thread_safe(GCHandleIntPtr p_gchandle_to_free, CSharpScriptBinding &r_script_binding) { + MonoGCHandleData &gchandle = r_script_binding.gchandle; + if (!gchandle.is_released() && gchandle.get_intptr() == p_gchandle_to_free) { // Do not lock unnecessarily + MutexLock lock(get_singleton()->script_gchandle_release_mutex); + if (!gchandle.is_released() && gchandle.get_intptr() == p_gchandle_to_free) { + gchandle.release(); + r_script_binding.inited = false; // Here too, to be thread safe + } + } } CSharpLanguage::CSharpLanguage() { @@ -1309,6 +1316,10 @@ bool CSharpLanguage::setup_csharp_script_binding(CSharpScriptBinding &r_script_b ERR_FAIL_COND_V_MSG(!parent_is_object_class, false, "Type inherits from native type '" + type_name + "', so it can't be instantiated in object of type: '" + p_object->get_class() + "'."); +#ifdef DEBUG_ENABLED + CRASH_COND(!r_script_binding.gchandle.is_released()); +#endif + GCHandleIntPtr strong_gchandle = GDMonoCache::managed_callbacks.ScriptManagerBridge_CreateManagedForGodotObjectBinding(&type_name, p_object); @@ -1419,9 +1430,9 @@ GDNativeBool CSharpLanguage::_instance_binding_reference_callback(void *p_token, // Release the current weak handle and replace it with a strong handle. GCHandleIntPtr old_gchandle = gchandle.get_intptr(); - gchandle.handle = GCHandleIntPtr(); // No longer owns the handle (released by swap function) + gchandle.handle = { nullptr }; // No longer owns the handle (released by swap function) - GCHandleIntPtr new_gchandle; + GCHandleIntPtr new_gchandle = { nullptr }; bool create_weak = false; bool target_alive = GDMonoCache::managed_callbacks.ScriptManagerBridge_SwapGCHandleForType( old_gchandle, &new_gchandle, create_weak); @@ -1443,9 +1454,9 @@ GDNativeBool CSharpLanguage::_instance_binding_reference_callback(void *p_token, // Release the current strong handle and replace it with a weak handle. GCHandleIntPtr old_gchandle = gchandle.get_intptr(); - gchandle.handle = GCHandleIntPtr(); // No longer owns the handle (released by swap function) + gchandle.handle = { nullptr }; // No longer owns the handle (released by swap function) - GCHandleIntPtr new_gchandle; + GCHandleIntPtr new_gchandle = { nullptr }; bool create_weak = true; bool target_alive = GDMonoCache::managed_callbacks.ScriptManagerBridge_SwapGCHandleForType( old_gchandle, &new_gchandle, create_weak); @@ -1569,10 +1580,10 @@ void CSharpLanguage::tie_user_managed_to_unmanaged(GCHandleIntPtr p_gchandle_int Ref script = p_script; - CSharpScript::initialize_for_managed_type(script); - CRASH_COND(script.is_null()); + CSharpScript::initialize_for_managed_type(script); + CSharpInstance *csharp_instance = CSharpInstance::create_for_managed_type(p_unmanaged, script.ptr(), gchandle); p_unmanaged->set_script_and_instance(script, csharp_instance); @@ -1920,7 +1931,7 @@ bool CSharpInstance::_internal_new_managed() { return true; } -void CSharpInstance::mono_object_disposed() { +void CSharpInstance::mono_object_disposed(GCHandleIntPtr p_gchandle_to_free) { // Must make sure event signals are not left dangling disconnect_event_signals(); @@ -1928,10 +1939,10 @@ void CSharpInstance::mono_object_disposed() { CRASH_COND(base_ref_counted); CRASH_COND(gchandle.is_released()); #endif - CSharpLanguage::get_singleton()->release_script_gchandle(nullptr, gchandle); + CSharpLanguage::get_singleton()->release_script_gchandle_thread_safe(p_gchandle_to_free, gchandle); } -void CSharpInstance::mono_object_disposed_baseref(bool p_is_finalizer, bool &r_delete_owner, bool &r_remove_script_instance) { +void CSharpInstance::mono_object_disposed_baseref(GCHandleIntPtr p_gchandle_to_free, bool p_is_finalizer, bool &r_delete_owner, bool &r_remove_script_instance) { #ifdef DEBUG_ENABLED CRASH_COND(!base_ref_counted); CRASH_COND(gchandle.is_released()); @@ -1947,7 +1958,7 @@ void CSharpInstance::mono_object_disposed_baseref(bool p_is_finalizer, bool &r_d r_delete_owner = true; } else { r_delete_owner = false; - CSharpLanguage::get_singleton()->release_script_gchandle(nullptr, gchandle); + CSharpLanguage::get_singleton()->release_script_gchandle_thread_safe(p_gchandle_to_free, gchandle); if (!p_is_finalizer) { // If the native instance is still alive and Dispose() was called @@ -2000,9 +2011,9 @@ void CSharpInstance::refcount_incremented() { // Release the current weak handle and replace it with a strong handle. GCHandleIntPtr old_gchandle = gchandle.get_intptr(); - gchandle.handle = GCHandleIntPtr(); // No longer owns the handle (released by swap function) + gchandle.handle = { nullptr }; // No longer owns the handle (released by swap function) - GCHandleIntPtr new_gchandle; + GCHandleIntPtr new_gchandle = { nullptr }; bool create_weak = false; bool target_alive = GDMonoCache::managed_callbacks.ScriptManagerBridge_SwapGCHandleForType( old_gchandle, &new_gchandle, create_weak); @@ -2032,9 +2043,9 @@ bool CSharpInstance::refcount_decremented() { // Release the current strong handle and replace it with a weak handle. GCHandleIntPtr old_gchandle = gchandle.get_intptr(); - gchandle.handle = GCHandleIntPtr(); // No longer owns the handle (released by swap function) + gchandle.handle = { nullptr }; // No longer owns the handle (released by swap function) - GCHandleIntPtr new_gchandle; + GCHandleIntPtr new_gchandle = { nullptr }; bool create_weak = true; bool target_alive = GDMonoCache::managed_callbacks.ScriptManagerBridge_SwapGCHandleForType( old_gchandle, &new_gchandle, create_weak); @@ -2207,62 +2218,6 @@ void CSharpScript::_update_exports_values(HashMap &values, base_cache->_update_exports_values(values, propnames); } } - -void CSharpScript::_update_member_info_no_exports() { - if (exports_invalidated) { - exports_invalidated = false; - - member_info.clear(); - -#warning TODO -#if 0 - GDMonoClass *top = script_class; - List props; - - while (top && top != native) { - PropertyInfo prop_info; - bool exported; - - const Vector &fields = top->get_all_fields(); - - for (int i = fields.size() - 1; i >= 0; i--) { - GDMonoField *field = fields[i]; - - if (_get_member_export(field, /* inspect export: */ false, prop_info, exported)) { - StringName member_name = field->get_name(); - - member_info[member_name] = prop_info; - props.push_front(prop_info); - exported_members_defval_cache[member_name] = Variant(); - } - } - - const Vector &properties = top->get_all_properties(); - - for (int i = properties.size() - 1; i >= 0; i--) { - GDMonoProperty *property = properties[i]; - - if (_get_member_export(property, /* inspect export: */ false, prop_info, exported)) { - StringName member_name = property->get_name(); - - member_info[member_name] = prop_info; - props.push_front(prop_info); - exported_members_defval_cache[member_name] = Variant(); - } - } - - exported_members_cache.push_back(PropertyInfo(Variant::NIL, top->get_name(), PROPERTY_HINT_NONE, get_path(), PROPERTY_USAGE_CATEGORY)); - for (const PropertyInfo &E : props) { - exported_members_cache.push_back(E); - } - - props.clear(); - - top = top->get_parent_class(); - } -#endif - } -} #endif bool CSharpScript::_update_exports(PlaceHolderScriptInstance *p_instance_to_update) { @@ -2282,170 +2237,61 @@ bool CSharpScript::_update_exports(PlaceHolderScriptInstance *p_instance_to_upda if (exports_invalidated) #endif { -#warning TODO -#if 0 - GD_MONO_SCOPE_THREAD_ATTACH; + exports_invalidated = false; changed = true; member_info.clear(); #ifdef TOOLS_ENABLED - MonoObject *tmp_object = nullptr; - Object *tmp_native = nullptr; - uint32_t tmp_pinned_gchandle = 0; - - if (is_editor) { - exports_invalidated = false; - - exported_members_cache.clear(); - exported_members_defval_cache.clear(); - - // Here we create a temporary managed instance of the class to get the initial values - tmp_object = mono_object_new(mono_domain_get(), script_class->get_mono_ptr()); - - if (!tmp_object) { - ERR_PRINT("Failed to allocate temporary MonoObject."); - return false; - } - - tmp_pinned_gchandle = GDMonoUtils::new_strong_gchandle_pinned(tmp_object); // pin it (not sure if needed) - - GDMonoMethod *ctor = script_class->get_method(CACHED_STRING_NAME(dotctor), 0); - - ERR_FAIL_NULL_V_MSG(ctor, false, - "Cannot construct temporary MonoObject because the class does not define a parameterless constructor: '" + get_path() + "'."); - - MonoException *ctor_exc = nullptr; - ctor->invoke(tmp_object, nullptr, &ctor_exc); - - tmp_native = GDMonoMarshal::unbox(GDMonoCache::cached_data.field_GodotObject_ptr->get_value(tmp_object)); - - if (ctor_exc) { - // TODO: Should we free 'tmp_native' if the exception was thrown after its creation? - - GDMonoUtils::free_gchandle(tmp_pinned_gchandle); - tmp_object = nullptr; - - ERR_PRINT("Exception thrown from constructor of temporary MonoObject:"); - GDMonoUtils::debug_print_unhandled_exception(ctor_exc); - return false; - } - } + exported_members_cache.clear(); + exported_members_defval_cache.clear(); #endif - GDMonoClass *top = script_class; - List props; - - while (top && top != native) { - PropertyInfo prop_info; - bool exported; - - const Vector &fields = top->get_all_fields(); - - for (int i = fields.size() - 1; i >= 0; i--) { - GDMonoField *field = fields[i]; - - if (_get_member_export(field, /* inspect export: */ true, prop_info, exported)) { - StringName member_name = field->get_name(); - - member_info[member_name] = prop_info; - - if (exported) { + if (GDMonoCache::godot_api_cache_updated) { + GDMonoCache::managed_callbacks.ScriptManagerBridge_GetPropertyInfoList(this, + [](CSharpScript *p_script, const String *p_current_class_name, GDMonoCache::godotsharp_property_info *p_props, int32_t p_count) { #ifdef TOOLS_ENABLED - if (is_editor) { - props.push_front(prop_info); + p_script->exported_members_cache.push_back(PropertyInfo( + Variant::NIL, *p_current_class_name, PROPERTY_HINT_NONE, + p_script->get_path(), PROPERTY_USAGE_CATEGORY)); +#endif - if (tmp_object) { - exported_members_defval_cache[member_name] = GDMonoMarshal::mono_object_to_variant(field->get_value(tmp_object)); - } - } + for (int i = 0; i < p_count; i++) { + const GDMonoCache::godotsharp_property_info &prop = p_props[i]; + + StringName name = *reinterpret_cast(&prop.name); + String hint_string = *reinterpret_cast(&prop.hint_string); + + PropertyInfo pinfo(prop.type, name, prop.hint, hint_string, prop.usage); + + p_script->member_info[name] = pinfo; + + if (prop.exported) { + +#ifdef TOOLS_ENABLED + p_script->exported_members_cache.push_back(pinfo); #endif #if defined(TOOLS_ENABLED) || defined(DEBUG_ENABLED) - exported_members_names.insert(member_name); + p_script->exported_members_names.insert(name); #endif - } - } - } - - const Vector &properties = top->get_all_properties(); - - for (int i = properties.size() - 1; i >= 0; i--) { - GDMonoProperty *property = properties[i]; - - if (_get_member_export(property, /* inspect export: */ true, prop_info, exported)) { - StringName member_name = property->get_name(); - - member_info[member_name] = prop_info; - - if (exported) { -#ifdef TOOLS_ENABLED - if (is_editor) { - props.push_front(prop_info); - if (tmp_object) { - MonoException *exc = nullptr; - MonoObject *ret = property->get_value(tmp_object, &exc); - if (exc) { - exported_members_defval_cache[member_name] = Variant(); - GDMonoUtils::debug_print_unhandled_exception(exc); - } else { - exported_members_defval_cache[member_name] = GDMonoMarshal::mono_object_to_variant(ret); - } } } -#endif + }); -#if defined(TOOLS_ENABLED) || defined(DEBUG_ENABLED) - exported_members_names.insert(member_name); -#endif - } - } - } + GDMonoCache::managed_callbacks.ScriptManagerBridge_GetPropertyDefaultValues(this, + [](CSharpScript *p_script, GDMonoCache::godotsharp_property_def_val_pair *p_def_vals, int32_t p_count) { + for (int i = 0; i < p_count; i++) { + const GDMonoCache::godotsharp_property_def_val_pair &def_val_pair = p_def_vals[i]; -#ifdef TOOLS_ENABLED - exported_members_cache.push_back(PropertyInfo(Variant::NIL, top->get_name(), PROPERTY_HINT_NONE, get_path(), PROPERTY_USAGE_CATEGORY)); + StringName name = *reinterpret_cast(&def_val_pair.name); + Variant value = *reinterpret_cast(&def_val_pair.value); - for (const PropertyInfo &E : props) { - exported_members_cache.push_back(E); - } - - props.clear(); -#endif // TOOLS_ENABLED - - top = top->get_parent_class(); + p_script->exported_members_defval_cache[name] = value; + } + }); } - -#ifdef TOOLS_ENABLED - if (is_editor) { - // Need to check this here, before disposal - bool base_ref_counted = Object::cast_to(tmp_native) != nullptr; - - // Dispose the temporary managed instance - - MonoException *exc = nullptr; - GDMonoUtils::dispose(tmp_object, &exc); - - if (exc) { - ERR_PRINT("Exception thrown from method Dispose() of temporary MonoObject:"); - GDMonoUtils::debug_print_unhandled_exception(exc); - } - - GDMonoUtils::free_gchandle(tmp_pinned_gchandle); - tmp_object = nullptr; - - if (tmp_native && !base_ref_counted) { - Node *node = Object::cast_to(tmp_native); - if (node && node->is_inside_tree()) { - ERR_PRINT("Temporary instance was added to the scene tree."); - } else { - memdelete(tmp_native); - } - } - } -#endif - -#endif // #if 0 } #ifdef TOOLS_ENABLED @@ -2472,237 +2318,6 @@ bool CSharpScript::_update_exports(PlaceHolderScriptInstance *p_instance_to_upda return changed; } -#warning TODO -#if 0 -/** - * Returns false if there was an error, otherwise true. - * If there was an error, r_prop_info and r_exported are not assigned any value. - */ -bool CSharpScript::_get_member_export(IMonoClassMember *p_member, bool p_inspect_export, PropertyInfo &r_prop_info, bool &r_exported) { - GD_MONO_ASSERT_THREAD_ATTACHED; - - // Goddammit, C++. All I wanted was some nested functions. -#define MEMBER_FULL_QUALIFIED_NAME(m_member) \ - (m_member->get_enclosing_class()->get_full_name() + "." + (String)m_member->get_name()) - - if (p_member->is_static()) { -#ifdef TOOLS_ENABLED - if (p_member->has_attribute(GDMonoCache::cached_data.class_ExportAttribute)) { - ERR_PRINT("Cannot export member because it is static: '" + MEMBER_FULL_QUALIFIED_NAME(p_member) + "'."); - } -#endif - return false; - } - - if (member_info.has(p_member->get_name())) { - return false; - } - - ManagedType type; - - if (p_member->get_member_type() == IMonoClassMember::MEMBER_TYPE_FIELD) { - type = static_cast(p_member)->get_type(); - } else if (p_member->get_member_type() == IMonoClassMember::MEMBER_TYPE_PROPERTY) { - type = static_cast(p_member)->get_type(); - } else { - CRASH_NOW(); - } - - bool exported = p_member->has_attribute(GDMonoCache::cached_data.class_ExportAttribute); - - if (p_member->get_member_type() == IMonoClassMember::MEMBER_TYPE_PROPERTY) { - GDMonoProperty *property = static_cast(p_member); - if (!property->has_getter()) { -#ifdef TOOLS_ENABLED - if (exported) { - ERR_PRINT("Cannot export a property without a getter: '" + MEMBER_FULL_QUALIFIED_NAME(p_member) + "'."); - } -#endif - return false; - } - if (!property->has_setter()) { -#ifdef TOOLS_ENABLED - if (exported) { - ERR_PRINT("Cannot export a property without a setter: '" + MEMBER_FULL_QUALIFIED_NAME(p_member) + "'."); - } -#endif - return false; - } - } - - bool nil_is_variant = false; - Variant::Type variant_type = GDMonoMarshal::managed_to_variant_type(type, &nil_is_variant); - - if (!p_inspect_export || !exported) { - r_prop_info = PropertyInfo(variant_type, (String)p_member->get_name(), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_SCRIPT_VARIABLE); - r_exported = false; - return true; - } - -#ifdef TOOLS_ENABLED - MonoObject *attr = p_member->get_attribute(GDMonoCache::cached_data.class_ExportAttribute); -#endif - - PropertyHint hint = PROPERTY_HINT_NONE; - String hint_string; - - if (variant_type == Variant::NIL && !nil_is_variant) { -#ifdef TOOLS_ENABLED - ERR_PRINT("Unknown exported member type: '" + MEMBER_FULL_QUALIFIED_NAME(p_member) + "'."); -#endif - return false; - } - -#ifdef TOOLS_ENABLED - int hint_res = _try_get_member_export_hint(p_member, type, variant_type, /* allow_generics: */ true, hint, hint_string); - - ERR_FAIL_COND_V_MSG(hint_res == -1, false, - "Error while trying to determine information about the exported member: '" + - MEMBER_FULL_QUALIFIED_NAME(p_member) + "'."); - - if (hint_res == 0) { - hint = PropertyHint(GDMonoCache::cached_data.field_ExportAttribute_hint->get_int_value(attr)); - hint_string = GDMonoCache::cached_data.field_ExportAttribute_hintString->get_string_value(attr); - } -#endif - - uint32_t prop_usage = PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE; - - if (variant_type == Variant::NIL) { - // System.Object (Variant) - prop_usage |= PROPERTY_USAGE_NIL_IS_VARIANT; - } - - r_prop_info = PropertyInfo(variant_type, (String)p_member->get_name(), hint, hint_string, prop_usage); - r_exported = true; - - return true; - -#undef MEMBER_FULL_QUALIFIED_NAME -} - -#ifdef TOOLS_ENABLED -int CSharpScript::_try_get_member_export_hint(IMonoClassMember *p_member, ManagedType p_type, Variant::Type p_variant_type, bool p_allow_generics, PropertyHint &r_hint, String &r_hint_string) { - if (p_variant_type == Variant::NIL) { - // System.Object (Variant) - return 1; - } - - GD_MONO_ASSERT_THREAD_ATTACHED; - - if (p_variant_type == Variant::INT && p_type.type_encoding == MONO_TYPE_VALUETYPE && mono_class_is_enum(p_type.type_class->get_mono_ptr())) { - MonoReflectionType *reftype = mono_type_get_object(mono_domain_get(), p_type.type_class->get_mono_type()); - r_hint = GDMonoUtils::Marshal::type_has_flags_attribute(reftype) ? PROPERTY_HINT_FLAGS : PROPERTY_HINT_ENUM; - - Vector fields = p_type.type_class->get_enum_fields(); - - MonoType *enum_basetype = mono_class_enum_basetype(p_type.type_class->get_mono_ptr()); - - String name_only_hint_string; - - // True: enum Foo { Bar, Baz, Quux } - // True: enum Foo { Bar = 0, Baz = 1, Quux = 2 } - // False: enum Foo { Bar = 0, Baz = 7, Quux = 5 } - bool uses_default_values = true; - - for (int i = 0; i < fields.size(); i++) { - MonoClassField *field = fields[i]; - - if (i > 0) { - r_hint_string += ","; - name_only_hint_string += ","; - } - - String enum_field_name = String::utf8(mono_field_get_name(field)); - r_hint_string += enum_field_name; - name_only_hint_string += enum_field_name; - - // TODO: - // Instead of using mono_field_get_value_object, we can do this without boxing. Check the - // internal mono functions: ves_icall_System_Enum_GetEnumValuesAndNames and the get_enum_field. - - MonoObject *val_obj = mono_field_get_value_object(mono_domain_get(), field, nullptr); - - ERR_FAIL_NULL_V_MSG(val_obj, -1, "Failed to get '" + enum_field_name + "' constant enum value."); - - bool r_error; - uint64_t val = GDMonoUtils::unbox_enum_value(val_obj, enum_basetype, r_error); - ERR_FAIL_COND_V_MSG(r_error, -1, "Failed to unbox '" + enum_field_name + "' constant enum value."); - - unsigned int expected_val = r_hint == PROPERTY_HINT_FLAGS ? 1 << i : i; - if (val != expected_val) { - uses_default_values = false; - } - - r_hint_string += ":"; - r_hint_string += String::num_uint64(val); - } - - if (uses_default_values) { - // If we use the format NAME:VAL, that's what the editor displays. - // That's annoying if the user is not using custom values for the enum constants. - // This may not be needed in the future if the editor is changed to not display values. - r_hint_string = name_only_hint_string; - } - } else if (p_variant_type == Variant::OBJECT && GDMonoCache::cached_data.class_GodotResource->is_assignable_from(p_type.type_class)) { - GDMonoClass *field_native_class = GDMonoUtils::get_class_native_base(p_type.type_class); - CRASH_COND(field_native_class == nullptr); - - r_hint = PROPERTY_HINT_RESOURCE_TYPE; - r_hint_string = String(NATIVE_GDMONOCLASS_NAME(field_native_class)); - } else if (p_variant_type == Variant::OBJECT && CACHED_CLASS(Node)->is_assignable_from(p_type.type_class)) { - GDMonoClass *field_native_class = GDMonoUtils::get_class_native_base(p_type.type_class); - CRASH_COND(field_native_class == nullptr); - - r_hint = PROPERTY_HINT_NODE_TYPE; - r_hint_string = String(NATIVE_GDMONOCLASS_NAME(field_native_class)); - } else if (p_allow_generics && p_variant_type == Variant::ARRAY) { - // Nested arrays are not supported in the inspector - - ManagedType elem_type; - - if (!GDMonoMarshal::try_get_array_element_type(p_type, elem_type)) { - return 0; - } - - Variant::Type elem_variant_type = GDMonoMarshal::managed_to_variant_type(elem_type); - - PropertyHint elem_hint = PROPERTY_HINT_NONE; - String elem_hint_string; - - ERR_FAIL_COND_V_MSG(elem_variant_type == Variant::NIL, -1, "Unknown array element type."); - - bool preset_hint = false; - if (elem_variant_type == Variant::STRING) { - MonoObject *attr = p_member->get_attribute(CACHED_CLASS(ExportAttribute)); - if (PropertyHint(CACHED_FIELD(ExportAttribute, hint)->get_int_value(attr)) == PROPERTY_HINT_ENUM) { - r_hint_string = itos(elem_variant_type) + "/" + itos(PROPERTY_HINT_ENUM) + ":" + CACHED_FIELD(ExportAttribute, hintString)->get_string_value(attr); - preset_hint = true; - } - } - - if (!preset_hint) { - int hint_res = _try_get_member_export_hint(p_member, elem_type, elem_variant_type, /* allow_generics: */ false, elem_hint, elem_hint_string); - - ERR_FAIL_COND_V_MSG(hint_res == -1, -1, "Error while trying to determine information about the array element type."); - - // Format: type/hint:hint_string - r_hint_string = itos(elem_variant_type) + "/" + itos(elem_hint) + ":" + elem_hint_string; - } - - r_hint = PROPERTY_HINT_TYPE_STRING; - - } else if (p_allow_generics && p_variant_type == Variant::DICTIONARY) { - // TODO: Dictionaries are not supported in the inspector - } else { - return 0; - } - - return 1; -} -#endif -#endif - bool CSharpScript::_get(const StringName &p_name, Variant &r_ret) const { if (p_name == CSharpLanguage::singleton->string_names._script_source) { r_ret = get_source_code(); @@ -2742,9 +2357,7 @@ void CSharpScript::initialize_for_managed_type(Ref p_script) { update_script_class_info(p_script); -#ifdef TOOLS_ENABLED - p_script->_update_member_info_no_exports(); -#endif + p_script->_update_exports(); } // Extract information about the script using the mono class. @@ -3125,7 +2738,7 @@ void CSharpScript::get_script_property_list(List *r_list) const { for (const KeyValue &E : member_info) { props.push_front(E.value); } -#endif // TOOLS_ENABLED +#endif for (const PropertyInfo &prop : props) { r_list->push_back(prop); diff --git a/modules/mono/csharp_script.h b/modules/mono/csharp_script.h index 9be4c9c1300..d4891395baf 100644 --- a/modules/mono/csharp_script.h +++ b/modules/mono/csharp_script.h @@ -116,7 +116,6 @@ private: bool placeholder_fallback_enabled = false; bool exports_invalidated = true; void _update_exports_values(HashMap &values, List &propnames); - void _update_member_info_no_exports(); void _placeholder_erased(PlaceHolderScriptInstance *p_placeholder) override; #endif @@ -130,14 +129,6 @@ private: bool _update_exports(PlaceHolderScriptInstance *p_instance_to_update = nullptr); -#warning TODO -#if 0 - bool _get_member_export(IMonoClassMember *p_member, bool p_inspect_export, PropertyInfo &r_prop_info, bool &r_exported); -#ifdef TOOLS_ENABLED - static int _try_get_member_export_hint(IMonoClassMember *p_member, ManagedType p_type, Variant::Type p_variant_type, bool p_allow_generics, PropertyHint &r_hint, String &r_hint_string); -#endif -#endif - CSharpInstance *_create_instance(const Variant **p_args, int p_argcount, Object *p_owner, bool p_is_ref_counted, Callable::CallError &r_error); Variant _new(const Variant **p_args, int p_argcount, Callable::CallError &r_error); @@ -261,13 +252,13 @@ public: bool has_method(const StringName &p_method) const override; Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override; - void mono_object_disposed(); + void mono_object_disposed(GCHandleIntPtr p_gchandle_to_free); /* * If 'r_delete_owner' is set to true, the caller must memdelete the script instance's owner. Otherwise, if * 'r_remove_script_instance' is set to true, the caller must destroy the script instance by removing it from its owner. */ - void mono_object_disposed_baseref(bool p_is_finalizer, bool &r_delete_owner, bool &r_remove_script_instance); + void mono_object_disposed_baseref(GCHandleIntPtr p_gchandle_to_free, bool p_is_finalizer, bool &r_delete_owner, bool &r_remove_script_instance); void connect_event_signal(const StringName &p_event_signal); void disconnect_event_signals(); @@ -384,7 +375,8 @@ public: #endif static void release_script_gchandle(MonoGCHandleData &p_gchandle); - static void release_script_gchandle(void *p_expected_mono_obj_unused, MonoGCHandleData &p_gchandle); + static void release_script_gchandle_thread_safe(GCHandleIntPtr p_gchandle_to_free, MonoGCHandleData &r_gchandle); + static void release_binding_gchandle_thread_safe(GCHandleIntPtr p_gchandle_to_free, CSharpScriptBinding &r_script_binding); bool debug_break(const String &p_error, bool p_allow_continue = true); bool debug_break_parse(const String &p_file, int p_line, const String &p_error); diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedFields.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedFields.cs new file mode 100644 index 00000000000..7b106ef63cb --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedFields.cs @@ -0,0 +1,133 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +#pragma warning disable CS0169 +#pragma warning disable CS0414 + +namespace Godot.SourceGenerators.Sample +{ + [SuppressMessage("ReSharper", "BuiltInTypeReferenceStyle")] + [SuppressMessage("ReSharper", "RedundantNameQualifier")] + [SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeEvident")] + [SuppressMessage("ReSharper", "InconsistentNaming")] + public partial class ExportedFields : Godot.Object + { + [Export] private Boolean field_Boolean = true; + [Export] private Char field_Char = 'f'; + [Export] private SByte field_SByte = 10; + [Export] private Int16 field_Int16 = 10; + [Export] private Int32 field_Int32 = 10; + [Export] private Int64 field_Int64 = 10; + [Export] private Byte field_Byte = 10; + [Export] private UInt16 field_UInt16 = 10; + [Export] private UInt32 field_UInt32 = 10; + [Export] private UInt64 field_UInt64 = 10; + [Export] private Single field_Single = 10; + [Export] private Double field_Double = 10; + [Export] private String field_String = "foo"; + + // Godot structs + [Export] private Vector2 field_Vector2 = new(10f, 10f); + [Export] private Vector2i field_Vector2i = Vector2i.Up; + [Export] private Rect2 field_Rect2 = new(new Vector2(10f, 10f), new Vector2(10f, 10f)); + [Export] private Rect2i field_Rect2i = new(new Vector2i(10, 10), new Vector2i(10, 10)); + [Export] private Transform2D field_Transform2D = Transform2D.Identity; + [Export] private Vector3 field_Vector3 = new(10f, 10f, 10f); + [Export] private Vector3i field_Vector3i = Vector3i.Back; + [Export] private Basis field_Basis = new Basis(Quaternion.Identity); + [Export] private Quaternion field_Quaternion = new Quaternion(Basis.Identity); + [Export] private Transform3D field_Transform3D = Transform3D.Identity; + [Export] private Vector4 field_Vector4 = new(10f, 10f, 10f, 10f); + [Export] private Vector4i field_Vector4i = Vector4i.One; + [Export] private Projection field_Projection = Projection.Identity; + [Export] private AABB field_AABB = new AABB(10f, 10f, 10f, new Vector3(1f, 1f, 1f)); + [Export] private Color field_Color = Colors.Aquamarine; + [Export] private Plane field_Plane = Plane.PlaneXZ; + [Export] private Callable field_Callable = new Callable(Engine.GetMainLoop(), "_process"); + [Export] private SignalInfo field_SignalInfo = new SignalInfo(Engine.GetMainLoop(), "property_list_changed"); + + // Enums + [SuppressMessage("ReSharper", "UnusedMember.Local")] + enum MyEnum + { + A, + B, + C + } + + [Export] private MyEnum field_Enum = MyEnum.C; + + [Flags] + [SuppressMessage("ReSharper", "UnusedMember.Local")] + enum MyFlagsEnum + { + A, + B, + C + } + + [Export] private MyFlagsEnum field_FlagsEnum = MyFlagsEnum.C; + + // Arrays + [Export] private Byte[] field_ByteArray = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Int32[] field_Int32Array = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Int64[] field_Int64Array = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Single[] field_SingleArray = { 0f, 1f, 2f, 3f, 4f, 5f, 6f }; + [Export] private Double[] field_DoubleArray = { 0d, 1d, 2d, 3d, 4d, 5d, 6d }; + [Export] private String[] field_StringArray = { "foo", "bar" }; + [Export(PropertyHint.Enum, "A,B,C")] private String[] field_StringArrayEnum = { "foo", "bar" }; + [Export] private Vector2[] field_Vector2Array = { Vector2.Up, Vector2.Down, Vector2.Left, Vector2.Right }; + [Export] private Vector3[] field_Vector3Array = { Vector3.Up, Vector3.Down, Vector3.Left, Vector3.Right }; + [Export] private Color[] field_ColorArray = { Colors.Aqua, Colors.Aquamarine, Colors.Azure, Colors.Beige }; + [Export] private Godot.Object[] field_GodotObjectOrDerivedArray = { null }; + [Export] private object[] field_SystemObjectArray = { 0, 1f, 2d, "foo", Vector3i.Up }; + + // Generics + [Export] private Godot.Collections.Dictionary field_GodotGenericDictionary = + new Godot.Collections.Dictionary { { "key1", "value1" }, { "key2", "value2" } }; + + [Export] private Godot.Collections.Array field_GodotGenericArray = + new Godot.Collections.Array { "elem1", "elem2", "elem3" }; + + [Export] private System.Collections.Generic.Dictionary field_SystemGenericDictionary = + new System.Collections.Generic.Dictionary { { "key1", "value1" }, { "key2", "value2" } }; + + [Export] private System.Collections.Generic.List field_SystemGenericList = + new System.Collections.Generic.List { "elem1", "elem2", "elem3" }; + + [Export] private System.Collections.Generic.IDictionary field_GenericIDictionary = + new System.Collections.Generic.Dictionary { { "key1", "value1" }, { "key2", "value2" } }; + + [Export] private System.Collections.Generic.ICollection field_GenericICollection = + new System.Collections.Generic.List { "elem1", "elem2", "elem3" }; + + [Export] private System.Collections.Generic.IEnumerable field_GenericIEnumerable = + new System.Collections.Generic.List { "elem1", "elem2", "elem3" }; + + // Variant + [Export] private object field_SystemObject = "foo"; + + // Classes + [Export] private Godot.Object field_GodotObjectOrDerived; + [Export] private Godot.Texture field_GodotResourceTexture; + [Export] private StringName field_StringName = new StringName("foo"); + [Export] private NodePath field_NodePath = new NodePath("foo"); + [Export] private RID field_RID; + + [Export] private Godot.Collections.Dictionary field_GodotDictionary = + new() { { "foo", 10 }, { Vector2.Up, Colors.Chocolate } }; + + [Export] private Godot.Collections.Array field_GodotArray = + new() { "foo", 10, Vector2.Up, Colors.Chocolate }; + + [Export] private System.Collections.IDictionary field_IDictionary = + new System.Collections.Generic.Dictionary + { { "foo", 10 }, { Vector2.Up, Colors.Chocolate } }; + + [Export] private System.Collections.ICollection field_ICollection = + new System.Collections.Generic.List { "foo", 10, Vector2.Up, Colors.Chocolate }; + + [Export] private System.Collections.IEnumerable field_IEnumerable = + new System.Collections.Generic.List { "foo", 10, Vector2.Up, Colors.Chocolate }; + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedProperties.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedProperties.cs new file mode 100644 index 00000000000..71025c1d43f --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedProperties.cs @@ -0,0 +1,133 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +#pragma warning disable CS0169 +#pragma warning disable CS0414 + +namespace Godot.SourceGenerators.Sample +{ + [SuppressMessage("ReSharper", "BuiltInTypeReferenceStyle")] + [SuppressMessage("ReSharper", "RedundantNameQualifier")] + [SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeEvident")] + [SuppressMessage("ReSharper", "InconsistentNaming")] + public partial class ExportedProperties : Godot.Object + { + [Export] private Boolean property_Boolean { get; set; } = true; + [Export] private Char property_Char { get; set; } = 'f'; + [Export] private SByte property_SByte { get; set; } = 10; + [Export] private Int16 property_Int16 { get; set; } = 10; + [Export] private Int32 property_Int32 { get; set; } = 10; + [Export] private Int64 property_Int64 { get; set; } = 10; + [Export] private Byte property_Byte { get; set; } = 10; + [Export] private UInt16 property_UInt16 { get; set; } = 10; + [Export] private UInt32 property_UInt32 { get; set; } = 10; + [Export] private UInt64 property_UInt64 { get; set; } = 10; + [Export] private Single property_Single { get; set; } = 10; + [Export] private Double property_Double { get; set; } = 10; + [Export] private String property_String { get; set; } = "foo"; + + // Godot structs + [Export] private Vector2 property_Vector2 { get; set; } = new(10f, 10f); + [Export] private Vector2i property_Vector2i { get; set; } = Vector2i.Up; + [Export] private Rect2 property_Rect2 { get; set; } = new(new Vector2(10f, 10f), new Vector2(10f, 10f)); + [Export] private Rect2i property_Rect2i { get; set; } = new(new Vector2i(10, 10), new Vector2i(10, 10)); + [Export] private Transform2D property_Transform2D { get; set; } = Transform2D.Identity; + [Export] private Vector3 property_Vector3 { get; set; } = new(10f, 10f, 10f); + [Export] private Vector3i property_Vector3i { get; set; } = Vector3i.Back; + [Export] private Basis property_Basis { get; set; } = new Basis(Quaternion.Identity); + [Export] private Quaternion property_Quaternion { get; set; } = new Quaternion(Basis.Identity); + [Export] private Transform3D property_Transform3D { get; set; } = Transform3D.Identity; + [Export] private Vector4 property_Vector4 { get; set; } = new(10f, 10f, 10f, 10f); + [Export] private Vector4i property_Vector4i { get; set; } = Vector4i.One; + [Export] private Projection property_Projection { get; set; } = Projection.Identity; + [Export] private AABB property_AABB { get; set; } = new AABB(10f, 10f, 10f, new Vector3(1f, 1f, 1f)); + [Export] private Color property_Color { get; set; } = Colors.Aquamarine; + [Export] private Plane property_Plane { get; set; } = Plane.PlaneXZ; + [Export] private Callable property_Callable { get; set; } = new Callable(Engine.GetMainLoop(), "_process"); + [Export] private SignalInfo property_SignalInfo { get; set; } = new SignalInfo(Engine.GetMainLoop(), "property_list_changed"); + + // Enums + [SuppressMessage("ReSharper", "UnusedMember.Local")] + enum MyEnum + { + A, + B, + C + } + + [Export] private MyEnum property_Enum { get; set; } = MyEnum.C; + + [Flags] + [SuppressMessage("ReSharper", "UnusedMember.Local")] + enum MyFlagsEnum + { + A, + B, + C + } + + [Export] private MyFlagsEnum property_FlagsEnum { get; set; } = MyFlagsEnum.C; + + // Arrays + [Export] private Byte[] property_ByteArray { get; set; } = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Int32[] property_Int32Array { get; set; } = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Int64[] property_Int64Array { get; set; } = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Single[] property_SingleArray { get; set; } = { 0f, 1f, 2f, 3f, 4f, 5f, 6f }; + [Export] private Double[] property_DoubleArray { get; set; } = { 0d, 1d, 2d, 3d, 4d, 5d, 6d }; + [Export] private String[] property_StringArray { get; set; } = { "foo", "bar" }; + [Export(PropertyHint.Enum, "A,B,C")] private String[] property_StringArrayEnum { get; set; } = { "foo", "bar" }; + [Export] private Vector2[] property_Vector2Array { get; set; } = { Vector2.Up, Vector2.Down, Vector2.Left, Vector2.Right }; + [Export] private Vector3[] property_Vector3Array { get; set; } = { Vector3.Up, Vector3.Down, Vector3.Left, Vector3.Right }; + [Export] private Color[] property_ColorArray { get; set; } = { Colors.Aqua, Colors.Aquamarine, Colors.Azure, Colors.Beige }; + [Export] private Godot.Object[] property_GodotObjectOrDerivedArray { get; set; } = { null }; + [Export] private object[] property_SystemObjectArray { get; set; } = { 0, 1f, 2d, "foo", Vector3i.Up }; + + // Generics + [Export] private Godot.Collections.Dictionary property_GodotGenericDictionary { get; set; } = + new Godot.Collections.Dictionary { { "key1", "value1" }, { "key2", "value2" } }; + + [Export] private Godot.Collections.Array property_GodotGenericArray { get; set; } = + new Godot.Collections.Array { "elem1", "elem2", "elem3" }; + + [Export] private System.Collections.Generic.Dictionary property_SystemGenericDictionary { get; set; } = + new System.Collections.Generic.Dictionary { { "key1", "value1" }, { "key2", "value2" } }; + + [Export] private System.Collections.Generic.List property_SystemGenericList { get; set; } = + new System.Collections.Generic.List { "elem1", "elem2", "elem3" }; + + [Export] private System.Collections.Generic.IDictionary property_GenericIDictionary { get; set; } = + new System.Collections.Generic.Dictionary { { "key1", "value1" }, { "key2", "value2" } }; + + [Export] private System.Collections.Generic.ICollection property_GenericICollection { get; set; } = + new System.Collections.Generic.List { "elem1", "elem2", "elem3" }; + + [Export] private System.Collections.Generic.IEnumerable property_GenericIEnumerable { get; set; } = + new System.Collections.Generic.List { "elem1", "elem2", "elem3" }; + + // Variant + [Export] private object property_SystemObject { get; set; } = "foo"; + + // Classes + [Export] private Godot.Object property_GodotObjectOrDerived { get; set; } + [Export] private Godot.Texture property_GodotResourceTexture { get; set; } + [Export] private StringName property_StringName { get; set; } = new StringName("foo"); + [Export] private NodePath property_NodePath { get; set; } = new NodePath("foo"); + [Export] private RID property_RID { get; set; } + + [Export] private Godot.Collections.Dictionary property_GodotDictionary { get; set; } = + new() { { "foo", 10 }, { Vector2.Up, Colors.Chocolate } }; + + [Export] private Godot.Collections.Array property_GodotArray { get; set; } = + new() { "foo", 10, Vector2.Up, Colors.Chocolate }; + + [Export] private System.Collections.IDictionary property_IDictionary { get; set; } = + new System.Collections.Generic.Dictionary + { { "foo", 10 }, { Vector2.Up, Colors.Chocolate } }; + + [Export] private System.Collections.ICollection property_ICollection { get; set; } = + new System.Collections.Generic.List { "foo", 10, Vector2.Up, Colors.Chocolate }; + + [Export] private System.Collections.IEnumerable property_IEnumerable { get; set; } = + new System.Collections.Generic.List { "foo", 10, Vector2.Up, Colors.Chocolate }; + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic.cs index 2ddb8880c2f..b21b035b4d9 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic.cs @@ -1,16 +1,21 @@ +#pragma warning disable CS0169 + namespace Godot.SourceGenerators.Sample { partial class Generic : Godot.Object { + private int _field; } // Generic again but different generic parameters partial class Generic : Godot.Object { + private int _field; } // Generic again but without generic parameters partial class Generic : Godot.Object { + private int _field; } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj index c5a29a53f72..a042fb313f0 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj @@ -7,6 +7,8 @@ $(MSBuildProjectDirectory) + + $(DefineConstants);TOOLS diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ScriptBoilerplate.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ScriptBoilerplate.cs index 0c7328284eb..bfc8ef2fb5c 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ScriptBoilerplate.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ScriptBoilerplate.cs @@ -1,4 +1,4 @@ -using System; +#pragma warning disable CS0169 namespace Godot.SourceGenerators.Sample { diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs index fa41c853228..0b8a2777e59 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs @@ -1,3 +1,4 @@ +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -60,5 +61,110 @@ namespace Godot.SourceGenerators outerTypeDeclSyntax.GetLocation(), outerTypeDeclSyntax.SyntaxTree.FilePath)); } + + public static void ReportExportedMemberIsStatic( + GeneratorExecutionContext context, + ISymbol exportedMemberSymbol + ) + { + var locations = exportedMemberSymbol.Locations; + var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); + bool isField = exportedMemberSymbol is IFieldSymbol; + + string message = $"Attempted to export static {(isField ? "field" : "property")}: " + + $"'{exportedMemberSymbol.ToDisplayString()}'"; + + string description = $"{message}. Only instance fields and properties can be exported." + + " Remove the 'static' modifier or the '[Export]' attribute."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GODOT-G0101", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + location, + location?.SourceTree?.FilePath)); + } + + public static void ReportExportedMemberTypeNotSupported( + GeneratorExecutionContext context, + ISymbol exportedMemberSymbol + ) + { + var locations = exportedMemberSymbol.Locations; + var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); + bool isField = exportedMemberSymbol is IFieldSymbol; + + string message = $"The type of the exported {(isField ? "field" : "property")} " + + $"is not supported: '{exportedMemberSymbol.ToDisplayString()}'"; + + string description = $"{message}. Use a supported type or remove the '[Export]' attribute."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GODOT-G0102", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + location, + location?.SourceTree?.FilePath)); + } + + public static void ReportExportedMemberIsReadOnly( + GeneratorExecutionContext context, + ISymbol exportedMemberSymbol + ) + { + var locations = exportedMemberSymbol.Locations; + var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); + bool isField = exportedMemberSymbol is IFieldSymbol; + + string message = $"The exported {(isField ? "field" : "property")} " + + $"is read-only: '{exportedMemberSymbol.ToDisplayString()}'"; + + string description = isField ? + $"{message}. Exported fields cannot be read-only." : + $"{message}. Exported properties must be writable."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GODOT-G0103", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + location, + location?.SourceTree?.FilePath)); + } + + public static void ReportExportedMemberIsWriteOnly( + GeneratorExecutionContext context, + ISymbol exportedMemberSymbol + ) + { + var locations = exportedMemberSymbol.Locations; + var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); + + string message = $"The exported property is write-only: '{exportedMemberSymbol.ToDisplayString()}'"; + + string description = $"{message}. Exported properties must be readable."; + + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor(id: "GODOT-G0104", + title: message, + messageFormat: message, + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description), + location, + location?.SourceTree?.FilePath)); + } } } 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 9586e71d02e..2179aeea88d 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -24,30 +25,55 @@ namespace Godot.SourceGenerators toggle != null && toggle.Equals("true", StringComparison.OrdinalIgnoreCase); - private static bool InheritsFrom(this INamedTypeSymbol? symbol, string baseName) + public static bool InheritsFrom(this INamedTypeSymbol? symbol, string assemblyName, string typeFullName) { - if (symbol == null) - return false; - - while (true) + while (symbol != null) { - if (symbol.ToString() == baseName) + if (symbol.ContainingAssembly.Name == assemblyName && + symbol.ToString() == typeFullName) { return true; } - if (symbol.BaseType != null) - { - symbol = symbol.BaseType; - continue; - } - - break; + symbol = symbol.BaseType; } return false; } + public static INamedTypeSymbol? GetGodotScriptNativeClass(this INamedTypeSymbol classTypeSymbol) + { + var symbol = classTypeSymbol; + + while (symbol != null) + { + if (symbol.ContainingAssembly.Name == "GodotSharp") + return symbol; + + symbol = symbol.BaseType; + } + + return null; + } + + public static string? GetGodotScriptNativeClassName(this INamedTypeSymbol classTypeSymbol) + { + var nativeType = classTypeSymbol.GetGodotScriptNativeClass(); + + if (nativeType == null) + return null; + + var godotClassNameAttr = nativeType.GetAttributes() + .FirstOrDefault(a => a.AttributeClass?.IsGodotClassNameAttribute() ?? false); + + string? godotClassName = null; + + if (godotClassNameAttr is { ConstructorArguments: { Length: > 0 } }) + godotClassName = godotClassNameAttr.ConstructorArguments[0].Value?.ToString(); + + return godotClassName ?? nativeType.Name; + } + private static bool IsGodotScriptClass( this ClassDeclarationSyntax cds, Compilation compilation, out INamedTypeSymbol? symbol @@ -58,7 +84,7 @@ namespace Godot.SourceGenerators var classTypeSymbol = sm.GetDeclaredSymbol(cds); if (classTypeSymbol?.BaseType == null - || !classTypeSymbol.BaseType.InheritsFrom(GodotClasses.Object)) + || !classTypeSymbol.BaseType.InheritsFrom("GodotSharp", GodotClasses.Object)) { symbol = null; return false; @@ -129,7 +155,101 @@ namespace Godot.SourceGenerators public static string FullQualifiedName(this ITypeSymbol symbol) => symbol.ToDisplayString(NullableFlowState.NotNull, FullyQualifiedFormatOmitGlobal); + public static string NameWithTypeParameters(this INamedTypeSymbol symbol) + { + return symbol.IsGenericType ? + string.Concat(symbol.Name, "<", string.Join(", ", symbol.TypeParameters), ">") : + symbol.Name; + } + public static string FullQualifiedName(this INamespaceSymbol namespaceSymbol) => namespaceSymbol.ToDisplayString(FullyQualifiedFormatOmitGlobal); + + public static string SanitizeQualifiedNameForUniqueHint(this string qualifiedName) + => qualifiedName + // AddSource() doesn't support angle brackets + .Replace("<", "(Of ") + .Replace(">", ")"); + + public static bool IsGodotExportAttribute(this INamedTypeSymbol symbol) + => symbol.ToString() == GodotClasses.ExportAttr; + + public static bool IsGodotClassNameAttribute(this INamedTypeSymbol symbol) + => symbol.ToString() == GodotClasses.GodotClassNameAttr; + + public static bool IsSystemFlagsAttribute(this INamedTypeSymbol symbol) + => symbol.ToString() == GodotClasses.SystemFlagsAttr; + + public static IEnumerable WhereHasGodotCompatibleSignature( + this IEnumerable methods, + MarshalUtils.TypeCache typeCache + ) + { + foreach (var method in methods) + { + if (method.IsGenericMethod) + continue; + + var retType = method.ReturnsVoid ? + null : + MarshalUtils.ConvertManagedTypeToMarshalType(method.ReturnType, typeCache); + + if (retType == null && !method.ReturnsVoid) + continue; + + var parameters = method.Parameters; + + var paramTypes = parameters + // Currently we don't support `ref`, `out`, `in`, `ref readonly` parameters (and we never may) + .Where(p => p.RefKind == RefKind.None) + // Attempt to determine the variant type + .Select(p => MarshalUtils.ConvertManagedTypeToMarshalType(p.Type, typeCache)) + // Discard parameter types that couldn't be determined (null entries) + .Where(t => t != null).Cast().ToImmutableArray(); + + // If any parameter type was incompatible, it was discarded so the length won't match + if (parameters.Length > paramTypes.Length) + continue; + + yield return new GodotMethodData(method, paramTypes, parameters + .Select(p => p.Type).ToImmutableArray(), retType); + } + } + + public static IEnumerable WhereIsGodotCompatibleType( + this IEnumerable properties, + MarshalUtils.TypeCache typeCache + ) + { + foreach (var property in properties) + { + // Ignore properties without a getter. Godot properties must be readable. + if (property.IsWriteOnly) + continue; + + var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(property.Type, typeCache); + + if (marshalType == null) + continue; + + yield return new GodotPropertyData(property, marshalType.Value); + } + } + + public static IEnumerable WhereIsGodotCompatibleType( + this IEnumerable fields, + MarshalUtils.TypeCache typeCache + ) + { + foreach (var field in fields) + { + var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(field.Type, typeCache); + + if (marshalType == null) + continue; + + yield return new GodotFieldData(field, marshalType.Value); + } + } } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj index 791ad85572f..d61d9f7f142 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj @@ -1,7 +1,7 @@ netstandard2.0 - 8.0 + 9.0 enable diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs index 7cc8fa17fc6..0ea1b2f5ce8 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs @@ -4,5 +4,8 @@ namespace Godot.SourceGenerators { public const string Object = "Godot.Object"; public const string AssemblyHasScriptsAttr = "Godot.AssemblyHasScriptsAttribute"; + public const string ExportAttr = "Godot.ExportAttribute"; + public const string GodotClassNameAttr = "Godot.GodotClassName"; + public const string SystemFlagsAttr = "System.FlagsAttribute"; } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs new file mode 100644 index 00000000000..99d3a495465 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs @@ -0,0 +1,134 @@ +using System; + +namespace Godot.SourceGenerators +{ + internal enum VariantType + { + Nil = 0, + Bool = 1, + Int = 2, + Float = 3, + String = 4, + Vector2 = 5, + Vector2i = 6, + Rect2 = 7, + Rect2i = 8, + Vector3 = 9, + Vector3i = 10, + Transform2d = 11, + Vector4 = 12, + Vector4i = 13, + Plane = 14, + Quaternion = 15, + Aabb = 16, + Basis = 17, + Transform3d = 18, + Projection = 19, + Color = 20, + StringName = 21, + NodePath = 22, + Rid = 23, + Object = 24, + Callable = 25, + Signal = 26, + Dictionary = 27, + Array = 28, + PackedByteArray = 29, + PackedInt32Array = 30, + PackedInt64Array = 31, + PackedFloat32Array = 32, + PackedFloat64Array = 33, + PackedStringArray = 34, + PackedVector2Array = 35, + PackedVector3Array = 36, + PackedColorArray = 37, + Max = 38 + } + + internal enum PropertyHint + { + None = 0, + Range = 1, + Enum = 2, + EnumSuggestion = 3, + ExpEasing = 4, + Link = 5, + Flags = 6, + Layers2dRender = 7, + Layers2dPhysics = 8, + Layers2dNavigation = 9, + Layers3dRender = 10, + Layers3dPhysics = 11, + Layers3dNavigation = 12, + File = 13, + Dir = 14, + GlobalFile = 15, + GlobalDir = 16, + ResourceType = 17, + MultilineText = 18, + Expression = 19, + PlaceholderText = 20, + ColorNoAlpha = 21, + ImageCompressLossy = 22, + ImageCompressLossless = 23, + ObjectId = 24, + TypeString = 25, + NodePathToEditedNode = 26, + MethodOfVariantType = 27, + MethodOfBaseType = 28, + MethodOfInstance = 29, + MethodOfScript = 30, + PropertyOfVariantType = 31, + PropertyOfBaseType = 32, + PropertyOfInstance = 33, + PropertyOfScript = 34, + ObjectTooBig = 35, + NodePathValidTypes = 36, + SaveFile = 37, + GlobalSaveFile = 38, + IntIsObjectid = 39, + IntIsPointer = 41, + ArrayType = 40, + LocaleId = 42, + LocalizableString = 43, + NodeType = 44, + Max = 45 + } + + [Flags] + internal enum PropertyUsageFlags + { + None = 0, + Storage = 2, + Editor = 4, + Checkable = 8, + Checked = 16, + Internationalized = 32, + Group = 64, + Category = 128, + Subgroup = 256, + ClassIsBitfield = 512, + NoInstanceState = 1024, + RestartIfChanged = 2048, + ScriptVariable = 4096, + StoreIfNull = 8192, + AnimateAsTrigger = 16384, + UpdateAllIfModified = 32768, + ScriptDefaultValue = 65536, + ClassIsEnum = 131072, + NilIsVariant = 262144, + Internal = 524288, + DoNotShareOnDuplicate = 1048576, + HighEndGfx = 2097152, + NodePathFromSceneRoot = 4194304, + ResourceNotPersistent = 8388608, + KeyingIncrements = 16777216, + DeferredSetResource = 33554432, + EditorInstantiateObject = 67108864, + EditorBasicSetting = 134217728, + Array = 536870912, + Default = 6, + DefaultIntl = 38, + NoEditor = 2 + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotMemberData.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotMemberData.cs new file mode 100644 index 00000000000..ff640a7a96b --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotMemberData.cs @@ -0,0 +1,46 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace Godot.SourceGenerators +{ + public struct GodotMethodData + { + public GodotMethodData(IMethodSymbol method, ImmutableArray paramTypes, + ImmutableArray paramTypeSymbols, MarshalType? retType) + { + Method = method; + ParamTypes = paramTypes; + ParamTypeSymbols = paramTypeSymbols; + RetType = retType; + } + + public IMethodSymbol Method { get; } + public ImmutableArray ParamTypes { get; } + public ImmutableArray ParamTypeSymbols { get; } + public MarshalType? RetType { get; } + } + + public struct GodotPropertyData + { + public GodotPropertyData(IPropertySymbol propertySymbol, MarshalType type) + { + PropertySymbol = propertySymbol; + Type = type; + } + + public IPropertySymbol PropertySymbol { get; } + public MarshalType Type { get; } + } + + public struct GodotFieldData + { + public GodotFieldData(IFieldSymbol fieldSymbol, MarshalType type) + { + FieldSymbol = fieldSymbol; + Type = type; + } + + public IFieldSymbol FieldSymbol { get; } + public MarshalType Type { get; } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalType.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalType.cs index 7c8345d16ac..1c4c19569e7 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalType.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalType.cs @@ -30,6 +30,9 @@ namespace Godot.SourceGenerators Basis, Quaternion, Transform3D, + Vector4, + Vector4i, + Projection, AABB, Color, Plane, diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs index a77e1800fb5..5a4badd66e0 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs @@ -1,9 +1,10 @@ using System; +using System.Linq; using Microsoft.CodeAnalysis; namespace Godot.SourceGenerators { - public static class MarshalUtils + internal static class MarshalUtils { public class TypeCache { @@ -35,7 +36,73 @@ namespace Godot.SourceGenerators } } - public static MarshalType? ConvertManagedTypeToVariantType(ITypeSymbol type, TypeCache typeCache) + public static VariantType? ConvertMarshalTypeToVariantType(MarshalType marshalType) + => marshalType switch + { + MarshalType.Boolean => VariantType.Bool, + MarshalType.Char => VariantType.Int, + MarshalType.SByte => VariantType.Int, + MarshalType.Int16 => VariantType.Int, + MarshalType.Int32 => VariantType.Int, + MarshalType.Int64 => VariantType.Int, + MarshalType.Byte => VariantType.Int, + MarshalType.UInt16 => VariantType.Int, + MarshalType.UInt32 => VariantType.Int, + MarshalType.UInt64 => VariantType.Int, + MarshalType.Single => VariantType.Float, + MarshalType.Double => VariantType.Float, + MarshalType.String => VariantType.String, + MarshalType.Vector2 => VariantType.Vector2, + MarshalType.Vector2i => VariantType.Vector2i, + MarshalType.Rect2 => VariantType.Rect2, + MarshalType.Rect2i => VariantType.Rect2i, + MarshalType.Transform2D => VariantType.Transform2d, + MarshalType.Vector3 => VariantType.Vector3, + MarshalType.Vector3i => VariantType.Vector3i, + MarshalType.Basis => VariantType.Basis, + MarshalType.Quaternion => VariantType.Quaternion, + MarshalType.Transform3D => VariantType.Transform3d, + MarshalType.Vector4 => VariantType.Vector4, + MarshalType.Vector4i => VariantType.Vector4i, + MarshalType.Projection => VariantType.Projection, + MarshalType.AABB => VariantType.Aabb, + MarshalType.Color => VariantType.Color, + MarshalType.Plane => VariantType.Plane, + MarshalType.Callable => VariantType.Callable, + MarshalType.SignalInfo => VariantType.Signal, + MarshalType.Enum => VariantType.Int, + MarshalType.ByteArray => VariantType.PackedByteArray, + MarshalType.Int32Array => VariantType.PackedInt32Array, + MarshalType.Int64Array => VariantType.PackedInt64Array, + MarshalType.SingleArray => VariantType.PackedFloat32Array, + MarshalType.DoubleArray => VariantType.PackedFloat64Array, + MarshalType.StringArray => VariantType.PackedStringArray, + MarshalType.Vector2Array => VariantType.PackedVector2Array, + MarshalType.Vector3Array => VariantType.PackedVector3Array, + MarshalType.ColorArray => VariantType.PackedColorArray, + MarshalType.GodotObjectOrDerivedArray => VariantType.Array, + MarshalType.SystemObjectArray => VariantType.Array, + MarshalType.GodotGenericDictionary => VariantType.Dictionary, + MarshalType.GodotGenericArray => VariantType.Array, + MarshalType.SystemGenericDictionary => VariantType.Dictionary, + MarshalType.SystemGenericList => VariantType.Array, + MarshalType.GenericIDictionary => VariantType.Dictionary, + MarshalType.GenericICollection => VariantType.Array, + MarshalType.GenericIEnumerable => VariantType.Array, + MarshalType.SystemObject => VariantType.Nil, + MarshalType.GodotObjectOrDerived => VariantType.Object, + MarshalType.StringName => VariantType.StringName, + MarshalType.NodePath => VariantType.NodePath, + MarshalType.RID => VariantType.Rid, + MarshalType.GodotDictionary => VariantType.Dictionary, + MarshalType.GodotArray => VariantType.Array, + MarshalType.IDictionary => VariantType.Dictionary, + MarshalType.ICollection => VariantType.Array, + MarshalType.IEnumerable => VariantType.Array, + _ => null + }; + + public static MarshalType? ConvertManagedTypeToMarshalType(ITypeSymbol type, TypeCache typeCache) { var specialType = type.SpecialType; @@ -69,39 +136,44 @@ namespace Godot.SourceGenerators return MarshalType.String; case SpecialType.System_Object: return MarshalType.SystemObject; - case SpecialType.System_ValueType: - { - if (type.ContainingAssembly.Name == "GodotSharp" && - type.ContainingNamespace.Name == "Godot") - { - return type switch - { - { Name: "Vector2" } => MarshalType.Vector2, - { Name: "Vector2i" } => MarshalType.Vector2i, - { Name: "Rect2" } => MarshalType.Rect2, - { Name: "Rect2i" } => MarshalType.Rect2i, - { Name: "Transform2D" } => MarshalType.Transform2D, - { Name: "Vector3" } => MarshalType.Vector3, - { Name: "Vector3i" } => MarshalType.Vector3i, - { Name: "Basis" } => MarshalType.Basis, - { Name: "Quaternion" } => MarshalType.Quaternion, - { Name: "Transform3D" } => MarshalType.Transform3D, - { Name: "AABB" } => MarshalType.AABB, - { Name: "Color" } => MarshalType.Color, - { Name: "Plane" } => MarshalType.Plane, - { Name: "RID" } => MarshalType.RID, - { Name: "Callable" } => MarshalType.Callable, - { Name: "SignalInfo" } => MarshalType.SignalInfo, - { TypeKind: TypeKind.Enum } => MarshalType.Enum, - _ => null - }; - } - - return null; - } default: { - if (type.TypeKind == TypeKind.Array) + var typeKind = type.TypeKind; + + if (typeKind == TypeKind.Enum) + return MarshalType.Enum; + + if (typeKind == TypeKind.Struct) + { + if (type.ContainingAssembly.Name == "GodotSharp" && + type.ContainingNamespace.Name == "Godot") + { + return type switch + { + { Name: "Vector2" } => MarshalType.Vector2, + { Name: "Vector2i" } => MarshalType.Vector2i, + { Name: "Rect2" } => MarshalType.Rect2, + { Name: "Rect2i" } => MarshalType.Rect2i, + { Name: "Transform2D" } => MarshalType.Transform2D, + { Name: "Vector3" } => MarshalType.Vector3, + { Name: "Vector3i" } => MarshalType.Vector3i, + { Name: "Basis" } => MarshalType.Basis, + { Name: "Quaternion" } => MarshalType.Quaternion, + { Name: "Transform3D" } => MarshalType.Transform3D, + { Name: "Vector4" } => MarshalType.Vector4, + { Name: "Vector4i" } => MarshalType.Vector4i, + { Name: "Projection" } => MarshalType.Projection, + { Name: "AABB" } => MarshalType.AABB, + { Name: "Color" } => MarshalType.Color, + { Name: "Plane" } => MarshalType.Plane, + { Name: "RID" } => MarshalType.RID, + { Name: "Callable" } => MarshalType.Callable, + { Name: "SignalInfo" } => MarshalType.SignalInfo, + _ => null + }; + } + } + else if (typeKind == TypeKind.Array) { var arrayType = (IArrayTypeSymbol)type; var elementType = arrayType.ElementType; @@ -127,17 +199,24 @@ namespace Godot.SourceGenerators if (elementType.SimpleDerivesFrom(typeCache.GodotObjectType)) return MarshalType.GodotObjectOrDerivedArray; - if (type.ContainingAssembly.Name == "GodotSharp" && - type.ContainingNamespace.Name == "Godot") + if (elementType.ContainingAssembly.Name == "GodotSharp" && + elementType.ContainingNamespace.Name == "Godot") { - return elementType switch + switch (elementType) { - { Name: "Vector2" } => MarshalType.Vector2Array, - { Name: "Vector3" } => MarshalType.Vector3Array, - { Name: "Color" } => MarshalType.ColorArray, - _ => null - }; + case { Name: "Vector2" }: + return MarshalType.Vector2Array; + case { Name: "Vector3" }: + return MarshalType.Vector3Array; + case { Name: "Color" }: + return MarshalType.ColorArray; + } } + + if (ConvertManagedTypeToMarshalType(elementType, typeCache) != null) + return MarshalType.GodotArray; + + return null; } else if (type is INamedTypeSymbol { IsGenericType: true } genericType) { @@ -190,7 +269,10 @@ namespace Godot.SourceGenerators { Name: "NodePath" } => MarshalType.NodePath, _ => null }; - case "Godot.Collections" when !(type is INamedTypeSymbol { IsGenericType: true }): + case "Collections" + when !(type is INamedTypeSymbol { IsGenericType: true }) && + type.ContainingNamespace.FullQualifiedName() == + "Godot.Collections": return type switch { { Name: "Dictionary" } => MarshalType.GodotDictionary, @@ -220,5 +302,19 @@ namespace Godot.SourceGenerators return false; } + + public static ITypeSymbol? GetArrayElementType(ITypeSymbol typeSymbol) + { + if (typeSymbol.TypeKind == TypeKind.Array) + { + var arrayType = (IArrayTypeSymbol)typeSymbol; + return arrayType.ElementType; + } + + if (typeSymbol is INamedTypeSymbol { IsGenericType: true } genericType) + return genericType.TypeArguments.FirstOrDefault(); + + return null; + } } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptBoilerplateGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMemberInvokerGenerator.cs similarity index 64% rename from modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptBoilerplateGenerator.cs rename to modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMemberInvokerGenerator.cs index 51e9406c15f..6d3d03c4959 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptBoilerplateGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMemberInvokerGenerator.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; @@ -9,7 +7,7 @@ using Microsoft.CodeAnalysis.Text; namespace Godot.SourceGenerators { [Generator] - public class ScriptBoilerplateGenerator : ISourceGenerator + public class ScriptMemberInvokerGenerator : ISourceGenerator { public void Execute(GeneratorExecutionContext context) { @@ -61,8 +59,6 @@ namespace Godot.SourceGenerators INamedTypeSymbol symbol ) { - string className = symbol.Name; - INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace; string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ? namespaceSymbol.FullQualifiedName() : @@ -71,9 +67,8 @@ namespace Godot.SourceGenerators bool isInnerClass = symbol.ContainingType != null; - string uniqueName = hasNamespace ? - classNs + "." + className + "_ScriptBoilerplate_Generated" : - className + "_ScriptBoilerplate_Generated"; + string uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint() + + "_ScriptMemberInvoker_Generated"; var source = new StringBuilder(); @@ -97,7 +92,7 @@ namespace Godot.SourceGenerators source.Append("partial "); source.Append(containingType.GetDeclarationKeyword()); source.Append(" "); - source.Append(containingType.Name); + source.Append(containingType.NameWithTypeParameters()); source.Append("\n{\n"); containingType = containingType.ContainingType; @@ -105,7 +100,7 @@ namespace Godot.SourceGenerators } source.Append("partial class "); - source.Append(className); + source.Append(symbol.NameWithTypeParameters()); source.Append("\n{\n"); var members = symbol.GetMembers(); @@ -113,27 +108,28 @@ namespace Godot.SourceGenerators // TODO: Static static marshaling (no reflection, no runtime type checks) var methodSymbols = members - .Where(s => s.Kind == SymbolKind.Method) + .Where(s => !s.IsStatic && s.Kind == SymbolKind.Method && !s.IsImplicitlyDeclared) .Cast() - .Where(m => m.MethodKind == MethodKind.Ordinary && !m.IsImplicitlyDeclared); + .Where(m => m.MethodKind == MethodKind.Ordinary); var propertySymbols = members - .Where(s => s.Kind == SymbolKind.Property) + .Where(s => !s.IsStatic && s.Kind == SymbolKind.Property) .Cast(); var fieldSymbols = members - .Where(s => s.Kind == SymbolKind.Field) - .Cast() - .Where(p => !p.IsImplicitlyDeclared); + .Where(s => !s.IsStatic && s.Kind == SymbolKind.Field && !s.IsImplicitlyDeclared) + .Cast(); - var godotClassMethods = WhereHasCompatibleGodotType(methodSymbols, typeCache).ToArray(); - var godotClassProperties = WhereIsCompatibleGodotType(propertySymbols, typeCache).ToArray(); - var godotClassFields = WhereIsCompatibleGodotType(fieldSymbols, typeCache).ToArray(); + var godotClassMethods = methodSymbols.WhereHasGodotCompatibleSignature(typeCache).ToArray(); + var godotClassProperties = propertySymbols.WhereIsGodotCompatibleType(typeCache).ToArray(); + var godotClassFields = fieldSymbols.WhereIsGodotCompatibleType(typeCache).ToArray(); - source.Append(" private class GodotInternal {\n"); + source.Append(" private partial class GodotInternal {\n"); // Generate cached StringNames for methods and properties, for fast lookup + // TODO: Move the generation of these cached StringNames to its own generator + foreach (var method in godotClassMethods) { string methodName = method.Method.Name; @@ -144,26 +140,6 @@ namespace Godot.SourceGenerators source.Append("\";\n"); } - foreach (var property in godotClassProperties) - { - string propertyName = property.Property.Name; - source.Append(" public static readonly StringName PropName_"); - source.Append(propertyName); - source.Append(" = \""); - source.Append(propertyName); - source.Append("\";\n"); - } - - foreach (var field in godotClassFields) - { - string fieldName = field.Field.Name; - source.Append(" public static readonly StringName PropName_"); - source.Append(fieldName); - source.Append(" = \""); - source.Append(fieldName); - source.Append("\";\n"); - } - source.Append(" }\n"); // class GodotInternal // Generate InvokeGodotClassMethod @@ -191,8 +167,8 @@ namespace Godot.SourceGenerators // Setters - bool allPropertiesAreReadOnly = godotClassFields.All(fi => fi.Field.IsReadOnly) && - godotClassProperties.All(pi => pi.Property.IsReadOnly); + bool allPropertiesAreReadOnly = godotClassFields.All(fi => fi.FieldSymbol.IsReadOnly) && + godotClassProperties.All(pi => pi.PropertySymbol.IsReadOnly); if (!allPropertiesAreReadOnly) { @@ -202,21 +178,21 @@ namespace Godot.SourceGenerators isFirstEntry = true; foreach (var property in godotClassProperties) { - if (property.Property.IsReadOnly) + if (property.PropertySymbol.IsReadOnly) continue; - GeneratePropertySetter(property.Property.Name, - property.Property.Type.FullQualifiedName(), source, isFirstEntry); + GeneratePropertySetter(property.PropertySymbol.Name, + property.PropertySymbol.Type.FullQualifiedName(), source, isFirstEntry); isFirstEntry = false; } foreach (var field in godotClassFields) { - if (field.Field.IsReadOnly) + if (field.FieldSymbol.IsReadOnly) continue; - GeneratePropertySetter(field.Field.Name, - field.Field.Type.FullQualifiedName(), source, isFirstEntry); + GeneratePropertySetter(field.FieldSymbol.Name, + field.FieldSymbol.Type.FullQualifiedName(), source, isFirstEntry); isFirstEntry = false; } @@ -233,13 +209,13 @@ namespace Godot.SourceGenerators isFirstEntry = true; foreach (var property in godotClassProperties) { - GeneratePropertyGetter(property.Property.Name, source, isFirstEntry); + GeneratePropertyGetter(property.PropertySymbol.Name, source, isFirstEntry); isFirstEntry = false; } foreach (var field in godotClassFields) { - GeneratePropertyGetter(field.Field.Name, source, isFirstEntry); + GeneratePropertyGetter(field.FieldSymbol.Name, source, isFirstEntry); isFirstEntry = false; } @@ -285,11 +261,11 @@ namespace Godot.SourceGenerators source.Append("\n}\n"); } - context.AddSource(uniqueName, SourceText.From(source.ToString(), Encoding.UTF8)); + context.AddSource(uniqueHint, SourceText.From(source.ToString(), Encoding.UTF8)); } private static void GenerateMethodInvoker( - GodotMethodInfo method, + GodotMethodData method, StringBuilder source ) { @@ -399,7 +375,7 @@ namespace Godot.SourceGenerators } private static void GenerateHasMethodEntry( - GodotMethodInfo method, + GodotMethodData method, StringBuilder source, bool isFirstEntry ) @@ -417,118 +393,5 @@ namespace Godot.SourceGenerators public void Initialize(GeneratorInitializationContext context) { } - - private struct GodotMethodInfo - { - public GodotMethodInfo(IMethodSymbol method, ImmutableArray paramTypes, - ImmutableArray paramTypeSymbols, MarshalType? retType) - { - Method = method; - ParamTypes = paramTypes; - ParamTypeSymbols = paramTypeSymbols; - RetType = retType; - } - - public IMethodSymbol Method { get; } - public ImmutableArray ParamTypes { get; } - public ImmutableArray ParamTypeSymbols { get; } - public MarshalType? RetType { get; } - } - - private struct GodotPropertyInfo - { - public GodotPropertyInfo(IPropertySymbol property, MarshalType type) - { - Property = property; - Type = type; - } - - public IPropertySymbol Property { get; } - public MarshalType Type { get; } - } - - private struct GodotFieldInfo - { - public GodotFieldInfo(IFieldSymbol field, MarshalType type) - { - Field = field; - Type = type; - } - - public IFieldSymbol Field { get; } - public MarshalType Type { get; } - } - - private static IEnumerable WhereHasCompatibleGodotType( - IEnumerable methods, - MarshalUtils.TypeCache typeCache - ) - { - foreach (var method in methods) - { - if (method.IsGenericMethod) - continue; - - var retType = method.ReturnsVoid ? - null : - MarshalUtils.ConvertManagedTypeToVariantType(method.ReturnType, typeCache); - - if (retType == null && !method.ReturnsVoid) - continue; - - var parameters = method.Parameters; - - var paramTypes = parameters - // Currently we don't support `ref`, `out`, `in`, `ref readonly` parameters (and we never may) - .Where(p => p.RefKind == RefKind.None) - // Attempt to determine the variant type - .Select(p => MarshalUtils.ConvertManagedTypeToVariantType(p.Type, typeCache)) - // Discard parameter types that couldn't be determined (null entries) - .Where(t => t != null).Cast().ToImmutableArray(); - - // If any parameter type was incompatible, it was discarded so the length won't match - if (parameters.Length > paramTypes.Length) - continue; // Ignore incompatible method - - yield return new GodotMethodInfo(method, paramTypes, parameters - .Select(p => p.Type).ToImmutableArray(), retType); - } - } - - private static IEnumerable WhereIsCompatibleGodotType( - IEnumerable properties, - MarshalUtils.TypeCache typeCache - ) - { - foreach (var property in properties) - { - // Ignore properties without a getter. Godot properties must be readable. - if (property.IsWriteOnly) - continue; - - var marshalType = MarshalUtils.ConvertManagedTypeToVariantType(property.Type, typeCache); - - if (marshalType == null) - continue; - - yield return new GodotPropertyInfo(property, marshalType.Value); - } - } - - private static IEnumerable WhereIsCompatibleGodotType( - IEnumerable fields, - MarshalUtils.TypeCache typeCache - ) - { - foreach (var field in fields) - { - var marshalType = MarshalUtils.ConvertManagedTypeToVariantType(field.Type, typeCache); - - if (marshalType == null) - continue; - - yield return new GodotFieldInfo(field, marshalType.Value); - } - } } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs index b6252870873..e8a9e28d0c8 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs @@ -90,21 +90,14 @@ namespace Godot.SourceGenerators attributes.Append(@""")]"); } - string className = symbol.Name; - INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace; string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ? namespaceSymbol.FullQualifiedName() : string.Empty; bool hasNamespace = classNs.Length != 0; - var uniqueName = new StringBuilder(); - if (hasNamespace) - uniqueName.Append($"{classNs}."); - uniqueName.Append(className); - if (symbol.IsGenericType) - uniqueName.Append($"Of{string.Join(string.Empty, symbol.TypeParameters)}"); - uniqueName.Append("_ScriptPath_Generated"); + var uniqueHint = symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint() + + "_ScriptPath_Generated"; var source = new StringBuilder(); @@ -124,10 +117,8 @@ namespace Godot.SourceGenerators } source.Append(attributes); - source.Append("\n partial class "); - source.Append(className); - if (symbol.IsGenericType) - source.Append($"<{string.Join(", ", symbol.TypeParameters)}>"); + source.Append("\npartial class "); + source.Append(symbol.NameWithTypeParameters()); source.Append("\n{\n}\n"); if (hasNamespace) @@ -135,7 +126,7 @@ namespace Godot.SourceGenerators source.Append("\n}\n"); } - context.AddSource(uniqueName.ToString(), SourceText.From(source.ToString(), Encoding.UTF8)); + context.AddSource(uniqueHint.ToString(), SourceText.From(source.ToString(), Encoding.UTF8)); } private static void AddScriptTypesAssemblyAttr(GeneratorExecutionContext context, diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs new file mode 100644 index 00000000000..85fa65d1afa --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs @@ -0,0 +1,527 @@ +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Godot.SourceGenerators +{ + [Generator] + public class ScriptPropertiesGenerator : 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() + .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(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() + + "_ScriptProperties_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(); + + var fieldSymbols = members + .Where(s => !s.IsStatic && s.Kind == SymbolKind.Field && !s.IsImplicitlyDeclared) + .Cast(); + + var godotClassProperties = propertySymbols.WhereIsGodotCompatibleType(typeCache).ToArray(); + var godotClassFields = fieldSymbols.WhereIsGodotCompatibleType(typeCache).ToArray(); + + source.Append(" private partial class GodotInternal {\n"); + + // Generate cached StringNames for methods and properties, for fast lookup + + foreach (var property in godotClassProperties) + { + string propertyName = property.PropertySymbol.Name; + source.Append(" public static readonly StringName PropName_"); + source.Append(propertyName); + source.Append(" = \""); + source.Append(propertyName); + source.Append("\";\n"); + } + + foreach (var field in godotClassFields) + { + string fieldName = field.FieldSymbol.Name; + source.Append(" public static readonly StringName PropName_"); + source.Append(fieldName); + source.Append(" = \""); + source.Append(fieldName); + source.Append("\";\n"); + } + + source.Append(" }\n"); // class GodotInternal + + // Generate GetGodotPropertiesMetadata + + if (godotClassProperties.Length > 0 || godotClassFields.Length > 0) + { + source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n"); + + string dictionaryType = "System.Collections.Generic.List"; + + source.Append(" internal new static ") + .Append(dictionaryType) + .Append(" GetGodotPropertiesMetadata()\n {\n"); + + source.Append(" var properties = new ") + .Append(dictionaryType) + .Append("();\n"); + + foreach (var property in godotClassProperties) + { + var propertyInfo = GetPropertyMetadata(context, typeCache, + property.PropertySymbol, property.Type); + + if (propertyInfo == null) + continue; + + AppendPropertyInfo(source, propertyInfo.Value); + } + + foreach (var field in godotClassFields) + { + var propertyInfo = GetPropertyMetadata(context, typeCache, + field.FieldSymbol, field.Type); + + if (propertyInfo == null) + continue; + + AppendPropertyInfo(source, propertyInfo.Value); + } + + source.Append(" return properties;\n"); + source.Append(" }\n"); + + source.Append("#pragma warning restore CS0109\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)); + } + + private static void AppendPropertyInfo(StringBuilder source, PropertyInfo propertyInfo) + { + source.Append(" properties.Add(new Godot.Bridge.PropertyInfo(type: (Godot.Variant.Type)") + .Append((int)propertyInfo.Type) + .Append(", name: GodotInternal.PropName_") + .Append(propertyInfo.Name) + .Append(", hint: (Godot.PropertyHint)") + .Append((int)propertyInfo.Hint) + .Append(", hintString: \"") + .Append(propertyInfo.HintString) + .Append("\", usage: (Godot.PropertyUsageFlags)") + .Append((int)propertyInfo.Usage) + .Append(", exported: ") + .Append(propertyInfo.Exported ? "true" : "false") + .Append("));\n"); + } + + private struct PropertyInfo + { + public PropertyInfo(VariantType type, string name, PropertyHint hint, + string? hintString, PropertyUsageFlags usage, bool exported) + { + Type = type; + Name = name; + Hint = hint; + HintString = hintString; + Usage = usage; + Exported = exported; + } + + public VariantType Type { get; } + public string Name { get; } + public PropertyHint Hint { get; } + public string? HintString { get; } + public PropertyUsageFlags Usage { get; } + public bool Exported { get; } + } + + private static PropertyInfo? GetPropertyMetadata( + GeneratorExecutionContext context, + MarshalUtils.TypeCache typeCache, + ISymbol memberSymbol, + MarshalType marshalType + ) + { + var exportAttr = memberSymbol.GetAttributes() + .FirstOrDefault(a => a.AttributeClass?.IsGodotExportAttribute() ?? false); + + var propertySymbol = memberSymbol as IPropertySymbol; + var fieldSymbol = memberSymbol as IFieldSymbol; + + if (exportAttr != null && propertySymbol != null) + { + if (propertySymbol.GetMethod == null) + { + // This should never happen, as we filtered WriteOnly properties, but just in case. + Common.ReportExportedMemberIsWriteOnly(context, propertySymbol); + return null; + } + + if (propertySymbol.SetMethod == null) + { + // This should never happen, as we filtered ReadOnly properties, but just in case. + Common.ReportExportedMemberIsReadOnly(context, propertySymbol); + return null; + } + } + + var memberType = propertySymbol?.Type ?? fieldSymbol!.Type; + + var memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType)!.Value; + string memberName = memberSymbol.Name; + + if (exportAttr == null) + { + return new PropertyInfo(memberVariantType, memberName, PropertyHint.None, + hintString: null, PropertyUsageFlags.ScriptVariable, exported: false); + } + + if (!TryGetMemberExportHint(typeCache, memberType, exportAttr, memberVariantType, + isTypeArgument: false, out var hint, out var hintString)) + { + var constructorArguments = exportAttr.ConstructorArguments; + + if (constructorArguments.Length > 0) + { + var hintValue = exportAttr.ConstructorArguments[0].Value; + + hint = hintValue switch + { + null => PropertyHint.None, + int intValue => (PropertyHint)intValue, + _ => (PropertyHint)(long)hintValue + }; + + hintString = constructorArguments.Length > 1 ? + exportAttr.ConstructorArguments[1].Value?.ToString() : + null; + } + else + { + hint = PropertyHint.None; + } + } + + var propUsage = PropertyUsageFlags.Default | PropertyUsageFlags.ScriptVariable; + + if (memberVariantType == VariantType.Nil) + propUsage |= PropertyUsageFlags.NilIsVariant; + + return new PropertyInfo(memberVariantType, memberName, + hint, hintString, propUsage, exported: true); + } + + private static bool TryGetMemberExportHint( + MarshalUtils.TypeCache typeCache, + ITypeSymbol type, AttributeData exportAttr, + VariantType variantType, bool isTypeArgument, + out PropertyHint hint, out string? hintString + ) + { + hint = PropertyHint.None; + hintString = null; + + if (variantType == VariantType.Nil) + return true; // Variant, no export hint + + if (variantType == VariantType.Int && + type.IsValueType && type.TypeKind == TypeKind.Enum) + { + bool hasFlagsAttr = type.GetAttributes() + .Any(a => a.AttributeClass?.IsSystemFlagsAttribute() ?? false); + + hint = hasFlagsAttr ? PropertyHint.Flags : PropertyHint.Enum; + + var members = type.GetMembers(); + + var enumFields = members + .Where(s => s.Kind == SymbolKind.Field && s.IsStatic && + s.DeclaredAccessibility == Accessibility.Public && + !s.IsImplicitlyDeclared) + .Cast().ToArray(); + + var hintStringBuilder = new StringBuilder(); + var nameOnlyHintStringBuilder = new StringBuilder(); + + // True: enum Foo { Bar, Baz, Qux } + // True: enum Foo { Bar = 0, Baz = 1, Qux = 2 } + // False: enum Foo { Bar = 0, Baz = 7, Qux = 5 } + bool usesDefaultValues = true; + + for (int i = 0; i < enumFields.Length; i++) + { + var enumField = enumFields[i]; + + if (i > 0) + { + hintStringBuilder.Append(","); + nameOnlyHintStringBuilder.Append(","); + } + + string enumFieldName = enumField.Name; + hintStringBuilder.Append(enumFieldName); + nameOnlyHintStringBuilder.Append(enumFieldName); + + long val = enumField.ConstantValue switch + { + sbyte v => v, + short v => v, + int v => v, + long v => v, + byte v => v, + ushort v => v, + uint v => v, + ulong v => (long)v, + _ => 0 + }; + + uint expectedVal = (uint)(hint == PropertyHint.Flags ? 1 << i : i); + if (val != expectedVal) + usesDefaultValues = false; + + hintStringBuilder.Append(":"); + hintStringBuilder.Append(val); + } + + hintString = !usesDefaultValues ? + hintStringBuilder.ToString() : + // If we use the format NAME:VAL, that's what the editor displays. + // That's annoying if the user is not using custom values for the enum constants. + // This may not be needed in the future if the editor is changed to not display values. + nameOnlyHintStringBuilder.ToString(); + + return true; + } + + if (variantType == VariantType.Object && type is INamedTypeSymbol memberNamedType) + { + if (memberNamedType.InheritsFrom("GodotSharp", "Godot.Resource")) + { + string nativeTypeName = memberNamedType.GetGodotScriptNativeClassName()!; + + hint = PropertyHint.ResourceType; + hintString = nativeTypeName; + + return true; + } + + if (memberNamedType.InheritsFrom("GodotSharp", "Godot.Node")) + { + string nativeTypeName = memberNamedType.GetGodotScriptNativeClassName()!; + + hint = PropertyHint.NodeType; + hintString = nativeTypeName; + + return true; + } + } + + static bool GetStringArrayEnumHint(VariantType elementVariantType, + AttributeData exportAttr, out string? hintString) + { + var constructorArguments = exportAttr.ConstructorArguments; + + if (constructorArguments.Length > 0) + { + var presetHintValue = exportAttr.ConstructorArguments[0].Value; + + PropertyHint presetHint = presetHintValue switch + { + null => PropertyHint.None, + int intValue => (PropertyHint)intValue, + _ => (PropertyHint)(long)presetHintValue + }; + + if (presetHint == PropertyHint.Enum) + { + string? presetHintString = constructorArguments.Length > 1 ? + exportAttr.ConstructorArguments[1].Value?.ToString() : + null; + + hintString = (int)elementVariantType + "/" + (int)PropertyHint.Enum + ":"; + + if (presetHintString != null) + hintString += presetHintString; + + return true; + } + } + + hintString = null; + return false; + } + + if (!isTypeArgument && variantType == VariantType.Array) + { + var elementType = MarshalUtils.GetArrayElementType(type); + + if (elementType == null) + return false; // Non-generic Array, so there's no hint to add + + var elementMarshalType = MarshalUtils.ConvertManagedTypeToMarshalType(elementType, typeCache)!.Value; + var elementVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(elementMarshalType)!.Value; + + bool isPresetHint = false; + + if (elementVariantType == VariantType.String) + isPresetHint = GetStringArrayEnumHint(elementVariantType, exportAttr, out hintString); + + if (!isPresetHint) + { + bool hintRes = TryGetMemberExportHint(typeCache, elementType, + exportAttr, elementVariantType, isTypeArgument: true, + out var elementHint, out var elementHintString); + + // Format: type/hint:hint_string + if (hintRes) + { + hintString = (int)elementVariantType + "/" + (int)elementHint + ":"; + + if (elementHintString != null) + hintString += elementHintString; + } + else + { + hintString = (int)elementVariantType + "/" + (int)PropertyHint.None + ":"; + } + } + + hint = PropertyHint.TypeString; + + return hintString != null; + } + + if (!isTypeArgument && variantType == VariantType.PackedStringArray) + { + if (GetStringArrayEnumHint(VariantType.String, exportAttr, out hintString)) + { + hint = PropertyHint.TypeString; + return true; + } + } + + if (!isTypeArgument && variantType == VariantType.Dictionary) + { + // TODO: Dictionaries are not supported in the inspector + return false; + } + + return false; + } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs new file mode 100644 index 00000000000..3b8ba211077 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs @@ -0,0 +1,293 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Godot.SourceGenerators +{ + [Generator] + public class ScriptPropertyDefValGenerator : 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() + .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(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() + + "_ScriptPropertyDefVal_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 exportedMembers = new List(); + + var members = symbol.GetMembers(); + + var exportedProperties = members + .Where(s => !s.IsStatic && s.Kind == SymbolKind.Property) + .Cast() + .Where(s => s.GetAttributes() + .Any(a => a.AttributeClass?.IsGodotExportAttribute() ?? false)) + .ToArray(); + + var exportedFields = members + .Where(s => !s.IsStatic && s.Kind == SymbolKind.Field && !s.IsImplicitlyDeclared) + .Cast() + .Where(s => s.GetAttributes() + .Any(a => a.AttributeClass?.IsGodotExportAttribute() ?? false)) + .ToArray(); + + foreach (var property in exportedProperties) + { + if (property.IsStatic) + { + Common.ReportExportedMemberIsStatic(context, property); + continue; + } + + // 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) + { + Common.ReportExportedMemberIsWriteOnly(context, property); + continue; + } + + if (property.IsReadOnly) + { + Common.ReportExportedMemberIsReadOnly(context, property); + continue; + } + + + var propertyType = property.Type; + var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(propertyType, typeCache); + + if (marshalType == null) + { + Common.ReportExportedMemberTypeNotSupported(context, property); + continue; + } + + // TODO: Detect default value from simple property getters (currently we only detect from initializers) + + EqualsValueClauseSyntax? initializer = property.DeclaringSyntaxReferences + .Select(r => r.GetSyntax() as PropertyDeclarationSyntax) + .Select(s => s?.Initializer ?? null) + .FirstOrDefault(); + + string? value = initializer?.Value.ToString(); + + exportedMembers.Add(new ExportedPropertyMetadata( + property.Name, marshalType.Value, propertyType, value)); + } + + foreach (var field in exportedFields) + { + if (field.IsStatic) + { + Common.ReportExportedMemberIsStatic(context, field); + continue; + } + + // 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) + { + Common.ReportExportedMemberIsReadOnly(context, field); + continue; + } + + var fieldType = field.Type; + var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(fieldType, typeCache); + + if (marshalType == null) + { + Common.ReportExportedMemberTypeNotSupported(context, field); + continue; + } + + EqualsValueClauseSyntax? initializer = field.DeclaringSyntaxReferences + .Select(r => r.GetSyntax()) + .OfType() + .Select(s => s.Initializer) + .FirstOrDefault(i => i != null); + + string? value = initializer?.Value.ToString(); + + exportedMembers.Add(new ExportedPropertyMetadata( + field.Name, marshalType.Value, fieldType, value)); + } + + // Generate GetGodotExportedProperties + + if (exportedMembers.Count > 0) + { + source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n"); + + string dictionaryType = "System.Collections.Generic.Dictionary"; + + source.Append("#if TOOLS\n"); + source.Append(" internal new static "); + source.Append(dictionaryType); + source.Append(" GetGodotPropertyDefaultValues()\n {\n"); + + source.Append(" var values = new "); + source.Append(dictionaryType); + source.Append("("); + source.Append(exportedMembers.Count); + source.Append(");\n"); + + foreach (var exportedMember in exportedMembers) + { + string defaultValueLocalName = string.Concat("__", exportedMember.Name, "_default_value"); + + source.Append(" "); + source.Append(exportedMember.TypeSymbol.FullQualifiedName()); + source.Append(" "); + source.Append(defaultValueLocalName); + source.Append(" = "); + source.Append(exportedMember.Value ?? "default"); + source.Append(";\n"); + source.Append(" values.Add(GodotInternal.PropName_"); + source.Append(exportedMember.Name); + source.Append(", "); + source.Append(defaultValueLocalName); + source.Append(");\n"); + } + + source.Append(" return values;\n"); + source.Append(" }\n"); + source.Append("#endif\n"); + + source.Append("#pragma warning restore CS0109\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)); + } + + private struct ExportedPropertyMetadata + { + public ExportedPropertyMetadata(string name, MarshalType type, ITypeSymbol typeSymbol, string? value) + { + Name = name; + Type = type; + TypeSymbol = typeSymbol; + Value = value; + } + + public string Name { get; } + public MarshalType Type { get; } + public ITypeSymbol TypeSymbol { get; } + public string? Value { get; } + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index 4e27f4ed142..fe83e6a2813 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -513,20 +513,23 @@ namespace GodotTools protected override void Dispose(bool disposing) { - base.Dispose(disposing); - - if (_exportPluginWeak != null) + if (disposing) { - // We need to dispose our export plugin before the editor destroys EditorSettings. - // Otherwise, if the GC disposes it at a later time, EditorExportPlatformAndroid - // will be freed after EditorSettings already was, and its device polling thread - // will try to access the EditorSettings singleton, resulting in null dereferencing. - (_exportPluginWeak.GetRef() as ExportPlugin)?.Dispose(); + if (IsInstanceValid(_exportPluginWeak)) + { + // We need to dispose our export plugin before the editor destroys EditorSettings. + // Otherwise, if the GC disposes it at a later time, EditorExportPlatformAndroid + // will be freed after EditorSettings already was, and its device polling thread + // will try to access the EditorSettings singleton, resulting in null dereferencing. + (_exportPluginWeak.GetRef() as ExportPlugin)?.Dispose(); - _exportPluginWeak.Dispose(); + _exportPluginWeak.Dispose(); + } + + GodotIdeManager?.Dispose(); } - GodotIdeManager?.Dispose(); + base.Dispose(disposing); } public void OnBeforeSerialize() diff --git a/modules/mono/editor/bindings_generator.cpp b/modules/mono/editor/bindings_generator.cpp index 746cb8a1426..6c805c605dc 100644 --- a/modules/mono/editor/bindings_generator.cpp +++ b/modules/mono/editor/bindings_generator.cpp @@ -1407,6 +1407,13 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str } } + // We generate a `GodotClassName` attribute if the engine class name is not the same as the + // generated C# class name. This allows introspection code to find the name associated with + // the class. If the attribute is not present, the C# class name can be used instead. + if (itype.name != itype.proxy_name) { + output << INDENT1 "[GodotClassName(\"" << itype.name << "\")]\n"; + } + output.append(INDENT1 "public "); if (itype.is_singleton) { output.append("static partial class "); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Attributes/ExportAttribute.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Attributes/ExportAttribute.cs index 46eb128d37f..3d204bdf9f9 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Attributes/ExportAttribute.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Attributes/ExportAttribute.cs @@ -6,7 +6,7 @@ namespace Godot /// An attribute used to export objects. /// [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] - public class ExportAttribute : Attribute + public sealed class ExportAttribute : Attribute { private PropertyHint hint; private string hintString; diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs index fb1efa0ac84..7a6748b4eb7 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs @@ -24,6 +24,8 @@ namespace Godot.Bridge public delegate* unmanaged ScriptManagerBridge_RemoveScriptBridge; public delegate* unmanaged ScriptManagerBridge_UpdateScriptClassInfo; public delegate* unmanaged ScriptManagerBridge_SwapGCHandleForType; + public delegate* unmanaged, void> ScriptManagerBridge_GetPropertyInfoList; + public delegate* unmanaged, void> ScriptManagerBridge_GetPropertyDefaultValues; public delegate* unmanaged CSharpInstanceBridge_Call; public delegate* unmanaged CSharpInstanceBridge_Set; public delegate* unmanaged CSharpInstanceBridge_Get; @@ -57,6 +59,8 @@ namespace Godot.Bridge ScriptManagerBridge_RemoveScriptBridge = &ScriptManagerBridge.RemoveScriptBridge, ScriptManagerBridge_UpdateScriptClassInfo = &ScriptManagerBridge.UpdateScriptClassInfo, ScriptManagerBridge_SwapGCHandleForType = &ScriptManagerBridge.SwapGCHandleForType, + ScriptManagerBridge_GetPropertyInfoList = &ScriptManagerBridge.GetPropertyInfoList, + ScriptManagerBridge_GetPropertyDefaultValues = &ScriptManagerBridge.GetPropertyDefaultValues, CSharpInstanceBridge_Call = &CSharpInstanceBridge.Call, CSharpInstanceBridge_Set = &CSharpInstanceBridge.Set, CSharpInstanceBridge_Get = &CSharpInstanceBridge.Get, diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/PropertyInfo.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/PropertyInfo.cs new file mode 100644 index 00000000000..cfdfe2dab3c --- /dev/null +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/PropertyInfo.cs @@ -0,0 +1,26 @@ +using System; +using Godot.NativeInterop; + +namespace Godot.Bridge +{ + public struct PropertyInfo + { + public Variant.Type Type { get; init; } + public StringName Name { get; init; } + public PropertyHint Hint { get; init; } + public string HintString { get; init; } + public PropertyUsageFlags Usage { get; init; } + public bool Exported { get; init; } + + public PropertyInfo(Variant.Type type, StringName name, PropertyHint hint, string hintString, + PropertyUsageFlags usage, bool exported) + { + Type = type; + Name = name; + Hint = hint; + HintString = hintString; + Usage = usage; + Exported = exported; + } + } +} diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs index b86ced55cba..8348598b654 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs @@ -1,6 +1,8 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; using Godot.Collections; @@ -523,7 +525,7 @@ namespace Godot.Bridge Dictionary rpcFunctions = new(); - Type top = _scriptBridgeMap[scriptPtr]; + Type top = scriptType; Type native = Object.InternalGetClassNativeBase(top); while (top != null && top != native) @@ -602,5 +604,256 @@ namespace Godot.Bridge return false.ToGodotBool(); } } + + // ReSharper disable once InconsistentNaming + [SuppressMessage("ReSharper", "NotAccessedField.Local")] + [StructLayout(LayoutKind.Sequential)] + private ref struct godotsharp_property_info + { + // Careful with padding... + public godot_string_name Name; // Not owned + public godot_string HintString; + public int Type; + public int Hint; + public int Usage; + public godot_bool Exported; + + public void Dispose() + { + HintString.Dispose(); + } + } + + [UnmanagedCallersOnly] + internal static unsafe void GetPropertyInfoList(IntPtr scriptPtr, + delegate* unmanaged addPropInfoFunc) + { + try + { + Type scriptType = _scriptBridgeMap[scriptPtr]; + GetPropertyInfoListForType(scriptType, scriptPtr, addPropInfoFunc); + } + catch (Exception e) + { + ExceptionUtils.DebugUnhandledException(e); + } + } + + private static unsafe void GetPropertyInfoListForType(Type type, IntPtr scriptPtr, + delegate* unmanaged addPropInfoFunc) + { + try + { + var getGodotPropertiesMetadataMethod = type.GetMethod( + "GetGodotPropertiesMetadata", + BindingFlags.DeclaredOnly | BindingFlags.Static | + BindingFlags.NonPublic | BindingFlags.Public); + + if (getGodotPropertiesMetadataMethod == null) + return; + + var properties = (System.Collections.Generic.List) + getGodotPropertiesMetadataMethod.Invoke(null, null); + + if (properties == null || properties.Count <= 0) + return; + + int length = properties.Count; + + // There's no recursion here, so it's ok to go with a big enough number for most cases + // stackMaxSize = stackMaxLength * sizeof(godotsharp_property_info) + const int stackMaxLength = 32; + bool useStack = length < stackMaxLength; + + godotsharp_property_info* interopProperties; + + if (useStack) + { + // Weird limitation, hence the need for aux: + // "In the case of pointer types, you can use a stackalloc expression only in a local variable declaration to initialize the variable." + var aux = stackalloc godotsharp_property_info[length]; + interopProperties = aux; + } + else + { +#if NET6_0_OR_GREATER + interopProperties = ((godotsharp_property_info*)NativeMemory.Alloc(length))!; +#else + interopProperties = ((godotsharp_property_info*)Marshal.AllocHGlobal(length))!; +#endif + } + + try + { + for (int i = 0; i < length; i++) + { + var property = properties[i]; + + godotsharp_property_info interopProperty = new() + { + Type = (int)property.Type, + Name = (godot_string_name)property.Name.NativeValue, // Not owned + Hint = (int)property.Hint, + HintString = Marshaling.ConvertStringToNative(property.HintString), + Usage = (int)property.Usage, + Exported = property.Exported.ToGodotBool() + }; + + interopProperties[i] = interopProperty; + } + + using godot_string currentClassName = Marshaling.ConvertStringToNative(type.Name); + + addPropInfoFunc(scriptPtr, ¤tClassName, interopProperties, length); + + // We're borrowing the StringName's without making an owning copy, so the + // managed collection needs to be kept alive until `addPropInfoFunc` returns. + GC.KeepAlive(properties); + } + finally + { + for (int i = 0; i < length; i++) + interopProperties[i].Dispose(); + + if (!useStack) + { +#if NET6_0_OR_GREATER + NativeMemory.Free(interopProperties); +#else + Marshal.FreeHGlobal((IntPtr)interopProperties); +#endif + } + } + } + catch (Exception e) + { + ExceptionUtils.DebugUnhandledException(e); + } + } + + // ReSharper disable once InconsistentNaming + [SuppressMessage("ReSharper", "NotAccessedField.Local")] + [StructLayout(LayoutKind.Sequential)] + private ref struct godotsharp_property_def_val_pair + { + // Careful with padding... + public godot_string_name Name; // Not owned + public godot_variant Value; + + public void Dispose() + { + Value.Dispose(); + } + } + + [UnmanagedCallersOnly] + internal static unsafe void GetPropertyDefaultValues(IntPtr scriptPtr, + delegate* unmanaged addDefValFunc) + { + try + { + Type top = _scriptBridgeMap[scriptPtr]; + Type native = Object.InternalGetClassNativeBase(top); + + while (top != null && top != native) + { + GetPropertyDefaultValuesForType(top, scriptPtr, addDefValFunc); + + top = top.BaseType; + } + } + catch (Exception e) + { + ExceptionUtils.DebugUnhandledException(e); + } + } + + [SkipLocalsInit] + private static unsafe void GetPropertyDefaultValuesForType(Type type, IntPtr scriptPtr, + delegate* unmanaged addDefValFunc) + { + try + { + var getGodotPropertyDefaultValuesMethod = type.GetMethod( + "GetGodotPropertyDefaultValues", + BindingFlags.DeclaredOnly | BindingFlags.Static | + BindingFlags.NonPublic | BindingFlags.Public); + + if (getGodotPropertyDefaultValuesMethod == null) + return; + + var defaultValues = (System.Collections.Generic.Dictionary) + getGodotPropertyDefaultValuesMethod.Invoke(null, null); + + if (defaultValues == null || defaultValues.Count <= 0) + return; + + int length = defaultValues.Count; + + // There's no recursion here, so it's ok to go with a big enough number for most cases + // stackMaxSize = stackMaxLength * sizeof(godotsharp_property_def_val_pair) + const int stackMaxLength = 32; + bool useStack = length < stackMaxLength; + + godotsharp_property_def_val_pair* interopDefaultValues; + + if (useStack) + { + // Weird limitation, hence the need for aux: + // "In the case of pointer types, you can use a stackalloc expression only in a local variable declaration to initialize the variable." + var aux = stackalloc godotsharp_property_def_val_pair[length]; + interopDefaultValues = aux; + } + else + { +#if NET6_0_OR_GREATER + interopDefaultValues = ((godotsharp_property_def_val_pair*)NativeMemory.Alloc(length))!; +#else + interopDefaultValues = ((godotsharp_property_def_val_pair*)Marshal.AllocHGlobal(length))!; +#endif + } + + try + { + int i = 0; + foreach (var defaultValuePair in defaultValues) + { + godotsharp_property_def_val_pair interopProperty = new() + { + Name = (godot_string_name)defaultValuePair.Key.NativeValue, // Not owned + Value = Marshaling.ConvertManagedObjectToVariant(defaultValuePair.Value) + }; + + interopDefaultValues[i] = interopProperty; + + i++; + } + + addDefValFunc(scriptPtr, interopDefaultValues, length); + + // We're borrowing the StringName's without making an owning copy, so the + // managed collection needs to be kept alive until `addDefValFunc` returns. + GC.KeepAlive(defaultValues); + } + finally + { + for (int i = 0; i < length; i++) + interopDefaultValues[i].Dispose(); + + if (!useStack) + { +#if NET6_0_OR_GREATER + NativeMemory.Free(interopDefaultValues); +#else + Marshal.FreeHGlobal((IntPtr)interopDefaultValues); +#endif + } + } + } + catch (Exception e) + { + ExceptionUtils.DebugUnhandledException(e); + } + } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/MarshalUtils.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/MarshalUtils.cs deleted file mode 100644 index 733a8ac1143..00000000000 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/MarshalUtils.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Godot -{ - internal static class MarshalUtils - { - /// - /// Returns if the is applied to the given type. - /// - private static bool TypeHasFlagsAttribute(Type type) => type.IsDefined(typeof(FlagsAttribute), false); - } -} diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs index c2812b89199..01add1bf456 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs @@ -36,10 +36,13 @@ namespace Godot.NativeInterop public static extern IntPtr godotsharp_engine_get_singleton(in godot_string p_name); [DllImport(GodotDllName)] - internal static extern void godotsharp_internal_object_disposed(IntPtr ptr); + internal static extern IntPtr godotsharp_internal_object_get_associated_gchandle(IntPtr ptr); [DllImport(GodotDllName)] - internal static extern void godotsharp_internal_refcounted_disposed(IntPtr ptr, godot_bool isFinalizer); + internal static extern void godotsharp_internal_object_disposed(IntPtr ptr, IntPtr gcHandleToFree); + + [DllImport(GodotDllName)] + internal static extern void godotsharp_internal_refcounted_disposed(IntPtr ptr, IntPtr gcHandleToFree, godot_bool isFinalizer); [DllImport(GodotDllName)] internal static extern void godotsharp_internal_object_connect_event_signal(IntPtr obj, diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs index 6255ffbbc74..71a620716f7 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Object.base.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; using Godot.NativeInterop; namespace Godot @@ -121,23 +122,35 @@ namespace Godot if (_disposed) return; + _disposed = true; + if (NativePtr != IntPtr.Zero) { + IntPtr gcHandleToFree = NativeFuncs.godotsharp_internal_object_get_associated_gchandle(NativePtr); + + if (gcHandleToFree != IntPtr.Zero) + { + object target = GCHandle.FromIntPtr(gcHandleToFree).Target; + // The GC handle may have been replaced in another thread. Release it only if + // it's associated to this managed instance, or if the target is no longer alive. + if (target != this && target != null) + gcHandleToFree = IntPtr.Zero; + } + if (_memoryOwn) { - NativeFuncs.godotsharp_internal_refcounted_disposed(NativePtr, (!disposing).ToGodotBool()); + NativeFuncs.godotsharp_internal_refcounted_disposed(NativePtr, gcHandleToFree, + (!disposing).ToGodotBool()); } else { - NativeFuncs.godotsharp_internal_object_disposed(NativePtr); + NativeFuncs.godotsharp_internal_object_disposed(NativePtr, gcHandleToFree); } NativePtr = IntPtr.Zero; } DisposablesTracker.UnregisterGodotObject(_weakReferenceToSelf); - - _disposed = true; } /// diff --git a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj index b7fbb81f9c0..8b0c4218292 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj @@ -41,6 +41,7 @@ + @@ -63,7 +64,6 @@ - diff --git a/modules/mono/glue/runtime_interop.cpp b/modules/mono/glue/runtime_interop.cpp index c3d4f53048d..7150e2e35bb 100644 --- a/modules/mono/glue/runtime_interop.cpp +++ b/modules/mono/glue/runtime_interop.cpp @@ -81,7 +81,7 @@ GD_PINVOKE_EXPORT Object *godotsharp_engine_get_singleton(const String *p_name) return Engine::get_singleton()->get_singleton_object(*p_name); } -GD_PINVOKE_EXPORT void godotsharp_internal_object_disposed(Object *p_ptr) { +GD_PINVOKE_EXPORT GCHandleIntPtr godotsharp_internal_object_get_associated_gchandle(Object *p_ptr) { #ifdef DEBUG_ENABLED CRASH_COND(p_ptr == nullptr); #endif @@ -90,7 +90,35 @@ GD_PINVOKE_EXPORT void godotsharp_internal_object_disposed(Object *p_ptr) { CSharpInstance *cs_instance = CAST_CSHARP_INSTANCE(p_ptr->get_script_instance()); if (cs_instance) { if (!cs_instance->is_destructing_script_instance()) { - cs_instance->mono_object_disposed(); + return cs_instance->get_gchandle_intptr(); + } + return { nullptr }; + } + } + + void *data = CSharpLanguage::get_existing_instance_binding(p_ptr); + + if (data) { + CSharpScriptBinding &script_binding = ((RBMap::Element *)data)->get(); + if (script_binding.inited) { + MonoGCHandleData &gchandle = script_binding.gchandle; + return !gchandle.is_released() ? gchandle.get_intptr() : GCHandleIntPtr{ nullptr }; + } + } + + return { nullptr }; +} + +GD_PINVOKE_EXPORT void godotsharp_internal_object_disposed(Object *p_ptr, GCHandleIntPtr p_gchandle_to_free) { +#ifdef DEBUG_ENABLED + CRASH_COND(p_ptr == nullptr); +#endif + + if (p_ptr->get_script_instance()) { + CSharpInstance *cs_instance = CAST_CSHARP_INSTANCE(p_ptr->get_script_instance()); + if (cs_instance) { + if (!cs_instance->is_destructing_script_instance()) { + cs_instance->mono_object_disposed(p_gchandle_to_free); p_ptr->set_script_instance(nullptr); } return; @@ -102,16 +130,14 @@ GD_PINVOKE_EXPORT void godotsharp_internal_object_disposed(Object *p_ptr) { if (data) { CSharpScriptBinding &script_binding = ((RBMap::Element *)data)->get(); if (script_binding.inited) { - MonoGCHandleData &gchandle = script_binding.gchandle; - if (!gchandle.is_released()) { - CSharpLanguage::release_script_gchandle(nullptr, gchandle); - script_binding.inited = false; + if (!script_binding.gchandle.is_released()) { + CSharpLanguage::release_binding_gchandle_thread_safe(p_gchandle_to_free, script_binding); } } } } -GD_PINVOKE_EXPORT void godotsharp_internal_refcounted_disposed(Object *p_ptr, bool p_is_finalizer) { +GD_PINVOKE_EXPORT void godotsharp_internal_refcounted_disposed(Object *p_ptr, GCHandleIntPtr p_gchandle_to_free, bool p_is_finalizer) { #ifdef DEBUG_ENABLED CRASH_COND(p_ptr == nullptr); // This is only called with RefCounted derived classes @@ -127,7 +153,8 @@ GD_PINVOKE_EXPORT void godotsharp_internal_refcounted_disposed(Object *p_ptr, bo bool delete_owner; bool remove_script_instance; - cs_instance->mono_object_disposed_baseref(p_is_finalizer, delete_owner, remove_script_instance); + cs_instance->mono_object_disposed_baseref(p_gchandle_to_free, p_is_finalizer, + delete_owner, remove_script_instance); if (delete_owner) { memdelete(rc); @@ -150,10 +177,8 @@ GD_PINVOKE_EXPORT void godotsharp_internal_refcounted_disposed(Object *p_ptr, bo if (data) { CSharpScriptBinding &script_binding = ((RBMap::Element *)data)->get(); if (script_binding.inited) { - MonoGCHandleData &gchandle = script_binding.gchandle; - if (!gchandle.is_released()) { - CSharpLanguage::release_script_gchandle(nullptr, gchandle); - script_binding.inited = false; + if (!script_binding.gchandle.is_released()) { + CSharpLanguage::release_binding_gchandle_thread_safe(p_gchandle_to_free, script_binding); } } } @@ -188,7 +213,7 @@ GD_PINVOKE_EXPORT GCHandleIntPtr godotsharp_internal_unmanaged_get_script_instan } *r_has_cs_script_instance = false; - return GCHandleIntPtr(); + return { nullptr }; } GD_PINVOKE_EXPORT GCHandleIntPtr godotsharp_internal_unmanaged_get_instance_binding_managed(Object *p_unmanaged) { @@ -197,9 +222,9 @@ GD_PINVOKE_EXPORT GCHandleIntPtr godotsharp_internal_unmanaged_get_instance_bind #endif void *data = CSharpLanguage::get_instance_binding(p_unmanaged); - ERR_FAIL_NULL_V(data, GCHandleIntPtr()); + ERR_FAIL_NULL_V(data, { nullptr }); CSharpScriptBinding &script_binding = ((RBMap::Element *)data)->value(); - ERR_FAIL_COND_V(!script_binding.inited, GCHandleIntPtr()); + ERR_FAIL_COND_V(!script_binding.inited, { nullptr }); return script_binding.gchandle.get_intptr(); } @@ -210,9 +235,9 @@ GD_PINVOKE_EXPORT GCHandleIntPtr godotsharp_internal_unmanaged_instance_binding_ #endif void *data = CSharpLanguage::get_instance_binding(p_unmanaged); - ERR_FAIL_NULL_V(data, GCHandleIntPtr()); + ERR_FAIL_NULL_V(data, { nullptr }); CSharpScriptBinding &script_binding = ((RBMap::Element *)data)->value(); - ERR_FAIL_COND_V(!script_binding.inited, GCHandleIntPtr()); + ERR_FAIL_COND_V(!script_binding.inited, { nullptr }); MonoGCHandleData &gchandle = script_binding.gchandle; @@ -229,14 +254,14 @@ GD_PINVOKE_EXPORT GCHandleIntPtr godotsharp_internal_unmanaged_instance_binding_ #endif bool parent_is_object_class = ClassDB::is_parent_class(p_unmanaged->get_class_name(), script_binding.type_name); - ERR_FAIL_COND_V_MSG(!parent_is_object_class, GCHandleIntPtr(), + ERR_FAIL_COND_V_MSG(!parent_is_object_class, { nullptr }, "Type inherits from native type '" + script_binding.type_name + "', so it can't be instantiated in object of type: '" + p_unmanaged->get_class() + "'."); GCHandleIntPtr strong_gchandle = GDMonoCache::managed_callbacks.ScriptManagerBridge_CreateManagedForGodotObjectBinding( &script_binding.type_name, p_unmanaged); - ERR_FAIL_NULL_V(strong_gchandle.value, GCHandleIntPtr()); + ERR_FAIL_NULL_V(strong_gchandle.value, { nullptr }); gchandle = MonoGCHandleData(strong_gchandle, gdmono::GCHandleType::STRONG_HANDLE); script_binding.inited = true; @@ -420,25 +445,25 @@ GD_PINVOKE_EXPORT bool godotsharp_callable_get_data_for_marshalling(const Callab return true; } else if (compare_equal_func == SignalAwaiterCallable::compare_equal_func_ptr) { SignalAwaiterCallable *signal_awaiter_callable = static_cast(custom); - *r_delegate_handle = GCHandleIntPtr(); + *r_delegate_handle = { nullptr }; *r_object = ObjectDB::get_instance(signal_awaiter_callable->get_object()); memnew_placement(r_name, StringName(signal_awaiter_callable->get_signal())); return true; } else if (compare_equal_func == EventSignalCallable::compare_equal_func_ptr) { EventSignalCallable *event_signal_callable = static_cast(custom); - *r_delegate_handle = GCHandleIntPtr(); + *r_delegate_handle = { nullptr }; *r_object = ObjectDB::get_instance(event_signal_callable->get_object()); memnew_placement(r_name, StringName(event_signal_callable->get_signal())); return true; } // Some other CallableCustom. We only support ManagedCallable. - *r_delegate_handle = GCHandleIntPtr(); + *r_delegate_handle = { nullptr }; *r_object = nullptr; memnew_placement(r_name, StringName()); return false; } else { - *r_delegate_handle = GCHandleIntPtr(); + *r_delegate_handle = { nullptr }; *r_object = ObjectDB::get_instance(p_callable->get_object_id()); memnew_placement(r_name, StringName(p_callable->get_method())); return true; @@ -1256,10 +1281,11 @@ 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[178] = { +void *godotsharp_pinvoke_funcs[179] = { (void *)godotsharp_method_bind_get_method, (void *)godotsharp_get_class_constructor, (void *)godotsharp_engine_get_singleton, + (void *)godotsharp_internal_object_get_associated_gchandle, (void *)godotsharp_internal_object_disposed, (void *)godotsharp_internal_refcounted_disposed, (void *)godotsharp_internal_object_connect_event_signal, diff --git a/modules/mono/managed_callable.cpp b/modules/mono/managed_callable.cpp index a1f94aa590e..334de14a6bf 100644 --- a/modules/mono/managed_callable.cpp +++ b/modules/mono/managed_callable.cpp @@ -96,7 +96,7 @@ void ManagedCallable::call(const Variant **p_arguments, int p_argcount, Variant void ManagedCallable::release_delegate_handle() { if (delegate_handle.value) { GDMonoCache::managed_callbacks.GCHandleBridge_FreeGCHandle(delegate_handle); - delegate_handle = GCHandleIntPtr(); + delegate_handle = { nullptr }; } } diff --git a/modules/mono/mono_gc_handle.h b/modules/mono/mono_gc_handle.h index a921b241037..4e4c13fee65 100644 --- a/modules/mono/mono_gc_handle.h +++ b/modules/mono/mono_gc_handle.h @@ -44,7 +44,12 @@ enum class GCHandleType : char { extern "C" { struct GCHandleIntPtr { - void *value = nullptr; + void *value; + + _FORCE_INLINE_ bool operator==(const GCHandleIntPtr &p_other) { return value == p_other.value; } + _FORCE_INLINE_ bool operator!=(const GCHandleIntPtr &p_other) { return value != p_other.value; } + + GCHandleIntPtr() = delete; }; } @@ -52,7 +57,7 @@ static_assert(sizeof(GCHandleIntPtr) == sizeof(void *)); // Manual release of the GC handle must be done when using this struct struct MonoGCHandleData { - GCHandleIntPtr handle; + GCHandleIntPtr handle = { nullptr }; gdmono::GCHandleType type = gdmono::GCHandleType::NIL; _FORCE_INLINE_ bool is_released() const { return !handle.value; } diff --git a/modules/mono/mono_gd/gd_mono_cache.h b/modules/mono/mono_gd/gd_mono_cache.h index b993facff98..7a75532fb77 100644 --- a/modules/mono/mono_gd/gd_mono_cache.h +++ b/modules/mono/mono_gd/gd_mono_cache.h @@ -34,6 +34,7 @@ #include #include "../csharp_script.h" +#include "../interop_types.h" #include "../mono_gc_handle.h" #include "core/object/object.h" #include "core/string/string_name.h" @@ -52,16 +53,33 @@ namespace GDMonoCache { #define GD_CLR_STDCALL #endif +struct godotsharp_property_info { + godot_string_name name; // Not owned + godot_string hint_string; + Variant::Type type; + PropertyHint hint; + PropertyUsageFlags usage; + bool exported; +}; + +struct godotsharp_property_def_val_pair { + godot_string_name name; // Not owned + godot_variant value; +}; + struct ManagedCallbacks { + using Callback_ScriptManagerBridge_GetPropertyInfoList_Add = void(GD_CLR_STDCALL *)(CSharpScript *p_script, const String *, godotsharp_property_info *p_props, int32_t p_count); + using Callback_ScriptManagerBridge_GetPropertyDefaultValues_Add = void(GD_CLR_STDCALL *)(CSharpScript *p_script, godotsharp_property_def_val_pair *p_def_vals, int32_t p_count); + 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 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 **, int); + using FuncScriptManagerBridge_CreateManagedForGodotObjectScriptInstance = bool(GD_CLR_STDCALL *)(const CSharpScript *, Object *, const Variant **, int32_t); using FuncScriptManagerBridge_GetScriptNativeName = void(GD_CLR_STDCALL *)(const CSharpScript *, StringName *); using FuncScriptManagerBridge_SetGodotObjectPtr = void(GD_CLR_STDCALL *)(GCHandleIntPtr, Object *); - using FuncScriptManagerBridge_RaiseEventSignal = void(GD_CLR_STDCALL *)(GCHandleIntPtr, const StringName *, const Variant **, int, bool *); + using FuncScriptManagerBridge_RaiseEventSignal = void(GD_CLR_STDCALL *)(GCHandleIntPtr, const StringName *, const Variant **, int32_t, bool *); using FuncScriptManagerBridge_GetScriptSignalList = void(GD_CLR_STDCALL *)(const CSharpScript *, Dictionary *); using FuncScriptManagerBridge_HasScriptSignal = bool(GD_CLR_STDCALL *)(const CSharpScript *, const String *); using FuncScriptManagerBridge_ScriptIsOrInherits = bool(GD_CLR_STDCALL *)(const CSharpScript *, const CSharpScript *); @@ -69,7 +87,9 @@ struct ManagedCallbacks { using FuncScriptManagerBridge_RemoveScriptBridge = void(GD_CLR_STDCALL *)(const CSharpScript *); using FuncScriptManagerBridge_UpdateScriptClassInfo = void(GD_CLR_STDCALL *)(const CSharpScript *, bool *, Dictionary *); using FuncScriptManagerBridge_SwapGCHandleForType = bool(GD_CLR_STDCALL *)(GCHandleIntPtr, GCHandleIntPtr *, bool); - using FuncCSharpInstanceBridge_Call = bool(GD_CLR_STDCALL *)(GCHandleIntPtr, const StringName *, const Variant **, int, Callable::CallError *, Variant *); + using FuncScriptManagerBridge_GetPropertyInfoList = void(GD_CLR_STDCALL *)(CSharpScript *, Callback_ScriptManagerBridge_GetPropertyInfoList_Add); + using FuncScriptManagerBridge_GetPropertyDefaultValues = void(GD_CLR_STDCALL *)(CSharpScript *, Callback_ScriptManagerBridge_GetPropertyDefaultValues_Add); + using FuncCSharpInstanceBridge_Call = bool(GD_CLR_STDCALL *)(GCHandleIntPtr, const StringName *, const Variant **, int32_t, Callable::CallError *, Variant *); using FuncCSharpInstanceBridge_Set = bool(GD_CLR_STDCALL *)(GCHandleIntPtr, const StringName *, const Variant *); using FuncCSharpInstanceBridge_Get = bool(GD_CLR_STDCALL *)(GCHandleIntPtr, const StringName *, Variant *); using FuncCSharpInstanceBridge_CallDispose = void(GD_CLR_STDCALL *)(GCHandleIntPtr, bool); @@ -96,6 +116,8 @@ struct ManagedCallbacks { FuncScriptManagerBridge_RemoveScriptBridge ScriptManagerBridge_RemoveScriptBridge; FuncScriptManagerBridge_UpdateScriptClassInfo ScriptManagerBridge_UpdateScriptClassInfo; FuncScriptManagerBridge_SwapGCHandleForType ScriptManagerBridge_SwapGCHandleForType; + FuncScriptManagerBridge_GetPropertyInfoList ScriptManagerBridge_GetPropertyInfoList; + FuncScriptManagerBridge_GetPropertyDefaultValues ScriptManagerBridge_GetPropertyDefaultValues; FuncCSharpInstanceBridge_Call CSharpInstanceBridge_Call; FuncCSharpInstanceBridge_Set CSharpInstanceBridge_Set; FuncCSharpInstanceBridge_Get CSharpInstanceBridge_Get;