Emscripten 執行時環境與大多數 C/C++ 應用程式所預期的不同。Emscripten 努力抽象化並減輕這些差異,因此通常可以在幾乎沒有或沒有變更的情況下編譯程式碼。
本文將擴展一些差異以及由此產生的 API 限制,並概述您可能需要對 C/C++ 程式碼進行的一些變更。
Emscripten 為瀏覽器環境實作了簡單直接媒體層 API (SDL),它提供對音訊、鍵盤、滑鼠、搖桿和圖形硬體的低階存取。使用 SDL 的應用程式通常不需要進行任何輸入/輸出變更即可在瀏覽器中執行。
此外,我們對 glut、glfw、glew 和 xlib 的支援更有限。
不使用 SDL 或其他 API 的應用程式可以使用 Emscripten 特定的 API 進行輸入和輸出
許多 C/C++ 程式碼使用 libc 和 libcxx 中的同步檔案系統 API 來存取本機檔案系統中的程式碼。這是有問題的,因為瀏覽器會阻止程式碼直接存取主機系統上的檔案,而且因為 JavaScript 只支援 Web Worker 之外的非同步檔案存取。
Emscripten 提供了 libc 和 libcxx 的實作以及虛擬檔案系統,以便可以編譯和執行正常的 C/C++ 程式碼而無需變更。大多數開發人員只需要指定一組要 封裝 的檔案,以便在執行時預先載入到虛擬檔案系統中。
注意
使用虛擬檔案系統可繞過上面列出的限制。檔案資料在編譯時封裝,並在使用 非同步 JavaScript API 允許編譯程式碼執行之前下載到檔案系統中。然後,編譯程式碼會發出「檔案」呼叫,這些呼叫實際上只是對程式記憶體的呼叫。
預設檔案系統 (MEMFS) 將檔案儲存在記憶體中,因此當頁面重新載入時,任何變更都會遺失。如果檔案變更需要更永久地儲存,則開發人員可以掛載 IDBFS 檔案系統,這允許資料在瀏覽器中持續存在。在 node.js 中執行程式碼時,開發人員可以掛載 NODEFS,以便讓程式碼直接存取本機檔案系統。
Emscripten 也有一個 API 來支援 非同步檔案存取。
如需更多資訊和範例,請參閱 檔案和檔案系統。
瀏覽器事件模型使用合作多工處理 — 每個事件都有一個「回合」來執行,然後必須將控制權返回給瀏覽器,以便可以處理其他事件。HTML 頁面掛起的一個常見原因是 JavaScript 沒有完成並將控制權返回給瀏覽器。
圖形 C++ 應用程式通常在無限迴圈中執行。在迴圈的每次迭代中,應用程式都會執行事件處理、處理和呈現,然後延遲(「等待」)以保持幀速率恆定。這個無限迴圈在瀏覽器環境中是個問題,因為控制權無法返回給瀏覽器以便執行其他程式碼。一段時間後,瀏覽器會通知使用者頁面已卡住,並提供停止或關閉頁面的選項。
同樣地,只有在目前「回合」結束時才能執行像 WebGL 這樣的 JavaScript API,並且會自動在該點呈現和交換緩衝區。這與需要手動交換緩衝區的 OpenGL 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,它讓您可以指定呼叫該函數的頻率以及其他事項。
注意
只是呈現單一幀並停止。您也應該注意
如果您使用 emscripten_set_main_loop()
,目前 Emscripten 的 SDL_QUIT
實作將會正常運作。當頁面關閉時,它會強制直接最後呼叫主迴圈,使其有機會注意到 SDL_QUIT
事件。如果您不使用主迴圈,您的應用程式將在您有機會注意到此事件之前關閉。
當頁面關閉時(在 onunload
中),您可以執行的動作有其限制。某些動作(例如顯示警示)在此時會被瀏覽器禁止。
另一個選項是使用 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) 描述了許多其他用於控制執行的函數。
在 asm.js 和 WebAssembly 中,Emscripten 都以類似於原生架構的方式表示記憶體。指標表示記憶體中的偏移量,結構使用與正常情況下相同的位址空間,依此類推。
在 WebAssembly 中,這是透過使用為此目的設計的 WebAssembly.Memory
完成的。在 asm.js 中,Emscripten 使用單個 類型陣列,不同的檢視提供對不同類型(HEAPU32
用於 32 位元無符號整數等等)的存取。
Emscripten 過去曾嘗試過其他記憶體表示法,最終採用了用於 JS 和 asm.js 的「類型陣列模式 2」方法,如上所述,然後 WebAssembly 實作了類似的東西。