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:
Ignacio Roldán Etcheverry 2021-09-12 20:21:15 +02:00
parent 5e37d073bb
commit 513ee857a9
79 changed files with 2562 additions and 5223 deletions

View file

@ -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;