建置專案

Emscripten 提供兩個腳本,可設定您的 makefile 以使用 emcc 作為 gcc 的替代品 — 在大多數情況下,您專案的其餘部分目前建置系統保持不變。

與建置系統整合

要使用 Emscripten 建置,您需要在 makefile 中將 gcc 替換為 emcc。這可以使用 emconfigure 來完成,它會設定適當的環境變數,例如 CXX (C++ 編譯器) 和 CC (編譯器)。

考慮您通常使用以下命令進行建置的情況

./configure
make

提示

如果您不熟悉這些建置命令,configure、make、make install 背後的魔力 這篇文章是一個很好的入門。

要使用 Emscripten 建置,您應該改用以下命令

# Run emconfigure with the normal configure command as an argument.
emconfigure ./configure

# Run emmake with the normal make to generate Wasm object files.
emmake make

# Compile the linked code generated by make to JavaScript + WebAssembly.
# 'project.o' should be replaced with the make output for your project, and
# you may need to rename it if it isn't something emcc recognizes
# (for example, it might have a different suffix like 'project.so' or
# 'project.so.1', or no suffix like just 'project' for an executable).
# If the project output is a library, you may need to add your 'main.c' file
# here as well.
# [-Ox] represents build optimisations (discussed in the next section).
emcc [-Ox] project.o -o project.js

emconfigure 會以一般的 configure 作為引數呼叫 (在基於 configure 的建置系統中),而 emmake 會以 make 作為引數呼叫。如果您的建置系統使用 CMake,請將上例中的 ./configure 替換為 cmake . 等。如果您的建置系統不使用 configure 或 CMake,則您可以省略第一個步驟,而只執行 make (雖然這樣您可能需要手動編輯 Makefile)。

提示

我們建議您在基於 configureCMake 的建置系統中呼叫 emconfigureemmake 腳本。您是否真的需要呼叫這兩個工具取決於建置系統 (某些系統會在 configure 步驟中儲存環境變數,而其他系統則不會)。

Make 會產生 Wasm 物件檔案。它也可能會將物件檔案連結到程式庫和/或 Wasm 可執行檔中。除非已修改此類建置系統以同時發出 JavaScript 輸出,否則您需要執行額外的 emcc 命令,如上所示,這會發出最終可執行的 JavaScript 和 WebAssembly。

注意

make 的檔案輸出可能會有不同的後綴:靜態程式庫封存檔為 .a,共用程式庫為 .so,物件檔案為 .o (這些副檔名與 gcc 用於不同類型檔案的副檔名相同)。無論副檔名為何,這些檔案都包含 emcc 可以編譯成最終 JavaScript + WebAssembly 的內容 (通常內容會是 Wasm 物件檔案,但如果您使用 LTO 建置,則它們會包含 LLVM 位元碼)。

注意

某些建置系統可能無法使用上述程序正確發出 Wasm 物件檔案,您可能會看到 is not a valid input file 警告。您可以執行 file 來檢查檔案包含的內容 (您也可以手動檢查內容是否以 \0asm 開頭,以查看它們是否為 Wasm 物件檔案,或以 BC 開頭,以查看它們是否為 LLVM 位元碼)。也值得執行 emmake make VERBOSE=1,這會印出它執行的命令 - 您應該會看到正在使用 emcc,而不是原生系統編譯器。如果沒有使用 emcc,您可能需要修改 configure 或 cmake 腳本。

Emscripten 連結器輸出檔案

除非使用某些特定旗標執行 (例如 -c-S-r-shared),否則 emcc 將執行連結階段,該階段可以產生不只一個檔案。產生的檔案集會根據傳遞給 emcc 的最終旗標和指定輸出檔案的名稱而改變。以下是說明在哪些條件下會產生哪些檔案的快速參考

  • emcc ... -o output.html 建置一個 output.html 檔案作為輸出,以及一個隨附的 output.js 啟動器檔案和一個 output.wasm WebAssembly 檔案。

  • emcc ... -o output.js 省略產生 HTML 啟動器檔案 (如果您計劃在瀏覽器中執行,則會要求您自行提供),並產生兩個檔案,output.jsoutput.wasm。(可以在例如 node.js shell 中執行)

  • emcc ... -o output.wasm 省略產生 JavaScript 或 HTML 啟動器檔案,並產生一個以獨立模式建置的單一 Wasm 檔案,就像已使用 -sSTANDALONE_WASM 設定一樣。產生的檔案預期會與 WASI ABI 一起執行 - 特別是,當您初始化模組後,您必須先手動呼叫 _start 匯出,或 (在 --no-entry 的情況下) _initialize 匯出,然後才能執行任何其他操作。

  • emcc ... -o output.{html,js} -sWASM=0 會導致編譯器以 JavaScript 為目標,因此不會產生 .wasm 檔案。

  • emcc ... -o output.{html,js} --emit-symbol-map 如果是以 WebAssembly 為目標 (未指定 -sWASM=0),或如果以 JavaScript 為目標且指定 -Os-Oz-O2 或更高,但偵錯層級設定為 -g1 或更低 (即如果發生符號縮減),則會產生檔案 output.{html,js}.symbols

注意

# Sub-optimal - JavaScript/WebAssembly optimizations are omitted
emcc -O2 a.cpp -c -o a.o
emcc -O2 b.cpp -c -o b.o
emcc a.o b.o -o project.js

# Sub-optimal - LLVM optimizations omitted
emcc a.cpp -c -o a.o
emcc b.cpp -c -o b.o
emcc -O2 a.o b.o -o project.js

# Usually the right thing: The same options are provided at compile and link.
emcc -O2 a.cpp -c -o a.o
emcc -O2 b.cpp -c -o b.o
emcc -O2 a.o b.o -o project.js

# Optimize the first file for size, and the rest using `-O2`.
emcc -Oz a.cpp -c -o a.o
emcc -O2 b.cpp -c -o b.o
emcc -O2 a.o b.o -o project.js

注意

# Compile the object file to JavaScript with -O1 optimizations.
emcc -O1 project.o -o project.js

注意

# Compile the Wasm object file to JavaScript+WebAssembly, with debug info
# -g or -gN can be used to set the debug level (N)
emcc -g project.o -o project.js

# Compile libstuff to libstuff.a
emconfigure ./configure
emmake make

# Compile project to project.o
emconfigure ./configure
emmake make

# Link the library and code together.
emcc project.o libstuff.a -o final.html

emcc test/browser/test_sdl2_glshader.c --use-port=sdl2 -sLEGACY_GL_EMULATION -o sdl2.html

注意

注意

注意

注意

注意

Emscripten 也支援外部埠(非發行版本的一部分的埠)。若要使用這類埠,您只需提供其路徑: --use-port=/path/to/my_port.py

注意

請注意,如果您正在處理埠的程式碼,emscripten 使用的埠 API 並非 100% 穩定,可能會在不同版本之間變更。

建置系統問題

建置系統自我執行

某些大型專案會產生可執行檔並執行它們,以便為建置過程的後續部分產生輸入(例如,可能會先建置剖析器,然後在語法上執行,接著產生實作該語法的 C/C++ 程式碼)。當使用 Emscripten 時,這類建置過程會造成問題,因為您無法直接執行您正在產生的程式碼。

最簡單的解決方案通常是將專案建置兩次:一次在本機上,另一次建置為 JavaScript。當 JavaScript 建置程序因缺少產生的可執行檔而失敗時,您可以從本機建置中複製該可執行檔,然後繼續正常建置。例如,這種方法已成功用於編譯 Python(在建置期間需要執行其 pgen 可執行檔)。

在某些情況下,修改建置腳本使其在本機上建置產生的可執行檔是合理的。例如,這可以透過在建置腳本中指定兩個編譯器 emccgcc,並且僅將 gcc 用於產生的可執行檔來完成。但是,這可能比先前的解決方案更複雜,因為您需要修改專案建置腳本,並且您可能必須解決程式碼被編譯並同時用於最終結果和產生的可執行檔的情況。

虛擬動態連結

Emscripten 的目標是產生最快且最小的程式碼。因此,它專注於將整個專案編譯成單個 Wasm 檔案,盡可能避免動態連結。

預設情況下,當使用 -shared 旗標建置共享程式庫時,Emscripten 會產生一個 .so 程式庫,實際上它只是一個普通的 .o 物件檔案(在底層,它使用 ld -r 將物件組合到一個較大的物件中)。當這些虛擬「共享程式庫」連結到您的應用程式時,它們實際上會作為靜態程式庫連結。在建置這些共享程式庫時,Emcc 會忽略命令列上的其他共享程式庫。這是為了確保在中間建置階段不會多次連結相同的動態程式庫,否則會導致重複符號錯誤。

請參閱 實驗性支援,了解如何建置真正的動態程式庫,這些程式庫可以在載入時或在執行時(透過 dlopen)連結在一起。

設定可能會執行看似失敗的檢查

使用 configurecmake 或其他一些可移植設定方法的專案,可能會在設定階段執行檢查,以驗證工具鏈和路徑是否已正確設定。Emcc 會盡可能讓檢查通過,但您可能需要停用由於「誤判」(例如,在最終執行環境中會通過,但在 configure 期間的 Shell 中不會通過的測試)而失敗的測試。

提示

請確保在停用檢查後,受測試的功能確實可以運作。這可能涉及使用特定於建置系統的方法,手動將命令新增至 make 檔案。

注意

一般而言,configure 與 Emscripten 這類跨編譯器並不匹配。configure 的設計目的是在本機上為本地設定進行建置,並努力尋找本機建置系統和本地系統標頭。使用跨編譯器時,您的目標是不同的系統,並且會忽略這些標頭等等。

封存 (.a) 檔案

Emscripten 支援 .a 封存檔案,它們是物件檔案的組合。這是一種簡單的程式庫格式,具有特殊的語意 - 例如,連結順序對於 .a 檔案很重要,但對於普通物件檔案則不重要。在大多數情況下,這些特殊語意在 Emscripten 中的運作方式應與其他地方相同。

手動使用 emcc

Emscripten 教學課程 說明了如何使用 emcc 將單個檔案編譯為 JavaScript。Emcc 也可以用於您期望 gcc 的所有其他方式。

# Generate a.out.js from C++. Can also take .ll (LLVM assembly) or .bc (LLVM bitcode) as input
emcc src.cpp

# Generate an object file called src.o.
emcc src.cpp -c

# Generate result.js containing JavaScript.
emcc src.cpp -o result.js

# Generate an object file called result.o
emcc src.cpp -c -o result.o

# Generate a.out.js from two C++ sources.
emcc src1.cpp src2.cpp

# Generate object files src1.o and src2.o
emcc src1.cpp src2.cpp -c

# Combine two object files into a.out.js
emcc src1.o src2.o

# Combine two object files into another object file (not normally needed)
emcc src1.o src2.o -r -o combined.o

# Combine two object files into library file
emar rcs libfoo.a src1.o src2.o

除了與 gcc 相同的能力之外,emcc 還支援最佳化程式碼、控制發出的偵錯資訊、產生 HTML 和其他輸出格式等選項。這些選項記錄在 emcc 工具參考 中(命令列上的 emcc --help)。

在前置處理器中偵測 Emscripten

Emscripten 提供以下前置處理器巨集,可用於識別編譯器版本和平台

  • 使用 Emscripten 編譯程式時,始終會定義前置處理器定義 __EMSCRIPTEN__

  • 前置處理器變數 __EMSCRIPTEN_major____EMSCRIPTEN_minor____EMSCRIPTEN_tiny__emscripten/version.h 中定義,並以整數形式指定目前使用的 Emscripten 編譯器版本。

  • Emscripten 的行為類似於 Unix 的變體,因此在使用 Emscripten 編譯程式碼時,始終存在前置處理器定義 unix__unix__unix__

  • Emscripten 使用 Clang/LLVM 作為其底層程式碼產生編譯器,因此會定義前置處理器定義 __llvm____clang__,並且前置處理器定義 __clang_major____clang_minor____clang_patchlevel__ 指示所使用的 Clang 版本。

  • Clang/LLVM 與 GCC 相容,因此也定義了前置處理器定義 __GNUC____GNUC_MINOR____GNUC_PATCHLEVEL__,以表示 Clang/LLVM 提供的 GCC 相容性程度。

  • 前置處理器字串 __VERSION__ 指示 GCC 相容的版本,該版本會擴充以同時顯示 Emscripten 版本資訊。

  • 同樣地,__clang_version__ 存在並指示 Emscripten 和 LLVM 版本資訊。

  • Emscripten 是一個 32 位元平台,因此 size_t 是一個 32 位元無符號整數,__POINTER_WIDTH__=32__SIZEOF_LONG__=4__LONG_MAX__ 等於 2147483647L

  • 當使用命令列編譯器旗標 -msse-msse2-msse3-mssse3-msse4.1 之一,以 SSEx SIMD API 為目標時,將會存在一個或多個前置處理器旗標 __SSE____SSE2____SSE3____SSSE3____SSE4_1__,以指示對這些指令集的可用支援。

  • 如果使用編譯器和連結器旗標 -pthread 以 pthreads 多執行緒支援為目標,則會存在前置處理器定義 __EMSCRIPTEN_PTHREADS__

使用編譯器包裝函式

有時,使用編譯器包裝函式來執行 ccachedistccgomacc 等操作可能會很有用。對於 ccache,簡單地包裝整個編譯器的正常方法應該可以運作,例如 ccache emcc。對於分散式建置,在本地執行 emscripten 驅動程式並僅分散底層 clang 命令可能會很有益。如果這符合需求,則可以使用設定檔中的 COMPILER_WRAPPER 設定,在對 clang 的內部呼叫周圍新增一個包裝函式。與其他設定設定一樣,這也可以透過環境變數設定。例如

EM_COMPILER_WRAPPER=gomacc emcc -c hello.c

pkg-config

emconfigureemmake 設定 pkg-config 以進行交叉編譯,並設定環境變數 PKG_CONFIG_LIBDIRPKG_CONFIG_PATH。若要提供自訂的 pkg-config 路徑,請設定環境變數 EM_PKG_CONFIG_PATH

範例/測試程式碼

Emscripten 測試套件 (test/runner.py) 包含許多好的範例 — 使用其正常建置系統建置的大型 C/C++ 專案,如上所述:freetypeopenjpegzlibbulletpoppler

也值得查看 ammo.js 專案中的建置腳本。

疑難排解

  • 請務必使用 emar(它會呼叫 llvm-ar),因為系統的 ar 可能不支援我們的物件檔案。emmakeemconfigure 會正確設定 AR 環境變數,但建置系統可能會錯誤地硬式編碼 ar

  • 類似地,使用系統的 ranlib 而不是 emranlib (它會呼叫 llvm-ranlib) 可能會導致問題,例如不支援我們的物件檔並移除索引,導致 archive has no index; run ranlib to add one 這個訊息從 wasm-ld 出現。再次強調,使用 emmake/emconfigure 應該可以藉由設定環境變數 RANLIB 來避免此問題,但建置系統可能將其硬編碼,或要求您傳遞一個選項

  • 編譯錯誤 multiply defined symbol 表示專案多次連結了特定的靜態函式庫。需要修改專案,使有問題的函式庫只連結一次。

    注意

    您可以使用 llvm-nm 來查看每個物件檔中定義了哪些符號。

    一個解決方案是使用動態連結。這可確保函式庫僅在最終建置階段連結一次。

  • 產生獨立的 Wasm 時,請確保在嘗試使用模組之前呼叫 _start 或 (針對 --no-entry) _initialize 導出。