WebGL 使用 jslib 相关

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

原文地址:https://www.cnblogs.com/tiancaiwrk/p/13300339.html