MoonSharp 从一知到半解(2)

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
2
3
4
5
6
7
8
[MoonSharpUserData]
class MyClass1
{
public double calcHypotenuse(double a, double b)
{
return Math.Sqrt(a * a + b * b);
}
}

然后在代码中将 MyClass1 的一个实例传递到脚本中作为一个全局变量,并在脚本中通过此变量调用 MyClass1 中的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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,用来传递到脚本层。

1
2
3
4
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# 静态方法,定义一个包含了静态方法的类型:

1
2
3
4
5
6
7
8
[MoonSharpUserData]
class MyClassStatic
{
public static double calcHypotenuse(double a, double b)
{
return Math.Sqrt(a * a + b * b);
}
}

有两种方式对其进行调用(与使用 C# 代码调用相同),其一是通过类型实例(与前文无异),另外是通过直接传递类型(一个 placeholder userdata 会被创建;或者使用 UserData.CreateStatic)。

1
2
// the second method
script.Globals["MCS"] = typeof(MyClassStatic1);

‘.’ 还是 ‘:’

在大多数情况下,这两种调用方法是完全相同的(官方数据 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
2
3
4
5
6
public string ManipulateString(string input, ref string tobeconcat, out string lowercase)
{
tobeconcat = input + tobeconcat;
lowercase = input.ToLower();
return input.ToUpper();
}
1
2
3
4
5
x, y, z = myobj:manipulateString('CiAo', 'hello');

-- x will be "CIAO"
-- y will be "CiAohello"
-- z will be "ciao"

Indexers

在 C# 中,允许创建索引器方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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”,可以使用如下的脚本:

1
2
3
4
5
6
7
8
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,递归),则支持多索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[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;
}

另外对于算数运算符,可以使用隐式重载,找到的算数运算符将被自动处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

支持扩展方法,知道就行了……并不知道干吗用……

1
2
UserData.RegisterExtensionType
RegisterAssembly(<assembly>,true)

Events

MoonSharp 中也支持事件,但是只以非常简单的方式(minimalistic way),支持符合约束的事件:

  • 事件必须以引用类型(reference type)声明
  • 事件必须实现 add 和 remove 方法
  • 事件处理函数的返回类型必须是 System.Void (VB.NET 中必须是 Sub)
  • 事件处理函数的参数不能多于 16 个
  • 事件处理函数不能包含值类型参数或者 by-ref 参数
  • 事件处理函数的签名中不能包含指针或未解析的泛型
  • 事件处理函数的所有参数必须能够转换为 MoonSharp 类型

什么意思?(我好想说“字面意思”摸鱼啊……),反正,这些约束是为了尽量避免在运行时构建代码。看起来限制多多,但是至少可以使用 EventHandler EventHandler 这样的事件处理方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

有时候我们需要将已经注册了的类型的成员的可见性,让脚本不再可以调用。这有下面两种方法:

1
2
var descr = ((StandardUserDataDescriptor)(UserData.RegisterType<SomeType>()));
descr.RemoveMember("SomeMember");

另外,直接增加一个 attribute 到类型的声明上;

1
2
3
[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