從 Unity 棄用兩個跟網頁相關的API之后, 就開始使用 jslib 了:
[Obsolete("Application.ExternalEval is deprecated. See https://docs.unity3d.com/Manual/webgl-interactingwithbrowserscripting.html for alternatives.")] public static void ExternalEval(string script); [Obsolete("Application.ExternalCall is deprecated. See https://docs.unity3d.com/Manual/webgl-interactingwithbrowserscripting.html for alternatives.")] public static void ExternalCall(string functionName, params object[] args);
因為我之前也沒用過 WebGL 相關的東西, 有點不明所以, 也是上一篇中提到的
WebGL 內嵌網頁的一種解決方案
從 Unity 調用 javascript 代碼為什么用的是 [DllImport("__Internal")] 的形式, 到 javascript 代碼獲取 C# 傳來的數組為什么會這么復雜, 到甚至字符串的傳遞都不是正常邏輯來說, 既然注入 JavaScript 這么繞, 那肯定是為了性能了, 看了一下編譯方案, 頻繁出現 Emscripten 這個字眼, 查了一下, 就是這個編譯器, 在編輯器文件夾下也找到了 :

Emscripten 看介紹 :
Emscripten is a toolchain for compiling to asm.js and WebAssembly, built using LLVM, that lets you run C and C++ on the web at near-native speed without plugins.
是把 C / C++ 編譯成特殊的 JavaScript 代碼 asm.js 獲得很快的運行速度, 所以工程內的代碼都會通過 IL2CPP 生成 C++ 代碼, 然后轉換成 asm.js 和 WebAssembly, 看看生成出來的項目目錄下:

我覺得這些 xxx.asm.ooo.unityweb 的東西應該就是 WebAssembly 的二進制代碼吧, 那個 xxx.data.unityweb 應該是資源包, 而 UnityLoader.js 應該就是 asm.js 的代碼吧 :


反正這些都是自動生成的, 跟我無關, 知道原理就行了, 不過對於 .jslib 文件, 就不清楚它是怎樣編譯的或者是個什么對象了......
首先它的代碼近似於 javascript, 並且是在網頁端的代碼, 可是它的數據傳輸方式又類似於二進制數據, 其實它就是 asm.js ? 我從其它地方找到一個 C++ 調用 JS 代碼的例子來看 :
#include <emscripten.h> #include <string> void Alert(const std::string & msg) { EM_ASM_ARGS({ var msg = Pointer_stringify($0); // 跟 .jslib 里的代碼幾乎一樣的 alert(msg); }, msg.c_str()); } int main() { Alert("Hello from C++!"); }
上面代碼通過 Emscripten 編譯成為 asm.js 文件, 它接受的是C++的字符串輸入, 而我們寫的 .jslib 文件是這樣的 :
HelloString: function (str) { window.alert(Pointer_stringify(str)); },
並且C#調用引用的方法通過 [DllImport("__Internal")] 來的, 猜測生成代碼的過程就是把這個方法生成了C++對應的方法, 才能這樣調用 :
--------- .jslib --------------------- var myLib = { HelloString: function (str) { window.alert(Pointer_stringify(str)); }, }; mergeInto(LibraryManager.library, myLib);
-------- 生成IL代碼? ------------------
//.........
------- 把它生成C++代碼? ---------------- #include <emscripten.h> #include <string> #include <iostream> _DLLExport void HelloString(const char* c) { std::string str = c; // 不知道對不對, 差不多這個意思 EM_ASM_ARGS({ window.alert(Pointer_stringify($0)); // 把window.alert(Pointer_stringify(str)); 改成對應的index $0 }, str.c_str()); } ------- C# 引用C++代碼 ---------------- [DllImport("__Internal")] private static extern void HelloString(string str);
搞了半天都是些沒用的, 不管它怎樣復雜, 封裝一下都是可以用的, 不過代碼調用有點奇葩:
var myLib = { ConvertStrPtr: function (str) { return (Pointer_stringify(str)); }, HelloString: function (str) { window.alert(this.ConvertStrPtr(str)); }, }; mergeInto(LibraryManager.library, myLib);
這個你在調用的時候, 會報錯
[DllImport("__Internal")] private static extern void HelloString(string str); void Start() { HelloString("This is a string."); }

不能調用其他方法, 這是會死人的, 按照之前的猜測, 走C++編譯的套路的話, this 指代的對象不明, 並且它經過的是靜態編譯, 必須要有聲明才能調用, 所以要找一個聲明的方法 :
var myLib = { $myFuncs: { ConvertStrPtr: function (str) { return (Pointer_stringify(str)); }, }, HelloString: function (str) { window.alert(myFuncs.ConvertStrPtr(str)); }, }; autoAddDeps(myLib, '$myFuncs'); mergeInto(LibraryManager.library, myLib);
$myFuncs 就是一個聲明, 雖然寫法是參照官方來的, 不過相當於聲明了一個 myFuncs 的域內 Table 吧 :
$ 是合法的 IdentifierStart 就是可以作為變量名,函數名,形參的第一個字符.
$最初在ES3時代在標准中是建議保留使用的, 保留給機器自動生成代碼使用.比如以javascript作為編譯目標語言的語言等等.
這樣調用就正確了 :

jslib 作為高效的代碼, 使用起來沒有那么方便, 始終還是希望有簡單的代碼注入方法, 然后發現自帶的lib里面已經提供了eval的入口 :

var LibraryEvalWebGL = { JS_Eval_EvalJS: function (ptr) { var str = Pointer_stringify(ptr); try { eval(str); } catch (exception) { console.error(exception); } }, }; mergeInto(LibraryManager.library, LibraryEvalWebGL);
這不就是了嗎, 調用試試 :
[DllImport("__Internal")] private static extern void JS_Eval_EvalJS(string javascript); void Start() { JS_Eval_EvalJS(@" function Test(){ alert('JS_Eval_EvalJS'); } Test();"); }

簡單輕松, 不過沒有 Call 方法, 之后自己創建一個就行了, 就跟 ZFBrowser 提供的方案一樣了. 如果是簡單的代碼, 不管效率的話就這樣用就行了...
(2020.07.16)
今天又發現個問題, 通過 eval 注冊進去的方法, 在其它 eval 中無法進行調用, 找不到函數...
try { // 這個能打印出來 JS_Eval_EvalJS(@" function Test(){ var result = ''; for(var index in arguments) { result += arguments[index]; } alert('::' + result); } Test('aa', 'bb'); "); } finally { JS_Eval_EvalJS(@"var func = eval('Test'); func('1', '23');"); // 找不到 Test Application.ExternalEval("Test('123456')"); // 找不到 Test }
這是什么回事呢? 試試打印出來全局變量看看 :
JS_Eval_EvalJS(@"console.log(this);");
這里打印出了 Window 對象, 可是並沒有 Test 函數...

然后直接在網頁添加一個函數, 看看是否能出現在這里 :

再次運行后, 有這個函數在全局列表中 :

再試試通過其它邏輯創建函數會怎么樣 :
<script>
function CallFunc(){
Test123();
console.log(window);
}
eval("window.Test123 = function(){ console.log('Hello'); }")
window.Test123();
</script>
好吧, 是不是 eval 函數被修改了? 如果是調用的地方生成了一個臨時作用域, 只在調用期間存在的話, 那就沒話說了, 再試試 :
try { // 這里指定function 到 window.Test JS_Eval_EvalJS(@" this.Test = function(){ var result = ''; for(var index in arguments) { result += arguments[index]; } alert('::' + result); }"); } finally { JS_Eval_EvalJS(@"console.log(window)"); // 打印 window JS_Eval_EvalJS(@"Test('z', 'x', 123)"); // 直接調用 Test }
結果調用成功了, 看來需要自己設定域才行 :


沒有什么問題了, 再下來就是怎樣通過 eval 調用 asm.js 里的代碼的問題了, 因為C#調用的時候需要 [DllImport("__Internal")] 的硬編碼方式, 感覺不是很自在, 雖然WebGL可能沒有什么熱更的問題, 研究一下總沒錯的, 看之前的代碼 Eval.js 里面寫了一個注冊 (在工程中的后綴 .jslib 應該是為了跟以前的 TypeScript 分開才設定的這個后綴) :
mergeInto(LibraryManager.library, LibraryEvalWebGL);
顯然這個 LibraryManager.library 就是代碼編譯的地方, 在論壇找到一個獲取該對象的方法 :
var gameInstance = UnityLoader.instantiate("gameContainer", "Build/WebGL Built.json", {onProgress: UnityProgress}); // 這里就是 jslib 編譯到的節點 gameInstance.Module.asmLibraryArg
看到里面確實有 helloworld.jslib 中的方法, 不過多了一個下划線 :

這之后又添加了一個方法 Hello 進去, 可是沒有編譯出來, 估計是C#沒有加上 DllImport 的原因, 代碼被剝離了 :
Hello: function () { window.alert('Hello World'); },

如果我們不進行強引用, 只能在代碼中去設置依賴然后讓引擎不要剝離那一段代碼, 貌似沒有編輯器下的選項...
var myLib = { $myFuncs: { ConvertStrPtr: function (str) { return (Pointer_stringify(str)); }, }, HelloString__deps: ['Hello'], HelloString: function (str) { window.alert(myFuncs.ConvertStrPtr(str)); }, Hello: function () { window.alert('Hello World'); }, }; autoAddDeps(myLib, '$myFuncs'); mergeInto(LibraryManager.library, myLib);
因為 HelloString 被強引用了, Hello必須被引用才能不被剝離, 所以讓 HelloString 添加一個依賴 Hello的設置, 加了之后Hello函數就出來了 :

在 JavaScript 這邊調用看看 :
<script>
var gameInstance = UnityLoader.instantiate("gameContainer", "Build/WebGL Built.json", {onProgress: UnityProgress});
function CallASM()
{
gameInstance.Module.asmLibraryArg._Hello();
}
</script>
// ...
<button type="button", onclick="CallASM()">CallASM</button>
正常, 基本解決調用問題了 :

再回到我們C#這邊注冊代碼的邏輯看看, 它不直接注冊到全局也有它的好處, 不會因為錯誤注冊覆蓋其他人的函數...
沒有問題之后, 自己封裝一個函數調用方案吧, 調用比較麻煩, 設計輸入變量, 返回值, 因為兩個語言之間傳遞類型只有基礎類和string, 因為幾乎所有瀏覽器都內置了Json方案, 所以對象都以Json返回字符串即可, 不過字符串也是需要轉換的 :
var myLib = { $myFuncs: { ConvertStrPtr: function (str) { return (Pointer_stringify(str)); }, StringConvert_ToUnity: function (str) { var uStr = ((typeof (str) === "string") ? str : ""); var bufferSize = lengthBytesUTF8(uStr) + 1; var buffer = _malloc(bufferSize); stringToUTF8(uStr, buffer, bufferSize); return buffer; }, }, EvalJS: function (ptr) { var str = Pointer_stringify(ptr); try { console.log("Eval : " + str); var retVal = eval(str); if (retVal != null) { var json = JSON.stringify(retVal); return myFuncs.StringConvert_ToUnity(json); } } catch (exception) { console.error(exception); } }, }; autoAddDeps(myLib, '$myFuncs'); mergeInto(LibraryManager.library, myLib);
EvalJS 就是主要的封裝編譯代碼了, 比系統自帶的復雜點, 添加了返回值.
C#這邊創建函數方面, 也是因為只有基礎類型能夠傳遞, 所以只要判斷是否數字類型即可 :
[DllImport("__Internal")] private static extern string EvalJS(string javascript); static System.Text.StringBuilder _functionMaker = new System.Text.StringBuilder(); public static string Call_JSFunc(string funcName, params object[] args) { _functionMaker.Length = 0; _functionMaker.Append(funcName); _functionMaker.Append("("); if(args != null && args.Length > 0) { for(int i = 0, imax = args.Length; i < imax; i++) { var obj = args[i]; if(i > 0) { _functionMaker.Append(","); } if(obj == null || IsNumber(obj)) { if(obj == null) { _functionMaker.Append("null"); } else { _functionMaker.Append(obj.ToString()); } } else { _functionMaker.Append("'"); _functionMaker.Append(obj.ToString()); _functionMaker.Append("'"); } } } _functionMaker.Append(");"); var callStr = _functionMaker.ToString(); Debug.Log(callStr); return EvalJS(callStr); } private static bool IsNumber(object obj) { var type = obj.GetType(); if(type == typeof(int) || type == typeof(float)) { return true; } return false; }
注冊和調用函數也修改一下, 看看返回是一個 object 的時候是否正確 :
void Start() { const string GetWindowSize_JS_Name = "GetWindowSize"; const string GetWindowSize_JS = @" this.GetWindowSize = function(){ var size = {}; size['x'] = window.screen.width; size['y'] = window.screen.height; return size; }"; try { EvalJS(GetWindowSize_JS); } finally { var val = Call_JSFunc(GetWindowSize_JS_Name); Debug.Log(val); } }
Log打出來對的 :

一些參考 :
http://www.ruanyifeng.com/blog/2017/09/asmjs_emscripten.html
https://www.ucloud.cn/yun/92400.html
一些官方信息 :
https://www.sitepoint.com/asm-js-and-webgl-for-unity-and-unreal-engine/
https://blogs.unity3d.com/2018/08/15/webassembly-is-here/
https://forum.unity.com/threads/browser-scripting-and-function-calling.477716/
