0x00
请配合我的代码食用Github
本来以为可以搞定后面几章,但是没想到,就肝了一章,溜了溜了……溜了溜了……
0x01 Sharing objects
让 Lua 和 C# 进行对话
在默认情况下,一个 C# 类型传递到 Lua 脚本中,将可以访问其公共(public)的方法、属性等,这也可以通过名为 MoonSharpVisible 的属性来修改自定义类型的可见性。
使用专门的对象作为 CLR 与脚本代码的接口,而不是将 C# 代码中的模型直接或者全部暴露给脚本。可以通过一些设计模式来设计这一个接口层级,如 Adapter、Facade、Proxy。
这样做的好处是:
- 可以限制脚本什么能做,什么不能做(处于安全性考虑,mod 不能有过多的权限,如删除终端用户数据)
- 提供接口对于脚本作者很有帮助
- 单独写出接口的文档
- 是脚本与内部逻辑代码相对独立,修改内部代码不至于大改脚本使用方式。
出于以上原因,MoonSharp 默认要求显式地将类型注册到脚本来让脚本访问,当然,如果你完全信任脚本代码,也可以自动进行注册,但要承当由此带来的风险。
1 | UserData.RegistrationPolicy = InteropRegistrationPolicy.Automatic; |
Type descriptors
先简单说说类型描述器(Type descriptors),解释交互(introp)是怎么实现的,幕后发生的事情以及如何覆盖整个互操作系统。
每一个 CLR 都被包装在一个 “type descriptor”中,用来向脚本描述这个 CLR 类型。而注册一个类型(Type)用来与脚本交互,实际上就是将此类型与描述器相关联,描述器将用来分派方法,属性等。
我们可以选择使用 MoonSharp 提供的默认描述器,但也可以自己实现来提高速度,添加功能与安全性检测等,但这个过程并不容易(除非必要)。
简单案例
首先,我们定义一个类,并用 attribute [MoonSharpUserData] 来修饰,表示作为提供给脚本使用的类型,并标记来自动注册。
1 | [ ] |
然后在代码中将 MyClass1 的一个实例传递到脚本中作为一个全局变量,并在脚本中通过此变量调用 MyClass1 中的方法。
1 | public static double CallMyClass1() |
游戏开始,加点料
现在我们把 [MoonSharpUserData] 去掉,只需要显式地进行类型注册(注意使用上的区别),然后使用 显式创建一个 DynValue,用来传递到脚本层。
1 | UserData.RegisterType<MyClass1>(); |
在 C# 中定义的脚本到了 Lua 中可以不必完全一样(只要不存在完全相同的方法版本),这是由于 MoonSharp 中的匹配规则,如 SomeMethodWithLongName 可以在 lua 脚本中通过 someMethodWithLongName 或者 some_method_with_long_name 进行访问。
静态方法
对于脚本访问 C# 静态方法,定义一个包含了静态方法的类型:
1 | [ ] |
有两种方式对其进行调用(与使用 C# 代码调用相同),其一是通过类型实例(与前文无异),另外是通过直接传递类型(一个 placeholder userdata 会被创建;或者使用 UserData.CreateStatic)。
1 | // the second method |
‘.’ 还是 ‘:’
在大多数情况下,这两种调用方法是完全相同的(官方数据 99.999%……),MoonSharp 会对所调用的用户数据进行相应的表现。
有一些极端情况可能会产生影响 - 例如,如果属性返回一个委托,并且您将立即调用该委托,并将原始对象作为实例。这是一个远程场景(remote scenario),当发生这种情况时你必须手动处理它。
Overloads
支持重载方法,统一方法名,根据参数不同调用不同的具体实现。但是在 MoonSharp 中,重载方法类似黑暗魔法,具有“成功率”或者不稳定性,因为传递到 lua 脚本中的参数 int,float 都是 double 型,MoonSharp 通过启发式(Heuristic)的方法,来选择最佳的重载函数,如果认为重载错误了,可以去论坛吐槽,让他们校准啊2333。
ByRef(ref/out)
在 C# 中使用 ref 或 out 参数(ByRef 函数参数)时,MoonSharp 会对其正确的整理排列成多返回值。副作用是并没有对具有 ByRef 参数的方法进行优化(使用反射调用,注意影响 AOT 平台的性能)。
1 | public string ManipulateString(string input, ref string tobeconcat, out string lowercase) |
1 | x, y, z = myobj:manipulateString('CiAo', 'hello'); |
Indexers
在 C# 中,允许创建索引器方法:
1 | class IndexerTestClass |
MoonSharp 作为Lua语言的扩展,允许括号内的表达式列表来索引 userdata。
对任何非 userdata 的内容使用多索引都会引起错误,对上面的类在脚本中的一个实例为“o”,可以使用如下的脚本:
1 | o[5] = 19 |
要注意的是,对任何非 userdata 的对象使用多索引(multi-index)都会引发错误。这包括使用元方法的情况,但如果 metatable 的 __index 字段设置为 userdata(Emmm,递归),则支持多索引。
1 | m = { |
这里,“__newindex” 元方法用来对表更新,“__index” 则用来对表访问 。
Operators and metamethods on userdata
运算符重载 MoonSharp 中也有支持(也是黑魔法吗???)
对于 lua 中的运算符,只能通过 Attributes 的方式来进行重载,如 [MoonSharpUserDataMetamethod(“__concat”)] 对应 concat(..) 运算符,其他的有 __pow, __call, __pairs, __ipairs。
1 | [ ] |
另外对于算数运算符,可以使用隐式重载,找到的算数运算符将被自动处理。
1 | public static int operator +(ArithmOperatorsTestClass o, int v) |
上面的代码将允许在 lua 中使用“+”操作符来运算数字和这个给定的对象。加减乘除,取模,一元非运算等,都使用这种方式实现。
插播一条 lua tips:
模式 | 描述 |
---|---|
__add | 对应的运算符 ‘+’. |
__sub | 对应的运算符 ‘-‘. |
__mul | 对应的运算符 ‘*’. |
__div | 对应的运算符 ‘/‘. |
__mod | 对应的运算符 ‘%’. |
__unm | 对应的运算符 ‘-‘. |
__concat | 对应的运算符 ‘..’. |
__eq | 对应的运算符 ‘==’. |
__lt | 对应的运算符 ‘<’. |
__le | 对应的运算符 ‘<=’. |
最后是相对比较复杂的比较运算,长度运算符(#),__iterator metamethod 的实现。
等运算符(== ~=):使用 System.Object.Equals 方法自动解释;
比较运算符(< >= etc.):如果类型实现了 IComparable.CompareTo,则会自动解释;
长度运算符(#):如果类型实现了特定属性(Length Count)会自动分派;
__iterator:如果类型实现了 System.Collections.IEnumerable,则自动分派给 GetEnumerator。可以用来实现 ipairs 和 pairs,两者都是键值对,区别在于前者根据 key 的值递增遍历,如果遇到空值就会结束,而后者会遍历表中所有的键值对。
Extension Methods
支持扩展方法,知道就行了……并不知道干吗用……
1 | UserData.RegisterExtensionType |
Events
MoonSharp 中也支持事件,但是只以非常简单的方式(minimalistic way),支持符合约束的事件:
- 事件必须以引用类型(reference type)声明
- 事件必须实现 add 和 remove 方法
- 事件处理函数的返回类型必须是 System.Void (VB.NET 中必须是 Sub)
- 事件处理函数的参数不能多于 16 个
- 事件处理函数不能包含值类型参数或者 by-ref 参数
- 事件处理函数的签名中不能包含指针或未解析的泛型
- 事件处理函数的所有参数必须能够转换为 MoonSharp 类型
什么意思?(我好想说“字面意思”摸鱼啊……),反正,这些约束是为了尽量避免在运行时构建代码。看起来限制多多,但是至少可以使用 EventHandler EventHandler
1 | class MyClass |
这个例子中是在 lua 脚本中触发事件,在 C# 中处理,反之也同样可行,比如教程最开始就是获取了一个脚本中的方法并执行了,记得吗。需要注意的一点是,添加和移除事件处理函数是一个缓慢的操作,需要在线程锁定下使用反射执行(这个应用的还挺多)。
A word on InteropAccessMode
很多方法都有一个 InteopAccessMode 类型的可选参数,定义了标准描述器如何处理回调函数。
默认为 LazyOptimized。其他模式可以参考附录。
修改代码可见性
MoonSharp 可以通过两种方法修改可见性:MoonSharpHidden 和 MoonSharpVisible 来重写默认的成员可见性(也就是前面说的 public 成员是脚本中默认可见的)。
这里的 MoonSharpHidden 就是 MoonSharpVisible(false) 的简写。
Removing members
有时候我们需要将已经注册了的类型的成员的可见性,让脚本不再可以调用。这有下面两种方法:
1 | var descr = ((StandardUserDataDescriptor)(UserData.RegisterType<SomeType>())); |
另外,直接增加一个 attribute 到类型的声明上;
1 | [ ] |
这可以将继承的但不重写的成员隐藏。
0xff
附录I
InteopAccessMode
Mode | Description |
---|---|
Reflection | Optimization is not performed and reflection is used everytime to access members. This is the slowest approach but saves a lot of memory if members are seldomly used. |
LazyOptimized | This is a hint, and MoonSharp is free to “downgrade” this to Reflection. Optimization is done on the fly the first time a member is accessed. This saves memory for all members that are never accessed, at the cost of an increased script execution time. |
Preoptimized | This is a hint, and MoonSharp is free to “downgrade” this to Reflection. Optimization is done in a background thread which starts at registration time. If a member is accessed before optimization is completed, reflection is used. |
BackgroundOptimized | This is a hint, and MoonSharp is free to “downgrade” this to Reflection. Optimization is done at registration time. |
HideMembers | Members are simply not accessible at all. Can be useful if you need a userdata type whose members are hidden from scripts but can still be passed around to other functions. See also AnonWrapper and AnonWrapper |
Default | Use the default access mode |