在什么是 WebAssembly?它来自哪里?部分,我解释了我们今天是如何完成 WebAssembly 的。在本文中,我将向您展示如何将现有的 C 程序 mkbitmap
编译为 WebAssembly。它比 hello world 示例更复杂,因为它涉及到处理文件、在 WebAssembly 和 JavaScript 平台之间进行通信,以及在画布上绘制,但它仍然易于管理,不会让您感到无所适从。
本文面向想要学习 WebAssembly 的 Web 开发者,并逐步介绍了如果您想将 mkbitmap
等代码编译为 WebAssembly,应如何继续操作。诚挚的警告是,在首次运行时无法让应用或库进行编译是完全正常的,这就是下述某些步骤最终不起作用的原因,因此我需要回溯,然后以不同的方式重试。本文并没有展示神奇的最终编译命令,就好像它是从空中消失一样,而是描述了我的实际进度(包括一些令人沮丧的内容)。
关于mkbitmap
mkbitmap
C 程序会读取图片,并按以下顺序对图片应用以下一项或多项操作:反转、高通过滤、缩放和阈值。每项操作都可以单独控制以及开启或关闭。mkbitmap
的主要用途是将彩色或灰度图片转换为适合其他程序的输入格式,尤其是构成 SVGcode 基础的跟踪程序 potrace
。mkbitmap
作为预处理工具,在将扫描的线条画(例如卡通或手写文本)转换为高分辨率的双层图片时特别有用。
如需使用 mkbitmap
,您可以向其传递多个选项以及一个或多个文件名。如需了解所有详细信息,请参阅该工具的手册页面:
$ mkbitmap [options] [filename...]
![彩色卡通图片。](https://web.developers.google.cn/static/articles/compiling-mkbitmap-to-webassembly/image/cartoon-image-color.png?authuser=6&hl=cs)
![经过预处理的卡通图片转换为灰度图片。](https://web.developers.google.cn/static/articles/compiling-mkbitmap-to-webassembly/image/cartoon-image-converted-97142bc66e12e.png?authuser=6&hl=cs)
mkbitmap -f 2 -s 2 -t 0.48
(来源)。获取代码
第一步是获取 mkbitmap
的源代码。您可以在项目网站上找到该编号。在撰写本文时,potrace-1.16.tar.gz 是最新版本。
在本地编译并安装
下一步是在本地编译和安装该工具,以了解其行为方式。INSTALL
文件包含以下说明:
cd
到包含软件包源代码的目录,然后输入./configure
以针对您的系统配置软件包。运行
configure
可能需要一些时间。运行时,它会输出一些消息,告知其正在检查哪些功能。输入
make
以编译软件包。(可选)输入
make check
以运行软件包附带的任何自测(通常使用刚刚构建的已卸载二进制文件)。输入
make install
以安装程序以及任何数据文件和文档。将该软件包安装到由 root 所有的前缀中时,建议以常规用户的身份配置和构建软件包,并且仅以 root 权限执行make install
阶段。
按照上述步骤操作,最终应该会得到两个可执行文件:potrace
和 mkbitmap
。本文重点介绍后者。您可以运行 mkbitmap --version
来验证其是否正常运行。以下是我的机器中所有四个步骤的输出,为简洁起见,经过大量剪辑:
第 1 步,./configure
:
$ ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
checking whether make sets $(MAKE)... yes
[…]
config.status: executing libtool commands
第 2 步,make
:
$ make
/Applications/Xcode.app/Contents/Developer/usr/bin/make all-recursive
Making all in src
clang -DHAVE_CONFIG_H -I. -I.. -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all-am'.
第 3 步,make check
:
$ make check
Making check in src
make[1]: Nothing to be done for `check'.
Making check in doc
make[1]: Nothing to be done for `check'.
[…]
============================================================================
Testsuite summary for potrace 1.16
============================================================================
# TOTAL: 8
# PASS: 8
# SKIP: 0
# XFAIL: 0
# FAIL: 0
# XPASS: 0
# ERROR: 0
============================================================================
make[1]: Nothing to be done for `check-am'.
第 4 步,sudo make install
:
$ sudo make install
Password:
Making install in src
.././install-sh -c -d '/usr/local/bin'
/bin/sh ../libtool --mode=install /usr/bin/install -c potrace mkbitmap '/usr/local/bin'
[…]
make[2]: Nothing to be done for `install-data-am'.
如需检查是否有效,请运行 mkbitmap --version
:
$ mkbitmap --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.
如果您看到版本详细信息,就表示您已成功编译并安装了 mkbitmap
。接下来,使用 WebAssembly 执行与这些步骤等效的步骤。
将 mkbitmap
编译为 WebAssembly
Emscripten 是一款用于将 C/C++ 程序编译为 WebAssembly 的工具。Emscripten 的构建项目文档如下:
使用 Emscripten 构建大型项目非常容易。Emscripten 提供了两个简单的脚本,用于配置您的 makefile,以便使用
emcc
直接替代gcc
。在大多数情况下,项目当前构建系统的其余部分保持不变。
接下来,我们继续介绍以下文档(为简洁起见,我们稍微进行了修改):
设想一下您通常使用以下命令进行构建的情况:
./configure
make
如需使用 Emscripten 进行构建,您应改用以下命令:
emconfigure ./configure
emmake make
因此,从本质上讲,./configure
会变为 emconfigure ./configure
,make
会变为 emmake make
。以下代码演示了如何使用 mkbitmap
执行此操作。
第 0 步,make clean
:
$ make clean
Making clean in src
rm -f potrace mkbitmap
test -z "" || rm -f
rm -rf .libs _libs
[…]
rm -f *.lo
第 1 步,emconfigure ./configure
:
$ emconfigure ./configure
configure: ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
[…]
config.status: executing libtool commands
第 2 步,emmake make
:
$ emmake make
make: make
/Applications/Xcode.app/Contents/Developer/usr/bin/make all-recursive
Making all in src
/opt/homebrew/Cellar/emscripten/3.1.36/libexec/emcc -DHAVE_CONFIG_H -I. -I.. -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all'.
如果一切顺利,该目录中的某个位置现在应该有 .wasm
文件。您可以通过运行 find . -name "*.wasm"
找到它们:
$ find . -name "*.wasm"
./a.wasm
./src/mkbitmap.wasm
./src/potrace.wasm
最后两个命令看起来不错,因此请使用 cd
进入 src/
目录。现在还有两个新的对应文件:mkbitmap
和 potrace
。只有 mkbitmap
与本文相关。它们没有 .js
扩展程序,这一点有点令人困惑,但它们实际上是 JavaScript 文件,可通过快速的 head
调用进行验证:
$ cd src/
$ head -n 20 mkbitmap
// include: shell.js
// The Module object: Our interface to the outside world. We import
// and export values on it. There are various ways Module can be used:
// 1. Not defined. We create it here
// 2. A function parameter, function(Module) { ..generated code.. }
// 3. pre-run appended it, var Module = {}; ..generated code..
// 4. External script tag defines var Module.
// We need to check if Module already exists (e.g. case 3 above).
// Substitution will be replaced with actual code on later stage of the build,
// this way Closure Compiler will not mangle it (e.g. case 4. above).
// Note that if you want to run closure, and also to use Module
// after the generated code, you will need to define var Module = {};
// before the code. Then that object will be used in the code, and you
// can continue to use Module afterwards as well.
var Module = typeof Module != 'undefined' ? Module : {};
// --pre-jses are emitted after the Module integration code, so that they can
// refer to Module (if they choose; they can also define Module)
通过调用 mv mkbitmap mkbitmap.js
(如果需要,请分别调用 mv potrace potrace.js
)将 JavaScript 文件重命名为 mkbitmap.js
。现在,是时候运行 node mkbitmap.js --version
了,在命令行中使用 Node.js 执行第一个测试,看看测试是否有效:
$ node mkbitmap.js --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.
您已成功将 mkbitmap
编译为 WebAssembly。现在,下一步是使其在浏览器中正常运行。
在浏览器中使用 WebAssembly 执行 mkbitmap
将 mkbitmap.js
和 mkbitmap.wasm
文件复制到名为 mkbitmap
的新目录中,然后创建一个用于加载 mkbitmap.js
JavaScript 文件的 index.html
HTML 样板文件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>mkbitmap</title>
</head>
<body>
<script src="mkbitmap.js"></script>
</body>
</html>
启动提供 mkbitmap
目录的本地服务器,并在浏览器中打开该服务器。您应该会看到一条要求您提供输入的提示。这符合预期,因为根据该工具的手册页面,"[i]f 没有文件名参数,那么 mkbitmap 会充当过滤器,从标准输入读取数据",对 Emscripten 而言,其默认值为 prompt()
。
防止自动执行
如需阻止 mkbitmap
立即执行而改为等待用户输入,您需要了解 Emscripten 的 Module
对象。Module
是一个全局 JavaScript 对象,具有 Emscripten 生成的代码在其执行过程中的各个点调用的属性。您可以提供 Module
的实现来控制代码的执行。Emscripten 应用启动时,它会查看 Module
对象的值并应用这些值。
对于 mkbitmap
,请将 Module.noInitialRun
设置为 true
,以防止首次运行导致出现提示。创建一个名为 script.js
的脚本,将其添加到 index.html
中的 <script src="mkbitmap.js"></script>
之前,并将以下代码添加到 script.js
。现在重新加载应用后,提示应该就消失了。
var Module = {
// Don't run main() at page load
noInitialRun: true,
};
创建带有更多 build 标志的模块化 build
如需向应用提供输入,您可以在 Module.FS
中使用 Emscripten 的文件系统支持。该文档的添加文件系统支持部分指出:
Emscripten 会决定是否自动添加文件系统支持。很多程序都不需要文件,而且文件系统支持的大小同样可以忽略不计,因此 Emscripten 会在没有原因时避免包含此类文件。这意味着,如果您的 C/C++ 代码不访问文件,则
FS
对象和其他文件系统 API 将不会包含在输出中。另一方面,如果您的 C/C++ 代码使用文件,则系统会自动包括文件系统支持。
遗憾的是,mkbitmap
是 Emscripten 不会自动包含文件系统支持的一种情况,因此您需要明确指示它执行此操作。这意味着,您需要按照前面所述的 emconfigure
和 emmake
步骤操作,并通过 CFLAGS
参数设置更多标记。以下标志可能对其他项目也很有用。
- 设置
-sFILESYSTEM=1
以包含文件系统支持。 - 设置
-sEXPORTED_RUNTIME_METHODS=FS,callMain
,以便导出Module.FS
和Module.callMain
。 - 设置
-sMODULARIZE=1
和-sEXPORT_ES6
以生成新式 ES6 模块。 - 设置
-sINVOKE_RUN=0
可阻止首次运行导致显示提示。
此外,在此特定情况下,您需要将 --host
标志设置为 wasm32
,以告知 configure
脚本您要针对 WebAssembly 进行编译。
最终的 emconfigure
命令如下所示:
$ emconfigure ./configure --host=wasm32 CFLAGS='-sFILESYSTEM=1 -sEXPORTED_RUNTIME_METHODS=FS,callMain -sMODULARIZE=1 -sEXPORT_ES6 -sINVOKE_RUN=0'
别忘了再次运行 emmake make
,并将新创建的文件复制到 mkbitmap
文件夹。
修改 index.html
,使其仅加载 ES 模块 script.js
,然后从中导入 mkbitmap.js
模块。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>mkbitmap</title>
</head>
<body>
<!-- No longer load `mkbitmap.js` here -->
<script src="script.js" type="module"></script>
</body>
</html>
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
console.log(Module);
};
run();
现在在浏览器中打开应用时,您应该会看到 Module
对象已记录到开发者工具控制台中,并且提示已消失,因为启动时不再调用 mkbitmap
的 main()
函数。
手动执行 main 函数
下一步是通过运行 Module.callMain()
手动调用 mkbitmap
的 main()
函数。callMain()
函数接受一个参数数组,这些参数逐个匹配您要在命令行中传递的参数。如果要在命令行中运行 mkbitmap -v
,则应在浏览器中调用 Module.callMain(['-v'])
。这会将 mkbitmap
版本号记录到开发者工具控制台。
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
Module.callMain(['-v']);
};
run();
重定向标准输出
默认情况下,标准输出 (stdout
) 是控制台。不过,您可以将其重定向到其他内容,例如将输出存储到变量的函数。这意味着,您可以通过设置 Module.print
属性将输出添加到 HTML 中。
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
let consoleOutput = 'Powered by ';
const Module = await loadWASM({
print: (text) => (consoleOutput += text),
});
Module.callMain(['-v']);
document.body.textContent = consoleOutput;
};
run();
将输入文件获取到内存文件系统中
如需将输入文件放入内存文件系统中,您需要在命令行中使用 mkbitmap filename
的等效项。为了理解我是如何做到这一点的,首先,我们先介绍一些有关 mkbitmap
如何接收其输入并创建其输出的背景信息。
mkbitmap
支持的输入格式为 PNM(PBM、PGM、PPM)和 BMP。输出格式为 PBM(针对位图)和 PGM(针对灰图)。如果指定了 filename
参数,mkbitmap
将默认创建一个输出文件,该文件的名称会从输入文件名中获取,方法是将其后缀更改为 .pbm
。例如,对于输入文件名 example.bmp
,输出文件名将为 example.pbm
。
Emscripten 提供了一个模拟本地文件系统的虚拟文件系统,因此使用同步文件 API 的原生代码几乎无需更改或完全无需更改即可编译和运行。为了让 mkbitmap
读取输入文件,就像将其作为 filename
命令行参数传递一样,您需要使用 Emscripten 提供的 FS
对象。
FS
对象由内存文件系统(通常称为 MEMFS)提供支持,并且具有可用于将文件写入虚拟文件系统的 writeFile()
函数。您将使用 writeFile()
,如以下代码示例所示。
如需验证文件写入操作是否有效,请使用参数 '/'
运行 FS
对象的 readdir()
函数。您将看到example.bmp
和一些始终自动创建的默认文件。
请注意,之前用于打印版本号的 Module.callMain(['-v'])
调用已被移除。这是因为 Module.callMain()
是一个通常只应运行一次的函数。
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
console.log(Module.FS.readdir('/'));
};
run();
首次实际执行
一切就绪后,通过运行 Module.callMain(['example.bmp'])
来执行 mkbitmap
。记录 MEMFS 的 '/'
文件夹的内容,您应该会在 example.bmp
输入文件旁边看到新创建的 example.pbm
输出文件。
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
Module.callMain(['example.bmp']);
console.log(Module.FS.readdir('/'));
};
run();
从内存文件系统中获取输出文件
FS
对象的 readFile()
函数可用于将上一步中创建的 example.pbm
移出内存文件系统。该函数会返回一个 Uint8Array
,您可以将其转换为 File
对象并保存到磁盘,因为浏览器通常不支持在浏览器内直接查看 PBM 文件。
(保存文件还有更简洁的方法,但使用动态创建的 <a download>
是支持最广泛的方法。)文件保存后,您可以使用自己喜爱的图片查看器将其打开。
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
Module.callMain(['example.bmp']);
const output = Module.FS.readFile('example.pbm', { encoding: 'binary' });
const file = new File([output], 'example.pbm', {
type: 'image/x-portable-bitmap',
});
const a = document.createElement('a');
a.href = URL.createObjectURL(file);
a.download = file.name;
a.click();
};
run();
添加交互式界面
此时,输入文件已经过硬编码,并且 mkbitmap
会使用默认参数运行。最后一步是让用户动态选择输入文件,调整 mkbitmap
参数,然后使用所选选项运行该工具。
// Corresponds to `mkbitmap -o output.pbm input.bmp -s 8 -3 -f 4 -t 0.45`.
Module.callMain(['-o', 'output.pbm', 'input.bmp', '-s', '8', '-3', '-f', '4', '-t', '0.45']);
PBM 图片格式不是特别难以解析,因此借助一些 JavaScript 代码,您甚至可以显示输出图片的预览。请参阅下面的嵌入式演示的源代码,了解一种方式。
总结
恭喜,您已成功将 mkbitmap
编译为 WebAssembly,并使其可以在浏览器中正常运行!虽然经历了一些不通之处,您必须多次编译该工具才能正常运行,但正如我在前面所说,这是体验的一部分。如果遇到问题,还请记得 StackOverflow 的 webassembly
标记。祝您编译顺利!
致谢
本文由 Sam Clegg 和 Rachel Andrew 审核。