Improve handling of generic C# types

- Create CSharpScript for generic C# types.
  - `ScriptPathAttributeGenerator` registers the path for the generic type definition.
  - `ScriptManagerBridge` lookup uses the generic type definition that was registered by the generator.
  - Constructed generic types use a virtual `csharp://` path so they can be registered in the map and loaded as if there was a different file for each constructed type, even though they all share the same real path.
  - This allows getting the base type for a C# type that derives from a generic type.
- Shows base scripts in the _Add Node_ and _Create Resource_ dialogs even when they are generic types.
  - `get_global_class_name` implementation was moved to C# and now always returns the base type even if the script is not a global class (this behavior matches GDScript).
- Create `CSharpScript::TypeInfo` struct to hold all the type information about the C# type that corresponds to the `CSharpScript`, and use it as the parameter in `UpdateScriptClassInfo` to avoid adding more parameters.
This commit is contained in:
Raul Santos 2024-02-05 02:49:57 +01:00
parent 41564aaf77
commit 5815d1c8c8
No known key found for this signature in database
GPG Key ID: B532473AE3A803E4
10 changed files with 422 additions and 141 deletions

View File

@ -558,42 +558,9 @@ bool CSharpLanguage::handles_global_class_type(const String &p_type) const {
}
String CSharpLanguage::get_global_class_name(const String &p_path, String *r_base_type, String *r_icon_path) const {
Ref<CSharpScript> scr = ResourceLoader::load(p_path, get_type());
// Always assign r_base_type and r_icon_path, even if the script
// is not a global one. In the case that it is not a global script,
// return an empty string AFTER assigning the return parameters.
// See GDScriptLanguage::get_global_class_name() in modules/gdscript/gdscript.cpp
if (!scr.is_valid() || !scr->valid) {
// Invalid script.
return String();
}
if (r_icon_path) {
if (scr->icon_path.is_empty() || scr->icon_path.is_absolute_path()) {
*r_icon_path = scr->icon_path.simplify_path();
} else if (scr->icon_path.is_relative_path()) {
*r_icon_path = p_path.get_base_dir().path_join(scr->icon_path).simplify_path();
}
}
if (r_base_type) {
bool found_global_base_script = false;
const CSharpScript *top = scr->base_script.ptr();
while (top != nullptr) {
if (top->global_class) {
*r_base_type = top->class_name;
found_global_base_script = true;
break;
}
top = top->base_script.ptr();
}
if (!found_global_base_script) {
*r_base_type = scr->get_instance_base_type();
}
}
return scr->global_class ? scr->class_name : String();
String class_name;
GDMonoCache::managed_callbacks.ScriptManagerBridge_GetGlobalClassName(&p_path, r_base_type, r_icon_path, &class_name);
return class_name;
}
String CSharpLanguage::debug_get_error() const {
@ -697,25 +664,19 @@ struct CSharpScriptDepSort {
// Shouldn't happen but just in case...
return false;
}
const CSharpScript *I = get_base_script(B.ptr()).ptr();
const Script *I = B->get_base_script().ptr();
while (I) {
if (I == A.ptr()) {
// A is a base of B
return true;
}
I = get_base_script(I).ptr();
I = I->get_base_script().ptr();
}
// A isn't a base of B
return false;
}
// Special fix for constructed generic types.
Ref<CSharpScript> get_base_script(const CSharpScript *p_script) const {
Ref<CSharpScript> base_script = p_script->base_script;
return base_script.is_valid() && !base_script->class_name.is_empty() ? base_script : nullptr;
}
};
void CSharpLanguage::reload_all_scripts() {
@ -937,7 +898,7 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) {
obj->set_script(Ref<RefCounted>()); // Remove script and existing script instances (placeholder are not removed before domain reload)
}
scr->was_tool_before_reload = scr->tool;
scr->was_tool_before_reload = scr->type_info.is_tool;
scr->_clear();
}
@ -997,7 +958,7 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) {
scr->exports_invalidated = true;
#endif
if (!scr->get_path().is_empty()) {
if (!scr->get_path().is_empty() && !scr->get_path().begins_with("csharp://")) {
scr->reload(p_soft_reload);
if (!scr->valid) {
@ -1839,6 +1800,7 @@ bool CSharpInstance::_internal_new_managed() {
ERR_FAIL_NULL_V(owner, false);
ERR_FAIL_COND_V(script.is_null(), false);
ERR_FAIL_COND_V(!script->can_instantiate(), false);
bool ok = GDMonoCache::managed_callbacks.ScriptManagerBridge_CreateManagedForGodotObjectScriptInstance(
script.ptr(), owner, nullptr, 0);
@ -2161,7 +2123,7 @@ void GD_CLR_STDCALL CSharpScript::_add_property_info_list_callback(CSharpScript
#ifdef TOOLS_ENABLED
p_script->exported_members_cache.push_back(PropertyInfo(
Variant::NIL, *p_current_class_name, PROPERTY_HINT_NONE,
Variant::NIL, p_script->type_info.class_name, PROPERTY_HINT_NONE,
p_script->get_path(), PROPERTY_USAGE_CATEGORY));
#endif
@ -2334,9 +2296,7 @@ void CSharpScript::reload_registered_script(Ref<CSharpScript> p_script) {
// Extract information about the script using the mono class.
void CSharpScript::update_script_class_info(Ref<CSharpScript> p_script) {
bool tool = false;
bool global_class = false;
bool abstract_class = false;
TypeInfo type_info;
// TODO: Use GDExtension godot_dictionary
Array methods_array;
@ -2346,18 +2306,12 @@ void CSharpScript::update_script_class_info(Ref<CSharpScript> p_script) {
Dictionary signals_dict;
signals_dict.~Dictionary();
String class_name;
String icon_path;
Ref<CSharpScript> base_script;
GDMonoCache::managed_callbacks.ScriptManagerBridge_UpdateScriptClassInfo(
p_script.ptr(), &class_name, &tool, &global_class, &abstract_class, &icon_path,
p_script.ptr(), &type_info,
&methods_array, &rpc_functions_dict, &signals_dict, &base_script);
p_script->class_name = class_name;
p_script->tool = tool;
p_script->global_class = global_class;
p_script->abstract_class = abstract_class;
p_script->icon_path = icon_path;
p_script->type_info = type_info;
p_script->rpc_config.clear();
p_script->rpc_config = rpc_functions_dict;
@ -2436,7 +2390,7 @@ void CSharpScript::update_script_class_info(Ref<CSharpScript> p_script) {
bool CSharpScript::can_instantiate() const {
#ifdef TOOLS_ENABLED
bool extra_cond = tool || ScriptServer::is_scripting_enabled();
bool extra_cond = type_info.is_tool || ScriptServer::is_scripting_enabled();
#else
bool extra_cond = true;
#endif
@ -2445,10 +2399,10 @@ bool CSharpScript::can_instantiate() const {
// For tool scripts, this will never fire if the class is not found. That's because we
// don't know if it's a tool script if we can't find the class to access the attributes.
if (extra_cond && !valid) {
ERR_FAIL_V_MSG(false, "Cannot instance script because the associated class could not be found. Script: '" + get_path() + "'. Make sure the script exists and contains a class definition with a name that matches the filename of the script exactly (it's case-sensitive).");
ERR_FAIL_V_MSG(false, "Cannot instantiate C# script because the associated class could not be found. Script: '" + get_path() + "'. Make sure the script exists and contains a class definition with a name that matches the filename of the script exactly (it's case-sensitive).");
}
return valid && !abstract_class && extra_cond;
return valid && type_info.can_instantiate() && extra_cond;
}
StringName CSharpScript::get_instance_base_type() const {
@ -2458,6 +2412,8 @@ StringName CSharpScript::get_instance_base_type() const {
}
CSharpInstance *CSharpScript::_create_instance(const Variant **p_args, int p_argcount, Object *p_owner, bool p_is_ref_counted, Callable::CallError &r_error) {
ERR_FAIL_COND_V_MSG(!type_info.can_instantiate(), nullptr, "Cannot instantiate C# script. Script: '" + get_path() + "'.");
/* STEP 1, CREATE */
Ref<RefCounted> ref;
@ -2772,11 +2728,11 @@ bool CSharpScript::inherits_script(const Ref<Script> &p_script) const {
}
Ref<Script> CSharpScript::get_base_script() const {
return base_script.is_valid() && !base_script->get_path().is_empty() ? base_script : nullptr;
return base_script;
}
StringName CSharpScript::get_global_name() const {
return global_class ? StringName(class_name) : StringName();
return type_info.is_global_class ? StringName(type_info.class_name) : StringName();
}
void CSharpScript::get_script_property_list(List<PropertyInfo> *r_list) const {
@ -2833,7 +2789,7 @@ Error CSharpScript::load_source_code(const String &p_path) {
}
void CSharpScript::_clear() {
tool = false;
type_info = TypeInfo();
valid = false;
reload_invalidated = true;
}
@ -2881,17 +2837,25 @@ Ref<Resource> ResourceFormatLoaderCSharpScript::load(const String &p_path, const
// TODO ignore anything inside bin/ and obj/ in tools builds?
String real_path = p_path;
if (p_path.begins_with("csharp://")) {
// This is a virtual path used by generic types, extract the real path.
real_path = "res://" + p_path.trim_prefix("csharp://");
real_path = real_path.substr(0, real_path.rfind(":"));
}
Ref<CSharpScript> scr;
if (GDMonoCache::godot_api_cache_updated) {
GDMonoCache::managed_callbacks.ScriptManagerBridge_GetOrCreateScriptBridgeForPath(&p_path, &scr);
ERR_FAIL_NULL_V_MSG(scr, Ref<Resource>(), "Could not create C# script '" + real_path + "'.");
} else {
scr = Ref<CSharpScript>(memnew(CSharpScript));
}
#if defined(DEBUG_ENABLED) || defined(TOOLS_ENABLED)
Error err = scr->load_source_code(p_path);
ERR_FAIL_COND_V_MSG(err != OK, Ref<Resource>(), "Cannot load C# script file '" + p_path + "'.");
Error err = scr->load_source_code(real_path);
ERR_FAIL_COND_V_MSG(err != OK, Ref<Resource>(), "Cannot load C# script file '" + real_path + "'.");
#endif
// Only one instance of a C# script is allowed to exist.

View File

@ -60,14 +60,88 @@ class CSharpScript : public Script {
friend class CSharpInstance;
friend class CSharpLanguage;
friend struct CSharpScriptDepSort;
bool tool = false;
bool global_class = false;
bool abstract_class = false;
public:
struct TypeInfo {
/**
* Name of the C# class.
*/
String class_name;
/**
* Path to the icon that will be used for this class by the editor.
*/
String icon_path;
/**
* Script is marked as tool and runs in the editor.
*/
bool is_tool = false;
/**
* Script is marked as global class and will be registered in the editor.
* Registered classes can be created using certain editor dialogs and
* can be referenced by name from other languages that support the feature.
*/
bool is_global_class = false;
/**
* Script is declared abstract.
*/
bool is_abstract = false;
/**
* The C# type that corresponds to this script is a constructed generic type.
* E.g.: `Dictionary<int, string>`
*/
bool is_constructed_generic_type = false;
/**
* The C# type that corresponds to this script is a generic type definition.
* E.g.: `Dictionary<,>`
*/
bool is_generic_type_definition = false;
/**
* The C# type that corresponds to this script contains generic type parameters,
* regardless of whether the type parameters are bound or not.
*/
bool is_generic() const {
return is_constructed_generic_type || is_generic_type_definition;
}
/**
* Check if the script can be instantiated.
* C# types can't be instantiated if they are abstract or contain generic
* type parameters, but a CSharpScript is still created for them.
*/
bool can_instantiate() const {
return !is_abstract && !is_generic_type_definition;
}
};
private:
/**
* Contains the C# type information for this script.
*/
TypeInfo type_info;
/**
* Scripts are valid when the corresponding C# class is found and used
* to extract the script info using the [update_script_class_info] method.
*/
bool valid = false;
/**
* Scripts extract info from the C# class in the reload methods but,
* if the reload is not invalidated, then the current extracted info
* is still valid and there's no need to reload again.
*/
bool reload_invalidated = false;
/**
* Base script that this script derives from, or null if it derives from a
* native Godot class.
*/
Ref<CSharpScript> base_script;
HashSet<Object *> instances;
@ -88,9 +162,10 @@ class CSharpScript : public Script {
HashSet<ObjectID> pending_replace_placeholders;
#endif
/**
* Script source code.
*/
String source;
String class_name;
String icon_path;
SelfList<CSharpScript> script_list = this;
@ -167,7 +242,7 @@ public:
return docs;
}
virtual String get_class_icon_path() const override {
return icon_path;
return type_info.icon_path;
}
#endif // TOOLS_ENABLED
@ -185,13 +260,13 @@ public:
void get_members(HashSet<StringName> *p_members) override;
bool is_tool() const override {
return tool;
return type_info.is_tool;
}
bool is_valid() const override {
return valid;
}
bool is_abstract() const override {
return abstract_class;
return type_info.is_abstract;
}
bool inherits_script(const Ref<Script> &p_script) const override;

View File

@ -54,9 +54,7 @@ namespace Godot.SourceGenerators
)
.Where(x =>
// Ignore classes whose name is not the same as the file name
Path.GetFileNameWithoutExtension(x.cds.SyntaxTree.FilePath) == x.symbol.Name &&
// Ignore generic classes
!x.symbol.IsGenericType)
Path.GetFileNameWithoutExtension(x.cds.SyntaxTree.FilePath) == x.symbol.Name)
.GroupBy(x => x.symbol)
.ToDictionary(g => g.Key, g => g.Select(x => x.cds));
@ -160,6 +158,8 @@ namespace Godot.SourceGenerators
first = false;
sourceBuilder.Append("typeof(");
sourceBuilder.Append(qualifiedName);
if (godotClass.Key.IsGenericType)
sourceBuilder.Append($"<{new string(',', godotClass.Key.TypeParameters.Count() - 1)}>");
sourceBuilder.Append(")");
}

View File

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using Godot;
using GodotTools.Build;
@ -23,9 +24,19 @@ namespace GodotTools.Inspector
{
foreach (var script in EnumerateScripts(godotObject))
{
if (script is not CSharpScript) continue;
if (script is not CSharpScript)
continue;
if (File.GetLastWriteTime(script.ResourcePath) > BuildManager.LastValidBuildDateTime)
string scriptPath = script.ResourcePath;
if (scriptPath.StartsWith("csharp://"))
{
// This is a virtual path used by generic types, extract the real path.
var scriptPathSpan = scriptPath.AsSpan("csharp://".Length);
scriptPathSpan = scriptPathSpan[..scriptPathSpan.IndexOf(':')];
scriptPath = $"res://{scriptPathSpan}";
}
if (File.GetLastWriteTime(scriptPath) > BuildManager.LastValidBuildDateTime)
{
AddCustomControl(new InspectorOutOfSyncWarning());
break;

View File

@ -18,6 +18,7 @@ namespace Godot.Bridge
public delegate* unmanaged<godot_string_name*, IntPtr, IntPtr> ScriptManagerBridge_CreateManagedForGodotObjectBinding;
public delegate* unmanaged<IntPtr, IntPtr, godot_variant**, int, godot_bool> ScriptManagerBridge_CreateManagedForGodotObjectScriptInstance;
public delegate* unmanaged<IntPtr, godot_string_name*, void> ScriptManagerBridge_GetScriptNativeName;
public delegate* unmanaged<godot_string*, godot_string*, godot_string*, godot_string*, void> ScriptManagerBridge_GetGlobalClassName;
public delegate* unmanaged<IntPtr, IntPtr, void> ScriptManagerBridge_SetGodotObjectPtr;
public delegate* unmanaged<IntPtr, godot_string_name*, godot_variant**, int, godot_bool*, void> ScriptManagerBridge_RaiseEventSignal;
public delegate* unmanaged<IntPtr, IntPtr, godot_bool> ScriptManagerBridge_ScriptIsOrInherits;
@ -25,7 +26,7 @@ namespace Godot.Bridge
public delegate* unmanaged<godot_string*, godot_ref*, void> ScriptManagerBridge_GetOrCreateScriptBridgeForPath;
public delegate* unmanaged<IntPtr, void> ScriptManagerBridge_RemoveScriptBridge;
public delegate* unmanaged<IntPtr, godot_bool> ScriptManagerBridge_TryReloadRegisteredScriptWithClass;
public delegate* unmanaged<IntPtr, godot_string*, godot_bool*, godot_bool*, godot_bool*, godot_string*, godot_array*, godot_dictionary*, godot_dictionary*, godot_ref*, void> ScriptManagerBridge_UpdateScriptClassInfo;
public delegate* unmanaged<IntPtr, godot_csharp_type_info*, godot_array*, godot_dictionary*, godot_dictionary*, godot_ref*, void> ScriptManagerBridge_UpdateScriptClassInfo;
public delegate* unmanaged<IntPtr, IntPtr*, godot_bool, godot_bool> ScriptManagerBridge_SwapGCHandleForType;
public delegate* unmanaged<IntPtr, delegate* unmanaged<IntPtr, godot_string*, void*, int, void>, void> ScriptManagerBridge_GetPropertyInfoList;
public delegate* unmanaged<IntPtr, delegate* unmanaged<IntPtr, void*, int, void>, void> ScriptManagerBridge_GetPropertyDefaultValues;
@ -60,6 +61,7 @@ namespace Godot.Bridge
ScriptManagerBridge_CreateManagedForGodotObjectBinding = &ScriptManagerBridge.CreateManagedForGodotObjectBinding,
ScriptManagerBridge_CreateManagedForGodotObjectScriptInstance = &ScriptManagerBridge.CreateManagedForGodotObjectScriptInstance,
ScriptManagerBridge_GetScriptNativeName = &ScriptManagerBridge.GetScriptNativeName,
ScriptManagerBridge_GetGlobalClassName = &ScriptManagerBridge.GetGlobalClassName,
ScriptManagerBridge_SetGodotObjectPtr = &ScriptManagerBridge.SetGodotObjectPtr,
ScriptManagerBridge_RaiseEventSignal = &ScriptManagerBridge.RaiseEventSignal,
ScriptManagerBridge_ScriptIsOrInherits = &ScriptManagerBridge.ScriptIsOrInherits,

View File

@ -11,6 +11,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Runtime.Serialization;
using System.Text;
using Godot.NativeInterop;
namespace Godot.Bridge
@ -29,7 +30,7 @@ namespace Godot.Bridge
foreach (var type in typesInAlc.Keys)
{
if (_scriptTypeBiMap.RemoveByScriptType(type, out IntPtr scriptPtr) &&
!_pathTypeBiMap.TryGetScriptPath(type, out _))
(!_pathTypeBiMap.TryGetScriptPath(type, out string? scriptPath) || scriptPath.StartsWith("csharp://")))
{
// For scripts without a path, we need to keep the class qualified name for reloading
_scriptDataForReload.TryAdd(scriptPtr,
@ -220,6 +221,71 @@ namespace Godot.Bridge
}
}
[UnmanagedCallersOnly]
internal static unsafe void GetGlobalClassName(godot_string* scriptPath, godot_string* outBaseType, godot_string* outIconPath, godot_string* outClassName)
{
// This method must always return the outBaseType for every script, even if the script is
// not a global class. But if the script is not a global class it must return an empty
// outClassName string since it should not have a name.
string scriptPathStr = Marshaling.ConvertStringToManaged(*scriptPath);
Debug.Assert(!string.IsNullOrEmpty(scriptPathStr), "Script path can't be empty.");
if (!_pathTypeBiMap.TryGetScriptType(scriptPathStr, out Type? scriptType))
{
// Script at the given path does not exist, or it's not a C# type.
// This is fine, it may be a path to a generic script and those can't be global classes.
*outClassName = default;
return;
}
if (outIconPath != null)
{
var iconAttr = scriptType.GetCustomAttributes(inherit: false)
.OfType<IconAttribute>()
.FirstOrDefault();
*outIconPath = Marshaling.ConvertStringToNative(iconAttr?.Path);
}
if (outBaseType != null)
{
bool foundGlobalBaseScript = false;
Type native = GodotObject.InternalGetClassNativeBase(scriptType);
Type? top = scriptType.BaseType;
while (top != null && top != native)
{
if (IsGlobalClass(top))
{
*outBaseType = Marshaling.ConvertStringToNative(top.Name);
foundGlobalBaseScript = true;
break;
}
top = top.BaseType;
}
if (!foundGlobalBaseScript)
{
*outBaseType = Marshaling.ConvertStringToNative(native.Name);
}
}
if (!IsGlobalClass(scriptType))
{
// Scripts that are not global classes should not have a name.
// Return an empty string to prevent the class from being registered
// as a global class in the editor.
*outClassName = default;
return;
}
*outClassName = Marshaling.ConvertStringToNative(scriptType.Name);
static bool IsGlobalClass(Type scriptType) =>
scriptType.IsDefined(typeof(GlobalClassAttribute), inherit: false);
}
[UnmanagedCallersOnly]
internal static void SetGodotObjectPtr(IntPtr gcHandlePtr, IntPtr newPtr)
{
@ -333,7 +399,7 @@ namespace Godot.Bridge
foreach (var type in assembly.GetTypes())
{
if (type.IsNested || type.IsGenericType)
if (type.IsNested)
continue;
if (!typeOfGodotObject.IsAssignableFrom(type))
@ -352,9 +418,6 @@ namespace Godot.Bridge
{
foreach (var type in scriptTypes)
{
if (type.IsGenericType)
continue;
LookupScriptForClass(type);
}
}
@ -422,20 +485,8 @@ namespace Godot.Bridge
{
try
{
lock (_scriptTypeBiMap.ReadWriteLock)
{
if (!_scriptTypeBiMap.IsScriptRegistered(scriptPtr))
{
string scriptPathStr = Marshaling.ConvertStringToManaged(*scriptPath);
if (!_pathTypeBiMap.TryGetScriptType(scriptPathStr, out Type? scriptType))
return godot_bool.False;
_scriptTypeBiMap.Add(scriptPtr, scriptType);
}
}
return godot_bool.True;
string scriptPathStr = Marshaling.ConvertStringToManaged(*scriptPath);
return AddScriptBridgeCore(scriptPtr, scriptPathStr).ToGodotBool();
}
catch (Exception e)
{
@ -444,6 +495,22 @@ namespace Godot.Bridge
}
}
private static unsafe bool AddScriptBridgeCore(IntPtr scriptPtr, string scriptPath)
{
lock (_scriptTypeBiMap.ReadWriteLock)
{
if (!_scriptTypeBiMap.IsScriptRegistered(scriptPtr))
{
if (!_pathTypeBiMap.TryGetScriptType(scriptPath, out Type? scriptType))
return false;
_scriptTypeBiMap.Add(scriptPtr, scriptType);
}
}
return true;
}
[UnmanagedCallersOnly]
internal static unsafe void GetOrCreateScriptBridgeForPath(godot_string* scriptPath, godot_ref* outScript)
{
@ -455,6 +522,8 @@ namespace Godot.Bridge
return;
}
Debug.Assert(!scriptType.IsGenericTypeDefinition, $"Cannot get or create script for a generic type definition '{scriptType.FullName}'. Path: '{scriptPathStr}'.");
GetOrCreateScriptBridgeForType(scriptType, outScript);
}
@ -494,16 +563,51 @@ namespace Godot.Bridge
if (_pathTypeBiMap.TryGetScriptPath(scriptType, out scriptPath))
return true;
if (scriptType.IsConstructedGenericType)
{
// If the script type is generic, also try looking for the path of the generic type definition
// since we can use it to create the script.
Type genericTypeDefinition = scriptType.GetGenericTypeDefinition();
if (_pathTypeBiMap.TryGetGenericTypeDefinitionPath(genericTypeDefinition, out scriptPath))
return true;
}
CreateScriptBridgeForType(scriptType, outScript);
scriptPath = null;
return false;
}
}
static string GetVirtualConstructedGenericTypeScriptPath(Type scriptType, string scriptPath)
{
// Constructed generic types all have the same path which is not allowed by Godot
// (every Resource must have a unique path). So we create a unique "virtual" path
// for each type.
if (!scriptPath.StartsWith("res://"))
{
throw new ArgumentException("Script path must start with 'res://'.", nameof(scriptPath));
}
scriptPath = scriptPath.Substring("res://".Length);
return $"csharp://{scriptPath}:{scriptType}.cs";
}
if (GetPathOtherwiseGetOrCreateScript(scriptType, outScript, out string? scriptPath))
{
// This path is slower, but it's only executed for the first instantiation of the type
if (scriptType.IsConstructedGenericType && !scriptPath.StartsWith("csharp://"))
{
// If the script type is generic it can't be loaded using the real script path.
// Construct a virtual path unique to this constructed generic type and add it
// to the path bimap so they can be found later by their virtual path.
// IMPORTANT: The virtual path must be added to _pathTypeBiMap before the first
// load of the script, otherwise the loaded script won't be added to _scriptTypeBiMap.
scriptPath = GetVirtualConstructedGenericTypeScriptPath(scriptType, scriptPath);
_pathTypeBiMap.Add(scriptPath, scriptType);
}
// This must be done outside the read-write lock, as the script resource loading can lock it
using godot_string scriptPathIn = Marshaling.ConvertStringToNative(scriptPath);
if (!NativeFuncs.godotsharp_internal_script_load(scriptPathIn, outScript).ToBool())
@ -514,11 +618,23 @@ namespace Godot.Bridge
// with no path, as we do for types without an associated script file.
GetOrCreateScriptBridgeForType(scriptType, outScript);
}
if (scriptType.IsConstructedGenericType)
{
// When reloading generic scripts they won't be added to the script bimap because their
// virtual path won't be in the path bimap yet. The current method executes when a derived type
// is trying to get or create the script for their base type. The code above has now added
// the virtual path to the path bimap and loading the script with that path should retrieve
// any existing script, so now we have a chance to make sure it's added to the script bimap.
AddScriptBridgeCore(outScript->Reference, scriptPath);
}
}
}
private static unsafe void CreateScriptBridgeForType(Type scriptType, godot_ref* outScript)
{
Debug.Assert(!scriptType.IsGenericTypeDefinition, $"Script type must be a constructed generic type or not generic at all. Type: {scriptType}.");
NativeFuncs.godotsharp_internal_new_csharp_script(outScript);
IntPtr scriptPtr = outScript->Reference;
@ -605,45 +721,82 @@ namespace Godot.Bridge
}
}
private static unsafe void GetScriptTypeInfo(Type scriptType, godot_csharp_type_info* outTypeInfo)
{
Type native = GodotObject.InternalGetClassNativeBase(scriptType);
string typeName = scriptType.Name;
if (scriptType.IsGenericType)
{
var sb = new StringBuilder();
AppendTypeName(sb, scriptType);
typeName = sb.ToString();
}
godot_string className = Marshaling.ConvertStringToNative(typeName);
bool isTool = scriptType.IsDefined(typeof(ToolAttribute), inherit: false);
// If the type is nested and the parent type is a tool script,
// consider the nested type a tool script as well.
if (!isTool && scriptType.IsNested)
{
isTool = scriptType.DeclaringType?.IsDefined(typeof(ToolAttribute), inherit: false) ?? false;
}
// Every script in the GodotTools assembly is a tool script.
if (!isTool && scriptType.Assembly.GetName().Name == "GodotTools")
{
isTool = true;
}
bool isGlobalClass = scriptType.IsDefined(typeof(GlobalClassAttribute), inherit: false);
var iconAttr = scriptType.GetCustomAttributes(inherit: false)
.OfType<IconAttribute>()
.FirstOrDefault();
godot_string iconPath = Marshaling.ConvertStringToNative(iconAttr?.Path);
outTypeInfo->ClassName = className;
outTypeInfo->IconPath = iconPath;
outTypeInfo->IsTool = isTool.ToGodotBool();
outTypeInfo->IsGlobalClass = isGlobalClass.ToGodotBool();
outTypeInfo->IsAbstract = scriptType.IsAbstract.ToGodotBool();
outTypeInfo->IsGenericTypeDefinition = scriptType.IsGenericTypeDefinition.ToGodotBool();
outTypeInfo->IsConstructedGenericType = scriptType.IsConstructedGenericType.ToGodotBool();
static void AppendTypeName(StringBuilder sb, Type type)
{
sb.Append(type.Name);
if (type.IsGenericType)
{
sb.Append('<');
for (int i = 0; i < type.GenericTypeArguments.Length; i++)
{
Type typeArg = type.GenericTypeArguments[i];
AppendTypeName(sb, typeArg);
if (i != type.GenericTypeArguments.Length - 1)
{
sb.Append(", ");
}
}
sb.Append('>');
}
}
}
[UnmanagedCallersOnly]
internal static unsafe void UpdateScriptClassInfo(IntPtr scriptPtr, godot_string* outClassName,
godot_bool* outTool, godot_bool* outGlobal, godot_bool* outAbstract, godot_string* outIconPath,
godot_array* outMethodsDest, godot_dictionary* outRpcFunctionsDest,
godot_dictionary* outEventSignalsDest, godot_ref* outBaseScript)
internal static unsafe void UpdateScriptClassInfo(IntPtr scriptPtr, godot_csharp_type_info* outTypeInfo,
godot_array* outMethodsDest, godot_dictionary* outRpcFunctionsDest, godot_dictionary* outEventSignalsDest, godot_ref* outBaseScript)
{
try
{
// Performance is not critical here as this will be replaced with source generators.
var scriptType = _scriptTypeBiMap.GetScriptType(scriptPtr);
Debug.Assert(!scriptType.IsGenericTypeDefinition, $"Script type must be a constructed generic type or not generic at all. Type: {scriptType}.");
*outClassName = Marshaling.ConvertStringToNative(scriptType.Name);
*outTool = scriptType.GetCustomAttributes(inherit: false)
.OfType<ToolAttribute>()
.Any().ToGodotBool();
if (!(*outTool).ToBool() && scriptType.IsNested)
{
*outTool = (scriptType.DeclaringType?.GetCustomAttributes(inherit: false)
.OfType<ToolAttribute>()
.Any() ?? false).ToGodotBool();
}
if (!(*outTool).ToBool() && scriptType.Assembly.GetName().Name == "GodotTools")
*outTool = godot_bool.True;
var globalAttr = scriptType.GetCustomAttributes(inherit: false)
.OfType<GlobalClassAttribute>()
.FirstOrDefault();
*outGlobal = (globalAttr != null).ToGodotBool();
var iconAttr = scriptType.GetCustomAttributes(inherit: false)
.OfType<IconAttribute>()
.FirstOrDefault();
*outIconPath = Marshaling.ConvertStringToNative(iconAttr?.Path);
*outAbstract = scriptType.IsAbstract.ToGodotBool();
GetScriptTypeInfo(scriptType, outTypeInfo);
// Methods
@ -820,11 +973,7 @@ namespace Godot.Bridge
catch (Exception e)
{
ExceptionUtils.LogException(e);
*outClassName = default;
*outTool = godot_bool.False;
*outGlobal = godot_bool.False;
*outAbstract = godot_bool.False;
*outIconPath = default;
*outTypeInfo = default;
*outMethodsDest = NativeFuncs.godotsharp_array_new();
*outRpcFunctionsDest = NativeFuncs.godotsharp_dictionary_new();
*outEventSignalsDest = NativeFuncs.godotsharp_dictionary_new();

View File

@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
@ -19,6 +20,8 @@ public static partial class ScriptManagerBridge
{
// TODO: What if this is called while unloading a load context, but after we already did cleanup in preparation for unloading?
Debug.Assert(!scriptType.IsGenericTypeDefinition, $"A generic type definition must never be added to the script type map. Type: {scriptType}.");
_scriptTypeMap.Add(scriptPtr, scriptType);
_typeScriptMap.Add(scriptType, scriptPtr);
@ -85,10 +88,29 @@ public static partial class ScriptManagerBridge
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetScriptType(string scriptPath, [MaybeNullWhen(false)] out Type scriptType) =>
_pathTypeMap.TryGetValue(scriptPath, out scriptType);
// This must never return true for a generic type definition, we only consider script types
// the types that can be attached to a Node/Resource (non-generic or constructed generic types).
_pathTypeMap.TryGetValue(scriptPath, out scriptType) && !scriptType.IsGenericTypeDefinition;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetScriptPath(Type scriptType, [MaybeNullWhen(false)] out string scriptPath) =>
_typePathMap.TryGetValue(scriptType, out scriptPath);
public bool TryGetScriptPath(Type scriptType, [MaybeNullWhen(false)] out string scriptPath)
{
if (scriptType.IsGenericTypeDefinition)
{
// This must never return true for a generic type definition, we only consider script types
// the types that can be attached to a Node/Resource (non-generic or constructed generic types).
scriptPath = null;
return false;
}
return _typePathMap.TryGetValue(scriptType, out scriptPath);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetGenericTypeDefinitionPath(Type genericTypeDefinition, [MaybeNullWhen(false)] out string scriptPath)
{
Debug.Assert(genericTypeDefinition.IsGenericTypeDefinition);
return _typePathMap.TryGetValue(genericTypeDefinition, out scriptPath);
}
}
}

View File

@ -105,6 +105,61 @@ namespace Godot.NativeInterop
}
}
[StructLayout(LayoutKind.Sequential)]
// ReSharper disable once InconsistentNaming
public ref struct godot_csharp_type_info
{
private godot_string _className;
private godot_string _iconPath;
private godot_bool _isTool;
private godot_bool _isGlobalClass;
private godot_bool _isAbstract;
private godot_bool _isConstructedGenericType;
private godot_bool _isGenericTypeDefinition;
public godot_string ClassName
{
readonly get => _className;
set => _className = value;
}
public godot_string IconPath
{
readonly get => _iconPath;
set => _iconPath = value;
}
public godot_bool IsTool
{
readonly get => _isTool;
set => _isTool = value;
}
public godot_bool IsGlobalClass
{
readonly get => _isGlobalClass;
set => _isGlobalClass = value;
}
public godot_bool IsAbstract
{
readonly get => _isAbstract;
set => _isAbstract = value;
}
public godot_bool IsConstructedGenericType
{
readonly get => _isConstructedGenericType;
set => _isConstructedGenericType = value;
}
public godot_bool IsGenericTypeDefinition
{
readonly get => _isGenericTypeDefinition;
set => _isGenericTypeDefinition = value;
}
}
[StructLayout(LayoutKind.Sequential, Pack = 8)]
// ReSharper disable once InconsistentNaming
public ref struct godot_variant

View File

@ -59,6 +59,7 @@ void update_godot_api_cache(const ManagedCallbacks &p_managed_callbacks) {
CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, CreateManagedForGodotObjectBinding);
CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, CreateManagedForGodotObjectScriptInstance);
CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, GetScriptNativeName);
CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, GetGlobalClassName);
CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, SetGodotObjectPtr);
CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, RaiseEventSignal);
CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, ScriptIsOrInherits);

View File

@ -84,6 +84,7 @@ struct ManagedCallbacks {
using FuncScriptManagerBridge_CreateManagedForGodotObjectBinding = GCHandleIntPtr(GD_CLR_STDCALL *)(const StringName *, Object *);
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_GetGlobalClassName = void(GD_CLR_STDCALL *)(const String *, String *, String *, String *);
using FuncScriptManagerBridge_SetGodotObjectPtr = void(GD_CLR_STDCALL *)(GCHandleIntPtr, Object *);
using FuncScriptManagerBridge_RaiseEventSignal = void(GD_CLR_STDCALL *)(GCHandleIntPtr, const StringName *, const Variant **, int32_t, bool *);
using FuncScriptManagerBridge_ScriptIsOrInherits = bool(GD_CLR_STDCALL *)(const CSharpScript *, const CSharpScript *);
@ -91,7 +92,7 @@ struct ManagedCallbacks {
using FuncScriptManagerBridge_GetOrCreateScriptBridgeForPath = void(GD_CLR_STDCALL *)(const String *, Ref<CSharpScript> *);
using FuncScriptManagerBridge_RemoveScriptBridge = void(GD_CLR_STDCALL *)(const CSharpScript *);
using FuncScriptManagerBridge_TryReloadRegisteredScriptWithClass = bool(GD_CLR_STDCALL *)(const CSharpScript *);
using FuncScriptManagerBridge_UpdateScriptClassInfo = void(GD_CLR_STDCALL *)(const CSharpScript *, String *, bool *, bool *, bool *, String *, Array *, Dictionary *, Dictionary *, Ref<CSharpScript> *);
using FuncScriptManagerBridge_UpdateScriptClassInfo = void(GD_CLR_STDCALL *)(const CSharpScript *, CSharpScript::TypeInfo *, Array *, Dictionary *, Dictionary *, Ref<CSharpScript> *);
using FuncScriptManagerBridge_SwapGCHandleForType = bool(GD_CLR_STDCALL *)(GCHandleIntPtr, GCHandleIntPtr *, bool);
using FuncScriptManagerBridge_GetPropertyInfoList = void(GD_CLR_STDCALL *)(CSharpScript *, Callback_ScriptManagerBridge_GetPropertyInfoList_Add);
using FuncScriptManagerBridge_GetPropertyDefaultValues = void(GD_CLR_STDCALL *)(CSharpScript *, Callback_ScriptManagerBridge_GetPropertyDefaultValues_Add);
@ -120,6 +121,7 @@ struct ManagedCallbacks {
FuncScriptManagerBridge_CreateManagedForGodotObjectBinding ScriptManagerBridge_CreateManagedForGodotObjectBinding;
FuncScriptManagerBridge_CreateManagedForGodotObjectScriptInstance ScriptManagerBridge_CreateManagedForGodotObjectScriptInstance;
FuncScriptManagerBridge_GetScriptNativeName ScriptManagerBridge_GetScriptNativeName;
FuncScriptManagerBridge_GetGlobalClassName ScriptManagerBridge_GetGlobalClassName;
FuncScriptManagerBridge_SetGodotObjectPtr ScriptManagerBridge_SetGodotObjectPtr;
FuncScriptManagerBridge_RaiseEventSignal ScriptManagerBridge_RaiseEventSignal;
FuncScriptManagerBridge_ScriptIsOrInherits ScriptManagerBridge_ScriptIsOrInherits;