/Cs2Lua

CSharp代码转lua,适用于使用lua实现热更新而又想有一个强类型检查的语言的场合

Primary LanguageC#

Cs2Lua

CSharp代码转lua,适用于使用lua实现热更新而又想有一个强类型检查的语言的场合

【示例链接】

https://github.com/dreamanlan/Cs2Lua/tree/master/Test

【命令行】

Cs2Lua [-out dir] [-ext fileext] [-enableinherit] [-enablelinq] [-normallua/-slua/-xlua] [-outputresult] [-noautorequire] [-luacomponentbystring] [-usearraygetset] [-enabletranslationcheck] [-d macro] [-u macro] [-externpath path] [-ignorepath path] [-refbyname dllname alias] [-refbypath dllpath alias] [-systemdllpath dllpath] [-src] csfile|csprojfile

其中:

fileext = 生成的lua脚本文件扩展名,对unity3d和slua应该是txt,对普通lua则为lua。

macro = 宏定义,会影响被转化的c#代码里的#if/#elif/#else/#endif语句的结果。

dllname = 以名字(Assembly Name)提供的被引用的外部dotnet DLL,cs2lua尝试从名字获取这些DLL的路径(一般只有dotnet系统提供的DLL才可以这么用)。

dllpath = 以文件全路径提供的被引用的外部dotnet DLL。

alias = 外部dll顶层名空间别名,默认为global, 别名在c#代码里由'extern alias 名字;'语句使用。

enableinherit = 此选项指明是否允许继承。

normallua = 此选项指明输出为普通lua,此时与slua特定实现相关的语法会关闭(如输出参数调用时传入Slua.out)。

slua = 此选项默认打开,指明输出lua用于unity3d+slua。

outputresult = 此选项指明是否在控制台输出最终转化的结果(合并为单一文件样式)。

src = 此选项仅用在refbyname/refbypath选项未指明alias参数的情形,此时需要此选项在csfile|csprojfile前明确表明后面的参数是输入文件。

Cs2Lua的输出主要包括:

1、对应c#代码的转换出的lua代码,每个c#顶层类对应一个lua文件。

2、所有名字空间的定义lua文件,此文件被1中文件引用,输出文件为cs2lua_namespaces.lua/txt。

3、Cs2Lua依赖的lualib文件lualib.lua,输出文件名为cs2lua_lualib.lua/txt。

4、在c#代码里使用Cs2Lua.Require明确指明要依赖的lualib文件,这些文件需要自己放到Cs2Lua.exe所在目录的子目录lualib里,之后自动拷到输出目录。

【源由】

1、基于unity3d的移动游戏开发,在android与ios平台上的限制不同。在android上,我们可以拆分可执行文件为多个dll,然后运行时动态加载除主程序外的其它dll,这样也就允许了对dll的单独更新,然而ios上此路不通,ios禁止使用jit与动态加载dll。为了实现热更新,游戏行业一般采用lua。

2、从语言角度比较,c#是强类型的编译型语言,而lua是解释型的动态脚本语言,在工程规模达到一定程度时,lua由于缺少编译时类型推导与检查,错误更多推迟到运行时暴露,这显然是一个弱点。这与web前端语言javascript有点类似。

3、近年来在web前端语言javascript领域出现了一些添加编译器类型检查的需求,2个例子是CoffeeScript与微软的TypeScript,这2种语言都给javascript添加了强类型与推导,但他们不是直接编译或解释执行自身,而是编译到javascript,这样保证与web的兼容。这个工程尝试类似的工作,不过我们不设计新的语言,而是直接基于C#语法,自动编译为lua。

4、vs2015的c#编译器开源工程Rosyln在编译器语法与语义信息方面提供了非常完善的API,这帮我们完成了从c#->lua编译的主要工作。

5、c#->lua实现后,可以考虑两种用法:

a、以不同平台均运行lua,此时用c#编写程序主要利用编译器的类型检查与推导功能及c#语言的诸多适合架构大型工程的语言设施。

b、在ios平台上运行lua,在android平台上直接将用于转lua的c#工程编译为dll并动态加载,这样在不同平台实现热更新的机制不同,运行效率不同,但开发时只需要进行一次开发。

【C#->lua对c#的限制】

1、不完全支持类的继承与泛型(可以实现接口),主要原因是lua的元表机制很难完美实现c#对象继承的详细语义同时还保持良好的效率与可理解性。并且支持接口与partial类后,继承的很多特性可以很优雅的实现。

2、忽略异常相关语句(try/catch/finally/filter),简单支持只捕获Exception异常的try/catch/finally。

3、不支持指针与内存相关操作fixed/*/&/stackalloc。

4、不支持async/wait/lock等异步或并发相关设施。

5、不支持label与goto语句。

6、不支持checked/unchecked语句。

7、不支持linq语法糖(直接调用方法就可以,而且c#的linq支持本来也不如visual basic全,这语法风格与c#有点不搭,放弃了)。

8、自定义struct未完全实现拷贝语义(需要在lualib.lua里完善)。

9、不支持C# 7.0及以后版本引入的模式相关语法与本地方法。

*** CsToLuaUnimplemented.cs是目前明确不支持与不需要处理的语法特性(Visit开头的方法).

【主要支持的c#特性】

1、类、方法、indexer、特性、事件、委托、字段等类定义相关的机制,静态与实例2类。

2、基本语句与表达式。

3、接口(转换时忽略)。

4、partial类。

5、预处理与属性,转换时处理,目标lua不包含。

6、类方法重载(overload非override)。

7、lambda表达式与lambda函数。

8、匿名类、匿名委托等。

9、自定义类部分支持generic(为每个generic实例生成一份代码,generic类本身不生成代码)。

10、数组与集合支持。

11、枚举支持。

12、支持ref/out参数。

13、扩展方法。

14、yield与协程。

15、操作符重载。

16、部分支持Attribute(目前会生成cs2lua_attributes.lua/txt文件,包含了所有自定义代码里用到的attribute,但lualib.lua里未实现自定义属性的访问机制)

17、部分支持自定义struct,语法层面基本完成,拷贝语义需要在lualib.lua里实现。

*** CsToLua.cs是目前支持的语法(Visit开头的方法).

【额外特性】

1、Cs2Lua.Ignore属性

用于在c#代码里标记类或方法不进行转换处理,使用它们的代码就像它们已经存在一样转换。(这一属性主要用于手动用lua实现一些c#代码的功能)

2、Cs2Lua.Require(string luaModuleName)属性

用于指出c#代码在转换后需要require某个lua模块,与1同时用于不自动转换的c#代码用以支持手动实现c#代码对应的lua。单独使用就是简单在生成的lua代码里添加require语句。

3、Cs2Lua.Entry属性

用于指明某个c#类的对应lua文件要生成一个入口方法,具体实现在lualib.lua里的defineentry函数,生成代码会调用defineentry。

4、Cs2Lua.Export属性

用于指明某个c#类的构造在转换为lua后生成的供c#端调用的__new_object方法里用于对象构造。

5、Cs2Lua.EnableInherit属性   用于指明某个c#类可能使用继承(此时转换到lua时不会报错),Cs2Lua会采用一种继承实现机制来实现继承,但与c#的继承语义不太一样,此属性用于使用都能确保使用一致继承的情形(就是说语义与c#是一致的),一般继承层次只有2层并且只涉及复用代码与纯虚函数重载的情形是可以的,子类隐藏父类非虚函数的情形要避免使用。

6、Cs2Lua.TranslateTo(string luaModuleName, string targetMethodName)属性   用于指定某个方法翻译为调用指定lua模块的指定lua函数(要求目标lua函数签名与方法一致)。

【生成的lua与C#的互操作】

1、目标是可以不依赖特定的交互机制(相关依赖都在lualib.lua或明确引用的外部lua里,转换工具只处理语法直接支持的语义转换)。

2、目前生成的lua代码能使用的c# API假定由slua导出(可以用命令行开关切换为纯lua, 这时可能需要手写lua代码提供API)。

【基本思路】

1、语法制导方式翻译到DSL(可以理解为简化了语法特性的中间语言),再由DSL经由生成器转换为lua。(c#语法、语义直接使用Rosyln工程)

2、C# class/struct -> lua table + metatable

3、inherit/property/event interface implementation -> metatable __index/__newindex

4、lambda/delegate/event -> 函数对象

5、generic -> 为每个generic实例生成一份代码,generic类本身不生成代码

6、interface -> 直接忽略

7、数组、集合 -> lua table

8、表达式 -> lua表达式 + 匿名函数调用

9、c#语句 -> lua语句 + 匿名函数调用

【比较复杂的转换】

1、ref/out参数【假设C#函数定义int f(int a, int b, ref int c, out int d)】

r = f(a,b,ref c,out d)

=>  

r,c,d = f(a,b,c)

2、带ref/out参数函数出现在表达式中

v1+v2+f(a,b,ref c,out d)
  =>  

v1+v2+(function() local r; r,c,d = f(a,b,c); return r; end)()

3、复合赋值

a+=f(a,b,ref c,out d)

=>  

a = a + (function() local r; r,c,d = f(a,b,c); return r; end)()

4、对象创建

var o = new Class(a,b) { m_F = 123, m_F2 = 456 }
  =>  

local o = (function() local obj; obj = Class(a,b); (function(self) self.m_F = 123; self.m_F2 = 456; end)(obj); return obj; end)()

5、循环中的continue实现

while(a<b)

{

++a;

if(a<10)

  continue; 
  
if(a>100)

  break;
  
++a;

}

=>

while a < b do

local isBreak = false; 

repeat 

  a=a+1; 
  
  if a>2 then 
  
    isBreak=false; 
    
    break; 
    
  end;
  
  if a>100 then
  
    isBreak=true;
    
    break;
    
  end;
  
  a=a+1;
  
until true;

if isBreak then

  break;
  
end;

end;

6、switch中的break实现(与5类似,不需要引用额外变量)

switch(cond)

{

case 1:

  if(a+b<12)
  
    break;
    
  a+=2;
  
  b+=4;
  
  c=a*b;
  
  break;

default:

  c=123;
  
  break;

case 2:

  c=456;
  
  break;

}

=>

if cond==1 then

repeat

  if a+b<12 then
  
    break;
    
  end;
  
  a=a+2;
  
  b=b+4;
  
  c=a*b;
  
  break;
  
until true;

elseif cond==2 then

repeat

  c=456;
  
  break;
  
until true;

else

repeat

  c=123;
  
  break;
  
until true;

end;

【特殊处理】

1、转换出的lua代码不使用self表示对象自己,而是使用this表示对象自己,这样无需处理c#代码里用self作变量名的情形。类似的,转换出的lua使用base来表示父类子对象。类似的,property的get/set方法名也仍然是get/set,event接口实现的add/remove方法名也仍然使用add/remove。

2、泛型方法在转换时会将泛型参数当函数参数作用。

亦即

void GenericMethod()

{

}

=>

GenericMethod = function(this, T)

end

***这种转换方法能适应unity3d的GetComponent方法

GetComponent() => GetCompoent(Type)

3、为与Slua及dotnet reflection调用的机制一致,函数的out参数在调用时传入实参__cs2lua_out(使用Slua时此值为Slua.out否则为一空表)。

  【用法】

1、建立一个C#工程,引用Cs2Lualualib.dll。

2、用vs开发功能,只使用Cs2Lua支持的语法构造。

3、运行Cs2Lua C#.csproj,生成该工程各C#类对应的lua代码,输出按类组织,每个类一个文件,输出时的类已经合并过(支持c# partial)

4、用slua封装供前面C#工程使用的其它DLL里的类。

5、将生成的lua文件放到unity3d工程里,按slua的规则启动其入口类。

理论上按此顺序即可。

*** 注意第3步的输出lua在工程文件所在目录下的lua目录里,日志在log目录里,必须检查日志文件确定没有错误才能继续!!!

【lualib.lua】

*** cs2lua的实现假设C#导出给lua的API都采用slua。

Cs2Lua.exe负责按照c#语意选择合适的lua语法来实现对应语义,由于c#语言比lua复杂很多,在语言基础设施上很多是没法一一对应的,所以我们用lualib.lua来构建无法直接在lua语法层面简单实现的c#语义。主要包括:

1、基本运算

lua的运算符比c#少了很多,多出来的c#运算,有一些cs2lua经过语法变换对应到lua语句,比如复合赋值等;有一些cs2lua直接放弃,比如指针相关的操作;另一些则约定由lualib.lua提供一个对应的lua函数来实现,比如称位操作、位操作、条件表达式等。

2、对象语义

c#里的对象在lua里一般通过table+metatable来表示,与设计c#的对象运行时机制一样,我们需要在lua设计一套类似c#对象语义的运行时设施,这种机制也在lualib.lua里实现,cs2lua负责提供素材,比如method、property、field、event、indexer等对象的组成部分,组装成一个对象的工作则在lualib.lua里完成。

cs2lua将对象分类为被cs2lua转换的c#代码本身定义的对象与外部由slua导入的c#对象2大类,每类又分为普通对象、IList、IDictionary、ICollection这几种。

3、外部操作符重载处理

c#里的操作符重载比lua元表的操作符函数多,对lua元表支持的操作符重载,cs2lua按照slua的规则直接转换为lua对应操作(slua会在这些操作的元表函数里关联到实际的c#重载函数),不在lua元表里的操作符重载,需要在lualib.lua里处理,这个与slua的实现有关,本来c#的操作符重载函数都必须是类的静态方法,但slua在导出时将这些方法放到类实例上了,因此我们无法直接按c#操作符重载语义调用类的对应的静态操作符重载方法(对于被cs2lua转换的c#代码里定义的操作符重载,cs2lua在转换时采用c#的语义,所以可以直接转换为静态方法调用),而必须调用实例方法,由于操作符的实例有可能为空,不能直接写一个调用了事,判空的处理委托到lualib.lua里处理。

4、delegate的实现

cs2lua在转换delegate时委托到几个lua函数处理,这些函数在lualib.lua里实现,对被cs2lua转换的c#代码里定义的委托与slua导入的委托采用不同的函数,实现机制也不太相同。

5、外部indexer的实现

这块主要为了与slua的机制配合,放到lualib.lua里实现。

6、[]成员访问操作的实现

这个是预留,在语法上,对于数组访问[]与indexer已经分别独立处理,目前来看没发现其它种类的[]操作,但仍然预留了实现函数在lualib.lua里,由于这块不清楚对应的c#特性是什么,内部与外部实现都在lualib.lua里。

7、generic集合类型

cs2lua转换出的lua代码只使用lua的基础类型(不含string)与数组,其它类型假定都是经由slua导入。

由于lua不能直接对应generic语义,slua在导出c#类时要求导出的类必须是具体类,为了保证cs2lua在转换前后类名一致,所有generic类的实例类在导出时都需要进行一次继承,以保证导出的类在c#代码与lua代码里名字相同,比如导出List,我们需要命名为IntList:

public class IntList : List

{}

之后再由slua导出,之后在c#里就需要使用IntList而不是List,这样才能保证转换出的lua代码能正确访问导出的类。

对于slua导入的API,这个约定没有问题,但被cs2lua转换的c#代码里也会有很频繁的需求使用常见的集合类型,因为被cs2lua转换的c#类转换后就是lua的table,天然可以支持动态类型,从这一角度出发,我们认为在被cs2lua转换的c#代码里使用的集合对象可以考虑转换为lua的table,借助table的元表机制,我们可以实现与c#里的集合对象相同的操作方法,这些代码都需要lua实现,所以放在lualib.lua里。

*** 需要注意的是,List这类直接在被cs2lua转换的c#代码里使用的generic集合对象,由于转换为lua的table,不能作为参数传递给slua(除非修改slua的代码进行识别并转换,目前不采用这种思路)

【如何增加可以在c#里使用的API】

1、第一种方式就是在独立的C# dll里实现,然后用slua导出。

2、另一种方式其实可以在上面提到的lualib.lua里实现,就像我们提到的generic集合类一样(这种在dotnet系统dll里定义,所以c#代码可以直接使用,但由于slua并不导出,所以转化后的lua里要使用的话必须有额外的lua来实现)。这其实表明了两种情形:

a、所用的api在某个c# dll里已经定义了,但slua没有导出(上面说的就是这种情形),实现方式一种是在lualib.lua里实现,另外还有一个办法是单独加一个lua模块实现,并在c#代码里使用Cs2Lua.Require属性在使用它的类里标明一下依赖关系。

b、所用的api没有在c# dll里定义,所以也不会在slua里导出。这时需要在C#与lua里各实现一套,然后c#的实现标记为Cs2Lua.Ignore并同时使用Cs2Lua.Require标明对lua实现代码的依赖关系(当然也可将lua实现放在lualib.lua里,这样就不用标明依赖了,lualib.lua是默认要依赖的)。

【调试lua】

可以使用LuaStudio进行lua调试,但对于比较复杂的工程,调试实在是非常慢。

【示例链接】

https://github.com/dreamanlan/Cs2Lua/tree/master/Test