將現有 C 模組編譯到 WebAssembly
WebAssembly 的一個核心用例是利用現有的 C 庫生態系統,讓開發者可以在網頁上使用它們。
這些庫通常依賴於 C 的標準庫、作業系統、檔案系統和其他內容。Emscripten 提供了其中大部分功能,儘管存在一些 限制。
例如,讓我們將一個 WebP 編碼器編譯為 Wasm。WebP 編解碼器的原始碼是用 C 語言編寫的,並在 GitHub 上提供,以及一些廣泛的 API 文件。這是一個很好的起點。
git clone https://github.com/webmproject/libwebp
為了簡單起見,透過編寫一個名為 webp.c 的 C 檔案,將 encode.h 中的 WebPGetEncoderVersion() 函式暴露給 JavaScript。
#include "emscripten.h"
#include "src/webp/encode.h"
EMSCRIPTEN_KEEPALIVE
int version() {
return WebPGetEncoderVersion();
}
這是一個很好的簡單程式,用於測試您是否可以獲取 libwebp 的原始碼並進行編譯,因為它不需要任何引數或複雜的資料結構來呼叫此函式。
要編譯此程式,您需要告訴編譯器它可以在哪裡找到 libwebp 的標頭檔案(使用 -I 標誌),還需要傳遞它所需的 libwebp 的所有 C 檔案。一個有用的策略是直接提供 **所有** C 檔案,並依賴編譯器來剔除所有不必要的程式碼。對於此庫來說,這種方法似乎很有效。
emcc -O3 -s WASM=1 -s EXPORTED_RUNTIME_METHODS='["cwrap"]' \
-I libwebp \
webp.c \
libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c \
libwebp/sharpyuv/*.c
注意: 這種策略並不適用於所有 C 專案。許多專案依賴於 autoconf/automake 來生成系統特定的程式碼,然後再進行編譯。Emscripten 提供了 emconfigure 和 emmake 來包裝這些命令並注入適當的引數。您可以在 Emscripten 文件 中找到更多資訊。
現在您只需要一些 HTML 和 JavaScript 來載入您的新模組。
<script src="./a.out.js"></script>
<script>
Module.onRuntimeInitialized = async () => {
const api = {
version: Module.cwrap("version", "number", []),
};
console.log(api.version());
};
</script>
您將在 輸出 中看到正確的版本號。
注意: libwebp 將當前版本 a.b.c 返回為十六進位制數 0xabc。例如,v0.6.1 編碼為 0x000601 = 1537。
從 JavaScript 獲取影像到 Wasm
獲取編碼器的版本號很棒,但對實際影像進行編碼會更令人印象深刻。我們該如何做呢?
您需要回答的第一個問題是:如何將影像放入 Wasm 中?檢視 libwebp 的編碼 API,您會發現它期望一個包含 RGB、RGBA、BGR 或 BGRA 格式的位元組陣列。幸運的是,Canvas API 有 CanvasRenderingContext2D.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 中。為此,您需要暴露兩個額外的函式——一個在 Wasm 中為影像分配記憶體,另一個在完成操作後釋放記憶體。
#include <stdlib.h> // required for malloc definition
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"]),
encode: Module.cwrap("encode", "", ["number", "number", "number", "number"]),
free_result: Module.cwrap("free_result", "", ["number"]),
get_result_pointer: Module.cwrap("get_result_pointer", "number", []),
get_result_size: Module.cwrap("get_result_size", "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);
編碼影像
現在,影像已在 Wasm 中可用。現在可以呼叫 WebP 編碼器來完成其工作。檢視 WebP 文件,您會發現 WebPEncodeRGBA 似乎非常適合。該函式需要指向輸入影像及其尺寸的指標,以及 0 到 100 之間的質量選項。它還會為我們分配一個輸出緩衝區,我們需要在完成操作後使用 WebPFree() 來釋放該緩衝區。
編碼操作的結果是一個輸出緩衝區及其長度。由於 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 緩衝區。
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);
注意: new Uint8Array(someBuffer) 將在同一個記憶體塊上建立一個新檢視,而 new Uint8Array(someTypedArray) 將複製資料。
根據影像的大小,您可能會遇到 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;
img.alt = "a useful description";
document.body.appendChild(img);
瞧,新 WebP 影像的魅力。