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/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM