Emscripten 和 npm

如何将 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 输出的消息的开发者工具。

将 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 会使用 configuremake。幸运的是,Emscripten 可帮助确保 configuremake 使用 Emscripten 的编译器。为此,可使用封装容器 命令 emconfigureemmake

# ... 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 版本。

您还会注意到,构建流程需要很长时间。原因 可能因构建时间较长而有所不同对于 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 功能强大,并且有很多 命令,但大部分 而只需使用 FROMRUNADD 就可以轻松实现。在此示例中:

FROM trzeci/emscripten

RUN apt-get update && \
    apt-get install -qqy doxygen

使用 FROM,您可以声明要使用的 Docker 映像作为起始映像 。我选择 trzeci/emscripten 作为基础,也就是你一直使用的图片 一直以来借助 RUN,您可以指示 Docker 在 容器。无论这些命令对容器做出的任何更改现在 Docker 映像要确保您的 Docker 映像已构建完毕且 之前,您必须调整package.jsonbuild.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 时绑定装载。当然,也不需要 小睡片刻。