将现有 C 模块编译为 WebAssembly
WebAssembly 的一个核心用例是利用现有的 C 库生态系统,并允许开发者在 Web 上使用它们。
这些库通常依赖于 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 的源代码,因为它不需要任何参数或复杂的数据结构来调用此函数。
要编译此程序,您需要使用 -I
标志告诉编译器 libwebp 的头文件在哪里,并且还需要传递它需要的所有 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 以十六进制数 0xabc 的形式返回当前版本 a.b.c。例如,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 图像的辉煌吧。