Emscripten 執行時環境

Emscripten 執行時環境與大多數 C/C++ 應用程式所預期的不同。Emscripten 努力抽象化並減輕這些差異,因此通常可以在幾乎沒有或沒有變更的情況下編譯程式碼。

本文將擴展一些差異以及由此產生的 API 限制,並概述您可能需要對 C/C++ 程式碼進行的一些變更。

輸入/輸出

Emscripten 為瀏覽器環境實作了簡單直接媒體層 API (SDL),它提供對音訊、鍵盤、滑鼠、搖桿和圖形硬體的低階存取。使用 SDL 的應用程式通常不需要進行任何輸入/輸出變更即可在瀏覽器中執行。

此外,我們對 glutglfwglewxlib 的支援更有限。

不使用 SDL 或其他 API 的應用程式可以使用 Emscripten 特定的 API 進行輸入和輸出

  • html5.h,它定義了 Emscripten 低階連結綁定,以從原生程式碼與 HTML5 事件互動,包括存取按鍵、滑鼠、滾輪、裝置方向、電池電量、震動等。

  • 多媒體和圖形 API,包括 OpenGLEGL

檔案系統

許多 C/C++ 程式碼使用 libclibcxx 中的同步檔案系統 API 來存取本機檔案系統中的程式碼。這是有問題的,因為瀏覽器會阻止程式碼直接存取主機系統上的檔案,而且因為 JavaScript 只支援 Web Worker 之外的非同步檔案存取。

Emscripten 提供了 libclibcxx 的實作以及虛擬檔案系統,以便可以編譯和執行正常的 C/C++ 程式碼而無需變更。大多數開發人員只需要指定一組要 封裝 的檔案,以便在執行時預先載入到虛擬檔案系統中。

注意

使用虛擬檔案系統可繞過上面列出的限制。檔案資料在編譯時封裝,並在使用 非同步 JavaScript API 允許編譯程式碼執行之前下載到檔案系統中。然後,編譯程式碼會發出「檔案」呼叫,這些呼叫實際上只是對程式記憶體的呼叫。

預設檔案系統 (MEMFS) 將檔案儲存在記憶體中,因此當頁面重新載入時,任何變更都會遺失。如果檔案變更需要更永久地儲存,則開發人員可以掛載 IDBFS 檔案系統,這允許資料在瀏覽器中持續存在。在 node.js 中執行程式碼時,開發人員可以掛載 NODEFS,以便讓程式碼直接存取本機檔案系統。

Emscripten 也有一個 API 來支援 非同步檔案存取

如需更多資訊和範例,請參閱 檔案和檔案系統

瀏覽器主迴圈

瀏覽器事件模型使用合作多工處理 — 每個事件都有一個「回合」來執行,然後必須將控制權返回給瀏覽器,以便可以處理其他事件。HTML 頁面掛起的一個常見原因是 JavaScript 沒有完成並將控制權返回給瀏覽器。

圖形 C++ 應用程式通常在無限迴圈中執行。在迴圈的每次迭代中,應用程式都會執行事件處理、處理和呈現,然後延遲(「等待」)以保持幀速率恆定。這個無限迴圈在瀏覽器環境中是個問題,因為控制權無法返回給瀏覽器以便執行其他程式碼。一段時間後,瀏覽器會通知使用者頁面已卡住,並提供停止或關閉頁面的選項。

同樣地,只有在目前「回合」結束時才能執行像 WebGL 這樣的 JavaScript API,並且會自動在該點呈現和交換緩衝區。這與需要手動交換緩衝區的 OpenGL C++ 應用程式形成對比。

在 C/C++ 中實作非同步主迴圈

此問題的標準解決方案是定義一個 C 函數,該函數執行主迴圈的一次迭代(不包括「延遲」)。對於原生建置,可以在無限迴圈中呼叫此函數,讓行為保持有效不變。

在 Emscripten 編譯程式碼中,我們使用 emscripten_request_animation_frame_loop() 讓環境以正確的頻率呼叫此相同函數以呈現幀(也就是說,如果瀏覽器以 60fps 呈現,則它將每秒呼叫此函數 60 次)。迭代仍然「無限」執行,但現在其他程式碼可以在迭代之間執行,而且瀏覽器不會掛起。

通常,您會有一小段程式碼包含 #ifdef __EMSCRIPTEN__ 以處理這兩種情況。例如

#include <emscripten.h>
#include <emscripten/html5.h>
#include <stdio.h>

// Our "main loop" function. This callback receives the current time as
// reported by the browser, and the user data we provide in the call to
// emscripten_request_animation_frame_loop().
bool one_iter(double time, void* userData) {
  // Can render to the screen here, etc.
  puts("one iteration");
  // Return true to keep the loop running.
  return true;
}

int main() {
#ifdef __EMSCRIPTEN__
  // Receives a function to call and some user data to provide it.
  emscripten_request_animation_frame_loop(one_iter, 0);
#else
  while (1) {
    one_iter();
    // Delay to keep frame rate constant (using SDL).
    SDL_Delay(time_to_next_frame());
  }
#endif
}

注意

emscripten_set_main_loop() 中提供功能更完整 的 API,它讓您可以指定呼叫該函數的頻率以及其他事項。

注意

使用 SDL 時,您通常需要設定主迴圈,除非您

只是呈現單一幀並停止。您也應該注意

  • 如果您使用 emscripten_set_main_loop(),目前 Emscripten 的 SDL_QUIT 實作將會正常運作。當頁面關閉時,它會強制直接最後呼叫主迴圈,使其有機會注意到 SDL_QUIT 事件。如果您不使用主迴圈,您的應用程式將在您有機會注意到此事件之前關閉。

  • 當頁面關閉時(在 onunload 中),您可以執行的動作有其限制。某些動作(例如顯示警示)在此時會被瀏覽器禁止。

使用 Asyncify 產生給瀏覽器

另一個選項是使用 Asyncify,它將重新編寫程式,以便它可以透過僅呼叫 emscripten_sleep() 返回到瀏覽器的主事件迴圈。請注意,這種重新編寫會導致大小和速度的額外負擔,而如前所述的 emscripten_request_animation_frame_loop / emscripten_set_main_loop 則不會。

執行生命週期

當載入使用 Emscripten 編譯的應用程式時,它會先在 preloading(預載入)階段準備資料。您標記為預載入的檔案(使用 emcc --preload-file,或從 JavaScript 手動使用 FS.createPreloadedFile())會在該階段設定。

您可以使用 addRunDependency() 添加額外操作,這是一個計數器,用於計算編譯程式碼可以執行前需要完成的所有依賴項。當這些依賴項完成後,您可以呼叫 removeRunDependency() 來移除已完成的依賴項。

注意

一般而言,不需要添加額外操作 — 預載入適用於幾乎所有使用案例。

當所有依賴項都滿足時,Emscripten 將呼叫 run(),然後繼續呼叫您的 main() 函數。main() 函數應該用於執行初始化任務,並且通常會呼叫 emscripten_set_main_loop()(如上述說明)。接著,主迴圈函數會以要求的頻率被呼叫。

您可以使用幾種方式影響主迴圈的操作。

  • emscripten_push_main_loop_blocker() 會添加一個函數,該函數會阻塞主迴圈,直到該阻塞器完成。

    舉例來說,這對於管理載入新的遊戲關卡很有用。在關卡完成後,您可以為每個相關的操作推送阻塞器(解壓縮檔案、產生資料結構等)。當所有阻塞器都完成後,主迴圈將恢復,並且遊戲應執行新的關卡。您也可以將此函數與 emscripten_set_main_loop_expected_blockers() 一起使用,以讓使用者了解進度。

  • emscripten_pause_main_loop() 會暫停主迴圈,而 emscripten_resume_main_loop() 會恢復它。這些是阻塞器函數的低階(較不推薦的)替代方案。

  • emscripten_async_call() 可讓您在特定間隔後呼叫函數。這將使用 requestAnimationFrame(預設情況下),如果要求了特定的間隔,則使用 setTimeout

瀏覽器執行環境參考 (emscripten.h) 描述了許多其他用於控制執行的函數。

Emscripten 記憶體表示法

在 asm.js 和 WebAssembly 中,Emscripten 都以類似於原生架構的方式表示記憶體。指標表示記憶體中的偏移量,結構使用與正常情況下相同的位址空間,依此類推。

在 WebAssembly 中,這是透過使用為此目的設計的 WebAssembly.Memory 完成的。在 asm.js 中,Emscripten 使用單個 類型陣列,不同的檢視提供對不同類型(HEAPU32 用於 32 位元無符號整數等等)的存取。

Emscripten 過去曾嘗試過其他記憶體表示法,最終採用了用於 JS 和 asm.js 的「類型陣列模式 2」方法,如上所述,然後 WebAssembly 實作了類似的東西。