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

  • 相关阅读:
    个人总结---小水长流,则能穿石
    软件工程与UML作业3(互评作业)
    软件工程与UML作业2
    软件工程与UML作业1
    大创省级答辩总结
    C语言知识汇编
    C语言知识点汇集
    C语言汇总3
    C语言汇总2
    c语言汇总1
  • 原文地址:https://www.cnblogs.com/tiancaiwrk/p/13300339.html
Copyright © 2020-2023  润新知