注意
本文件有些過時,正在更新中。
Emscripten 支援靜態連結物件檔(以及包含物件檔的 ar 封存檔)。這讓大多數建置系統可以在 Emscripten 上運作,幾乎不需要任何變更(請參閱建置專案)。
此外,Emscripten 也支援 WebAssembly 模組的某種形式的動態連結。這可能會增加額外負荷,因此為了獲得最佳效能,仍然應該優先使用靜態連結。但是,可以使用某些命令列旗標來減少此額外負荷。請參閱下文了解詳細資訊。
在我們開始討論動態連結之前,讓我們先來談談靜態連結。Emscripten 的連結模型與大多數原生平台略有不同。為了理解它,請考慮原生連結模型在下列事實為真的情況下運作
應用程式直接在本地系統上執行,並且可以存取本地系統程式庫,例如 C 和 C++ 標準程式庫以及其他程式庫。
程式碼大小不是一個大問題。部分原因是系統程式庫已經存在於系統上,因此 C++ 中的「hello world」可以很小,即使它在 C++ 標準程式庫中使用大量的 iostream 程式碼。但是,程式碼大小可能是一個會影響冷啟動時間的問題,因為更多的程式碼需要更長的時間才能從磁碟載入,但是這個成本通常並不顯著,而且現代作業系統會以各種方式(例如快取預期載入的應用程式)來減輕這種成本。
在 Emscripten 的情況下,程式碼通常會在網路上執行。這表示下列情況
應用程式在沙箱中執行。它沒有可動態連結的本地系統程式庫;它必須運送自己的系統程式庫程式碼。
程式碼大小是一個主要問題,因為應用程式的程式碼是透過網際網路下載的,這比在本地電腦上安裝的原生應用程式慢了許多個數量級。
因此,Emscripten 會自動為您處理系統程式庫,並自動執行無效程式碼消除等等,以盡可能將它們縮小。
這裡的一個額外因素是 Emscripten 有「js 程式庫」- 以 JavaScript 撰寫的系統程式庫。此類系統程式庫是我們在網路上存取 API 的方式。這也是人們將編譯程式碼和手寫程式碼連接在同一頁上的便捷方式。這是 Emscripten 以特殊方式處理系統程式庫的另一個原因,尤其是,以一種讓它可以盡可能刪除這些 js 程式庫的方式,只留下實際使用的部分,而且同樣地,這在靜態連結獨立應用程式且沒有外部相依性的情況下效果最佳。
Emscripten 的動態連結相當簡單:您可以從原始碼建置幾個獨立的程式碼「模組」,並且可以在執行階段連結它們。連結基本上是以最簡單的方式將每個模組中未定義的符號與其他模組中已定義的符號連接起來。它目前不支援某些邊緣案例。
系統程式庫會利用一些更進階的連結功能,其中包括此類邊緣案例。因此,Emscripten 嘗試簡化問題,如下所示:有兩種類型的共用模組
主要模組,其中已連結系統程式庫。
側模組,其中未連結系統程式庫。
一個專案應該包含正好一個主要模組。然後,它可以在執行階段連結到多個側模組。此模型也使其他事情變得更簡單。例如,只有單例主要模組包含 JavaScript 環境,而側模組是純 WebAssembly 模組。
此設計的一個棘手之處是側模組可能會相依於主要模組未相依的系統程式庫。請參閱下方的系統程式庫章節,了解如何處理此問題。
請注意,「主要模組」不需要包含 main()
函式。它也可以位於側模組中。使主要模組成為「主要」模組的原因是,只有一個主要模組,並且只有它連結了系統程式庫。
(請注意,系統程式庫是靜態連結到主要模組的。即使我們無法像我們希望的那樣完全消除無效程式碼,我們仍然可以從這種方式中獲得一些最佳化。)
如果您想要跳到檢視正在執行的程式碼,您可以查看測試套件。有 test_dylink_*
測試一般動態連結,以及 test_dlfcn_*
測試 dlopen()
程式碼。否則,我們現在描述此程序。
載入時間動態連結是指側模組與主要模組一起在啟動期間載入,並且它們在應用程式開始執行之前連結在一起的情況。
將您的程式碼的一部分建置為主要模組,使用 -sMAIN_MODULE
連結它。
將您的程式碼的其他部分建置為側模組,使用 -sSIDE_MODULE
連結它。
對於主要模組,輸出副檔名應該是 .js
(WebAssembly 檔案會像平常一樣在其旁邊產生)。對於側模組,輸出只會是一個 WebAssembly 模組,我們建議輸出副檔名為 .wasm
或 .so
(這是 UNIX 系統使用的共用程式庫副檔名)。
為了讓側模組在啟動時載入,您需要告知主要模組它們的存在。您可以在連結主要模組時,在命令列上指定它們。例如
emcc -sMAIN_MODULE main.c libsomething.wasm
在執行階段,JavaScript 載入程式碼會在應用程式開始執行之前,載入 libsomthing.wasm
(以及任何其他側模組)以及主要模組。然後,執行的應用程式可以存取從連結在一起的任何模組中的程式碼。
dlopen()
執行階段動態連結¶可以使用呼叫 dlopen()
函式來在程式已經執行後載入側模組來執行執行階段動態連結。此程序以相同的方式開始,使用相同的旗標來建置主要模組和側模組。不同之處在於,您在連結主要模組時不會在命令列上指定側模組;相反地,您必須將側模組載入檔案系統,以便 dlopen
(或 fopen
等)可以存取它(dlopen(NULL)
除外,這表示開啟目前的執行檔,這在沒有檔案系統整合的情況下即可運作)。基本上就是這樣 - 您然後可以正常使用 dlopen()、 dlsym()
等。
預設情況下,主模組會停用無用程式碼消除。這表示所有編譯後的程式碼都會保留在輸出中,包括所有連結的系統函式庫以及所有 JS 函式庫程式碼。
這是預設行為,因為它最不容易產生意外。但也可以使用正常的無用程式碼消除,方法是使用 -sMAIN_MODULE=2
(而非 1)進行建置。在此模式下,主模組會正常建置,不會有任何保留程式碼的特殊行為。因此,您有責任確保側模組需要的程式碼保持啟用狀態。您可以透過將程式碼加入 EXPORTED_FUNCTIONS
或在原始碼中標記符號 EMSCRIPTEN_KEEPALIVE
來完成。請參閱 other.test_minimal_dynamic
以取得實際範例。
如果您正在執行載入時動態連結,則命令列上指定的側模組所需的任何符號都會自動保持啟用狀態。因此,我們強烈建議在執行載入時動態連結時使用 MAIN_MODULE=2
。
側模組也有對應的 -sSIDE_MODULE=2
選項。
如前所述,Emscripten 連結器會以特殊方式處理系統函式庫,在動態連結中,只有主模組會與系統函式庫連結。在連結主模組時,可以在命令列上傳遞側模組,在這種情況下,任何系統函式庫的相依性都會自動處理。
然而,當在沒有側模組的情況下連結主模組時(通常使用 -sMAIN_MODULE=1
),可能會遺漏所需的系統函式庫。本節說明如何強制主模組與特定函式庫連結來解決此問題。
您可以在環境中透過 EMCC_FORCE_STDLIBS=1
建置主模組,以強制包含所有標準函式庫。更精確的方法是指定您想要明確包含的系統函式庫。例如,使用類似 EMCC_FORCE_STDLIBS=libcxx,libcxxabi
的方式(如果您需要這兩個函式庫)。
原生連結器通常只會在所有符號都解析後才執行程式碼。Emscripten 的動態連結器會將符號與這些符號的未解析參考 **動態** 連接起來。因此,我們不會檢查是否還有任何符號未解析,即使有未解析的符號,程式碼也可以開始執行。如果這些未解析的符號在實際執行中未被呼叫,程式碼會順利執行。如果它們被呼叫,您會收到執行階段錯誤。從堆疊追蹤(在未縮減的版本中)應該可以清楚了解出了什麼問題;使用 -sASSERTIONS
進行建置可以提供更多幫助。
動態連結 + pthreads 仍然是實驗性的功能。因此,同時使用 MAIN_MODULE
和 -pthread
進行連結會產生警告。
雖然載入時動態連結沒有任何複雜性,但透過 dlopen
/dlsym
進行執行階段動態連結可能需要一些額外的考量。原因是保持執行緒之間的間接函式指標表同步必須由 emscripten 函式庫程式碼來完成。每次載入新的函式庫或透過 dlsym
請求新的符號時,都可以新增表格插槽,而這些變更需要在程序中的每個執行緒上反映。
表格的變更受到互斥鎖的保護,而且在任何執行緒從 dlopen
或 dlsym
返回之前,它會等待直到所有其他執行緒都同步為止。為了使這種同步盡可能無縫,我們掛鉤了 emscripten_futex_wait 和 emscirpten_yield 的底層基本型別。
在大多數使用案例中,這一切都會在幕後發生,而且不需要特殊操作。但是,有一類應用程式目前可能需要修改。如果您的應用程式忙碌等待,或直接使用 atomic.waitXX
指令(或 clang __builtin_wasm_memory_atomic_waitXX
內建函數),您可能需要將其切換為使用 emscripten_futex_wait
或避免死鎖。如果您在阻塞時不使用 emscripten_futex_wait
,您可能會阻塞其他正在呼叫 dlopen
和/或 dlsym
的執行緒。