0x00
MoonSharp 是一个支持使用 C# 调用 Lua 的类库,这个系列是通过官网的教程来入门类库与 Lua。
如果有需要请配合我的代码食用。
本来以为可以搞定后面几章,但是没想到,就肝了一章,溜了溜了……溜了溜了……
系列的第一部分说了一大堆,主要是基本用法,同时,谢谢不懂 Lua 的小伙伴们的问题,也谢谢熟悉 Lua 的小伙伴们的解答。
这一部分,我们要进入 C# 如何将类(Class)提供给脚本使用。
0x01 Sharing objects
让 Lua 和 C# 进行对话
在默认情况下,一个 C# 类型传递到 Lua 脚本中,将可以访问其公共(public)的方法、属性等,这也可以通过名为 MoonSharpVisible 的属性来修改自定义类型的可见性。
使用专门的对象作为 CLR 与脚本代码的接口,而不是将 C# 代码中的模型直接或者全部暴露给脚本。可以通过一些设计模式来设计这一个接口层级,如 Adapter、Facade、Proxy。
这样做的好处是:
- 可以限制脚本什么能做,什么不能做(处于安全性考虑,mod 不能有过多的权限,如删除终端用户数据)
- 提供接口对于脚本作者很有帮助
- 单独写出接口的文档
- 使脚本与内部逻辑代码相对独立,修改内部代码不至于大改脚本使用方式。
出于以上原因,MoonSharp 默认要求显式地将类型注册到脚本来让脚本访问,当然,如果你完全信任脚本代码,也可以自动进行注册,但要承当由此带来的风险。
UserData.RegistrationPolicy = InteropRegistrationPolicy.Automatic;
Type descriptors
先简单说说类型描述器(Type descriptors),解释交互(introp)是怎么实现的,幕后发生的事情以及如何覆盖整个互操作系统。
每一个 CLR 都被包装在一个 “type descriptor”中,用来向脚本描述这个 CLR 类型。而注册一个类型(Type)用来与脚本交互,实际上就是将此类型与描述器相关联,描述器将用来分派方法,属性等。
我们可以选择使用 MoonSharp 提供的默认描述器,但也可以自己实现来提高速度,添加功能与安全性检测等,但这个过程并不容易(除非必要)。
简单案例
首先,我们定义一个类,并用 attribute [MoonSharpUserData] 来修饰,表示作为提供给脚本使用的类型,并标记来自动注册。
[MoonSharpUserData]
class MyClass1
{
public double calcHypotenuse(double a, double b)
{
return Math.Sqrt(a * a + b * b);
}
}
然后在代码中将 MyClass1 的一个实例传递到脚本中作为一个全局变量,并在脚本中通过此变量调用 MyClass1 中的方法。
public static double CallMyClass1()
{
string scriptCode = @"
return MC.calcHypotenuse(3, 4)
";
// Automatically register all MoonSharpUserData types
UserData.RegisterAssembly();
Script script = new Script();
// Pass an instance of MyClass1 to the script in a global
script.Globals["MC"] = new MyClass1();
DynValue res = script.DoString(scriptCode);
Console.WriteLine(res);
Console.ReadKey();
return res.Number;
}
游戏开始,加点料
现在我们把 [MoonSharpUserData] 去掉,只需要显式地进行类型注册(注意使用上的区别),然后使用 显式创建一个 DynValue,用来传递到脚本层。
UserData.RegisterType<MyClass1>();
Script script = new Script();
DynValue obj = UserData.Create(new MyClass1());
script.Globals.Set("MC", obj);
在 C# 中定义的脚本到了 Lua 中可以不必完全一样(只要不存在完全相同的方法版本),这是由于 MoonSharp 中的匹配规则,如 SomeMethodWithLongName 可以在 lua 脚本中通过 someMethodWithLongName 或者 some_method_with_long_name 进行访问。
静态方法
对于脚本访问 C# 静态方法,定义一个包含了静态方法的类型:
[MoonSharpUserData]
class MyClassStatic
{
public static double calcHypotenuse(double a, double b)
{
return Math.Sqrt(a * a + b * b);
}
}
有两种方式对其进行调用(与使用 C# 代码调用相同),其一是通过类型实例(与前文无异),另外是通过直接传递类型(一个 placeholder userdata 会被创建;或者使用 UserData.CreateStatic)。
// the second method
script.Globals["MCS"] = typeof(MyClassStatic1);
未完全支持的函数重载
支持重载方法,统一方法名,根据参数不同调用不同的具体实现。但是在 MoonSharp 中,重载方法类似黑暗魔法,具有“成功率”或者不稳定性,因为传递到 lua 脚本中的参数 int,float 都是 double 型,MoonSharp 通过启发式(Heuristic)的方法,来选择最佳的重载函数,如果认为重载错误了,可以去论坛吐槽,让他们校准啊2333(听天由命吧)。
ByRef(ref/out)
在 C# 中使用 ref 或 out 参数(ByRef 函数参数)时,MoonSharp 会对其正确的整理排列成多返回值。副作用是并没有对具有 ByRef 参数的方法进行优化(使用反射调用,注意影响 AOT 平台的性能)。
public string ManipulateString(string input, ref string tobeconcat, out string lowercase)
{
tobeconcat = input + tobeconcat;
lowercase = input.ToLower();
return input.ToUpper();
}
x, y, z = myobj:manipulateString('CiAo', 'hello');
-- x will be "CIAO"
-- y will be "CiAohello"
-- z will be "ciao"
Indexers
在 C# 中,允许创建索引器方法:
class IndexerTestClass
{
Dictionary<int, int> mymap = new Dictionary<int, int>();
public int this[int idx]
{
get { return mymap[idx]; }
set { mymap[idx] = value; }
}
public int this[int idx1, int idx2, int idx3]
{
get { int idx = (idx1 + idx2) * idx3; return mymap[idx]; }
set { int idx = (idx1 + idx2) * idx3; mymap[idx] = value; }
}
}
MoonSharp 作为 Lua 语言的扩展,允许括号内的表达式列表来索引 userdata。
对任何非 userdata 的内容使用多索引都会引起错误,对上面的类在脚本中的一个实例为“o”,可以使用如下的脚本:
o[5] = 19
print(o[5]) -- 19
x = 5 + o[5]
print(x) -- 24
o[1,2,3] = 19
print(o[1,2,3]) -- 19
x = 5 + o[1,2,3] -- not Standard Lua!@
print(x) -- 24
要注意的是,对任何非 userdata 的对象使用多索引(multi-index)都会引发错误。这包括使用元方法的情况,但如果 metatable 的 __index 字段设置为 userdata(Emmm,递归),则支持多索引。
m = {
__index = o,
__newindex = o
-- pretend this is some meaningful functions...
--__index = function(obj, idx) return o[idx] end,
--__newindex = function(obj, idx, val) end
-- => :“cannot multi-index through metamethods. userdata expected”
}
t = { }
setmetatable(t, m)
t[10,11,12] = 1234
return t[10,11,12]
这里,“__newindex” 元方法用来对表更新,“__index” 则用来对表访问 。
Operators and metamethods on userdata
运算符重载 MoonSharp 中也有支持(也是黑魔法吗???)
对于 lua 中的运算符,只能通过 Attributes 的方式来进行重载,如 [MoonSharpUserDataMetamethod("__concat")] 对应 concat(…) 运算符,其他的有 __pow, __call, __pairs, __ipairs。
[MoonSharpUserDataMetamethod("__concat")]
public static int Concat(ArithmOperatorsTestClass o, int v)
{
return o.Value + v;
}
[MoonSharpUserDataMetamethod("__concat")]
public static int Concat(int v, ArithmOperatorsTestClass o)
{
return o.Value + v;
}
[MoonSharpUserDataMetamethod("__concat")]
public static int Concat(ArithmOperatorsTestClass o1, ArithmOperatorsTestClass o2)
{
return o1.Value + o2.Value;
}
另外对于算数运算符,可以使用隐式重载,找到的算数运算符将被自动处理。
public static int operator +(ArithmOperatorsTestClass o, int v)
{
return o.Value + v;
}
public static int operator +(int v, ArithmOperatorsTestClass o)
{
return o.Value + v;
}
public static int operator +(ArithmOperatorsTestClass o1, ArithmOperatorsTestClass o2)
{
return o1.Value + o2.Value;
}
上面的代码将允许在 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
支持扩展方法,知道就行了……并不知道干吗用……
UserData.RegisterExtensionType
RegisterAssembly(<assembly>,true)
Events
MoonSharp 中也支持事件,但是只以非常简单的方式(minimalistic way),支持符合约束的事件:
- 事件必须以引用类型(reference type)声明
- 事件必须实现 add 和 remove 方法
- 事件处理函数的返回类型必须是 System.Void (VB.NET 中必须是 Sub)
- 事件处理函数的参数不能多于 16 个
- 事件处理函数不能包含值类型参数或者 by-ref 参数
- 事件处理函数的签名中不能包含指针或未解析的泛型
- 事件处理函数的所有参数必须能够转换为 MoonSharp 类型
什么意思?(我好想说“字面意思”摸鱼啊……),反正,这些约束是为了尽量避免在运行时构建代码。看起来限制多多,但是至少可以使用 EventHandler EventHandler 这样的事件处理方式。
class MyClass
{
public event EventHandler SomethingHappened;
public void RaiseTheEvent()
{
if (SomethingHappened != null)
SomethingHappened(this, EventArgs.Empty);
}
}
static void Events()
{
string scriptCode = @"
function handler(o, a)
print('handled!', o, a);
end
myobj.somethingHappened.add(handler);
myobj.raiseTheEvent();
myobj.somethingHappened.remove(handler);
myobj.raiseTheEvent();
";
UserData.RegisterType<EventArgs>();
UserData.RegisterType<MyClass>();
Script script = new Script();
script.Globals["myobj"] = new MyClass();
script.DoString(scriptCode);
}
这个例子中是在 lua 脚本中触发事件,在 C# 中处理,反之也同样可行,比如教程最开始就是获取了一个脚本中的方法并执行了,记得吗。需要注意的一点是,添加和移除事件处理函数是一个缓慢的操作,需要在线程锁定下使用反射执行(这个应用的还挺多)。
A word on InteropAccessMode
很多方法都有一个 InteopAccessMode 类型的可选参数,定义了标准描述器如何处理回调函数。
默认为 LazyOptimized。其他模式可以参考附录。
修改代码可见性
MoonSharp 可以通过两种方法修改可见性:MoonSharpHidden 和 MoonSharpVisible 来重写默认的成员可见性(也就是前面说的 public 成员是脚本中默认可见的)。
这里的 MoonSharpHidden 就是 MoonSharpVisible(false) 的简写。
Removing members
有时候我们需要将已经注册了的类型的成员的可见性,让脚本不再可以调用。这有下面两种方法:
var descr = ((StandardUserDataDescriptor)(UserData.RegisterType<SomeType>()));
descr.RemoveMember("SomeMember");
另外,直接增加一个 attribute 到类型的声明上;
[MoonSharpHide("SomeMember")]
public class SomeType
...
这可以将继承的但不重写的成员隐藏。
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 |