將 C 程式庫編寫至 Wasm

有時候,您可能會想使用僅以 C 或 C++ 程式碼形式提供的程式庫。 傳統上,你可以在這裡放棄。我們再也不需要 EmscriptenWebAssembly (或是 Wasm)!

工具鍊

我自己的目標是想如何將一些現有的 C 程式碼編譯成 哇,LLVM 的 Wasm 後端也有一些雜訊 我開始深入研究雖然 您就能運用一些簡單的程式 如此一來,第二項 要能使用 C 的標準程式庫 多個檔案,可能會發生問題。這讓我認識到 我學到的課程:

雖然 Emscripten 過去是 C-to-asm.js 編譯器,但至今已成熟階段, 指定 Wasm,同時 在改用 Pixel 時 並將內部元件新增至官方 LLVM 後端Emscripten 也提供 與 C 標準程式庫相容的 Wasm 實作。使用 Emscripten。這項服務 出現許多隱藏性工作 模擬檔案系統、提供記憶體管理功能、將 OpenGL 與 WebGL 包裝在一起 — 許多您不需要親自開發的東西。

雖然這樣聽起來似乎是需要擔心的人身安全,但我也很擔心 :Emscripten 編譯器會移除所有不需要的項目。在我的 實驗結果顯示,系統產生的 Wasm 模組大小已針對邏輯 而 Emscripten 和 WebAssembly 團隊正在努力 日後就會更小

如要取得 Emscripten,請按照 網站或使用 Homebrew。如果你喜歡 我們也不希望在系統中安裝各種項目 超棒的軟體就是一個完善的專區 可用的 Docker 映像檔 請改採以下做法:

    $ docker pull trzeci/emscripten
    $ docker run --rm -v $(pwd):/src trzeci/emscripten emcc <emcc options here>

編寫簡單的程式碼

以下以幾乎標準的範例,說明如何在 C 中編寫函式 計算第 n 個費波那契數:

    #include <emscripten.h>

    EMSCRIPTEN_KEEPALIVE
    int fib(int n) {
      if(n <= 0){
        return 0;
      }
      int i, t, a = 0, b = 1;
      for (i = 1; i < n; i++) {
        t = a + b;
        a = b;
        b = t;
      }
      return b;
    }

如果您也知道 C,函式本身不應過於驚人。即使你 不知道 C 但知道 JavaScript,希望可以 到底是什麼?

emscripten.h 是 Emscripten 提供的標頭檔案。我們只需要用到 可存取 EMSCRIPTEN_KEEPALIVE 巨集,但這個巨集 提供更多功能。 這個巨集會指示編譯器不要移除顯示中的函式 未使用的。若省略該巨集,編譯器會將函式最佳化 — 沒人在使用它。

將所有內容儲存在名為 fib.c 的檔案中。如要將檔案轉換成 .wasm 檔案 需要切換 Emscripten 的編譯器指令 emcc

    $ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' fib.c

我們來剖析這個指令emcc 是 Emscripten 的編譯器。fib.c是我們的 C 檔案。到目前為止都很順利。-s WASM=1 要求 Emscripten 提供我們 Wasm 檔案 而非 asm.js 檔案。 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' 會指示編譯器 JavaScript 檔案提供 cwrap() 函式 - 有關這個函式的更多資訊 -O3 會指示編譯器主動進行最佳化。您可以選擇較低的 以縮短建構時間,但這樣產生的套裝組合 因為編譯器可能不會移除未使用的程式碼。

執行指令之後,您應該會看到一個 JavaScript 檔案, a.out.js 和一個名為 a.out.wasm 的 WebAssembly 檔案。Wasm 檔案 ( 「模組」) 包含經過編譯的 C 程式碼,且應相當小。 JavaScript 檔案負責載入及初始化 Wasm 模組和 提供更棒的 API如有需要,還能為您設定 堆疊、堆積和其他功能 搭載的作業系統因此 JavaScript 檔案 可使用 19 KB (約 5 KB gzip ) 稱重。

執行簡單的工作

如要載入及執行模組,最簡單的方法就是使用產生的 JavaScript 檔案。載入檔案後,您會擁有 全球 Module 由你決定使用 cwrap敬上 來建立可處理轉換參數的 JavaScript 原生函式 轉換為適合 C 的函式,並叫用已包裝的函式。cwrap獲得了 函式名稱、傳回類型和引數型別,並依序排列:

    <script src="a.out.js"></script>
    <script>
      Module.onRuntimeInitialized = _ => {
        const fib = Module.cwrap('fib', 'number', ['number']);
        console.log(fib(12));
      };
    </script>

如果發生以下情況: 執行這段程式碼 應該會看到「144」就是第 12 個費波那契數

神聖的努力:編譯 C 程式庫

到目前為止,我們根據 Wasm 編寫的 C 程式碼才寫得。核心肌群 WebAssembly 的應用實例是將 C 語言的現有生態系統 並允許開發人員在網路上使用這些程式庫經常提供 主要依賴 C 的標準程式庫、作業系統、檔案系統和其他 有些事物Emscripten 能夠提供大部分的這些功能 限制

讓我們回到原本的目標:編譯 WebP 的編碼器至 Wasm WebP 轉碼器的原始碼是以 C 語言撰寫, GitHub 和部分資源 API 說明文件。這很好的起點。

    $ git clone https://github.com/webmproject/libwebp

首先,我們要從WebPGetEncoderVersion() 將名為 webp.c 的 C 檔案編寫為 encode.h

    #include "emscripten.h"
    #include "src/webp/encode.h"

    EMSCRIPTEN_KEEPALIVE
    int version() {
      return WebPGetEncoderVersion();
    }

這個簡單的程式可用來測試我們能否取得 libwebp 的原始碼 進行編譯,因為我們不需要任何參數或複雜的資料結構, 叫用此函式。

如要編譯這個程式,我們必須告知編譯器可在哪個位置找到 使用 -I 旗標的 libwebp 標頭檔案,並傳送 libwebp。老實說,我剛剛把「所有」都拿去了 我能找到並仰賴編譯器來排除 不需要。它似乎能發揮巧思!

    $ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
        -I libwebp \
        webp.c \
        libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c

現在,我們只需要一些 HTML 和 JavaScript 即可載入全新的模組:

<script src="/a.out.js"></script>
<script>
  Module.onRuntimeInitialized = async (_) => {
    const api = {
      version: Module.cwrap('version', 'number', []),
    };
    console.log(api.version());
  };
</script>

修正版本號碼會在 輸出:

開發人員工具控制台的螢幕截圖,顯示正確版本
號碼。

從 JavaScript 擷取圖片到 Wasm

取得編碼器的版本編號固然重要,但需要對實際 更令人印象深刻,對吧?我們開始吧。

第一個要釐清的問題是:如何將圖片放到 Wasm 土地? 查看 libwebp 的編碼 API 採用 RGB、RGBA、BGR 或 BGRA 的位元組陣列。幸好 Canvas API getImageData()、 它提供了 Uint8ClampedArray 包含 RGBA 的圖片資料:

async function loadImage(src) {
  // Load image
  const imgBlob = await fetch(src).then((resp) => resp.blob());
  const img = await createImageBitmap(imgBlob);
  // Make canvas same size as image
  const canvas = document.createElement('canvas');
  canvas.width = img.width;
  canvas.height = img.height;
  // Draw image onto canvas
  const ctx = canvas.getContext('2d');
  ctx.drawImage(img, 0, 0);
  return ctx.getImageData(0, 0, img.width, img.height);
}

現在包含多個選項也就是將資料從 JavaScript 落地複製到 Wasm 土地上為此,我們需要公開兩個額外的函式。一個是 Pod 可以分配的 為 Wasm 登陸後釋放後再次釋放的 記憶體容量:

    EMSCRIPTEN_KEEPALIVE
    uint8_t* create_buffer(int width, int height) {
      return malloc(width * height * 4 * sizeof(uint8_t));
    }

    EMSCRIPTEN_KEEPALIVE
    void destroy_buffer(uint8_t* p) {
      free(p);
    }

create_buffer 會為 RGBA 圖片分配緩衝區,因此每像素 4 位元組。 malloc() 傳回的指標是下列項目的第一個記憶體儲存格位址 該緩衝區。指標傳回至 JavaScript 落地時,系統會將其視為 數字使用 cwrap 將函式公開至 JavaScript 後,我們 使用該數字尋找緩衝區開頭,並複製圖片資料。

const api = {
  version: Module.cwrap('version', 'number', []),
  create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
  destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
const image = await loadImage('/image.jpg');
const p = api.create_buffer(image.width, image.height);
Module.HEAP8.set(image.data, p);
// ... call encoder ...
api.destroy_buffer(p);

最終樂章:將圖片編碼

相片現已在瓦斯姆陸地拍攝。該呼叫 WebP 編碼器 做生意!查看 WebP 說明文件WebPEncodeRGBA 看起來都很完美這個函式會指向輸入圖片 大小也介於 0 到 100 之間也會分配 輸出緩衝區,之後我們就需要使用 WebPFree() 釋放 WebP 圖片

編碼運算的結果是一個輸出緩衝區及其長度。由於 C 中的函式不能有陣列做為傳回類型 (除非我們分配記憶體 會改為使用靜態全域陣列我知道了,不是乾淨的 C (事實上 它取決於 Wasm 指標的寬度是 32 位元 我認為這是相當不錯的做法

    int result[2];
    EMSCRIPTEN_KEEPALIVE
    void encode(uint8_t* img_in, int width, int height, float quality) {
      uint8_t* img_out;
      size_t size;

      size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);

      result[0] = (int)img_out;
      result[1] = size;
    }

    EMSCRIPTEN_KEEPALIVE
    void free_result(uint8_t* result) {
      WebPFree(result);
    }

    EMSCRIPTEN_KEEPALIVE
    int get_result_pointer() {
      return result[0];
    }

    EMSCRIPTEN_KEEPALIVE
    int get_result_size() {
      return result[1];
    }

現在一切就緒,我們可以呼叫編碼函式、擷取 將指標和圖片大小放置於我們自己的 JavaScript 區域緩衝區中 釋出程序中配置的所有 Wasm-land 緩衝區。

    api.encode(p, image.width, image.height, 100);
    const resultPointer = api.get_result_pointer();
    const resultSize = api.get_result_size();
    const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
    const result = new Uint8Array(resultView);
    api.free_result(resultPointer);

視圖片大小而定,您可能會遇到 Wasm 錯誤 無法增加記憶體,以容納輸入和輸出圖片:

開發人員工具控制台的螢幕截圖,顯示錯誤。

幸好,錯誤訊息提供這個問題的解決方法!我們只需 將 -s ALLOW_MEMORY_GROWTH=1 加入編譯指令中。

這樣就大功告成囉!我們編譯 WebP 編碼器,並將 JPEG 圖片轉碼 WebP。為證明測試過程,我們可以將結果緩衝區轉換成 blob,並使用 對 <img> 元素執行這項作業:

const blob = new Blob([result], { type: 'image/webp' });
const blobURL = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = blobURL;
document.body.appendChild(img);

看來,全新 WebP 映像檔的榮耀

開發人員工具的網路面板和產生的圖像。

結論

如要在瀏覽器中使用 C 程式庫,並非走進公園,只須花費一次 您瞭解整個流程及資料流程的運作方式 結果或許令人興奮

WebAssembly 為網路帶來許多新契機,可供處理,編號 例如重新製作和玩遊戲提醒你,Wasm 並非萬靈丹 可套用至所有細節,但如果碰到其中一個瓶頸, Wasm 可能就會 是非常實用的工具

額外內容:要簡單實現這一點

如要嘗試避免產生的 JavaScript 檔案,您或許可以 。讓我們回到 Fibonacci 的範例如要自行載入及執行 :

<!DOCTYPE html>
<script>
  (async function () {
    const imports = {
      env: {
        memory: new WebAssembly.Memory({ initial: 1 }),
        STACKTOP: 0,
      },
    };
    const { instance } = await WebAssembly.instantiateStreaming(
      fetch('/a.out.wasm'),
      imports,
    );
    console.log(instance.exports._fib(12));
  })();
</script>

Emscripten 建立的 WebAssembly 模組沒有記憶體可以運作 除非你能提供回憶提供 Wasm 模組的方式 anything 是使用 imports 物件,也就是 instantiateStreaming 函式。Wasm 模組可以存取 imports 物件,但不會加入其他物件。按照慣例 而 IScripting 會預期載入 JavaScript 時需要留意幾件事 環境:

  • 首先是 env.memory。Wasm 模組無人知曉 因此模型必須 具有一些記憶體可以使用輸入 WebAssembly.Memory。 代表一片線性記憶體 (可視需要擴充)。尺寸 參數位於「WebAssembly 頁面的單位中」,意即上述程式碼 可分配 1 頁的記憶體,每個頁面大小為 64 KiB。未提供 maximum 理論上,記憶體理論上沒有限制 (Chrome 目前採用 2GB 的硬性限制)。大部分 WebAssembly 模組不必設定 。
  • env.STACKTOP 會定義堆疊的開始位置。堆疊 才能發出函式呼叫,並為本機變數分配記憶體。 我們不曾針對基本記憶體管理工作 Fibonacci 程式可將整個記憶體做為堆疊使用 STACKTOP = 0