如何将 WebAssembly 集成到此设置中?在本文中,我们将以 C/C++ 和 Emscripten 为例来进行说明。
WebAssembly (wasm) 通常是 可作为性能基元或运行现有 C++ 代码的方式 代码库。通过 squoosh.app, 对 Wasm 至少还有第三种观点:利用大型语言模型 构建自己的代码生态系统。包含 Emscripten,您可以使用 C/C++ 代码、 Rust 内置了 wasm 支持,并且 Go 团队正在努力。我是 肯定会有很多其他语言的
在这些情况下, wasm 不是应用的核心,而是一个谜题 部分:另一个模块。您的应用已有 JavaScript、CSS、图片资源、 以 Web 为中心的构建系统,甚至是 React 这样的框架。你好 是否将 WebAssembly 集成到此设置中?在本文中,我们将 以 C/C++ 和 Emscripten 为例。
Docker
在使用 Emscripten 时,我发现 Docker 非常有用。C/C++ 通常是为了与构建它们的操作系统配合使用。 拥有一个一致的环境是非常有帮助的。借助 Docker,您可以获得 该虚拟化 Linux 系统已经设置为使用 Emscripten,并且 安装所有工具和依赖项如果缺少某些信息 而不必担心它会对您自己的计算机或您的 其他项目。如果出现问题,则丢弃该容器并启动 结束。如果运行一次,就可以确定它会一直运行 会产生相同的结果。
Docker Registry 包含一个 Emscripten 图片作者: trzeci - 我一直在使用它。
与 npm 集成
在大多数情况下,网络项目的入口点是 npm
package.json
。按照惯例,大多数项目都可以使用 npm install &&
npm run build
进行构建。
一般来说,Emscripten 生成的 build 工件(.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
是接下来您要编写的 Shell 脚本!--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
将 shell 置于“快速失败”状态模式。如果脚本中的任何命令
返回错误,整个脚本会立即被取消。可以是
这非常有用,因为脚本的最后输出
消息或导致构建失败的错误。
借助 export
语句,您可以定义一些环境的值,
变量。它们允许您将其他命令行参数传递到 C
编译器 (CFLAGS
)、C++ 编译器 (CXXFLAGS
) 和链接器 (LDFLAGS
)。
它们都通过 OPTIMIZE
接收优化器设置,以确保
一切都以同样的方式进行优化下面是一些可能的值
为 OPTIMIZE
变量指定以下值:
-O0
:不进行任何优化。消除了无死代码,Emscripten 也不会缩减其发出的 JavaScript 代码。适用于调试。-O3
:积极优化以提升效果。-Os
:积极优化以提升性能,将规模作为次要优化目标 条件。-Oz
:积极优化尺寸,但在必要时会牺牲效果。
对于 Web 应用,我主要推荐使用 -Os
。
emcc
命令自带大量选项。请注意,emcc
它应该“可轻松替代 GCC 或 Clang 等编译器”。因此,
GCC 中可能知道的标记很可能是由 emcc 实现的,
。-s
标志的特殊之处在于它允许我们配置 Emscripten。
。所有可用选项都可以在 Emscripten 的
settings.js
,
但这个文件可能会让人应接不暇以下是 Emscripten 标志的列表
对网站开发者而言最重要的几个方面:
--bind
启用 embind。-s STRICT=1
不再支持所有已废弃的构建选项。这样可以确保 确保代码以向前兼容的方式构建。-s ALLOW_MEMORY_GROWTH=1
允许在以下情况下自动增加内存: 。在写入时,Emscripten 将分配 16MB 的内存 。当您的代码分配内存块时,此选项将决定 这些操作会使整个 wasm 模块在内存被占用时 或者是允许胶水代码将总内存扩展到 来适应分配-s MALLOC=...
选择要使用的malloc()
实现。“emmalloc
”现为 专为 Emscripten 量身打造的小型malloc()
实现。通过 也可以使用dlmalloc
,这是一个成熟的malloc()
实现。仅您本人 如果您要分配大量小对象,则需要切换到dlmalloc
还是想要使用线程处理-s EXPORT_ES6=1
会将 JavaScript 代码转换为 ES6 模块,其中包含 适用于任何捆绑器的默认导出功能。还需要-s MODULARIZE=1
才能 。
以下标志并非总是必要的,或者仅有助于调试 用途:
-s FILESYSTEM=0
是与 Emscripten 相关的标志, 可以在 C/C++ 代码使用文件系统操作时为您模拟文件系统。 它会对编译的代码进行一些分析,以决定是否包含 文件系统模拟。不过,有时 而且您就需要支付巨额 70kB 的额外胶水 可能不需要的文件系统模拟代码。借助-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 输出的消息的开发者工具。](https://web.developers.google.cn/static/articles/emscripten-npm/image/devtools-showing-message-6e40fef465a41.png?authuser=8&hl=nl)
将 C/C++ 代码添加为依赖项
如果要为 Web 应用构建 C/C++ 库,其代码必须是
部分。您可以手动将代码添加到项目的代码库中
您也可以使用 npm 管理这些依赖项。假设
想在我的 Web 应用中使用 libvpx。libvpx
是一个 C++ 库,可使用 VP8(.webm
文件中使用的编解码器)对图像进行编码。
但是,libvpx 不在 npm 上,也没有 package.json
,因此我无法
直接使用 npm 安装
要解决这一难题,
napa。通过 napa,你可以安装任何 git
代码库网址作为依赖项添加到您的 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 会使用 configure
和 make
。幸运的是,Emscripten 可帮助确保 configure
和
make
使用 Emscripten 的编译器。为此,可使用封装容器
命令 emconfigure
和 emmake
:
# ... 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
显示通过 emscripten 输出的 libvpx 的 ABI 版本。](https://web.developers.google.cn/static/articles/emscripten-npm/image/devtools-showing-the-abi-ab9a0f24d2c5c.png?authuser=8&hl=nl)
您还会注意到,构建流程需要很长时间。原因
可能因构建时间较长而有所不同对于 libvpx,需要很长时间
每次运行时,它都会为 VP8 和 VP9 编译编码器和解码器
您的构建命令,即使源文件没有更改。即使是很小
对 my-module.cpp
进行更改将需要很长时间。这对于
有助于保留 libvpx 的 build 工件
第一次构建
实现此目的的方法之一是使用环境变量。
# ... 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
自定义构建环境
有时,库依赖于其他工具进行构建。如果这些依赖项
构建环境中缺失的所有文件,您需要
也可以自行添加举个例子,假设您还想构建
使用 doxygen 的 libvpx 文档。Doxygen 不是
但您可以使用 apt
进行安装。
如果您要在 build.sh
中执行此操作,则需要重新下载并安装
每次想要构建自己的库时都没问题这样不仅会是
浪费资源,但这样也会妨碍您在离线状态下处理项目。
这种情况下,可以构建您自己的 Docker 映像。Docker 映像
编写描述构建步骤的 Dockerfile
。Dockerfile
功能强大,并拥有大量
命令,但大部分
而只需使用 FROM
、RUN
和 ADD
就可以轻松实现。在此示例中:
FROM trzeci/emscripten
RUN apt-get update && \
apt-get install -qqy doxygen
使用 FROM
,您可以声明要使用的 Docker 映像作为起始映像
。我选择 trzeci/emscripten
作为基础,也就是你一直使用的图片
一直以来借助 RUN
,您可以指示 Docker 在
容器。无论这些命令对容器做出的任何更改现在
Docker 映像要确保您的 Docker 映像已构建完毕且
之前,您必须调整package.json
build.sh
位:
{
// ...
"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 中的命令没有更改 实际上不会在重新构建 Dockerfile 时重新运行该步骤改为 它会重复使用上次构建映像的图层。
以前,您必须付出一些努力才能避免每次都重新编译 libvpx
构建应用您可以改为将 libvpx 的构建说明移至
从 build.sh
复制到 Dockerfile
,以利用 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
时绑定装载。当然,也不需要
小睡片刻。