7.5 CLR/Lua

Limax提供一个clrlua项目,粘合C#与Lua,支持C#与Lua代码之间的互操作,用于实现C#脚本模式框架下的LuaScriptHandle。这一章介绍clrlua提供的功能。


  • 7.5.3 类型映射

    C#,Lua两种语言的类型系统没有兼容性可言,所以类型映射无法用一张明确的表格进行描述。分3种规则讨论。


    • 规则1:C#向Lua传递(C#通过eval向Lua传递;Lua访问C#时,构造返回对象,方法返回值,委托返回值,读取字段,读取属性)

      这种情况下,C#类型信息可以明确获知。

      • null,使用lua_pushnil传递。

      • bool以及装箱类型Boolean,使用lua_pushboolean传递。

      • byte, sbyte, short, ushort, int, uint, long, ulong, 以及装箱类型Byte, SByte, Int16, Uint16, Int32, UInt32, Int64, Uint64, 使用lua_pushinteger传递

      • float,double,decimal以及装箱类型Single,Double,Decimal,使用lua_pushnumber传递

      • char, string以及装箱类型Char, String, UTF8编码后使用lua_pushstring传递。需要注意, 字符串传递给Lua之后, 已经变成Lua字符串, 不可能调用C#的字符串方法进行访问。之前的字段访问例子中, 如果执行lua.eval("print(t0.mystr.Length)"), 将输出nil, 改为lua.eval("print(t0.mystr:len())"), 才能获得正确结果。语法上比较怪异,小心。

      • LuaObject,LuaTable, LuaFunction之外的类型使用lua_newuserdata创建一个用户数据类型与之关联,特别的,如果类型为委托类型,则获取委托对象的Invoke方法作为实际需要关联的对象,这样委托对象就能被正确调用了。

      • LuaObject,LuaTable, LuaFunction是之前返回给C#的类型,这时取回之前关联的Lua对象。

      • 特别的, C#方法的返回void, 或者委托返回void, 或者非可读属性的读取, 根本不会在栈上push任何值,详见章节《脚本语言访问C#规范》中的实验。


    • 规则2:Lua向C#返回值(eval的返回)

      这种情况下,只能根据Lua当前已知的信息返回,C#获取这样的返回值以后,必须严格检查类型然后使用,不小心就可能导致类型转换异常,System.InvalidCastException。

      • LUA_TNIL,返回null

      • LUA_TBOOLEAN,返回为Boolean

      • LUA_TSTRING,UTF8解码后返回为String

      • LUA_TNUMBER,根据lua_isinteger进行判断,返回为Int64,或者Double。这一点非常特殊,并不存在LUA_TINTEGER这一类型,估计是Lua在保证兼容性的前提上为了支持Int64加入了一个补丁——在类型信息之外,内部增加了一个tag进行区分,无需影响原有的类型规范。

      • LUA_TUSERDATA,该对象是C#之前传入的非数值对象,直接向C#返回该对象,特别的,如果该对象是委托的包装,返回委托本身。

      • LUA_TTABLE,该对象是一个Lua表,创建并返回一个与之关联的LuaTable对象,使得C#能够通过LuaTable的IDictionary接口直接访问该表。LuaTable对象按照Lua规范访问,而不是IDictionary规范。例如,IDictionary重复Add抛异常,Lua的重复Add执行替换;Lua中Add nil值表示删除,所以LuaTable.Add(key,null)等同于LuaTable.Remve(key)

      • LUA_TFUNCTION,该对象是一个Lua函数,创建一个与之关联的LuaObject派生对象,这个对象再使用LuaFunction委托进行包装,使得C#能够通过LuaFunction委托,将函数调用转发给Lua虚拟机,调用相应的Lua函数。

      • 特别的,对于无返回值的情况,返回DBNull.Value。对于多返回值的情况,将各个返回值按照上面的方式转换为相应的C#对象之后,再包装成C#对象数组返回。

      • 注意,(int)lua.eval("return 1"); 这样的转换必然导致System.InvalidCastException, 原因在于lua_Integer是64位的, 对应long, 返回类型为System.Int64, 这样的装箱类型无法像简单类型long. 特别的, 如果类型为委托类型, 则获取委托对象的Invoke方法作为实际需要关联的对象, 这样委托对象就能被正确调用了. 可以在返回对象上调用getType()方法检查实际类型, 判断是否符合需求, 进而实现正确的转换。


    • 规则3:Lua向C#传递(构造参数,方法调用参数,委托调用参数,字段设置,属性设置)

      首先按照规则2,转换为C#对象,然后根据期待的参数类型在C#中进行转换。细节参见章节《脚本语言访问C#规范》


  • 7.5.4 异常规范

    • Lua代码的运行时异常,由Lua虚拟机捕获,通过Lua操作对象构造时传入的委托传递给C#。

    • Lua调用C#构造函数或者成员函数或者委托时, 如果抛出异常,这样的异常将被内部捕获, 转换为C#异常的串格式, 再后缀Lua栈描述, 生成错误信息, 提交给Lua虚拟机, 最终传递给C#。这里需要强调一个问题, C#和Lua互相调用, 间接递归的情形下, 异常实际上到达不了整个递归栈的最上层, 而是打断当前eval的执行, 传送出异常串, 最后eval返回DBNull.Value。如果有回滚整个递归栈的需求, 可以这样设计:逻辑上避免eval返回DBNull.Value, 一旦eval返回DBNull.Value, 在C#内抛出异常, 带上最后一条错误信息。


  • 7.5.5 注意事项

    • 1. C#中的泛型对象不可往Lua虚拟机中传递,否则将抛出异常。原因在于,System::Runtime::InteropServices::Marshal::GetNativeVariantForObject,System::Runtime::InteropServices::Marshal::GetObjectForNativeVariant这两个关键方法的泛型版本从.NET4.5.1之后才开始提供,为了兼容较老的.NET版本,该项目使用.NET4.0开发(.NET3.5应该也可以使用)。

    • 2. LuaObject返回给C#持有时,将被C#的gc控制,gc线程不一定是操作Lua虚拟机的线程,所以该项目必须按线程安全的方式设计。实现上,通过对Lua操作对象加锁保证安全,为了在LuaObject析构时进行锁定,需要将Lua操作对象的引用传递给LuaObject构造器,这样的传递只能通过lua_State结构实现,问题是lua_State结构缺少一个用户自定义指针的字段,所以实现上挪用了lua_State.hook指针,这相当于扔掉了Lua的hook能力,全局空间中的debug.sethook,debug.gethook两个方法也被删除掉, 避免用户使用, 导致错误。如果需要保留hook能力, 则只能修订Lua源码, 在lua_State结构上添加一个用户定义指针,对应修改clrlua.cpp,重新编译项目。

    • 3. 可以创建多个Lua操作对象,从一个Lua操作对象获取的LuaObject,LuaTable,不能够传入另一个Lua操作对象,这个显而易见;其它的C#对象传入多个Lua操作对象,没有限制,如果不同线程使用不同的Lua操作对象,这些传入的C#对象的线程安全性必须应用自己保证。多个Lua操作对象,如果确有数据交换需求,可以通过JSON实现,Limax自带json.lua, 限制是对象中不能存在环。

    • 4. 多线程的情况下通过LuaTable访问Lua虚拟机中的table时需要注意,使用foreach方式遍历table前,必须在lua操作对象或者LuaTable.SyncRoot上加锁,这两者实际上指向同一对象。其它访问方法,已经在内部通过加锁保证安全了。

    • 5. json.lua集成在项目中,全局名字空间中已经存在JSON.stringify,JSON.parse,可以直接使用。


上一页 下一页