MoonSharp 从一知到半解(2)

本文介绍MoonSharp库,使Lua脚本与C#代码无缝交互。涵盖类注册、对象共享、方法调用、事件处理及运算符重载等高级特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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
ModeDescription
ReflectionOptimization 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.
LazyOptimizedThis 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.
PreoptimizedThis 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.
BackgroundOptimizedThis is a hint, and MoonSharp is free to “downgrade” this to Reflection. Optimization is done at registration time.
HideMembersMembers 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.
DefaultUse the default access mode
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值