文章里的代码粘过来的时候格式有点问题,原因是一开始文章是在订阅号上写的(gamedev101,文末有二维码),不知道为啥贴过来就没了格式,还要手动删行号,就没搞了。


介绍下问题背景:

小说君正在参与的项目,服务端逻辑以C#为主。

之前的一篇文章,《公式计算机》也有提到,这个项目的服务端需要提供让策划写游戏业务的能力。

不过跟文章里的方案不同,最后策划用来写业务的语言是C#。

实践下来,策划写的业务分为两大类:

  1. 战斗相关的流程性质的逻辑。例如技能结算的流程性逻辑。

  2. 各模块中经常变动的运算逻辑。例如面板属性的运算逻辑。

如图,简单直接,就是程序写的Foo调用策划写的Formula。


这些逻辑如果只放在服务端,那就什么问题也没有。

第一类逻辑,由于游戏类型的原因(MMO),基本上只有服务端会用,服务端想怎么更新就怎么更新。

第二类逻辑,面板属性运算,不仅服务端需要计算完推给客户端做显示用,客户端自己也需要做属性预览。

要解决这个问题,一般的做法要么是客户端每次问服务端计算下数据,显示出来;要么是客户端也维护一份相关逻辑的定义。

第一种做法,是项目实在改不动了才不得不用。

第二种做法,如果客户端大部分面板逻辑跑在C#上,那还好办,策划的逻辑打成Assembly,客户端服务端两边共用。

但是现在Lua普及程度已经这么高了,很少还有面板主要靠C#的手游。

这样,如标题所说,如果我们有个工具可以把C#转成Lua,平时策划用C#写业务,持续集成流程自动把客户端服务端共用的业务逻辑转成Lua,客户端用自动生成的对应的Lua函数做一些面板预览计算逻辑,就解决了上面说的所有问题。

  • 策划用C#写Formula,强类型,减少犯错。

  • 工具把C#版本的Formula转成Formula.lua。

  • 客户端的xxx.lua直接require Formula,调用。


把C#翻译成Lua的方法有很多种。

比如可以直接给Roslyn写插件,集成在编译流程里,取到C#的语法树,然后做自动生成。

再比如可以读C#的编译后程序集,反编译,拿到语法树,然后做代码生成。

两种方法相比较,小说君更倾向于后者。原因也很简单:

  • C#是一种多范式编程语言,语法特性多而杂。而且C#版本越新,语法糖越多,Lua很难覆盖。前者拿到的就是源代码对应的语法树,要变换的东西太多。

  • C#编译出的IL就简单多了,由于抽象层次介于底层语言和高级语言之间,基本上不用做任何变换就可以用任一门高级语言完整表达。

最关键的是,对于IL来说,有强大的ILSpy工具,可以读取IL,可以选择性地做反编译变换,方便生成适用于目标语言的语法树。


接下来进一段背景知识,用过C#的同学都知道,C#源代码会被编译为IL Assembly。然后由具体的runtime加载Assembly,编译为native code并执行,这也是现在几乎所有虚拟机语言的执行流程。

.Net Core/Mono是两个比较常见的加载执行Assembly的backend。既可以运行时JIT编译为native code直接执行,也可以编译期AOT。

IL2CPP与上面两个稍微不同,但是本质属于一种AOT。Assembly被翻译成CPP代码集合,与支持库编译、link为目标文件。

Assembly的信息除了一些元信息比如模块、类定义之外,主要存的是每个方法的IL指令集合。

IL是一种基于操作栈的虚拟机语言,所有的IL指令要么是把参数或返回值push到操作栈,要么是从操作栈pop值。

围绕IL称呼的名词比较多,不过由于这次的系列主题不会太深入,所以就简单统称为IL了。有兴趣的同学可以查阅ECMA335深入学习下IL。

C#中的200+100,翻译为IL后,就是依次push 200、push 100,然后调用add指令,从操作栈pop两个值,相加把结果push回操作栈。


介绍完IL,我们继续看把IL翻译成Lua的方案。

先看下参考方案,Unity的IL2CPP。

Unity在4.x开始引入了IL2CPP,用来在一些平台上替代mono这个逻辑脚本的backend。

IL2CPP整套工具链除了支持工具以外,主要分为两块:

  • 把CIL Assembly翻译成CPP的工具集。

  • 支撑翻译后的CPP正常运行在各个目标平台上的Native库。

总的来说,IL2CPP做的事情就是把IL Assembly翻译成C++文件集合,然后提供一些库函数,保证原来的IL能怎么在Mono上跑起来,现在的so就也能直接跑起来。

相比之下,由于我们的需求比较简单,所以ILToLua要做的事情就简单很多了。比如IL2CPP需要提供gc相关的库支持,lua就不用考虑这个问题。

再比如IL2CPP需要自己搞一套异常处理机制在C++中支持IL中的try-catch-finally语义,我们就可以有限支持。

先订个小目标:我们实现一个工具,可以解析IL Assembly,将其中特定类型的定义转为一个Lua module。

比如这样一个简单的类定义:

 1public class Test
{
private Random r = new Random(); public void Foo(Custom a, Custom b, Context ctx)
{
if ((a.Count - b.Count) > )
{
b.Rate = Modify(b.Rate, 0.003f * (b.Count - a.Count) * (b.Count - a.Count)); var t = Math.Min( - b.Rate, a.Rate); a.Rate = Modify(a.Rate, t - a.Rate);
}
} private float Modify(float old, float diff)
{
var newVal = old + diff; if (newVal < 0f)
{
newVal = 0f;
}
return newVal;
}
}

里面的逻辑也比较简单,刚入门的策划写起来完全没问题。

我们需要的大概的翻译效果:

 1local Prelude = require("LX6/Base/Prelude")
2local Math = require("LX6/Base/Math")
local Random = System.Random
local Formula = {} 8Formula.r = Random.New()
function Formula:Foo(a, b, ctx)
if a.Count - b.Count > then
b.Rate = self:Modify(b.Rate, 0.003 * (b.Count - a.Count) * (b.Count - a.Count))
local t = Math.Min( - b.Rate, a.Rate)
a.Rate = self:Modify(a.Rate, t - a.Rate)
endend
function Formula:Modify(old, diff)
local newVal = old + diff
if newVal < then
newVal =
end
return newVal
24end
return Formula

把这个类翻译为Lua中的一个table。

简化起见,这里就略去了table的构造函数。

两个特点:

  1. 只翻译一个类型。

  2. 由于lua本身的特性,函数用到的所有复杂参数都是鸭子类型(具体为table或udata)。

这两点跟IL2CPP很不一样,我们只需要把一个类型翻译成Lua,不需要递归地去翻译这个类型引用的其他类型。比如例子中的Custom和Context。

外面想调用的时候传一个有Count和Rate成员的table也可以,传一个真的符合类型的udata也可以。


接下来就开始进入正题了。不过由于这次文章的主题关联的内容比较多,小说君打算分成几篇短文来写。每篇聚焦的内容稍微少一点。

大概的安排是:

  • 本篇剩下的篇幅介绍下Mono.Cecil,然后初步认识下ILSpy。

  • 接下来介绍ILSpy的一些原理性质的东西,以及相应的实现细节。

  • 然后开始进入ILToLua的主题,跟大家分享下实现细节。

IL2CPP把IL Assembly翻译成CPP的部分,就是靠Mono.Cecil做的。

Mono.Cecil,官方解释

Cecil is a library written by Jb Evain to generate and inspect programs and libraries in the ECMA CIL format.

简单来说,就是Mono.Cecil是符合ECMA335规范的。我们借助这个库,可以结构化地读Assembly,用起来跟.Net带的反射库差不多,只不过Mono.Cecil有自己的类型定义。可以修改Assembly。可以运行时Emit代码。

Mono.Cecil可以用来写编译器,写反编译器,以及各种东西。

Unity用到的大量工具集都用了这个库,比如用来裁剪未引用的字节码的工具,用来在Editor热更新脚本的工具等等。

Mono.Cecil wiki上介绍了现在用到这个库的一些工具。基本上编译、反编译、混淆、AOP相关的工具都有用到。


IL本身是一种抽象层次比较高的语言,用Mono.Cecil可以比较容易地拿到Assembly中定义的全部类型,以及每个类型包含方法的IL集合。

还是之前的代码示例,抠出来一个简单函数:

 1private float Modify(float old, float diff)
{
var newVal = old + diff; if (newVal < 0f)
{
newVal = 0f;
}
return newVal;
}

用ILSpy看到的IL是这样的:

 .method private hidebysig
instance float32 Modify (
float32 old,
float32 diff
) cil managed
{
// Method begins at RVA 0x2110
// Code size 20 (0x14)
.maxstack
.locals init (
[] float32
) // float newVal = old + diff;
IL_0000: ldarg.
IL_0001: ldarg.
IL_0002: add
IL_0003: stloc.
// if (newVal < 0f)
IL_0004: ldloc.
IL_0005: ldc.r4 0.0
IL_000a: bge.un.s IL_0012 // newVal = 0f;
IL_000c: ldc.r4 0.0
IL_0011: stloc. // return newVal;
IL_0012: ldloc.
// (no C# code)
IL_0013: ret
} // end of method Test::Modify

IL2CPP翻译成这样:

 // System.Single ConsoleApplication13.Test::Modify(System.Single,System.Single)
2extern "C" float Test_Modify_m3633460209 (Test_t2103423000 * __this, float ___old0, float ___diff1, const RuntimeMethod* method)
{
float V_0 = 0.0f;
{
float L_0 = ___old0;
float L_1 = ___diff1;
V_0 = ((float)((float)L_0+(float)L_1));
float L_2 = V_0;
if ((!(((float)L_2) < ((float)(0.0f)))))
{
goto IL_0012;
}
}
{
V_0 = (0.0f);
} 19IL_0012:
{
float L_3 = V_0;
return L_3;
}
}

比较直接。只做了比较简单的块划分,和数据流分析,没做Inlining,也没做控制流分析。

我们在ILSpy中看到的信息,如果不反编译的话,大部分都是借助Mono.Cecil读出来的。比如Assembly依赖的其他Assembly,Assembly里面的命名空间和类型定义,具体到每个类型定义的Method、Field、Property等定义,以及最关键的,每个Method的IL Instruction。

Mono.Cecil拿到的Assembly元信息层次关系图:

然后是BCL反射库拿到的:

除了叫法有区别,其他能拿到的信息都是差不多的。

最大的区别就是Mono.Cecil可以直接拿到带类型的IL Instruction,比较方便。当然,修改,回写的接口就不用说了,BCL反射库是没有的。


ILSpy反编译的流程,就是根据Mono.Cecil,拿到具体类型,拿到类型定义的方法,以及各自的MethodBody。

然后对MethodBody中的IL Instructions做数据流分析,控制流分析,最后转为AST,再输出为C#代码。

这篇就到这里。

下篇小说君重点介绍下ILSpy的数据流分析和控制流分析过程和具体实现细节。


个人订阅号:gamedev101「说给开发游戏的你」,聊聊服务端,聊聊游戏开发。

动手写IL到Lua的翻译器——准备的更多相关文章

  1. 【原创】自己动手写控件----XSmartNote控件

    一.前面的话 在上一篇博文自己动手写工具----XSmartNote [Beta 3.0]中,用到了若干个自定义控件,其中包含用于显示Note内容的简单的Label扩展控件,用于展示标签内容的labe ...

  2. 【原创】自己动手写工具----XSmartNote [Beta 3.0]

    一.前面的话 在动笔之前,一直很纠结到底要不要继续完成这个工具,因为上次给它码代码还是一年多之前的事情,参考自己动手写工具----XSmartNote [Beta 2.0],这篇博文里,很多园友提出了 ...

  3. 【原创】自己动手写工具----XSmartNote [Beta 2.0]

    一.前面的话 在上一篇自己动手写工具----XSmartNote中,我简单介绍了这个小玩意儿的大致界面和要实现的功能,看了一下园子里的评论,评价褒贬不一,有人说“现在那么多云笔记的工具”,“极简版ev ...

  4. 【原创】自己动手写工具----签到器[Beta 2.0]

    一.前面的话 上一篇中基本实现了简单的签到任务,但是不够灵活.在上一篇自己动手写工具----签到器的结尾中,我设想了几个新增功能来提高工具的灵活程度,下面把新增功能点列出来看看: (1)新增其他的进程 ...

  5. 自己动手写ORM的感受

    之前看到奋斗前辈和时不我待前辈的自己动手写ORM系列博客,感觉讲解的通俗易懂,清晰透彻.作为一个菜鸟,闲来也想着自己写一个ORM,一来加深自己对 ORM的理解,以求对EF,NHibernate等ROM ...

  6. 自己动手写插件底层篇—基于jquery移动插件实现

    序言 本章作为自己动手写插件的第一篇文章,会尽可能的详细描述一些实现的方式和预备知识的讲解,随着知识点积累的一点点深入,可能到了后期讲解也会有所跳跃.所以,希望知识点不是很扎实的读者或者是初学者,不要 ...

  7. Python - 动手写个ORM

    Python - 动手写个ORM 任务: 模拟简单的ORM - Object Relational Mapping 为model添加create方法 代码很简单,直接上 字段类型类 class Fie ...

  8. 【转】自己动手写SC语言编译器

    自序 编译原理与技术的一整套理论在整个计算机科学领域占有相当重要的地位,学习它对程序设计人员有很大的帮助.我们考究历史会发现那些人人称颂的程序设 计大师都是编译领域的高手,像写出BASIC语言的BIL ...

  9. 自己动手写CPU之第九阶段(8)——MIPS32中的LL、SC指令说明

    将陆续上传新书<自己动手写CPU>,今天是第47篇. 9.7 ll.sc指令实现思路 9.7.1 实现思路 这2条指令都涉及到訪问链接状态位LLbit,能够将LLbit当做寄存器处理,ll ...

随机推荐

  1. Couldn't save uncommitted changes.

    关于idea切换分支报错的问题. Couldn't save uncommitted changes. Tried to save uncommitted changes in stash befor ...

  2. Vue的组件为什么要export default

    Vue 的模块机制 Vue 是通过 webpack 实现的模块化,因此可以使用 import 来引入模块,例如: 此外,你还可以在bulid/webpack.base.conf.js文件中修改相关配置 ...

  3. 【noip模拟】tree

    000 ms        Memory Limit: 128 MB [吐槽] 点分治点分治点分治 嗯..场上思考树状数组的时候好像傻掉了..反正就是挂了就是了.. [题解] 首先如果没有环的话就是一 ...

  4. 关系型数据库工作原理-查询优化器之数据访问方式(翻译自Coding-Geek文章)

    本文翻译自Coding-Geek文章:< How does a relational database work>.原文链接:http://coding-geek.com/how-data ...

  5. office 2013补丁包更新

    总是有客户发现使用office 2013 下的插件有问题,这个问题出现在低版本上,所以要给office 2013打上补丁,打上后,运行插件ok,出现的bug解决掉了.那么给office打补丁包就成了解 ...

  6. 关于Android attrs 自定义属性的说明

    写个自定义控件时经常要自定义一些自己的属性,平时用的都是那几个,今天就顺便一起总结一下这个东东吧- 一.定义:属性的定义都在attrs.xml文件里面: 二.读取:通过都是通过TypedArray去读 ...

  7. Java .classpath文件Classpath entry org.maven.eclipse.MAVEN2_CLASSPATH_CONTAINER will not be exported or published异常解决办法

    在实际用Maven构建Java Web项目开发过程中,有时候会出现上述情况的警告,如果不解决这个警告,就会在启动Web服务器的时候抛出无法加载Maven管理的第三方jar包的异常. 所以,要解决上面的 ...

  8. scala开发环境安装

    安装JDK    java 运行环境 ,这里不详说了,熟悉java的朋友应该都会,我们主要关注下Scala的安装. 安装scala    1.下载scala    http://yunpan.cn/c ...

  9. java中断

    理解java中断 Java中断机制是一种协作机制,即通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断.例如,当线程t1想中断线程t2,只需要在线程t1中将线程t2对象的中断标识置为tr ...

  10. 如何使用svn命令行更新想要的目录?

    内容来自网络. 一 某些原因想在svn co的时候排除某些目录,可以绕个圈子,分三步来完成:co外层目录:svn checkout --depth empty URL[URL[LOCATION]完成之 ...