MoonSharp 从一知到半解(5)

0x00

官网的教程

请配合我的代码食用Github

一知半解系列最后一篇,撒花。

ps:零散的知识点 - Asset 使用。

0x01 Coroutines

学习过 Lua 的语法的小伙伴们都知道,在 Lua 中原生提供了协程,开箱即用。除非你在“sandbox”模式下故意移除 coroutines 模块……

Usages

CLR 代码创建协程,可以通过 script 对象的 CreateCoroutine 方法创建,参数只需要为 function 的 DynValue。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
string code = @"
return function()
local x = 0
while true do
x = x + 1
coroutine.yield(x)
end
end
";

// Load the code and get the returned function
Script script = new Script();
DynValue function = script.DoString(code);

// Create the coroutine in C#
DynValue coroutine = script.CreateCoroutine(function);

// Resume the coroutine forever and ever..
while (true)
{
DynValue x = coroutine.Coroutine.Resume();
Console.WriteLine("{0}", x);
}

平时的我会说,我看的懂这个程序,我给大家理理,但是现在的我矜持,你肯定看得懂这是一个怎样的流程:从 string 脚本中载入了一个协程函数,之后,在 C# 中创建这个协程,并在循环中打印返回的数字……所以我就不说了~

也可以写像迭代器一样调用协程:

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
string code = @"
return function()
local x = 0
while true do
x = x + 1
coroutine.yield(x)
if (x > 5) then
return 7
end
end
end
";

// Load the code and get the returned function
Script script = new Script();
DynValue function = script.DoString(code);

// Create the coroutine in C#
DynValue coroutine = script.CreateCoroutine(function);

// Loop the coroutine
string ret = "";

foreach (DynValue x in coroutine.Coroutine.AsTypedEnumerable())
{
ret = ret + x.ToString();
}

Assert.AreEqual("1234567", ret);

这里在得到 lua 中带有 yield 返回的方法后,以迭代器的方式调用。

这里最后一行的断言代码,是使用了 Microsoft 的 MSTEst.TestFramework 框架,来检测最终结果,如果不符将会抛出异常,使用的方法:

1
PM> Install-Package MSTest.TestFramework

安装包,即可使用(好敷衍……),文档

当然也可以使用 Debug.Asset…参数是条件和失败消息。

Caveats

原生 Lua 中,协程不能产生嵌套调用。

如果在 Lua 中调用了一个 C# 函数,而在这个 C# 函数中又调用脚本,这时就不能使用 yield 恢复到这个 C# 调用外部的协程(等会……Emmm)。

有一个解决方法是返回 TailCallRequest DynValue

1
return DynValue.NewTailCallReq(luafunction, arg1, arg2...);

这只在你需要自己实现 load,pcall 或者 coroutine.resume 等 API 时需要注意。

在 MoonSharp 中,即使没有调用 coroutine.yeild 也可以暂停协程(如果要限制脚本的 CPU 时间,又不破坏脚本执行),栗子环节:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
public static void PreemptiveCoroutines()
{
string code = @"
function fib(n)
if (n == 0 or n == 1) then
return 1;
else
return fib(n - 1) + fib(n - 2);
end
end
";

// Load the code and get the returned function
Script script = new Script(CoreModules.None);
script.DoString(code);

// get the function
DynValue function = script.Globals.Get("fib");

// Create the coroutine in C#
DynValue coroutine = script.CreateCoroutine(function);

// Set the automatic yield counter every 10 instructions.
// 10 is likely too small! Use a much bigger value in your code to avoid interrupting too often!
coroutine.Coroutine.AutoYieldCounter = 10;

int cycles = 0;
DynValue result = null;

// Cycle until we get that the coroutine has returned something useful and not an automatic yield..
for (result = coroutine.Coroutine.Resume(8);
result.Type == DataType.YieldRequest;
result = coroutine.Coroutine.Resume())
{
cycles += 1;
}
Console.WriteLine(cycles);
Console.WriteLine(result.Number);
// Check the values of the operation
Assert.AreEqual(DataType.Number, result.Type);
Assert.AreEqual(34, result.Number);
Console.ReadKey();
}

这里使用没有 yield 指令的函数创建了一个协程,通过参数 AutoYieldCounter 设置一个大于 0 的数(官推 1000 开调),指的是执行的指令数量。这里使用一个 for 循环判断返回值是否为我们需要的,调用 coroutine.Coroutine.Resume(…) 或 coroutine.Coroutine.Resume() 方法继续执行协程,直到得到真正的返回值。

0x02 hardwire descriptors

就是通过一些手段避免使用反射。

0x03 Sandboxing

用来限制 Lua 脚本能做的事情。如果在运行时加载脚本,就存在安全性问题,而沙盒(sandboxing)可以理解为对 Lua 可以使用的功能的剪裁,比如作为 mod 语言,开发者应该不想让用户可以访问“os”、“io”或是“file”模块。

像上面说的“os”、“io”以及“file”模块是危险的,当然根据代码的不同也可能包含更多。

一条中要的建议是永远不要使用 InteropRegistrationPolicy.Automatic。

对不应暴露的类型或成员进行检查,避免直接暴露 .NET 框架类型或是 Unity 类型,如果需要应该使用 Proxy objects。

删除危险 API 的方法,是使用接受 CoreModules 枚举的 Script 构造函数,CoreModules 的例子放在附录中。

0x04 Unity3d tips & tricks

支持平台:旨在支持全部平台,但……

  • 全面支持的:Standalone Windows, Linux and OS/X, Android, iOS, tvOS
  • 会尽量支持的(可能会有小问题,不支持的功能):WebGL, Windows Store Applications, Windows Phone

另外,不要暴露 Unity 类型,如果需要请用 Proxy Objects;

在使用 IL2CPP 或在 AOT 平台上运行时使用 hardwire;

不要暴露 struct。

最后,使用一个更明确的构造函数初始化脚本加载器,比如 UnityAssetsScriptLoader 的显式构造函数来注册所有脚本文件。通过在项目的最开始执行,就不需要将 Unity 自己的库添加到 link.xml 以在 IL2CPP 平台上运行。

1
2
3
4
5
6
7
8
9
10
Dictionary<string, string> scripts = new  Dictionary<string, string>();

object[] result = Resources.LoadAll("Lua", typeof(TextAsset));

foreach(TextAsset ta in result.OfType<TextAsset>())
{
scripts.Add(ta.name, ta.text);
}

Script.DefaultOptions.ScriptLoader = new MoonSharp.Interpreter.Loaders.UnityAssetsScriptLoader(scripts);


0xff

附录I

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/// <summary>
/// Enumeration (combinable as flags) of all the standard library modules
/// </summary>
[Flags]
public enum CoreModules
{
/// <summary>
/// Value used to specify no modules to be loaded (equals 0).
/// </summary>
None = 0,

/// <summary>
/// The basic methods. Includes "assert", "collectgarbage", "error", "print", "select", "type", "tonumber" and "tostring".
/// </summary>
Basic = 0x40,
/// <summary>
/// The global constants: "_G", "_VERSION" and "_MOONSHARP".
/// </summary>
GlobalConsts = 0x1,
/// <summary>
/// The table iterators: "next", "ipairs" and "pairs".
/// </summary>
TableIterators = 0x2,
/// <summary>
/// The metatable methods : "setmetatable", "getmetatable", "rawset", "rawget", "rawequal" and "rawlen".
/// </summary>
Metatables = 0x4,
/// <summary>
/// The string package
/// </summary>
String = 0x8,
/// <summary>
/// The load methods: "load", "loadsafe", "loadfile", "loadfilesafe", "dofile" and "require"
/// </summary>
LoadMethods = 0x10,
/// <summary>
/// The table package
/// </summary>
Table = 0x20,
/// <summary>
/// The error handling methods: "pcall" and "xpcall"
/// </summary>
ErrorHandling = 0x80,
/// <summary>
/// The math package
/// </summary>
Math = 0x100,
/// <summary>
/// The coroutine package
/// </summary>
Coroutine = 0x200,
/// <summary>
/// The bit32 package
/// </summary>
Bit32 = 0x400,
/// <summary>
/// The time methods of the "os" package: "clock", "difftime", "date" and "time"
/// </summary>
OS_Time = 0x800,
/// <summary>
/// The methods of "os" package excluding those listed for OS_Time. These are not supported under Unity.
/// </summary>
OS_System = 0x1000,
/// <summary>
/// The methods of "io" and "file" packages. These are not supported under Unity.
/// </summary>
IO = 0x2000,
/// <summary>
/// The "debug" package (it has limited support)
/// </summary>
Debug = 0x4000,
/// <summary>
/// The "dynamic" package (introduced by MoonSharp).
/// </summary>
Dynamic = 0x8000,


/// <summary>
/// A sort of "hard" sandbox preset, including string, math, table, bit32 packages, constants and table iterators.
/// </summary>
Preset_HardSandbox = GlobalConsts | TableIterators | String | Table | Basic | Math Bit32,
/// <summary>
/// A softer sandbox preset, adding metatables support, error handling, coroutine, time functions and dynamic evaluations.
/// </summary>
Preset_SoftSandbox = Preset_HardSandbox | Metatables | ErrorHandling | Coroutine | OS_Time | Dynamic,
/// <summary>
/// The default preset. Includes everything except "debug" as now.
/// Beware that using this preset allows scripts unlimited access to the system.
/// </summary>
Preset_Default = Preset_SoftSandbox | LoadMethods | OS_System | IO,
/// <summary>
/// The complete package.
/// Beware that using this preset allows scripts unlimited access to the system.
/// </summary>
Preset_Complete = Preset_Default | Debug,

}

附录II

  • How can I redirect the output of the print function ?
    script.Options.DebugPrint = s => { Console.WriteLine(s); }
  • How can I redirect the input to the Lua program ?
    script.Options.DebugInput = () => { return Console.ReadLine(); }
  • How can I redirect the IO streams of a Lua program ?
    IoModule.SetDefaultFile(script, Platforms.StandardFileType.StdIn, myInputStream);
    IoModule.SetDefaultFile(script, Platforms.StandardFileType.StdOut, myOutputStream);
    IoModule.SetDefaultFile(script, Platforms.StandardFileType.StdErr, myErrorStream);