Mono is, in my view, a potentially great option for game scripting. Perhaps the most obvious example of this in action is the Unity game engine, and there are a number of benefits to Mono over, say, rolling your own scripting language:
- Nice IDE's like Visual Studio and Xamarin, with code completion and all that good stuff
- The Mono Soft-Mode Debugger makes it easy to step through code in an IDE
- It's (relatively) fast, i.e fast for a scripting language
- C# is type-safe, but easy to learn, a lot of people know it, and there's plenty of learning resources on the web
- .NET interface can be used by modders
So I started to play around with embedding Mono, and providing a way for C# code to call code in a C++ application, a bit like this:
// C/C++ code struct Widget { int number; } // Meanwhile in .NET land public class Widget { public int Number { get; } private IntPtr native_handle; }
I would've implemented it like this:
// .NET side public class Widget { public int Number { get { return get_Number_Internal(this.native_handle); } } private IntPtr native_handle; [MethodImpl(MethodImplOptions.InternalCall)] public static int get_Number_Internal(IntPtr native_handle); } // C++ side int get_Number_Internal(Widget* widget) { return widget->number; }
However, taking a look at the managed UnityEngine.dll in ILSpy shows that this isn't the way Unity does it. Unity does it like this:
// .NET side public class Widget { public int Number { [MethodImpl(MethodImplOptions.InternalCall)]get; } private IntPtr nativeHandle; } // C++ side (approximated) int get_Number(MonoObject* obj) { Widget* widget; // Note native_handle_field is a MonoClassField* cached during application startup mono_field_get_value(obj, native_handle_field, reinterpret_cast<void*>(&widget)); return widget->number; }
I assumed this would be slower due to overhead from having to read the Widget* back from managed memory, rather than passing it straight to C++ land when the internal function is called. So I tried both to see.
I created a managed dll, imaginatively named "ManagedLibrary.dll", with two classes:
// Widget.cs namespace ManagedLibrary { public class Widget { public int Number { get { return get_Number_Internal( this.native_handle ); } } [MethodImpl( MethodImplOptions.InternalCall )] public extern static int get_Number_Internal( IntPtr native_handle ); public int Number2 { [MethodImpl( MethodImplOptions.InternalCall )] get; } [MethodImpl( MethodImplOptions.InternalCall )] public extern static Widget[] GetWidgets(); private IntPtr native_handle = (IntPtr)0; } } // Main.cs namespace ManagedLibrary { public static class Main { public static void EntryPoint() { Widget[] widgets = Widget.GetWidgets(); const uint num_iterations = 1000; DateTime t = DateTime.Now; for (uint i = 0; i < num_iterations; ++i) { int total = 0; foreach (Widget widget in widgets) { total += widget.Number; } } TimeSpan span = DateTime.Now - t; Console.WriteLine( span.TotalMilliseconds ); t = DateTime.Now; for (uint i = 0; i < num_iterations; ++i) { int total = 0; foreach (Widget widget in widgets) { total += widget.Number2; } } span = DateTime.Now - t; Console.WriteLine( span.TotalMilliseconds ); } } } // C++ app #include <mono/jit/jit.h> #include <mono/metadata/assembly.h> #include <mono/metadata/class.h> #include <mono/metadata/debug-helpers.h> #include <mono/metadata/mono-config.h> struct Widget { int number; }; Widget* widgets; uint32_t num_widgets; MonoClassField* native_handle_field; MonoDomain* domain; MonoClass* widget_class; int ManagedLibrary_Widget_get_Number_Internal( const Widget* widget ) { return widget->number; } int ManagedLibrary_Widget_get_Number2( MonoObject* this_ptr ) { Widget* widget; mono_field_get_value( this_ptr, native_handle_field, reinterpret_cast<void*>(&widget) ); return widget->number; } MonoArray* ManagedLibrary_Widget_GetWidgets() { MonoArray* array = mono_array_new( domain, widget_class, num_widgets ); for( uint32_t i = 0; i < num_widgets; ++i ) { MonoObject* obj = mono_object_new( domain, widget_class ); mono_runtime_object_init( obj ); void* native_handle_value = &widgets[i]; mono_field_set_value( obj, native_handle_field, &native_handle_value ); mono_array_set( array, MonoObject*, i, obj ); } return array; } int main( int argc, const char * argv[] ) { mono_set_dirs( "/Library/Frameworks/Mono.framework/Home/lib", "/Library/Frameworks/Mono.framework/Home/etc" ); mono_config_parse( nullptr ); const char* managed_binary_path = "/path/to/ManagedLibrary.dll"; domain = mono_jit_init( managed_binary_path ); MonoAssembly* assembly = mono_domain_assembly_open( domain, managed_binary_path ); MonoImage* image = mono_assembly_get_image( assembly ); mono_add_internal_call( "ManagedLibrary.Widget::get_Number_Internal", reinterpret_cast<void*>(ManagedLibrary_Widget_get_Number_Internal) ); mono_add_internal_call( "ManagedLibrary.Widget::get_Number2", reinterpret_cast<void*>(ManagedLibrary_Widget_get_Number2) ); mono_add_internal_call( "ManagedLibrary.Widget::GetWidgets", reinterpret_cast<void*>(ManagedLibrary_Widget_GetWidgets) ); widget_class = mono_class_from_name( image, "ManagedLibrary", "Widget" ); native_handle_field = mono_class_get_field_from_name( widget_class, "native_handle" ); num_widgets = 5; widgets = new Widget[5]; for( uint32_t i = 0; i < num_widgets; ++i ) { widgets[i].number = i * 4; } MonoClass* main_class = mono_class_from_name( image, "ManagedLibrary", "Main" ); const bool include_namespace = true; MonoMethodDesc* entry_point_method_desc = mono_method_desc_new( "ManagedLibrary.Main:EntryPoint()", include_namespace ); MonoMethod* entry_point_method = mono_method_desc_search_in_class( entry_point_method_desc, main_class ); mono_method_desc_free( entry_point_method_desc ); mono_runtime_invoke( entry_point_method, nullptr, nullptr, nullptr ); mono_jit_cleanup( domain ); delete[] widgets; return 0; }
In my test, the property using mono_field_get_value took around 0.1ms, while the property with the getter calling a static method accepting native_handle as an argument, took around 25ms! This was pretty surprising, but I thought perhaps another managed function call was greater overhead than poking around in managed memory after all.
However, after an embarrassingly long amount of time, I reversed the test so the first loop used Number2, and the second used Number. Sure enough, it was just whichever property which was tested first that came out slower. I then adjusted the test, so there were 3 loops, the first 2 used Number, the third used Number2. The property using mono_field_get_value took about twice as long as the other.
Even if I rolled my own super simple (and entirely contrived) method of looking up a Widget* from a MonoObject*, it was still slower than just passing this.native_handle to a static getter function.
So, is this just a roundabout way of saying that "the way you think it'd be faster to call C++ code from CIL, is the way you'd expect"? Yes, I suppose so. It also means that Unity likely implements the CIL->C++ bridge in a suboptimal way, however they may be doing some kind of magic under the hood which is even better. I've tried compiling with varying amounts of optimisation, still with the same result, but I'd be happy to be proven wrong. Anywho, sleep beckons.
Full source on github.