同步虛擬 XHR 後端檔案系統使用方式

Emscripten 支援使用 XHR 從 HTTP 伺服器延遲載入二進位資料。此功能可用於建立從已編譯程式碼同步存取檔案的後端。

此後端可以縮短啟動時間,因為在執行已編譯程式碼之前,不需要預先載入整個檔案系統。如果網路伺服器支援位元組服務,它也可以非常有效率 — 在這種情況下,Emscripten 可以僅讀取實際需要的檔案部分。

警告

此機制僅在Web Worker 中可行(由於瀏覽器限制)。

注意

如果網路伺服器不支援位元組服務,即使僅讀取單一位元組,Emscripten 也必須載入整個檔案(無論多大)。

測試程式碼

test/test_browser.py 的測試程式碼中提供如何實作同步虛擬 XHR 後端檔案系統的範例(請參閱 test_chunked_synchronous_xhr)。測試案例還包含一個 HTTP 伺服器(請參閱 test_chunked_synchronous_xhr_server),顯示可能需要設定的 CORS 標頭(如果資源是從 Emscripten 執行的相同網域託管,則沒有問題)。

測試使用 checksummer.c 作為 Emscripten 編譯的程式。這只是一個使用同步 *libc* 檔案系統呼叫(例如 fopen()fread()fclose() 等)的原始 C 程式。

加入 JavaScript 程式碼(使用 *emcc* 的 pre-js 選項)將 checksummer.c 中的檔案系統呼叫對應到虛擬檔案系統中的檔案。此檔案是在 Emscripten 初始化早期使用 FS.createLazyFile() *建立* 的,但只有在編譯程式碼首次存取該檔案時,才會從伺服器載入內容。加入的 JavaScript 程式碼也設定了 Web Worker 和主執行緒之間的通訊。

說明

  1. 您需要將 JavaScript 加入到產生的程式碼中,以將已編譯的本機程式碼存取的檔案對應到伺服器。

    測試程式碼僅使用 FS.createLazyFile() 在虛擬檔案系統中建立一個檔案,並設定已編譯程式碼使用相同的檔案(/bigfile

    , r"""
          Module.arguments = ["/bigfile"];
          Module.preInit = () => {
            FS.createLazyFile('/', "bigfile", "https://127.0.0.1:11111/bogus_file_path", true, false);
          };
          
    

    注意

    • (在本例中)已編譯的測試程式碼從命令列引數中取得檔案名稱 — 這些引數是使用 Emscripten 中的 Module.arguments 設定的。

    • 建立檔案的呼叫會加入到 Module.preInit 中。這可確保它在任何已編譯程式碼之前執行。

    • 其他 JavaScript 是使用 *emcc* 的 prejs 選項加入的。

  2. 加入的 JavaScript 還應包含允許 Web Worker 與原始執行緒通訊的程式碼。

    測試程式碼為此目的將以下 JavaScript 加入到 Web Worker 中。它使用 postMessage() 將其 stdout 送回主執行緒。

    
          Module.print = (s) => self.postMessage({channel: "stdout", line: s});
          Module.printErr = (s) => { self.postMessage({channel: "stderr", char: s, trace: ((doTrace && s === 10) ? new Error().stack : null)}); doTrace = false; };
        
    

    注意

    如果您使用上述解決方案,父頁面可能應包含手寫的 glue code 來處理 stdout 資料。

  3. 您將需要一個產生 Web Worker 的頁面。

    執行此操作的測試程式碼如下所示

     '''
          <html>
          <body>
            Worker Test
            <script>
              var worker = new Worker('worker.js');
              worker.onmessage = async (event) => {
                await fetch('https://127.0.0.1:%s/report_result?' + event.data);
                window.close();
              };
            </script>
          </body>
          </html>
        ''' % self.port)
    
        for file_data in (1, 0):
          cmd = [EMCC, test_file('hello_world_worker.cpp'), '-o', 'worker.js'] + self.get_emcc_args()
          if file_data:
            cmd += ['--preload-file', 'file.dat']
          self.run_process(cmd)
          self.assertExists('worker.js')
          self.run_browser('main.html', '/report_result?hello from worker, and :' + ('data for w' if file_data else '') + ':')
    
        # code should run standalone too
        # To great memories >4gb we need the canary version of node
        if self.is_4gb():
          self.require_node_canary()
        self.assertContained('you should not see this text when in a worker!', self.run_js('worker.js'))
    
      @no_wasmfs('https://github.com/emscripten-core/emscripten/issues/19608')
      def test_mmap_lazyfile(self):
        create_file('lazydata.dat', 'hello world')
        create_file('pre.js', '''
          Module["preInit"] = () => {
            FS.createLazyFile('/', "lazy.txt", "lazydata.dat", true, false);
          }
        ''')
        self.emcc_args += ['--pre-js=pre.js', '--proxy-to-worker']
        self.btest_exit('test_mmap_lazyfile.c')
    
      @no_wasmfs('https://github.com/emscripten-core/emscripten/issues/19608')
      @no_firefox('keeps sending OPTIONS requests, and eventually errors')
      def test_chunked_synchronous_xhr(self):
        main = 'chunked_sync_xhr.html'
        worker_filename = "download_and_checksum_worker.js"
    
        create_file(main, r"""
          <!doctype html>
          <html>
          <head><meta charset="utf-8"><title>Chunked XHR</title></head>
          <body>
            Chunked XHR Web Worker Test
            <script>
              var worker = new Worker("%s");
              var buffer = [];
              worker.onmessage = async (event) => {
                if (event.data.channel === "stdout") {
                  await fetch('https://127.0.0.1:%s/report_result?' + event.data.line);
                  window.close();
                } else {
                  if (event.data.trace) event.data.trace.split("\n").map(function(v) { console.error(v); });
                  if (event.data.line) {
                    console.error(event.data.line);
                  } else {
                    var v = event.data.char;
                    if (v == 10) {
                      var line = buffer.splice(0);
                      console.error(line = line.map(function(charCode){return String.fromCharCode(charCode);}).join(''));
                    } else {
                      buffer.push(v);
                    }
                  }
                }
              };
            </script>
          </body>
          </html>