編寫與 npm

如何將 WebAssembly 整合至這項設定中?在本文中,我們會以 C/C++ 和 Emscripten 做為範例。

WebAssembly (wasm) 通常經常出現 設計成效能入門版本,或執行現有 C++ 的方法 網路程式碼集我們希望透過 squoosh.app 至少還有第三種觀點 生態系統取代為 Emscripten 可用於使用 C/C++ 程式碼。 內建的 Rust 支援以及 Go 團隊也會著手處理我 也有許多語言會跟著推出

在這些情境中,不是應用程式的核心,而是謎題 另一個單元您的應用程式含有 JavaScript、CSS、圖片素材資源、 以網站為中心的建構系統,甚至是 React 這類架構。如何 要將 WebAssembly 整合至這個設定中嗎?本文將進行這項操作 C/C++ 和 Emscripten 以做為範例。

Docker

我發現 Docker 在使用 Emscripten 時非常寶貴。C 與 C++ 通常編寫的程式庫可與建構的作業系統搭配使用。 能有一致的環境也非常有幫助。Docker 可讓您 可與 Emscripten 搭配使用的虛擬化 Linux 系統,並且 所有工具和依附元件的安裝作業如果缺少任何項目 安裝程式,不必擔心它會如何影響自己的機器或 和其他專案若發生問題,請丟棄容器並開始啟動 。如果這個方法只運作一次,就能確保系統能繼續運作 會產生相同的結果

Docker RegistryEmscripten 圖片,者: trzeci 是我廣泛使用的產品。

與 npm 整合

在大多數情況下,網路專案的進入點為 npm 的 package.json。按照慣例,大多數專案都能使用 npm install && npm run build 建構。

一般來說,由 Emscripten 產生的建構構件 (.js.wasm) 檔案) 應視為另一個 JavaScript 模組, 資產。JavaScript 檔案可透過 webpack 或 Rollup 等整合工具處理、 和 wasm 檔案應視為任何其他較大的二進位資產,例如 所以映像檔較小

因此,您必須在「正常」工作之前建立 Emscripten 版本構件。 建構程序開始於:

{
    "name": "my-worldchanging-project",
    "scripts": {
    "build:emscripten": "docker run --rm -v $(pwd):/src trzeci/emscripten
./build.sh",
    "build:app": "<the old build command>",
    "build": "npm run build:emscripten && npm run build:app",
    // ...
    },
    // ...
}

新的 build:emscripten 工作可以直接叫用 Emscripten,但如 之前所述,建議您使用 Docker 確保建構環境是在 保持一致

docker run ... trzeci/emscripten ./build.sh 會要求 Docker 啟動新的 容器中執行 trzeci/emscripten 映像檔,然後執行 ./build.sh 指令。 build.sh 是您接下來要編寫的殼層指令碼!--rm 分享 Docker 會在容器執行完畢後刪除該容器。這樣一來 持續收集到一組過時的機器映像檔-v $(pwd):/src 表示 您希望 Docker 「鏡像」目前目錄 ($(pwd)) 到 /src 中的 您對目錄內 /src 目錄中的檔案所做的任何變更 就會反映到實際專案這些鏡像的目錄 稱為「繫結掛接」

讓我們看看 build.sh

#!/bin/bash

set -e

export OPTIMIZE="-Os"
export LDFLAGS="${OPTIMIZE}"
export CFLAGS="${OPTIMIZE}"
export CXXFLAGS="${OPTIMIZE}"

echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
    # Compile C/C++ code
    emcc \
    ${OPTIMIZE} \
    --bind \
    -s STRICT=1 \
    -s ALLOW_MEMORY_GROWTH=1 \
    -s MALLOC=emmalloc \
    -s MODULARIZE=1 \
    -s EXPORT_ES6=1 \
    -o ./my-module.js \
    src/my-module.cpp

    # Create output folder
    mkdir -p dist
    # Move artifacts
    mv my-module.{js,wasm} dist
)
echo "============================================="
echo "Compiling wasm bindings done"
echo "============================================="

這裡有許多需要深入剖析的項目!

set -e 可讓殼層進入「失敗快速」模式。如果指令碼中的任何指令 傳回錯誤,就會立即取消整個指令碼。可用的值包括 非常實用,因為指令碼的最後輸出 永遠都是成功的 訊息或導致建構失敗的錯誤。

使用 export 陳述式,您可以定義幾個環境的值 變數。這樣您就能將額外的指令列參數傳送給 C 編譯器 (CFLAGS)、C++ 編譯器 (CXXFLAGS) 和連接器 (LDFLAGS)。 所有人都會透過「OPTIMIZE」接收最佳化工具設定,以確保 系統會一律以相同方式最佳化所有內容有幾個可能的值 用於 OPTIMIZE 變數:

  • -O0:不進行任何最佳化作業。不會刪除無效程式碼,且 Emscripten 也不會壓縮其產生的 JavaScript 程式碼。適合偵錯。
  • -O3:積極進行最佳化以提升成效。
  • -Os:以次要方式為效能和大小積極進行最佳化 條件。
  • -Oz:針對大小積極最佳化,視需要犧牲效能。

關於網頁版,我通常會建議使用 -Os

emcc 指令有好幾種選項。請注意,密件副本是 應該是「GCC 或 clang 等編譯器的直接替換」所有 你可能知道的 GCC 最可能是 -s 標記的特別之處,在於讓我們設定 Emscripten 。你可以在 Emscripten 的 settings.js、 但這個檔案可能會顯得不堪負荷以下是 Emscripten 標記的清單 我認為對網頁程式開發人員來說最重要的是:

  • --bind 啟用 embind
  • -s STRICT=1 停止支援所有已淘汰的建構選項。這可以確保 能以前瞻相容性的方式建構程式碼
  • -s ALLOW_MEMORY_GROWTH=1 允許在下列情況下自動增加記憶體: 無從得知撰寫文字時,Emscripten 會分配 16 MB 的記憶體 最初當程式碼配置了記憶體區塊時,這個選項會判斷 這些作業會導致整個 wasm 模組失敗 或黏土程式碼已用盡,或允許將記憶體總量擴大至 也就是系統必須因應配置 才能自動調整資源配置
  • -s MALLOC=... 會選擇要使用哪個 malloc() 實作項目。emmalloc 是 專為 Emscripten 而可快速執行的小型 malloc() 實作。 其次是 dlmalloc,這是一種完善的 malloc() 實作方式。您只需要 如要分配大量小型物件,則需要切換至 dlmalloc 或您想使用執行緒
  • -s EXPORT_ES6=1 會將 JavaScript 程式碼轉換為 ES6 模組,並加上 可與任何 Bundler 搭配使用的預設匯出功能一併規定-s MODULARIZE=1才能 。

下列標記並非一律必要,或僅供偵錯時使用 用途:

  • -s FILESYSTEM=0 是與 Emscripten 相關的標記,可以 在 C/C++ 程式碼使用檔案系統作業時,為您模擬檔案系統。 它會對所編譯的程式碼執行一些分析,以判定是否要在系統中納入 會模擬 glue 程式碼中的檔案系統。不過,有時這個做法 如果分析結果出錯,您支付了高達 70 KB 的額外固體 執行不需要的檔案系統模擬程式碼。使用 -s FILESYSTEM=0 時,你可以強制 Emscripten 不要加入這段程式碼。
  • -g4 可讓 Emscripten 在 .wasm 中加入偵錯資訊, 也會發出 wasm 模組的來源對應檔案如要瞭解詳情,請前往 使用 Emscripten 在偵錯 專區

就是這樣!為測試這項設定,讓我們建立新的 my-module.cpp

    #include <emscripten/bind.h>

    using namespace emscripten;

    int say_hello() {
      printf("Hello from your wasm module\n");
      return 0;
    }

    EMSCRIPTEN_BINDINGS(my_module) {
      function("sayHello", &say_hello);
    }

以及 index.html

    <!doctype html>
    <title>Emscripten + npm example</title>
    Open the console to see the output from the wasm module.
    <script type="module">
    import wasmModule from "./my-module.js";

    const instance = wasmModule({
      onRuntimeInitialized() {
        instance.sayHello();
      }
    });
    </script>

(這是 gist 。)

如要建構所有內容,請執行

$ npm install
$ npm run build
$ npm run serve

前往 localhost:8080 應會顯示下列輸出內容 開發人員工具控制台:

開發人員工具顯示透過 C++ 和 Emscripten 列印的訊息。

新增 C/C++ 程式碼做為依附元件

如要為網頁應用程式建構 C/C++ 程式庫,必須採用 您可以手動將程式碼新增至專案的存放區 或使用 npm 管理這些依附元件類型假設 想在我的網頁應用程式中使用 libvpx。libvpx 是透過 VP8 編碼圖片的 C++ 程式庫,VP8 是 .webm 檔案中所使用的轉碼器。 不過,libvpx 並非採用 npm 且沒有 package.json,因此我無法 請直接使用 npm 進行安裝

可以解決這種難題 napa:napa 可讓您安裝 存放區網址做為 node_modules 資料夾中的依附元件。

安裝 napa 做為依附元件:

$ npm install --save napa

並確認將 napa 做為安裝指令碼執行:

{
// ...
"scripts": {
    "install": "napa",
    // ...
},
"napa": {
    "libvpx": "git+https://github.com/webmproject/libvpx"
}
// ...
}

執行 npm install 時,napa 會負責複製 libvpx GitHub 在您的 node_modules 中建立名為 libvpx 的存放區。

您現在可以擴充建構指令碼來建構 libvpx。libvpx 使用 configuremake。幸好,Escripten 可協助確保configuremake 使用 Emscripten 的編譯器。為此,我們使用包裝函式 指令 emconfigureemmake

# ... above is unchanged ...
echo "============================================="
echo "Compiling libvpx"
echo "============================================="
(
    rm -rf build-vpx || true
    mkdir build-vpx
    cd build-vpx
    emconfigure ../node_modules/libvpx/configure \
    --target=generic-gnu
    emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="

echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
# ... below is unchanged ...

C/C++ 程式庫分為兩個部分:標頭 (傳統為 .h.hpp 檔案),定義資料結構、類別、常數等。 程式庫會公開發布,以及實際的程式庫 (傳統的 .so.a 檔案)。目的地: 請使用程式庫的 VPX_CODEC_ABI_VERSION 常數, 使用 #include 陳述式加入程式庫的標頭檔案:

#include "vpxenc.h"
#include <emscripten/bind.h>

int say_hello() {
    printf("Hello from your wasm module with libvpx %d\n", VPX_CODEC_ABI_VERSION);
    return 0;
}

問題在於編譯器不知道要尋找 vpxenc.h 的「位置」。 這就是 -I 標記的用途。會告知編譯器要對哪些目錄 檢查是否有標頭檔案此外,您還需要向編譯器提供 實際程式庫檔案:

# ... above is unchanged ...
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
    # Compile C/C++ code
    emcc \
    ${OPTIMIZE} \
    --bind \
    -s STRICT=1 \
    -s ALLOW_MEMORY_GROWTH=1 \
    -s ASSERTIONS=0 \
    -s MALLOC=emmalloc \
    -s MODULARIZE=1 \
    -s EXPORT_ES6=1 \
    -o ./my-module.js \
    -I ./node_modules/libvpx \
    src/my-module.cpp \
    build-vpx/libvpx.a

# ... below is unchanged ...

如果現在執行 npm run build,您會發現該程序會建構新的 .js 及新的 .wasm 檔案,示範網頁會確實輸出常數:

DevTools
透過文字框顯示 libvpx 的 ABI 版本。

您也會發現建構程序花費的時間較長。理由 建構時間可能不同以 libvpx 為例 您每次執行應用程式時 都會為 VP8 和 VP9 編譯編碼器和解碼器 。即使只是 變更 my-module.cpp 需要很長的時間才能完成。如果能 隨後保留 libvpx 的建構構件 。

而其中一種做法就是使用環境變數。

# ... above is unchanged ...
eval $@

echo "============================================="
echo "Compiling libvpx"
echo "============================================="
test -n "$SKIP_LIBVPX" || (
    rm -rf build-vpx || true
    mkdir build-vpx
    cd build-vpx
    emconfigure ../node_modules/libvpx/configure \
    --target=generic-gnu
    emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="
# ... below is unchanged ...

(您可在此查閱要點 。)

eval 指令可透過傳送參數來設定環境變數 傳送至建構指令碼如果發生下列情況,test 指令會略過建構 libvpx $SKIP_LIBVPX 已設為任何值。

您現在可以編譯模組,但略過重新建構 libvpx:

$ npm run build:emscripten -- SKIP_LIBVPX=1

自訂建構環境

有時程式庫必須仰賴額外工具進行建構。如果這些依附元件 Docker 映像檔提供的建構環境缺少,您需要 自行新增舉例來說,假設您想建立 說明文件。Doxygen 不是 ,但您可以使用 apt 安裝。

如果在「build.sh」中這麼做,請重新下載再重新安裝 doxygen 即可。不僅如此 可免除不必要的麻煩,但也會妨礙您離線製作專案。

在這裡建構自己的 Docker 映像檔是很合理的。Docker 映像檔的建構基礎是 編寫說明建構步驟的 Dockerfile。Dockerfile 而且強大的 指令,但大部分 你可以使用 FROMRUNADD 離開。在這種情況下:

FROM trzeci/emscripten

RUN apt-get update && \
    apt-get install -qqy doxygen

透過 FROM,您可以宣告要用來啟動範本的 Docker 映像檔 點。我選擇「trzeci/emscripten」做為現有圖片 使用 RUN 即可指示 Docker 在 都會在 Docker 容器中執行現在這些指令對容器所做的變更 以及 Docker 映像檔確保您的 Docker 映像檔 執行build.sh前,請務必調整你的 package.json 位元:

{
    // ...
    "scripts": {
    "build:dockerimage": "docker image inspect -f '.' mydockerimage || docker build -t mydockerimage .",
    "build:emscripten": "docker run --rm -v $(pwd):/src mydockerimage ./build.sh",
    "build": "npm run build:dockerimage && npm run build:emscripten && npm run build:app",
    // ...
    },
    // ...
}

(您可在此查閱要點 。)

這會建構 Docker 映像檔,但僅限尚未建構的映像檔。接著 一切都像之前一樣執行,但現在建構環境具有 doxygen 指令,系統將在建構 libvpx 的說明文件

結論

C/C++ 程式碼和 npm 並不令人意外,但你可以 同時搭配一些額外的工具和隔離機制 儲存空間抽象化機制類型這項設定並不適用於所有專案 您可以視需要調整。如果 敬請分享!

附錄:使用 Docker 映像檔層

另一種替代方法是使用 Docker 和 Docker 的智慧型快取方法。Docker 會逐步執行 Dockerfile 並將每個步驟的結果指派給自身專屬的圖片。這些中繼圖片 通常稱為「圖層」如果 Dockerfile 中的指令未變更,Docker 因此在重新建構 Dockerfile 時不會實際重新執行該步驟。改為 並重複使用上次建構映像檔時建立的圖層

先前您必須花些功夫,不要每次都重新建構 libvpx 建構應用程式您可以改為移動 libvpx 的建構指示 從 build.shDockerfile,以便使用 Docker 的快取 機制:

FROM trzeci/emscripten

RUN apt-get update && \
    apt-get install -qqy doxygen git && \
    mkdir -p /opt/libvpx/build && \
    git clone https://github.com/webmproject/libvpx /opt/libvpx/src
RUN cd /opt/libvpx/build && \
    emconfigure ../src/configure --target=generic-gnu && \
    emmake make

(您可在此查閱要點 。)

請注意,您需要手動安裝 Git 並複製 libvpx 執行 docker build 時繫結掛接。基本上,您不需要使用 小睡片刻。