目錄
13.Coroutines(協程)
Lua中的協程
從CLR代碼中的協程
從CLR代碼中的協程作為CLR迭代器
注意事項
搶占式協程
14.Hardwire descriptors(硬編碼描述符)
為什么需要“硬編碼”
什么是“硬編碼”
如何進行硬編碼
硬編碼的優缺點
硬編碼是實現IL2CPP、AOT或iOS兼容性的必要條件嗎?
術語表
15.Sandboxing(沙盒)
為什么需要沙盒化
沙盒化檢查清單
移除“危險”的 API
16.Tips and tricks for Unity3D(Unity3D的提示和技巧)
支持的平臺
其他建議
使用更顯式的構造函數之一初始化腳本加載器
17.FAQ / Recipes
如何重定向打印函數的輸出?
如何將輸入重定向到Lua程序?
如何重定向Lua程序的IO流?
如何限制腳本在不丟失狀態的情況下執行的指令數?
MoonSharp 文檔一-CSDN博客
MoonSharp 文檔二-CSDN博客
MoonSharp 文檔三-CSDN博客
MoonSharp 文檔四-CSDN博客
13.Coroutines(協程)
來自 C# 和 Lua。
文檔地址:MoonSharp
Lua中的協程
Lua 中的協程是開箱即用的支持。實際上,只要你不故意排除協程模塊(參見沙盒化),它們就可以免費使用。有很多注意事項(這些也偶然適用于原始的 Lua 實現),并在下面的 “注意事項” 部分進行了討論。
使用任何 Lua 協程教程來操作它們。
從CLR代碼中的協程
協程可以通過腳本 CreateCoroutine 方法創建,該方法接受一個必須是函數的 DynValue。
string code = @"return function()local x = 0while true dox = x + 1coroutine.yield(x)endend";// 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);
}
從CLR代碼中的協程作為CLR迭代器
可以像調用迭代器一樣調用協程:
string code = @"return function()local x = 0while true dox = x + 1coroutine.yield(x)if (x > 5) thenreturn 7endendend";// 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 操作。
特別是在 MoonSharp 中,如果你在從 Lua 調用的 C# 函數中調用一個腳本,你不能使用 yield 來恢復到 C# 調用外部的協程。
不過有一個解決辦法:返回一個?TailCallRequest
?類型的?DynValue
。
return DynValue.NewTailCallReq(luafunction, arg1, arg2...);
還可以指定一個?continuation(延續)——這是一段函數,它將在尾調用執行完成后被調用。
在 99% 的情況下,這可能是過度設計——甚至在大多數情況下,Lua 標準庫也無法正確處理回調與 yield 的結合。但如果你計劃自己實現像 `load`、`pcall` 或 `coroutine.resume` 這樣的 API,這就是必需的。
另外,在某些邊緣情況下,MoonSharp 處理 yield 的方式與標準 Lua 不同(在我嘗試過的所有情況中,MoonSharp 的方式都更好,但誰知道呢)。例如,`tostring()` 支持在調用 `__tostring` 元方法時執行 yield 操作,而不會引發錯誤。
搶占式協程
在 MoonSharp 中,即使協程沒有調用 `coroutine.yield`,也可以將其掛起。例如,如果想要非破壞性地限制腳本占用的 CPU 時間,這可能會很有用。但為了保持腳本的一致性,有一些需要注意的地方。
讓我們從一個例子開始:
string code = @"function fib(n)if (n == 0 or n == 1) thenreturn 1;elsereturn fib(n - 1) + fib(n - 2);endend";// 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;
}// Check the values of the operation
Assert.AreEqual(DataType.Number, result.Type);
Assert.AreEqual(34, result.Number);
步驟如下:
1. 創建一個協程?
2. 將 `AutoYieldCounter` 設置為一個大于 0 的數字。1000 是一個不錯的起點,可以根據需要調整。這個數字表示在執行多少條指令后,協程會主動掛起并返回給調用者。 ?
3. 調用 `coroutine.Coroutine.Resume(...)` 并傳入適當的參數。
4. 如果上述調用的返回類型是 `DataType.YieldRequest`,則不帶參數調用 `coroutine.Coroutine.Resume()`。?
5. 重復上一步,直到返回一個真正的結果類型。?
6. 完成。??
注意事項:
1. 如果代碼重新進入(例如,`string.gsub` 的回調函數),在返回到“可掛起狀態”之前,它不會被搶占。
2. 將標準協程操作與搶占式操作混合使用是可能的,但危險且復雜。請記住,協程可以在任何地方被搶占式掛起,因此無法保證操作的原子性等。如果混合使用,請務必小心。
14.Hardwire descriptors(硬編碼描述符)
自2016年以來幫助 AOT 和 IL2CPP
文檔地址:MoonSharp
如果您不知道本頁中使用的某些術語,請參閱底部的術語表部分。
為什么需要“硬編碼”
MoonSharp 像大多數類庫一樣,使用反射,有時還會在運行時生成代碼,以便從 Lua 腳本中訪問 C# 代碼。
從根本上說,MoonSharp 已經比大多數其他腳本引擎更加溫和,原因在于:
理論上,它可以在完全不使用反射的情況下運行(盡管需要編寫大量代碼),這有助于提高速度并避免一些麻煩(如 IL2CPP 的 link.xml 問題)。
它不會在不應該生成IL代碼的平臺上生成IL代碼(如 Xamarin 的 iOS、Unity 的 IL2CPP 平臺等)。
前一點不會導致功能損失,只是速度會稍微慢一些。
這些都是優點,并且帶來了很大的兼容性,但還可以做得更多。
如果大部分反射都被移除(剩下的少量反射也無害),并且還能獲得更好的性能,那會怎樣?
聽起來好得不像真的?從某種意義上說,是的。這就是“硬編碼”的用武之地,但需要付出一些代價。
什么是“硬編碼”
這個想法很簡單。為了避免反射,目前唯一的方法是編寫大量的 C# 膠水代碼。非常多。而且是難以編寫的 C# 代碼。但這并不是必須的——我們可以稍微簡化一下編寫方式。但這仍然是一項枯燥的工作,而且很容易出錯。
然而,事實證明,這種枯燥、困難且容易出錯的工作正是自動化的理想場景。而 MoonSharp 擁有完成這項工作所需的所有信息。
所以,簡而言之,通過“硬編碼”,MoonSharp 可以生成一堆不需要進一步反射類型就能運行的 C# 代碼。
如何進行硬編碼
很簡單。嗯,幾乎很簡單。
MoonSharp 需要將已注冊的所有類型的信息導出到某個地方。為此,需要編寫一小段臨時代碼:
Table dump = UserData.GetDescriptionOfRegisteredTypes(true);
File.WriteAllText(@"c:\temp\testdump.lua", dump.Serialize());
重要的是,上述代碼必須在所有類型都已注冊之后運行。因此,硬編碼的第一條規則是確保所有注冊的順序正確。
現在,可以使用 MoonSharp 的 REPL 解釋器生成代碼:
moonsharp -W c:\temp\testdump.lua c:\temp\out.cs --internals --class:MyClass --namespace:MyNamespace
此時,生成過程可能會輸出大量警告/錯誤。它們會顯示在控制臺輸出中,并作為代碼中的注釋。即使存在警告/錯誤,生成的代碼仍然可以工作,但請花時間閱讀這些警告/錯誤,因為如果你的腳本代碼引用了觸發這些警告/錯誤的成員,行為可能會與預期不同。
如果你沒有 Windows 電腦,你可以輕松地使用 Mono 在 Mac 或 Linux 上運行 MoonSharp 解釋器!
生成的代碼將包含一個單一的公共靜態類,其中有一個公共的靜態 `Initialize` 方法,你應該在使用 MoonSharp 進行任何其他操作之前調用這個方法。非常簡單。
如果你注冊了一個系統類型(例如,數組),在硬編碼時可能會遇到一些問題。原因是硬編碼將使用生成 “dump” 時運行時的類型定義,如果你更改了 .NET 版本或平臺,運行時可能會有所不同。雖然這看起來像是一個邊緣情況,但在 Unity 的 Windows Phone 和 Windows Store 應用中會發生這種情況。在最壞的情況下,可以在生成的代碼周圍加上條件編譯指令。
硬編碼的優缺點
優點:
???大幅減少反射的使用
???在元數據被剝離的情況下(如IL2CPP),無需白名單類型
???無需運行時代碼生成
???顯著提升性能,尤其是在不支持運行時代碼生成的平臺上
缺點:
???需要在工作流程中加入代碼生成步驟
???代碼生成僅支持 C# 或 VB.NET
???暴露的類型和成員必須對其他類可訪問——簡而言之,必須是`public`的,或者在同一程序集中為`internal`
???某些類型的成員可能不受支持(例如事件,至少在當前版本中)??
???值類型的設置器可能無法正常工作——或者比平時更糟,盡量避免使用它們
???硬編碼系統目前涵蓋了所有通過 `UserDataType` 注冊的類型,而不傳遞用戶數據描述符。除了自定義描述符(顯然需要你自己處理)之外,這意味著與 `PropertyTableAssigner` 和擴展方法一起使用的類型不包括在內。通常這問題不大,因為它們往往用于 IL2CPP 啟發式算法可見的類型,未來可能會實現支持。
硬編碼是實現IL2CPP、AOT或iOS兼容性的必要條件嗎?
AOT 兼容性通過 `PlatformAccessor` 管理。實際上,唯一的行為變化是在 AOT 平臺上不執行運行時代碼生成,而是使用純反射。
對于 IL2CPP 兼容性,硬編碼簡化了這一過程——通過消除為不應剝離的元數據維護 `link.xml` 文件的需求——并使其更高效,因為反射速度較慢。但你仍然可以通過向 `link.xml` 添加條目來實現兼容性。
術語表
反射(Reflection): ?
一組允許代碼“檢查”自身并最終調用方法、讀取和寫入字段及屬性等的類型和方法。雖然功能強大,但速度較慢。詳見 MSDN 文檔。
運行時代碼生成(Runtime code generation): ?
在運行時生成代碼以優化通過反射完成的操作的做法。MoonSharp 在某些地方使用了這一技術,但在 AOT 平臺上會被禁用。
IL(Intermediate Language,中間語言): ?
.NET/mono 程序集中的代碼以 IL(中間語言字節碼)形式存儲。這種字節碼不能直接執行,必須在執行前轉換為本地指令。
JIT(Just-In-Time,即時編譯): ?
通常,當 .NET 或 mono 加載程序集時,它們會對IL代碼進行即時編譯,將其轉換為本地代碼。這也可能在程序集的生命周期后期發生——例如,如果泛型類型的某個類型參數是值類型,通常會即時重新編譯。
AOT(Ahead-of-Time,預先編譯): ?
mono 提供的一種選項(某種程度上 ngen 和 .NET Native 也提供,但與此討論無關,至少目前如此),可以提前將代碼編譯為本地代碼或其他形式。這并不簡單,可能無法編譯所有需要的代碼。例如,如果使用反射來實例化代碼中從未引用的類型,則該特定代碼可能未被編譯。在 iOS 設備上運行需要 AOT 執行。
IL2CPP: ?
一種將 IL 轉換為 C++ 源代碼的出色軟件。它是那種極難編寫但大多數人談論它時只是為了抱怨的軟件。說真的,它試圖解決的問題非常復雜,需要一些合作來確保兼容性。特別是,除了所有 AOT 問題(IL2CPP 必須使用 AOT)外,IL2CPP 還會對程序集元數據進行選擇性剝離,這可能會干擾反射。在?Unity3D 下,IL2CPP 是 iOS、tvOS 和 WebGL 的強制要求,并在更多平臺上作為選項提供。
link.xml: ?
一個告訴 IL2CPP 應保留哪些類型的元數據的文件。
值類型(Value-type): ?
值類型是按值傳遞而不是按引用傳遞的類型。它包括數值基元類型、枚舉和結構體。當談論值類型問題時,通常指的是結構體。詳見 MSDN 上的結構體文檔。最佳實踐建議值類型應為不可變的,即它們的字段和屬性應為只讀的,如果需要更改它們,應創建一個新對象(對于結構體來說,這非常廉價)。MoonSharp 與可變結構體的兼容性不佳,因為它們是一種與許多事物都不兼容的奇怪存在,因此請不要使用它們。如果必須使用,請考慮使用代理對象進行變通。
15.Sandboxing(沙盒)
限制 Lua 腳本的功能
文檔地址:MoonSharp
為什么需要沙盒化
沙盒化很可能是你使用 MoonSharp 時的關鍵功能。除非你以某種方式控制腳本提供者(可能還包括用戶),否則在運行時加載腳本(或任何類型的代碼,有時甚至是數據)時存在一個基本的信任問題:安全性。
例如,假設你正在編寫一款視頻游戲,并使用 Lua 使游戲“可修改”。用戶可以訪問某個社區網站并下載包含新關卡、AI等的模組,這些模組都使用 MoonSharp 編寫以實現最大靈活性。大家都很開心。現在想象一下,如果某個惡意用戶上傳了一些隱藏在漂亮關卡中的惡意腳本,會發生什么……比如一個在桌面上創建 .exe 文件的腳本,其他用戶可能會不小心點擊它。
除非你信任用戶,或者明顯可以防止惡意使用,否則你不想向普通用戶公開某些 API(如`os`、`io`和`file`)。
沙盒化檢查清單
這份清單可以幫助你入門,但并不全面。
1. 確保腳本無法訪問所有“危險”的標準 API。這肯定包括`io`、`os`和`file`,但根據你的腳本和偏執程度,可能還包括更多。請參閱下一章,了解如何輕松地使整個API集不可訪問。
2. 不要使用 `InteropRegistrationPolicy.Automatic`。永遠不要。
3. 檢查你是否向腳本暴露了不應暴露的類型或類型成員。避免直接暴露你不擁有的類型,如 .NET 框架類型或 Unity 類型。如果需要,請參閱關于“代理對象”的部分,以及如何使用 `MoonSharpHide` 和 `MoonSharpHidden`。
4. 如果你希望腳本的某些部分訪問這些危險的 API,請將它們放在單獨的 `Script` 對象中,并確保其他腳本無法訪問這些對象。
5. 如果絕對必須僅對腳本的一部分進行“沙盒化”,請參考這份經典指南。
移除“危險”的 API
移除危險 API 的最簡單方法是使用接受 `CoreModules` 枚舉的 `Script` 構造函數。
`CoreModules` 枚舉的含義非常直觀:
/// <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,}
16.Tips and tricks for Unity3D(Unity3D的提示和技巧)
為在 Unity 上使用 MoonSharp 的用戶提供的快速提示集
文檔地址:MoonSharp
支持的平臺
一般來說,MoonSharp 的目標是支持所有平臺。然而,在 Unity 中測試 MoonSharp 在所有平臺上的表現是非常困難的,幾乎是不可能的。
因此,平臺被分為不同的等級。
1.預計完全支持獨立的 Windows、Linux 和 OS/X * 安卓 * iOS * tvOS
2.預計零星支持,將在可能的情況下測試小問題/不支持的功能 WebGL * Windows 應用商店應用程序 Windows Phone
3.應該有效,但我很難提供支持其他層沒有的東西
4.不支持,您只能自己使用目前尚不清楚,將進行更新以防
其他建議
???如果可能,不要暴露 Unity 類型 ?
???盡可能使用代理對象 ?
???在運行于 IL2CPP 和/或 AOT 平臺之前,使用硬編碼 ?
???如果可能,永遠不要暴露結構體 ?
使用更顯式的構造函數之一初始化腳本加載器
使用 UnityAssetsScriptLoader 的顯式構造函數來注冊所有腳本文件。通過在項目的最開始使用這個小片段,您不需要將 Unity 自帶的庫添加到 link.xml 中,就可以在 IL2CPP 平臺上運行。
示例:
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);
17.FAQ / Recipes
常見問題的簡單示例
文檔地址:MoonSharp
如何重定向打印函數的輸出?
script.Options.DebugPrint = s => { Console.WriteLine(s); }
如何將輸入重定向到Lua程序?
script.Options.DebugInput = () => { return Console.ReadLine(); }
如何重定向Lua程序的IO流?
IoModule.SetDefaultFile(script, Platforms.StandardFileType.StdIn, myInputStream);
IoModule.SetDefaultFile(script, Platforms.StandardFileType.StdOut, myOutputStream);
IoModule.SetDefaultFile(script, Platforms.StandardFileType.StdErr, myErrorStream);
如何限制腳本在不丟失狀態的情況下執行的指令數?
參考?Preemptive coroutines.
end