|
这是基于unity mono代码阅读的第四篇。
本文主要介绍怎样从CS代码生成我们需要的包含CLR字节码的exe和dll文件。
上文已经介绍了怎样创建一个可以调试的mono工程。现在我们的测试工程已经可以运行包含入口的CLR,现在我们需要做的构建一个可以测试的C#生成CLR的环境。
其实本章内容要说简单也可以很简单。。。
有同学说,我直接用VS2017或者19创建一个C#工程,生成一个EXE不就行了么。是的,你没说错,.Net是一个标准,所以无论是VS,还是MonoDevlop还是unity调用的msc都是可以生成符合标准可运行的CIL字节码的。。
打开vs2017 创建一个C#控制台程序,然后框架选择.net2.0然后简单打一个HelloWorld。
using System;
using System.Collections.Generic;
using System.Text;
namespace TestCPlus
{
class Program
{
static void Main(/*string[] args 注 默认参数需要注视掉不然堆栈溢出的报错。。*/)
{
System.Console.WriteLine("Hello World.");
}
}
}
编译生成。然后将我们一开始代码里面加载CIL的代码改成我们新生成的这个exe看一下。
assembly = mono_domain_assembly_open(domain, "D:\\MonoTest\\TestCPlus\\bin\\Debug\\TestCPlus.exe");
编译运行
不用理会上面的assertion,Hello World出现啦。
可见,即使用VS2017,只要你选择对了.net版本,那么也是可以在我们的调试环境中运行的。
当然,如果只想了解这些,我们的确可以不用继续往下看了。不过作为一个unity程序员,我觉得我们还是需要了解一下,Unity是怎样生成CIL字节码的。
稍微有点了解unity安卓包的同学可能知道。
在最终打包的untiy工程中,C#最终会被编译成Assembly-CSharp.dll打包在我们的apk或者windows发布包内(在没选il2cpp的情况下)。那么我们怎样将我们的代码生成一个Dll并且像Unity一样调用里面一堆monobehavior呢?
下面我们一个一个来看,
1.生成一个DLL后缀的文件
这个比较简单。如果你用VS在生成部分改成Dll是可以完成这个需求的。但是Unity发布的时候是没有强制要求VS的,你可以用MonoDevlop,甚至你用Notepad,Vim来写代码都没有问题。这又是怎么做到的呢?
这里面的道理其实很简单,在本系列的第一篇文章里面就有介绍。
Mono自带了一套完全用C#写的C#编译器,unity就是调用这几个mono提供的编译器来编译C#代码的。本文现在用适合.net2.0的gmcs编译器为例重新编译一下刚刚我们生产的VS2017C#工程内的Hello World。
gmsc文件在Unity文件夹下的Mono\lib\mono\2.0 目录下
E:\trunk\Data\Mono\bin\mono.exe "E:\trunk\Data\Mono\lib\mono\2.0\gmcs.exe" TestCPlus.cs很快我们就生成了TestCPlus.exe。用我们刚刚生成的测试环境跑一下
完美,可见unity目录自带的mono .net2.0C#编译器gmcs跟vs2017一样都可以生成符合CIL标准的字节码达到一样的效果。
接下来我们看一下Unity引擎自带的mono编译器包含哪些参数
-target:KIND Specifies the format of the output assembly (short: -t),KIND can be one of: exe, winexe, library, module
根据这个参数,我们可以尝试生成一下跟unity引擎以前的dll试试。
E:\trunk\Data\Mono\bin\mono.exe "E:\trunk\Data\Mono\lib\mono\2.0\gmcs.exe" TestCPlus.cs -target:library生成dll后,我们尝试运行一下我们刚刚的mono调试环境试试。在正常win32编程中,其实dll和exe主要的差别在于入口点的不同,也就是说exe自带入口点,程序运行会自动搜寻main进行运行。dll有点区别,dll一般可以从dllmain运行,也可以只加载,加载后,通过调用dll提供的各个接口进行工作。
int retval = mono_jit_exec(domain, assembly, argc - 1, argv + 1);
我们刚刚一直用的是mono_jit_exec运行exe,现在exe变成dll,应该会存在一定的入口点找不到问题。不过我们先试一下。
assembly = mono_domain_assembly_open(domain, "D:\\MonoTest\\TestCPlus\\bin\\Debug\\TestCPlus.dll");
我们将测试工程内的assembly由TestCPlus.exe改成TestCPlus.dll后,编译运行。
果然,这次与我们想象的一样找不到入口点。接下来就是我们要解决的第二个问题
2.对于dll类型的CIL字节码,我们要怎样像unity一样找到入口点并运行。
这里有几个思路可以参考。
1.老子有unity源码,老子直接翻代码不就得了。(是,如果你有unity源码,无论是泄露的还是掏钱买的,这都是比较正经的解决方案,其实也不那么正经,你会发现unity代码里面想找点东西还不如下面几种方法)
2.我google一下,查查文档,看看有没有别人处理过。
这个是最靠谱的一个方法,如果你用心搜索,只要姿势正确,一定是可以找到如下几个函数
//Get a image from the assembly
MonoImage* image;
image = mono_assembly_get_image(assembly);
if (!image)
{
std::cout << &#34;mono_assembly_get_image failed&#34; << std::endl;
system(&#34;pause&#34;);
return;
}
//Build a method description object
MonoMethodDesc* TypeMethodDesc;
const char* TypeMethodDescStr = &#34;TestCPlus.Program:Main()&#34;;
TypeMethodDesc = mono_method_desc_new(TypeMethodDescStr, NULL);
if (!TypeMethodDesc)
{
std::cout << &#34;mono_method_desc_new failed&#34; << std::endl;
system(&#34;pause&#34;);
return;
}
//Search the method in the image
MonoMethod* method;
method = mono_method_desc_search_in_image(TypeMethodDesc, image);
if (!method)
{
std::cout << &#34;mono_method_desc_search_in_image failed&#34; << std::endl;
system(&#34;pause&#34;);
return;
}
//run the method
std::cout << &#34;Running the static method: &#34; << TypeMethodDescStr << std::endl;
mono_runtime_invoke(method, nullptr, nullptr, nullptr);
3.看代码,看为什么mono_jit_exec这个函数失效了
本文会从mono_jit_exec开始做起,其实mono_jit_exec的代码比较简单,大概猜一下,就是完成我们上一步2搜索到的操作,只是他把2的操作合并了一下,为一些exe直接运行提供了方便。但是猜归猜。一切都在代码之中。打开我们用来编译mono.dll的vs工程,开始吧。
/**
* mono_jit_exec:
* @assembly: reference to an assembly
* @argc: argument count
* @argv: argument vector
*
* Start execution of a program.
*/
int
mono_jit_exec (MonoDomain *domain, MonoAssembly *assembly, int argc, char *argv[])
{
MonoImage *image = mono_assembly_get_image (assembly);
MonoMethod *method;
guint32 entry = mono_image_get_entry_point (image);
if (!entry) {
g_print (&#34;Assembly &#39;%s&#39; doesn&#39;t have an entry point.\n&#34;, mono_image_get_filename (image));
/* FIXME: remove this silly requirement. */
mono_environment_exitcode_set (1);
return 1;
}
method = mono_get_method (image, entry, NULL);
if (method == NULL){
g_print (&#34;The entry point method could not be loaded\n&#34;);
mono_environment_exitcode_set (1);
return 1;
}
return mono_runtime_run_main (method, argc, argv, NULL);
}
mono_jit_exec的代码跟我们想的差不多,他通过函数mono_image_get_entry_point来获取在exe中描述的入口点。我们一般以为入口点就是写好的mian函数而已,其实
/**
* mono_image_get_entry_point:
* @image: the image where the entry point will be looked up.
*
* Use this routine to determine the metadata token for method that
* has been flagged as the entry point.
*
* Returns: the token for the entry point method in the image
*/
guint32
mono_image_get_entry_point (MonoImage *image)
{
return ((MonoCLIImageInfo*)image->image_info)->cli_cli_header.ch_entry_point;
}
代码告诉我们,这个是在image里面定义的,也就是说,你可以随便选择一个入口点,只要在exe内指定即可,这个实际上也可以在vs里面进行操作,C#工程的属性就可以指定入口点。
如果找不到入口点,就像刚刚我们拿了一个dll来鱼目混珠,就会被检查出来并提示报错。
首先是通过mono_get_method获取到MonoMethod指针,没有找到这个入口点方法的话也是一样报错,然后就会进入mono_runtime_run_main。
现在入口点方法已经找到了,我们继续往下看方法怎么被运行。
/**
* mono_runtime_run_main:
* @method: the method to start the application with (usually Main)
* @argc: number of arguments from the command line
* @argv: array of strings from the command line
* @exc: excetption results
*
* Execute a standard Main() method (argc/argv contains the
* executable name). This method also sets the command line argument value
* needed by System.Environment.
*
*
*/
int
mono_runtime_run_main (MonoMethod *method, int argc, char* argv[],
MonoObject **exc)
{
int i;
MonoArray *args = NULL;
MonoDomain *domain = mono_domain_get ();
int result;
g_assert (method != NULL);
mono_thread_set_main (mono_thread_current ());
mono_set_commandline_arguments(argc, argv,method->klass->image->assembly->basedir);
argc--;
argv++;
if (mono_method_signature (method)->param_count) {
args = (MonoArray*)mono_array_new (domain, mono_defaults.string_class, argc);
for (i = 0; i < argc; ++i) {
/* The encodings should all work, given that
* we&#39;ve checked all these args for the
* main_args array.
*/
gchar *str = mono_utf8_from_external (argv );
MonoString *arg = mono_string_new (domain, str);
mono_array_setref (args, i, arg);
g_free (str);
}
} else {
args = (MonoArray*)mono_array_new (domain, mono_defaults.string_class, 0);
}
mono_assembly_set_main (method->klass->image->assembly);
result = mono_runtime_exec_main (method, args, exc);
fire_process_exit_event ();
return result;
}
mono_runtime_run_main里面是个巨大的宝库,mono的函数命名很好,从这几个函数的命名上面我们就可以看出当前的这几个函数作用。
首先我们会设置当前线程为mono的主线程。然后我们会获取命令行参数,然后我们就会给运行的exe生成当前需要的命令行参数。
最后就是设置当前mian assembly,然后调用mono_runtime_exec_main来运行。
运行结束后,调用fire_process_exit_event退出。
上面涉及到的几个函数,都是跟mono运行环境初始化相关的函数,有兴趣的就可以深入进去再看一下。
然后就是运行部分了
我们进入mono_runtime_exec_main看一下,代码较多,我进行了一下删减。
/*
* Execute a standard Main() method (args doesn&#39;t contain the
* executable name).
*/
int
mono_runtime_exec_main (MonoMethod *method, MonoArray *args, MonoObject **exc)
{
MonoDomain *domain;
gpointer pa [1];
int rval;
MonoCustomAttrInfo* cinfo;
gboolean has_stathread_attribute;
MonoThread* thread = mono_thread_current ();
g_assert (args);
pa [0] = args;
....
/* FIXME: check signature of method */
if (mono_method_signature (method)->ret->type == MONO_TYPE_I4) {
MonoObject *res;
res = mono_runtime_invoke (method, NULL, pa, exc);
if (!exc || !*exc)
rval = *(guint32 *)((char *)res + sizeof (MonoObject));
else
rval = -1;
mono_environment_exitcode_set (rval);
} else {
mono_runtime_invoke (method, NULL, pa, exc);
if (!exc || !*exc)
rval = 0;
else {
/* If the return type of Main is void, only
* set the exitcode if an exception was thrown
* (we don&#39;t want to blow away an
* explicitly-set exit code)
*/
rval = -1;
mono_environment_exitcode_set (rval);
}
}
return rval;
}
mono_runtime_exec_main主要就是设置一下运行前的环境,然后设置一下线程相关的参数。最后是根据方法里面描述的字节编码相关信息,最终调用mono_runtime_invoke。
看到mono_runtime_invoke我们就可以稍微休息一下了。是不是跟我们想的一样,运行exe与运行dll并没有太大的差别,只是exe在他的描述文件内说明了他的入口点,并且会涉及到一些命令行传入参数和线程相关的设置。而dll没有这么麻烦,我们只需要把dll加载进来,然后通过上面学到的函数,找到我们要调用的方法名,用mono_runtime_invoke调用就好了。
好的,那么我们现在就按我们上文学到的方法,来试着调用一下我们刚刚生成的dll看一下。
void RunCILInDll(MonoAssembly *assembly)
{
//Get a image from the assembly
MonoImage* image;
image = mono_assembly_get_image(assembly);
if (!image)
{
std::cout << &#34;mono_assembly_get_image failed&#34; << std::endl;
system(&#34;pause&#34;);
return;
}
#pragma endregion
#pragma region Run a static method
{
//Build a method description object
MonoMethodDesc* TypeMethodDesc;
const char* TypeMethodDescStr = &#34;TestCPlus.Program:Main()&#34;;
TypeMethodDesc = mono_method_desc_new(TypeMethodDescStr, NULL);
if (!TypeMethodDesc)
{
std::cout << &#34;mono_method_desc_new failed&#34; << std::endl;
system(&#34;pause&#34;);
return;
}
//Search the method in the image
MonoMethod* method;
method = mono_method_desc_search_in_image(TypeMethodDesc, image);
if (!method)
{
std::cout << &#34;mono_method_desc_search_in_image failed&#34; << std::endl;
system(&#34;pause&#34;);
return;
}
//run the method
std::cout << &#34;Running the static method: &#34; << TypeMethodDescStr << std::endl;
mono_runtime_invoke(method, nullptr, nullptr, nullptr);
}
}
我们添加一个新的RunCILInDll函数用来调用dll中的方法。
其实这里面有个方法名的问题,有同学可能会问,在哪里找到这个TestCPlus.Program:Main方法名的呢?其实经验丰富的C#开发者可以通过命名空间类名,方法名这样来写。
不过笔者还是推荐一个神器,ildasm.exe这个是微软官方提供的il反汇编器。可以将编译好的il字节码反编译成可读的il代码
是的,我们可以通过ildasm.exe来获取编译后的方法名。
这里还有个地方要注意一下,其中的一些定义会引入新的include文件。
#include <mono/metadata/assembly.h>
#include <mono/metadata/class.h>
#include <mono/metadata/debug-helpers.h>
说实话,这个也是笔者挺讨厌c++的地方,现在都2020年了,各大编辑器还不能智能补全include,当然也许会有人说,工程情况复杂,多版本兼容,同名函数冲突这些。我觉得C++就是历史包袱太重了。。我不太喜欢。。。
然后我们把一开始的jit_exec改成我们现在的RunCILInDll
//int retval = mono_jit_exec(domain, assembly, argc - 1, argv + 1);
RunCILInDll(assembly);
运行
完美。
好了,本章到此就结束了,至于mono_runtime_invoke的实现,我们留到下面章节进行分析。 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|