Mono运行时的嵌入
既然我们明确了Mono运行时嵌入应用的重要性,那么如何将它嵌入应用中就成为了下一个值得讨论的话题。
这个小节我会为大家分析一下Mono运行时究竟是如何被嵌入到应用中的,以及如何在原生代码中调用托管方法,以及如何在托管代码中调用原生方法。而众所周知的一点是,Unity3D游戏引擎本身是用C/C++写成的,所以本节就以Unity3D游戏引擎为例,假设此时我们已经有了一个用C/C++写好的应用(Unity3D)。
将Mono运行时嵌入到这个应用之后,应用就获取了一个完整的虚拟机运行环境。而这一步需要将“libmono”和应用链接,链接完成后,C++应用的地址空间就会像下图这样:
而在C/C++代码中,我们需要将Mono运行时初始化,一旦Mono运行时初始化成功,那么下一步最重要的就是将CIL/.Net代码加载进来。加载之后的地址空间将会如下图所示:
那些C/C++代码,我们通常称之为非托管代码,而通过CIL编译器生成CIL代码我们通常称之为托管代码。
将Mono运行时嵌入应用可以分为3个步骤:
编译C++程序和链接Mono运行时;
初始化Mono运行时;
C/C++和C#/CIL的交互。
下面我们一步一步地进行。首先我们需要将C++程序进行编译并链接Mono运行时。此时我们会用到pkg-config工具。在Mac上使用homebrew进行安装,在终端中输入命令”brew install pkgconfig”即可。
待pkg-config安装完毕之后,我们新建一个C++文件,命名为unity.cpp,作为原生代码部分。我们需要将这个C++文件进行编译,并和Mono运行时链接。
在终端输入:
g++ unity.cpp -framework CoreFoundation -lobjc -liconv `pkg-config --cflags --libs mono-2`此时,经过编译和链接之后,unity.cpp和Mono运行时被编译成了可执行文件。
到此,我们需要将Mono的运行时初始化。所以再重新回到刚刚新建的unity.cpp文件中,我们要在C++文件中来进行运行时的初始化工作,即调用mono_jit_init方法。代码如下:
#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> MonoDomain* domain; domain = mono_jit_init(managed_binary_path);mono_jit_init这个方法会返回一个MonoDomain,用来作为盛放托管代码的容器。其中的参数managed_binary_path,即应用运行域的名字。除了返回MonoDomain之外,这个方法还会初始化默认框架版本,即2.0或4.0,这个主要由使用的Mono版本决定。当然,我们也可以手动指定版本。只需要调用下面的方法即可:
domain = mono_jit_init_version ("unity", ""v2.0.50727);这样就获取应用域——domain。但是当Mono运行时被嵌入一个原生应用时,它显然需要一种方法来确定自己所需要的运行时程序集以及配置文件。默认情况下它会使用系统中定义的位置。
如图,可以看到,在一台电脑上可以存在很多不同版本的Mono,如果我们的应用需要特定的运行时,就也需要指定其程序集和配置文件的位置。
为了选择所需Mono版本,可以使用mono_set_dirs方法:
mono_set_dirs("/Library/Frameworks/Mono.framework/Home/lib", "/Library/Frameworks/Mono.framework/Home/etc");这样,我们就设置了Mono运行时的程序集和配置文件路径。
当然,Mono运行时在执行一些具体功能时,可能还需要依靠额外的配置文件来进行。所以我们有时也需要为Mono运行时加载这些配置文件,通常我们使用mono_config_parse方法来加载这些配置文件。
当mono_config_parse的参数为NULL时,Mono运行时将加载Mono的配置文件。当然作为开发者,我们也可以加载自己的配置文件,只需要将配置文件的文件名作为mono_config_parse方法的参数即可。
Mono运行时的初始化工作到此完成。接下来需要加载程序集并且运行它。
这里需要用到MonoAssembly和mono_domain_assembly_open方法。
const char* managed_binary_path = "./ManagedLibrary.dll"; MonoAssembly *assembly; assembly = mono_domain_assembly_open (domain, managed_binary_path); if (!assembly) error ();上面的代码会将当前目录下的ManagedLibrary.dll文件中的内容加载进已经创建好的domain中。此时需要注意的是Mono运行时仅仅是加载代码而没有立刻执行这些代码。
如果要执行这些代码,则需要调用被加载的程序集中的方法。或者当你有一个静态的主方法时(也就是程序入口),可以通过mono_jit_exec方法调用这个静态入口。
下面我将举一个将Mono运行时嵌入C/C++程序的例子,这个例子的主要流程是加载一个由C#文件编译成的DLL文件,之后调用一个C#的方法输出Hello World。
首先,我们完成C#部分的代码。
namespace ManagedLibrary { public static class MainTest { public static void Main() { System.Console.WriteLine("Hello World"); } } }在这个文件中,我们实现了输出Hello World的功能。之后我们将它编译为DLL文件。这里我也直接使用了Mono的编译器——mcs。在终端命令行使用mcs编译该cs文件。同时为了生成DLL文件,还需要加上-t:library选项。
mcs ManagedLibrary.cs -t:library这样便得到了cs文件编译之后的DLL文件,叫做ManagedLibrary.dll。
接下来,完成C++部分的代码。嵌入Mono的运行时,同时加载刚刚生成ManagedLibrary.dll文件,并且执行其中的Main方法输出Hello World。
#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> MonoDomain *domain; int main() { const char* managed_binary_path = "./ManagedLibrary.dll"; //获取应用域 domain = mono_jit_init (managed_binary_path); //mono运行时的配置 mono_set_dirs("/Library/Frameworks/Mono.framework/Home/lib", "/Library/Frameworks/Mono.framework/Home/etc"); mono_config_parse(NULL); //加载程序集ManagedLibrary.dll MonoAssembly* assembly = mono_domain_assembly_open(domain, managed_binary_path); MonoImage* image = mono_assembly_get_image(assembly); //获取MonoClass MonoClass* main_class = mono_class_from_name(image, "ManagedLibrary", "MainTest"); //获取要调用的MonoMethodDesc MonoMethodDesc* entry_point_method_desc = mono_method_desc_new("ManagedLibrary.MainTest:Main()", true); 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, NULL, NULL, NULL); //释放应用域 mono_jit_cleanup(domain); return 0; }之后编译运行,可以看到屏幕上输出的Hello World。
既然要提供脚本功能,将Mono运行时嵌入C/C++程序之后,只在C/C++程序中调用C#中定义的方法显然还是不够的。脚本机制的最终目的还是希望能够在脚本语言中使用原生的代码,所以下面我将站在Unity3D游戏引擎开发者的角度,继续探索一下如何在C#文件(脚本文件)中调用C/C++程序中的代码(游戏引擎)。