Calling C Code From Mono/.NET

Calling C Code From Mono/.NET

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.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值