C#: Restructure code prior move to .NET Core
The main focus here was to remove the majority of code that relied on Mono's embedding APIs, specially the reflection APIs. The embedding APIs we still use are the bare minimum we need for things to work. A lot of code was moved to C#. We no longer deal with any managed objects (`MonoObject*`, and such) in native code, and all marshaling is done in C#. The reason for restructuring the code and move away from embedding APIs is that once we move to .NET Core, we will be limited by the much more minimal .NET hosting. PERFORMANCE REGRESSIONS ----------------------- Some parts of the code were written with little to no concern about performance. This includes code that calls into script methods and accesses script fields, properties and events. The reason for this is that all of that will be moved to source generators, so any work prior to that would be a waste of time. DISABLED FEATURES ----------------- Some code was removed as it no longer makes sense (or won't make sense in the future). Other parts were commented out with `#if 0`s and TODO warnings because it doesn't make much sense to work on them yet as those parts will change heavily when we switch to .NET Core but also when we start introducing source generators. As such, the following features were disabled temporarily: - Assembly-reloading (will be done with ALCs in .NET Core). - Properties/fields exports and script method listing (will be handled by source generators in the future). - Exception logging in the editor and stack info for errors. - Exporting games. - Building of C# projects. We no longer copy the Godot API assemblies to the project directory, so MSBuild won't be able to find them. The idea is to turn them into NuGet packages in the future, which could also be obtained from local NuGet sources during development.
This commit is contained in:
parent
5e37d073bb
commit
513ee857a9
79 changed files with 2562 additions and 5223 deletions
|
|
@ -96,9 +96,9 @@ StringBuilder &operator<<(StringBuilder &r_sb, const char *p_cstring) {
|
|||
|
||||
#define C_CLASS_NATIVE_FUNCS "NativeFuncs"
|
||||
#define C_NS_MONOUTILS "InteropUtils"
|
||||
#define C_METHOD_TIE_MANAGED_TO_UNMANAGED "NativeInterop." C_NS_MONOUTILS ".TieManagedToUnmanaged"
|
||||
#define C_METHOD_TIE_MANAGED_TO_UNMANAGED C_NS_MONOUTILS ".TieManagedToUnmanaged"
|
||||
#define C_METHOD_UNMANAGED_GET_MANAGED C_NS_MONOUTILS ".UnmanagedGetManaged"
|
||||
#define C_METHOD_ENGINE_GET_SINGLETON "NativeInterop." C_NS_MONOUTILS ".EngineGetSingleton"
|
||||
#define C_METHOD_ENGINE_GET_SINGLETON C_NS_MONOUTILS ".EngineGetSingleton"
|
||||
|
||||
#define C_NS_MONOMARSHAL "Marshaling"
|
||||
#define C_METHOD_MANAGED_TO_VARIANT C_NS_MONOMARSHAL ".mono_object_to_variant"
|
||||
|
|
@ -1376,6 +1376,7 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str
|
|||
|
||||
output.append("using System;\n"); // IntPtr
|
||||
output.append("using System.Diagnostics;\n"); // DebuggerBrowsable
|
||||
output.append("using Godot.NativeInterop;\n");
|
||||
|
||||
output.append("\n"
|
||||
"#pragma warning disable CS1591 // Disable warning: "
|
||||
|
|
@ -1408,7 +1409,11 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str
|
|||
if (itype.is_singleton) {
|
||||
output.append("static partial class ");
|
||||
} else {
|
||||
output.append(itype.is_instantiable ? "partial class " : "abstract partial class ");
|
||||
// Even if the class is not instantiable, we can't declare it abstract because
|
||||
// the engine can still instantiate them and return them via the scripting API.
|
||||
// Example: `SceneTreeTimer` returned from `SceneTree.create_timer`.
|
||||
// See the reverted commit: ef5672d3f94a7321ed779c922088bb72adbb1521
|
||||
output.append("partial class ");
|
||||
}
|
||||
output.append(itype.proxy_name);
|
||||
|
||||
|
|
@ -1533,6 +1538,10 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str
|
|||
|
||||
// Add native name static field
|
||||
|
||||
if (is_derived_type) {
|
||||
output << MEMBER_BEGIN "private static readonly System.Type _cachedType = typeof(" << itype.proxy_name << ");\n";
|
||||
}
|
||||
|
||||
output.append(MEMBER_BEGIN "private static readonly StringName " BINDINGS_NATIVE_NAME_FIELD " = \"");
|
||||
output.append(itype.name);
|
||||
output.append("\";\n");
|
||||
|
|
@ -1540,27 +1549,19 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str
|
|||
if (itype.is_instantiable) {
|
||||
// Add native constructor static field
|
||||
|
||||
String get_constructor_method = ICALL_CLASSDB_GET_CONSTRUCTOR;
|
||||
|
||||
if (itype.is_singleton) {
|
||||
// Singletons are static classes. They don't derive Godot.Object,
|
||||
// so we need to specify the type to call the static method.
|
||||
get_constructor_method = "Object." + get_constructor_method;
|
||||
}
|
||||
|
||||
output << MEMBER_BEGIN << "[DebuggerBrowsable(DebuggerBrowsableState.Never)]\n"
|
||||
|
||||
<< "#if NET\n"
|
||||
|
||||
<< INDENT2 "private static unsafe readonly delegate* unmanaged<IntPtr> "
|
||||
<< CS_STATIC_FIELD_NATIVE_CTOR " = " << get_constructor_method
|
||||
<< CS_STATIC_FIELD_NATIVE_CTOR " = " ICALL_CLASSDB_GET_CONSTRUCTOR
|
||||
<< "(" BINDINGS_NATIVE_NAME_FIELD ");\n"
|
||||
|
||||
<< "#else\n"
|
||||
|
||||
// Get rid of this one once we switch to .NET 5/6
|
||||
<< INDENT2 "private static readonly IntPtr " CS_STATIC_FIELD_NATIVE_CTOR
|
||||
<< " = " << get_constructor_method << "(" BINDINGS_NATIVE_NAME_FIELD ");\n"
|
||||
<< " = " ICALL_CLASSDB_GET_CONSTRUCTOR "(" BINDINGS_NATIVE_NAME_FIELD ");\n"
|
||||
|
||||
<< "#endif\n";
|
||||
}
|
||||
|
|
@ -1589,8 +1590,12 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str
|
|||
|
||||
<< "#endif\n"
|
||||
|
||||
<< INDENT4 C_METHOD_TIE_MANAGED_TO_UNMANAGED "(this, " BINDINGS_PTR_FIELD ");\n"
|
||||
<< CLOSE_BLOCK_L3
|
||||
<< INDENT4 C_METHOD_TIE_MANAGED_TO_UNMANAGED "(this, " BINDINGS_PTR_FIELD ", "
|
||||
<< BINDINGS_NATIVE_NAME_FIELD << ", refCounted: " << (itype.is_ref_counted ? "true" : "false")
|
||||
<< ", ((object)this).GetType(), _cachedType);\n" CLOSE_BLOCK_L3
|
||||
<< INDENT3 "else\n" INDENT3 OPEN_BLOCK
|
||||
<< INDENT4 "InteropUtils.TieManagedToUnmanagedWithPreSetup(this, "
|
||||
<< BINDINGS_PTR_FIELD ", ((object)this).GetType(), _cachedType);\n" CLOSE_BLOCK_L3
|
||||
<< INDENT3 "_InitializeGodotScriptInstanceInternals();\n" CLOSE_BLOCK_L2;
|
||||
} else {
|
||||
// Hide the constructor
|
||||
|
|
@ -1606,6 +1611,8 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str
|
|||
}
|
||||
}
|
||||
|
||||
// Methods
|
||||
|
||||
int method_bind_count = 0;
|
||||
for (const MethodInterface &imethod : itype.methods) {
|
||||
Error method_err = _generate_cs_method(itype, imethod, method_bind_count, output);
|
||||
|
|
@ -1613,12 +1620,89 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str
|
|||
"Failed to generate method '" + imethod.name + "' for class '" + itype.name + "'.");
|
||||
}
|
||||
|
||||
// Signals
|
||||
|
||||
for (const SignalInterface &isignal : itype.signals_) {
|
||||
Error method_err = _generate_cs_signal(itype, isignal, output);
|
||||
ERR_FAIL_COND_V_MSG(method_err != OK, method_err,
|
||||
"Failed to generate signal '" + isignal.name + "' for class '" + itype.name + "'.");
|
||||
}
|
||||
|
||||
// Script calls
|
||||
|
||||
if (!itype.is_singleton && (is_derived_type || itype.has_virtual_methods)) {
|
||||
// TODO: string is ok for now. But should be replaced with StringName in the future for performance.
|
||||
|
||||
output << MEMBER_BEGIN "internal " << (is_derived_type ? "override" : "virtual")
|
||||
<< " unsafe bool InternalGodotScriptCall(string method, godot_variant** args, "
|
||||
<< "int argCount, out godot_variant ret)\n"
|
||||
<< INDENT2 "{\n";
|
||||
|
||||
for (const MethodInterface &imethod : itype.methods) {
|
||||
if (!imethod.is_virtual) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// Compare with cached StringName. We already have a cached StringName
|
||||
// field for the proxy name. We need one for the original snake_case name.
|
||||
output << INDENT3 "if ((method == nameof(" << imethod.proxy_name << ") || method == \"" << imethod.name
|
||||
<< "\") && argCount == " << itos(imethod.arguments.size()) << ")\n"
|
||||
<< INDENT3 "{\n";
|
||||
|
||||
if (imethod.return_type.cname != name_cache.type_void) {
|
||||
output << INDENT4 "object retBoxed = ";
|
||||
} else {
|
||||
output << INDENT4;
|
||||
}
|
||||
|
||||
output << imethod.proxy_name << "(";
|
||||
|
||||
for (int i = 0; i < imethod.arguments.size(); i++) {
|
||||
const ArgumentInterface &iarg = imethod.arguments[i];
|
||||
|
||||
const TypeInterface *arg_type = _get_type_or_null(iarg.type);
|
||||
ERR_FAIL_NULL_V(arg_type, ERR_BUG); // Argument type not found
|
||||
|
||||
if (i != 0) {
|
||||
output << ", ";
|
||||
}
|
||||
|
||||
// TODO: static marshaling (no reflection, no runtime type checks)
|
||||
if (arg_type->cname == name_cache.type_Array_generic || arg_type->cname == name_cache.type_Dictionary_generic) {
|
||||
String arg_cs_type = arg_type->cs_type + _get_generic_type_parameters(*arg_type, iarg.type.generic_type_parameters);
|
||||
|
||||
output << "new " << arg_cs_type << "((" << arg_type->cs_type << ")Marshaling.variant_to_mono_object_of_type(args["
|
||||
<< itos(i) << "], typeof(" << arg_type->cs_type << ")))";
|
||||
} else {
|
||||
output << "(" << arg_type->cs_type << ")Marshaling.variant_to_mono_object_of_type(args["
|
||||
<< itos(i) << "], typeof(" << arg_type->cs_type << "))";
|
||||
}
|
||||
}
|
||||
|
||||
output << ");\n";
|
||||
|
||||
if (imethod.return_type.cname != name_cache.type_void) {
|
||||
// TODO: static marshaling (no reflection, no runtime type checks)
|
||||
output << INDENT4 "ret = Marshaling.mono_object_to_variant(retBoxed);\n";
|
||||
output << INDENT4 "return true;\n";
|
||||
} else {
|
||||
output << INDENT4 "ret = default;\n";
|
||||
output << INDENT4 "return true;\n";
|
||||
}
|
||||
|
||||
output << INDENT3 "}\n";
|
||||
}
|
||||
|
||||
if (is_derived_type) {
|
||||
output << INDENT3 "return base.InternalGodotScriptCall(method, args, argCount, out ret);\n";
|
||||
} else {
|
||||
output << INDENT3 "return InternalGodotScriptCallViaReflection(method, args, argCount, out ret);\n";
|
||||
}
|
||||
|
||||
output << INDENT2 "}\n";
|
||||
}
|
||||
|
||||
output.append(INDENT1 CLOSE_BLOCK /* class */
|
||||
CLOSE_BLOCK /* namespace */);
|
||||
|
||||
|
|
@ -2726,6 +2810,7 @@ bool BindingsGenerator::_populate_object_type_interfaces() {
|
|||
|
||||
if (method_info.flags & METHOD_FLAG_VIRTUAL) {
|
||||
imethod.is_virtual = true;
|
||||
itype.has_virtual_methods = true;
|
||||
}
|
||||
|
||||
PropertyInfo return_info = method_info.return_val;
|
||||
|
|
@ -2865,7 +2950,7 @@ bool BindingsGenerator::_populate_object_type_interfaces() {
|
|||
ERR_FAIL_COND_V_MSG(itype.find_property_by_name(imethod.cname), false,
|
||||
"Method name conflicts with property: '" + itype.name + "." + imethod.name + "'.");
|
||||
|
||||
// Classes starting with an underscore are ignored unless they're used as a property setter or getter
|
||||
// Methods starting with an underscore are ignored unless they're used as a property setter or getter
|
||||
if (!imethod.is_virtual && imethod.name[0] == '_') {
|
||||
for (const PropertyInterface &iprop : itype.properties) {
|
||||
if (iprop.setter == imethod.name || iprop.getter == imethod.name) {
|
||||
|
|
@ -3335,7 +3420,7 @@ void BindingsGenerator::_populate_builtin_type_interfaces() {
|
|||
itype.proxy_name = "string";
|
||||
itype.cs_type = itype.proxy_name;
|
||||
itype.c_in = "%5using %0 %1_in = " C_METHOD_MONOSTR_TO_GODOT "(%1);\n";
|
||||
itype.c_out = "%5return " C_METHOD_MONOSTR_FROM_GODOT "(&%1);\n";
|
||||
itype.c_out = "%5return " C_METHOD_MONOSTR_FROM_GODOT "(%1);\n";
|
||||
itype.c_arg_in = "&%s_in";
|
||||
itype.c_type = "godot_string";
|
||||
itype.c_type_in = itype.cs_type;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue