MoonSharp 从一知到半解(1)

官网的教程

请配合我的代码食用Github

在 c# 工程中安装 MoonSharp 2.0

Nuget 程序包管理器控制台

Install-Package MoonSharp

安装好 MoonShap

(如果是 Unity 工程如何)


发现了一个问题,当我使用 VS 打开 C++ 工程时,(项目-xxx属性 or 右键工程-属性)可以打开属性页;但是安装了 unity tool 后,c# 工程却只能打开一个与文件相似的页面,本不可以打开,需要对 unity tool 进行设置,将属性页打开,但为什么不是正常的属性页呢?

为什么哟?


0x01 通过 string 执行 Lua 程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public double MoonSharpFactorial()
{
string script = @"
-- defines a factorial function
function fact (n)
if (n == 0) then
return 1
else
return n*fact(n-1)
end
end

return fact(5)";
DynValue res = Script.RunString(script);
Console.WriteLine(res.Number);
Console.ReadKey();
return res.Number;
}

这里第一次见到 DynValue 类和 Script 类

Script 类控制对字符串中的脚本进行解析,DynValue 得到了脚本执行的结果,通过其成员 Number 最终得到了 lua 程序的计算结果。

0x02

不再使用静态方法来运行脚本。

创建一个 Script 实例,通过实例来调用 DoString 方法,执行 MoonSharp 脚本。


访问脚本中的全局变量

通过 Script 实例( script ),引用 script.Globals table,将值传入脚本中,可以传入的数值有 浮点型,布尔值,字符串,后面也可以传递方法以及对象。


用 C# 调用 Lua 方法。

通过 DoString 执行脚本,我们在全局环境( global environment )下得到了 fact 方法

使用 script 的 Call 方法,像传入数值一样,找到 lua 函数的全局变量,并传入参数。(最简单,但是有点不安全)

0x03

DynValue 是 MoonSharp 的根本概念,需要在更深入使用 MoonSharp 之前有所接触。MoonSharp 中几乎全部都是 DynValue 的实例,它可以代表脚本中任何类型的值,无论是 table,number,string 等等。

1
2
DynValue luaFactFunction = script.Globals.Get("fact");
DynValue res = script.Call(luaFactFunction, 5);

使用 Globals 的 Get 方法,与使用索引器不同,可以得到一个 DynValue 的返回值,索引会将其转化为一个 System.Object.

在 DynValue 中有很多 factory methods,都以 New 开头( NewString,NewNumber,NewBoolean 等等),同时,也可以使用 FromObject 方法,通过一个 object 创建 DynValue( 这正是调用 Call 方法时使用的方案 )。

认识 DynValue

DynValue 中最重要的属性就是“类型( Type )”了。它是一个告诉我们 DynValue 中保存了什么类型数据的枚举类型。

DynValue 的一些属性只有在特定类型下才是有意义的,比如 Number 属性,只有当 DynValue 的 Type 为 DataType.Number 时,才是有意义的值。因此在使用 DynValue 包含的属性值之前,要记得检查 DynValue 的类型。

Lua ( 以及 python )可以多值返回,为了处理这种特殊句法,MoonSharp 提供了特殊的 DynValue 类型,Tuple,一个 Tuple 就是一个 DynValue 数组,每个成员都是一个 DynValue,可以用索引来访问。

0x04

重头戏了,回调 Calling back,无论将脚本嵌入到软件,游戏还是其他什么工具中,首要的考量就是主体语言和脚本之间的协同工作的能力。假设我们要在脚本使用目标语言写好的方法 API。

Lua 中的函数可以作为变量传递,完全可以将函数名看作一个指向函数的变量。因此在定义好 API 后,可以将 API 方法作为全局变量定义到 Lua 的全局表中。

1
script.Globals["Mul"] = (Func<int, int, int>)Mul;

这样就将全局表中的 Mul 变量指向了作为 API 的 Mul 方法(这里 static 或实例方法都可以,在什么时候会有区别)

这里将 Mul 方法显式转换为了委托 Func<int, int, int>。在转换时,只能将委托( System.Delegate )或 MoonSharp.Interpreter.CallbackFunction 自动转换为脚本中的 function 类型。

对于一个简单任务,将 1 到 10 累加的任务,可以使用:

  1. 迭代方法作为 Api 传到 Lua 中进行计算
  2. 返回 List 转换为 table
  3. 构建一个 Lua Table

构建 Lua Table 使用 MoonSharp 提供的 Table 类。


这一部分要好好写写了,如果碰到没见过的语法句法结构的处理吼。我下面先写思考过程,最后用比较明显强调的方法写结论(请直接去看结论,过程很低能)。


上面所写的将 c# 中的 Mul 方法在脚本中使用很容易理解,是将脚本中的方法名与 c# 中利用委托进行了关联。

但是接下来,在脚本中使用了新的句法:

1
2
3
string scriptCode = @"
return dosum {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
";

诶?我懵逼了,这是什么,返回了一个 Table 吗?名字叫 testTableRevese 貌似是返回了一个 table 是吧?

那么函数这是怎么关联到一起的呢?可读可写?查查有什么教程(看看别人嚼过的(误)??)

猜测永远都不是好方法,还是要实践:

在 lua 交互式编程模式中测试一下,就很容易明白这是 lua 一种函数调用方式了(只是没看到相关的教程):

1
2
3
4
5
6
7
function hi (s)
print(s)
end

hi("something") -- something
hi "something" -- something
print "something" -- something

这几种调用都是正确的,但是

1
2
3
4
5
s = "something"

hi s -- !error
print s -- !error
hi(s) -- something

函数定义时必须要有括号包围参数列表,调用时如果是变量,必须使用括号,如果是常量可以不带括号。

那么上面的脚本就好理解了,调用一个 dosum 函数,参数是一个 table,并将返回值作为脚本的返回值返回。

想到这才考虑到,这怎么会是返回 table 的语句呢?table 的初始化是要赋值的啊!

tb = {…}

说到 return 多说一句:Lua 中的 return 必须紧跟 end 语句,否则会报错,但有一个特例,可以有 print 方法,并且 print 也会执行,如果非要逆着这个规矩来,就只能将 return 放在 do … end 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function foo()
i = 0
return i -- !error
i = i + 1
return ... -- correct
end

funtion foo1()
i = 0
do return i end -- correct
i = i + 1 -- cant reach
end

function foo2()
i = 0
return i -- correct
print (i+1) -- 1
end

结论:

  1. 用尽一切尽量简单的方法,查看打印输出,与自己已经储备的知识对比,完善知识网络;
  2. 对于关键字之类的东西还是查文档吧;
  3. lua 的 return 在使用时要紧挨 end;
  4. 正经内容,在与脚本交互时,table 可以与 List 自动转换,也可以自己构建 MoonSharp 提供的 Table 传递。

0x05

CLR 与 Lua 类型的对应

CLR ( Common Language Runtime )公共语言运行库,与 Java 虚拟机一样是一个运行时环境,负责资源管理(内存分配和垃圾收集等),并保证应用和底层操作系统之间必要的分离。

  • DynValue
  • 自定义转换器( converter )

比使用自动转换更快。

自定义转换器是全局的,对所有的脚本都会造成影响。只需要在 Script.GlobalOptions.CustomConverters 设置合适的回调函数即可。

1
2
3
4
5
6
7
8
// convert StringBuilder to uppercase strings
Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion<StringBuilder>(
v => DynValue.NewString(v.ToString().ToUpper())
);

// convert Table which match a IList<int> to ...
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Table, typeof(IList<int>),
v => new List<int>() { ... });

如果自定义转换器返回值为 null,Moonsharp 会使用自动转换。关于自动转换的规则可以参阅官方,或参考文末附录I。



0xff

附录I

(1)从 CLR 类型自动转换到 MoonSharp 类型

转换时机:

  • When returning an object from a function called by script
  • When returning an object from a property in a user data
  • When setting a value in a table using the indexing operator
  • When calling DynValue.FromObject
  • When using any overload of functions which takes a System.Object in place of a DynValue

CLR 类型 | C# | Lua | Notes
—|—|—|—|—
void | |(no value)|只能在作为方法的返回值时应用
null | | nil | 任何的 null 都会转换为 nil
MoonSharp.Interpreter.DynValue | | * | 不多说了
System.SByte | sbyte | number |
System.Byte | byte | number |
System.Int16 | short | number |
System.UInt16 | ushort | number |
System.Int32 | int | number |
System.UInt32 | uint | number |
System.Int64 | long | number | 这个转换会导致精度损失
System.UInt64 | ulong | number | 这个转换会导致精度损失
System.Single | float | number |
System.Decimal | decimal | number | 这个转换会导致精度损失
System.Double | double | number |
System.Boolean | bool | boolean |
System.String | string | string |
System.Text.StringBuilder | | string |
System.Char | char | string |
MoonSharp.Interpreter.Table | | table |
MoonSharp.Interpreter.CallbackFunction | | function |
System.Delegate | | function |
System.Object | object | userdata | 当这个类型被注册为 userdata 时
System.Type | | userdata | 当这个类型被注册为 userdata 时,静态成员访问
MoonSharp.Interpreter.Closure | | function |
System.Reflection.MethodInfo | | function |
System.Collections.IList | | table | 转换得到的 table 是从 1 开始索引的,所有值都会根据规则转换
System.Collections.IDictionary | | table | 所有的键和值都会根据规则转换
System.Collections.IEnumerable | | iterator | 所有的值都会根据规则转换
System.Collections.IEnumerator | | iterator | 所有的值都会根据规则转换

无法进行转换的会抛出 ScriptRuntimeException 异常。

(2)从 MoonSharp 类型自动转换(Standard)到 CLR 类型

( 比上一节更复杂,分成了 Standard 和 constrained 两种 )每当要求将 DynValue 转换为对象而不指定实际要接收的内容时,使用 Standard 方法:

  • 当调用 DynValue.ToObject 方法时
  • When retrieving values from a table using indexers
  • In some specific subcase of the constrained conversion (see below)
MoonSharp type CLR type Notes
nil null 所有这个一一对应啊
boolean System.Boolean
number System.Double
string System.String
function MoonSharp.Interpreter.Closure 如果 DataType 是 Function
function MoonSharp.Interpreter.CallbackFunction 如果 DataType 是 ClrFunction.
table MoonSharp.Interpreter.Table
tuple MoonSharp.Interpreter.DynValue[]
userdata (special) 返回保存在 userdata 中的对象。如果是 “static” userdata 返回 Type。

(3)从 MoonSharp 类型自动转换(Constrained)到 CLR 类型

(更复杂的来了)MoonSharp 值必须保存在一个给定类型的 CLR 变量中,转换的方法有..很多…

当:

  • When calling DynValue.ToObject
  • When converting a script value to a parameter in a CLR function call, or property set

MoonSharp attempts very hard to convert values, but the conversion surely shows some limits, specially when tables are involved.

In this case, the conversion is more a process than a simple table of mapping, so let’s analyze the target types one by one.

特殊地:

  • if the target is of type MoonSharp.Interpreter.DynValue it is not converted and the original value is returned.
  • If the target is of type System.Object, the default conversion, detailed before, is applied.
  • In case of a nil value to be mapped, null is mapped to reference types and nullable value types and an attempt to match a default value is used in some cases (for example function calls for which a default is specified) for non-nullable value types, otherwise an exception is thrown.
  • In case of no value provided, the default value is used if possible.

Strings

Strings can be automatically converted to System.String, System.Text.StringBuilder or System.Char.

Booleans

Booleans can be automatically converted to System.Boolean and/or System.Nullable<System.Boolean>. They can also be converted to System.String, System.Text.StringBuilder or System.Char.

Numbers

Numbers can be automatically converted over System.SByte, System.Byte, System.Int16, System.UInt16, System.Int32, System.UInt32, System.Int64, System.UInt64, System.Single, System.Decimal, System.Double and their nullable counterparts. They can also be converted to System.String, System.Text.StringBuilder or System.Char.

Functions

Script functions are converted to MoonSharp.Interpreter.Closure or MoonSharp.Interpreter.ScriptFunctionDelegate. Callback functions are converted to MoonSharp.Interpreter.ClrFunction or System.Func<ScriptExecutionContext, CallbackArguments, DynValue>.

Userdata

Userdatas are converted only if they are not “static” (see the userdata section in the tutorials). They are converted if the desired type can be assigned with the object to be converted.

They can also be converted to System.String, System.Text.StringBuilder or System.Char, by calling the object ToString() method.

Tables

Tables can be converted to:

  • MoonSharp.Interpreter.Table - of course
  • Types assignable from Dictionary<DynValue, DynValue>.
  • Types assignable from Dictionary<object, object>. Keys and values are mapped using the default mapping.
  • Types assignable from List.
  • Types assignable from List. Elements are mapped using the default mapping.
  • Types assignable from DynValue[].
  • Types assignable from object[]. Elements are mapped using the default mapping.
  • T[], IList, List, ICollection, IEnumerable, where T is a convertible type (including other lists, etc.)
  • IDictionary<K,V>, Dictionary<K,V>, where K and V are a convertible type (including other lists, etc.)
  • Note that conversion to generics and typed arrays have the following limitations:

    • They are slower, as types are created at runtime through reflection
    • If the item type is a value type, there is the potential that it will introduce incompatibilities with iOS and other platforms running in AOT mode. Do tests beforehand.
    • To compensate with these problems, you can always add custom converters in a later stage of development!