From 124fbf95f8ef065215e9fcc937a370dbef3196e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20Rold=C3=A1n=20Etcheverry?= Date: Mon, 3 May 2021 15:21:06 +0200 Subject: C#: Move marshaling logic and generated glue to C# We will be progressively moving most code to C#. The plan is to only use Mono's embedding APIs to set things at launch. This will make it much easier to later support CoreCLR too which doesn't have rich embedding APIs. Additionally the code in C# is more maintainable and makes it easier to implement new features, e.g.: runtime codegen which we could use to avoid using reflection for marshaling everytime a field, property or method is accessed. SOME NOTES ON INTEROP We make the same assumptions as GDNative about the size of the Godot structures we use. We take it a bit further by also assuming the layout of fields in some cases, which is riskier but let's us squeeze out some performance by avoiding unnecessary managed to native calls. Code that deals with native structs is less safe than before as there's no RAII and copy constructors in C#. It's like using the GDNative C API directly. One has to take special care to free values they own. Perhaps we could use roslyn analyzers to check this, but I don't know any that uses attributes to determine what's owned or borrowed. As to why we maily use pointers for native structs instead of ref/out: - AFAIK (and confirmed with a benchmark) ref/out are pinned during P/Invoke calls and that has a cost. - Native struct fields can't be ref/out in the first place. - A `using` local can't be passed as ref/out, only `in`. Calling a method or property on an `in` value makes a silent copy, so we want to avoid `in`. REGARDING THE BUILD SYSTEM There's no longer a `mono_glue=yes/no` SCons options. We no longer need to build with `mono_glue=no`, generate the glue and then build again with `mono_glue=yes`. We build only once and generate the glue (which is in C# now). However, SCons no longer builds the C# projects for us. Instead one must run `build_assemblies.py`, e.g.: ```sh %godot_src_root%/modules/mono/build_scripts/build_assemblies.py \ --godot-output-dir=%godot_src_root%/bin \ --godot-target=release_debug` ``` We could turn this into a custom build target, but I don't know how to do that with SCons (it's possible with Meson). OTHER NOTES Most of the moved code doesn't follow the C# naming convention and still has the word Mono in the names despite no longer dealing with Mono's embedding APIs. This is just temporary while transitioning, to make it easier to understand what was moved where. --- modules/mono/managed_callable.cpp | 60 ++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 32 deletions(-) (limited to 'modules/mono/managed_callable.cpp') diff --git a/modules/mono/managed_callable.cpp b/modules/mono/managed_callable.cpp index c159bb9eea..72b353d8c1 100644 --- a/modules/mono/managed_callable.cpp +++ b/modules/mono/managed_callable.cpp @@ -31,6 +31,7 @@ #include "managed_callable.h" #include "csharp_script.h" +#include "mono_gd/gd_mono_cache.h" #include "mono_gd/gd_mono_marshal.h" #include "mono_gd/gd_mono_utils.h" @@ -44,18 +45,20 @@ bool ManagedCallable::compare_equal(const CallableCustom *p_a, const CallableCus const ManagedCallable *a = static_cast(p_a); const ManagedCallable *b = static_cast(p_b); - MonoDelegate *delegate_a = (MonoDelegate *)a->delegate_handle.get_target(); - MonoDelegate *delegate_b = (MonoDelegate *)b->delegate_handle.get_target(); - - if (!delegate_a || !delegate_b) { - if (!delegate_a && !delegate_b) { + if (!a->delegate_handle || !b->delegate_handle) { + if (!a->delegate_handle && !b->delegate_handle) { return true; } return false; } // Call Delegate's 'Equals' - return GDMonoUtils::mono_delegate_equal(delegate_a, delegate_b); + MonoException *exc = nullptr; + MonoBoolean res = CACHED_METHOD_THUNK(DelegateUtils, DelegateEquals) + .invoke(a->delegate_handle, + b->delegate_handle, &exc); + UNHANDLED_EXCEPTION(exc); + return (bool)res; } bool ManagedCallable::compare_less(const CallableCustom *p_a, const CallableCustom *p_b) { @@ -66,8 +69,7 @@ bool ManagedCallable::compare_less(const CallableCustom *p_a, const CallableCust } uint32_t ManagedCallable::hash() const { - uint32_t hash = delegate_invoke->get_name().hash(); - return hash_murmur3_one_64(delegate_handle.handle, hash); + return hash_murmur3_one_64((uint64_t)delegate_handle); } String ManagedCallable::get_as_text() const { @@ -91,41 +93,34 @@ void ManagedCallable::call(const Variant **p_arguments, int p_argcount, Variant r_call_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD; // Can't find anything better r_return_value = Variant(); -#ifdef GD_MONO_HOT_RELOAD - // Lost during hot-reload - ERR_FAIL_NULL(delegate_invoke); - ERR_FAIL_COND(delegate_handle.is_released()); -#endif - - ERR_FAIL_COND(delegate_invoke->get_parameters_count() < p_argcount); - - MonoObject *delegate = delegate_handle.get_target(); - MonoException *exc = nullptr; - MonoObject *ret = delegate_invoke->invoke(delegate, p_arguments, &exc); + CACHED_METHOD_THUNK(DelegateUtils, InvokeWithVariantArgs) + .invoke(delegate_handle, p_arguments, + p_argcount, &r_return_value, &exc); if (exc) { GDMonoUtils::set_pending_exception(exc); } else { - r_return_value = GDMonoMarshal::mono_object_to_variant(ret); r_call_error.error = Callable::CallError::CALL_OK; } } -void ManagedCallable::set_delegate(MonoDelegate *p_delegate) { - delegate_handle = MonoGCHandleData::new_strong_handle((MonoObject *)p_delegate); - MonoMethod *delegate_invoke_raw = mono_get_delegate_invoke(mono_object_get_class((MonoObject *)p_delegate)); - const StringName &delegate_invoke_name = CSharpLanguage::get_singleton()->get_string_names().delegate_invoke_method_name; - delegate_invoke = memnew(GDMonoMethod(delegate_invoke_name, delegate_invoke_raw)); // TODO: Use pooling for this GDMonoMethod instances -} +void ManagedCallable::release_delegate_handle() { + if (delegate_handle) { + MonoException *exc = nullptr; + CACHED_METHOD_THUNK(DelegateUtils, FreeGCHandle).invoke(delegate_handle, &exc); -ManagedCallable::ManagedCallable(MonoDelegate *p_delegate) { -#ifdef DEBUG_ENABLED - CRASH_COND(p_delegate == nullptr); -#endif + if (exc) { + GDMonoUtils::debug_print_unhandled_exception(exc); + } - set_delegate(p_delegate); + delegate_handle = nullptr; + } +} +// Why you do this clang-format... +/* clang-format off */ +ManagedCallable::ManagedCallable(void *p_delegate_handle) : delegate_handle(p_delegate_handle) { #ifdef GD_MONO_HOT_RELOAD { MutexLock lock(instances_mutex); @@ -133,6 +128,7 @@ ManagedCallable::ManagedCallable(MonoDelegate *p_delegate) { } #endif } +/* clang-format on */ ManagedCallable::~ManagedCallable() { #ifdef GD_MONO_HOT_RELOAD @@ -143,5 +139,5 @@ ManagedCallable::~ManagedCallable() { } #endif - delegate_handle.release(); + release_delegate_handle(); } -- cgit v1.2.3 From 513ee857a938c466e0f7146f66db771b9c6e2024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20Rold=C3=A1n=20Etcheverry?= Date: Sun, 12 Sep 2021 20:21:15 +0200 Subject: 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. --- modules/mono/managed_callable.cpp | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) (limited to 'modules/mono/managed_callable.cpp') diff --git a/modules/mono/managed_callable.cpp b/modules/mono/managed_callable.cpp index 72b353d8c1..e01d0c6e18 100644 --- a/modules/mono/managed_callable.cpp +++ b/modules/mono/managed_callable.cpp @@ -32,7 +32,6 @@ #include "csharp_script.h" #include "mono_gd/gd_mono_cache.h" -#include "mono_gd/gd_mono_marshal.h" #include "mono_gd/gd_mono_utils.h" #ifdef GD_MONO_HOT_RELOAD @@ -45,8 +44,8 @@ bool ManagedCallable::compare_equal(const CallableCustom *p_a, const CallableCus const ManagedCallable *a = static_cast(p_a); const ManagedCallable *b = static_cast(p_b); - if (!a->delegate_handle || !b->delegate_handle) { - if (!a->delegate_handle && !b->delegate_handle) { + if (!a->delegate_handle.value || !b->delegate_handle.value) { + if (!a->delegate_handle.value && !b->delegate_handle.value) { return true; } return false; @@ -54,7 +53,7 @@ bool ManagedCallable::compare_equal(const CallableCustom *p_a, const CallableCus // Call Delegate's 'Equals' MonoException *exc = nullptr; - MonoBoolean res = CACHED_METHOD_THUNK(DelegateUtils, DelegateEquals) + MonoBoolean res = GDMonoCache::cached_data.methodthunk_DelegateUtils_DelegateEquals .invoke(a->delegate_handle, b->delegate_handle, &exc); UNHANDLED_EXCEPTION(exc); @@ -69,7 +68,7 @@ bool ManagedCallable::compare_less(const CallableCustom *p_a, const CallableCust } uint32_t ManagedCallable::hash() const { - return hash_murmur3_one_64((uint64_t)delegate_handle); + return hash_murmur3_one_64((uint64_t)delegate_handle.value); } String ManagedCallable::get_as_text() const { @@ -94,7 +93,7 @@ void ManagedCallable::call(const Variant **p_arguments, int p_argcount, Variant r_return_value = Variant(); MonoException *exc = nullptr; - CACHED_METHOD_THUNK(DelegateUtils, InvokeWithVariantArgs) + GDMonoCache::cached_data.methodthunk_DelegateUtils_InvokeWithVariantArgs .invoke(delegate_handle, p_arguments, p_argcount, &r_return_value, &exc); @@ -106,21 +105,21 @@ void ManagedCallable::call(const Variant **p_arguments, int p_argcount, Variant } void ManagedCallable::release_delegate_handle() { - if (delegate_handle) { + if (delegate_handle.value) { MonoException *exc = nullptr; - CACHED_METHOD_THUNK(DelegateUtils, FreeGCHandle).invoke(delegate_handle, &exc); + GDMonoCache::cached_data.methodthunk_GCHandleBridge_FreeGCHandle.invoke(delegate_handle, &exc); if (exc) { GDMonoUtils::debug_print_unhandled_exception(exc); } - delegate_handle = nullptr; + delegate_handle = GCHandleIntPtr(); } } // Why you do this clang-format... /* clang-format off */ -ManagedCallable::ManagedCallable(void *p_delegate_handle) : delegate_handle(p_delegate_handle) { +ManagedCallable::ManagedCallable(GCHandleIntPtr p_delegate_handle) : delegate_handle(p_delegate_handle) { #ifdef GD_MONO_HOT_RELOAD { MutexLock lock(instances_mutex); -- cgit v1.2.3 From f9a67ee9da1d6cc3562fa5a7443a2a66a673bd8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20Rold=C3=A1n=20Etcheverry?= Date: Sun, 12 Sep 2021 20:23:05 +0200 Subject: C#: Begin move to .NET Core We're targeting .NET 5 for now to make development easier while .NET 6 is not yet released. TEMPORARY REGRESSIONS --------------------- Assembly unloading is not implemented yet. As such, many Godot resources are leaked at exit. This will be re-implemented later together with assembly hot-reloading. --- modules/mono/managed_callable.cpp | 29 ++++++----------------------- 1 file changed, 6 insertions(+), 23 deletions(-) (limited to 'modules/mono/managed_callable.cpp') diff --git a/modules/mono/managed_callable.cpp b/modules/mono/managed_callable.cpp index e01d0c6e18..a1f94aa590 100644 --- a/modules/mono/managed_callable.cpp +++ b/modules/mono/managed_callable.cpp @@ -32,7 +32,6 @@ #include "csharp_script.h" #include "mono_gd/gd_mono_cache.h" -#include "mono_gd/gd_mono_utils.h" #ifdef GD_MONO_HOT_RELOAD SelfList::List ManagedCallable::instances; @@ -52,12 +51,8 @@ bool ManagedCallable::compare_equal(const CallableCustom *p_a, const CallableCus } // Call Delegate's 'Equals' - MonoException *exc = nullptr; - MonoBoolean res = GDMonoCache::cached_data.methodthunk_DelegateUtils_DelegateEquals - .invoke(a->delegate_handle, - b->delegate_handle, &exc); - UNHANDLED_EXCEPTION(exc); - return (bool)res; + return GDMonoCache::managed_callbacks.DelegateUtils_DelegateEquals( + a->delegate_handle, b->delegate_handle); } bool ManagedCallable::compare_less(const CallableCustom *p_a, const CallableCustom *p_b) { @@ -92,27 +87,15 @@ void ManagedCallable::call(const Variant **p_arguments, int p_argcount, Variant r_call_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD; // Can't find anything better r_return_value = Variant(); - MonoException *exc = nullptr; - GDMonoCache::cached_data.methodthunk_DelegateUtils_InvokeWithVariantArgs - .invoke(delegate_handle, p_arguments, - p_argcount, &r_return_value, &exc); + GDMonoCache::managed_callbacks.DelegateUtils_InvokeWithVariantArgs( + delegate_handle, p_arguments, p_argcount, &r_return_value); - if (exc) { - GDMonoUtils::set_pending_exception(exc); - } else { - r_call_error.error = Callable::CallError::CALL_OK; - } + r_call_error.error = Callable::CallError::CALL_OK; } void ManagedCallable::release_delegate_handle() { if (delegate_handle.value) { - MonoException *exc = nullptr; - GDMonoCache::cached_data.methodthunk_GCHandleBridge_FreeGCHandle.invoke(delegate_handle, &exc); - - if (exc) { - GDMonoUtils::debug_print_unhandled_exception(exc); - } - + GDMonoCache::managed_callbacks.GCHandleBridge_FreeGCHandle(delegate_handle); delegate_handle = GCHandleIntPtr(); } } -- cgit v1.2.3 From 92503ae8dbdf8f0f543dd785b79d3ec13b19092f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20Rold=C3=A1n=20Etcheverry?= Date: Sun, 27 Feb 2022 21:57:30 +0100 Subject: C#: Add source generator for properties and exports default values The editor no longer needs to create temporary instances to get the default values. The initializer values of the exported properties are still evaluated at runtime. For example, in the following example, `GetInitialValue()` will be called when first looks for default values: ``` [Export] int MyValue = GetInitialValue(); ``` Exporting fields with a non-supported type now results in a compiler error rather than a runtime error when the script is used. --- modules/mono/managed_callable.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'modules/mono/managed_callable.cpp') diff --git a/modules/mono/managed_callable.cpp b/modules/mono/managed_callable.cpp index a1f94aa590..334de14a6b 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 }; } } -- cgit v1.2.3 From e235cef09f71d0cd752ba4931640be24dcb551ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20Rold=C3=A1n=20Etcheverry?= Date: Sat, 28 May 2022 04:56:46 +0200 Subject: C#: Re-implement assembly reloading with ALCs --- modules/mono/managed_callable.cpp | 2 ++ 1 file changed, 2 insertions(+) (limited to 'modules/mono/managed_callable.cpp') diff --git a/modules/mono/managed_callable.cpp b/modules/mono/managed_callable.cpp index 334de14a6b..9305dc645a 100644 --- a/modules/mono/managed_callable.cpp +++ b/modules/mono/managed_callable.cpp @@ -87,6 +87,8 @@ void ManagedCallable::call(const Variant **p_arguments, int p_argcount, Variant r_call_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD; // Can't find anything better r_return_value = Variant(); + ERR_FAIL_COND(delegate_handle.value == nullptr); + GDMonoCache::managed_callbacks.DelegateUtils_InvokeWithVariantArgs( delegate_handle, p_arguments, p_argcount, &r_return_value); -- cgit v1.2.3