从 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/