将新的 C/C++ 模块编译为 WebAssembly

当您用 C/C++ 等语言编写新的代码模块时,可以使用 Emscripten 等工具将其编译为 WebAssembly。让我们来看看它是如何工作的。

Emscripten 环境设置

首先,让我们设置必要的开发环境。

先决条件

编译示例

环境设置完毕后,让我们看看如何使用它将 C 示例编译为 Wasm。使用 Emscripten 编译时,有多种选项可用,但我们将介绍的两种主要情况是

  • 编译为 Wasm 并创建 HTML 来运行我们的代码,以及运行 Wasm 在 Web 环境中所需的所有 JavaScript“粘合”代码。
  • 编译为 Wasm 并仅创建 JavaScript。

我们将在下面介绍这两种方法。

创建 HTML 和 JavaScript

这是我们将介绍的最简单的情况,您可以让 emscripten 生成运行代码所需的一切,包括 WebAssembly 在浏览器中的运行方式。

  1. 首先,我们需要一个示例来编译。复制以下简单的 C 示例,并将其保存在本地驱动器上新目录中的名为 hello.c 的文件中
    cpp
    #include <stdio.h>
    
    int main() {
        printf("Hello World\n");
        return 0;
    }
    
  2. 现在,使用您用来进入 Emscripten 编译器环境的终端窗口,导航到与您的 hello.c 文件相同的目录,并运行以下命令
    bash
    emcc hello.c -o hello.html
    

我们通过命令传入的选项如下

  • -o hello.html — 指定我们希望 Emscripten 生成一个 HTML 页面来运行我们的代码(以及要使用的文件名),以及 Wasm 模块和 JavaScript“粘合”代码来编译和实例化 Wasm,以便它可以在 Web 环境中使用。

此时,您的源目录中应该有

  • 二进制 Wasm 模块代码 (hello.wasm)
  • 一个 JavaScript 文件,其中包含粘合代码,用于在原生 C 函数与 JavaScript/Wasm 之间进行转换 (hello.js)
  • 一个 HTML 文件,用于加载、编译和实例化您的 Wasm 代码,并在浏览器中显示其输出 (hello.html)

运行您的示例

现在,剩下的就是将生成的 hello.html 加载到支持 WebAssembly 的浏览器中。从 Firefox 52、Chrome 57、Edge 57、Opera 44 开始,它默认启用。

注意:如果您尝试直接从本地硬盘驱动器打开生成的 HTML 文件 (hello.html)(例如 file://your_path/hello.html),您将收到类似于 both async and sync fetching of the wasm failed 的错误消息。您需要通过 HTTP 服务器 (http://) 运行 HTML 文件 — 有关更多信息,请参阅 如何设置本地测试服务器?

如果一切按计划进行,您应该在网页上和浏览器的 JavaScript 控制台中看到 Emscripten 控制台中显示的“Hello world”输出。恭喜,您刚刚将 C 编译为 WebAssembly 并将其在浏览器中运行! image

使用自定义 HTML 模板

有时您会想要使用自定义 HTML 模板。让我们看看如何做到这一点。

  1. 首先,将以下 C 代码保存在名为 hello2.c 的文件中,并将该文件保存在新目录中
    cpp
    #include <stdio.h>
    
    int main() {
        printf("Hello World\n");
        return 0;
    }
    
  2. 在您的 emsdk 存储库中搜索 shell_minimal.html 文件。将其复制到您先前的新目录中的名为 html_template 的子目录中。
  3. 现在,导航到您的新目录(同样,在 Emscripten 编译器环境终端窗口中),并运行以下命令
    bash
    emcc -o hello2.html hello2.c -O3 --shell-file html_template/shell_minimal.html
    
    我们这次传入的选项略有不同
    • 我们指定了 -o hello2.html,这意味着编译器仍然会输出 JavaScript 粘合代码和 .html
    • 我们指定了 -O3,用于优化代码。Emcc 具有与其他 C 编译器相同的优化级别,包括:-O0(无优化)、-O1-O2-Os-Oz-Og-O3-O3 是发行版构建的良好设置。
    • 我们还指定了 --shell-file html_template/shell_minimal.html — 这提供了要用于创建将运行示例的 HTML 的 HTML 模板的路径。
  4. 现在让我们运行这个示例。上面的命令将生成 hello2.html,它将具有与模板相同的内容,并添加了一些粘合代码来加载生成的 Wasm、运行它等等。在浏览器中打开它,您将看到与上一个示例相同的输出。

注意:您可以通过在 -o 标志中指定 .js 文件而不是 HTML 文件来指定仅输出 JavaScript“粘合”文件*,而不是完整的 HTML,例如 emcc -o hello2.js hello2.c -O3。然后,您可以从头开始构建自己的自定义 HTML,尽管这是一种高级方法;使用提供的 HTML 模板通常更容易。

  • Emscripten 需要大量 JavaScript“粘合”代码来处理内存分配、内存泄漏以及许多其他问题

调用在 C 中定义的自定义函数

如果您想从 JavaScript 调用在 C 代码中定义的函数,可以使用 Emscripten ccall() 函数和 EMSCRIPTEN_KEEPALIVE 声明,它将您的函数添加到导出的函数列表中(请参阅 为什么当我编译到 JavaScript 时,我的 C/C++ 源代码中的函数会消失,或者我收到“没有函数可处理”?)。让我们看看它是如何工作的。

  1. 首先,将以下代码保存在名为 hello3.c 的文件中,并将该文件保存在新目录中
    cpp
    #include <stdio.h>
    #include <emscripten/emscripten.h>
    
    int main() {
        printf("Hello World\n");
        return 0;
    }
    
    #ifdef __cplusplus
    #define EXTERN extern "C"
    #else
    #define EXTERN
    #endif
    
    EXTERN EMSCRIPTEN_KEEPALIVE void myFunction(int argc, char ** argv) {
        printf("MyFunction Called\n");
    }
    
    默认情况下,Emscripten 生成的代码始终只调用 main() 函数,其他函数将被视为死代码并被删除。在函数名前面加上 EMSCRIPTEN_KEEPALIVE 可以阻止这种情况发生。您还需要导入 emscripten.h 库才能使用 EMSCRIPTEN_KEEPALIVE

    注意:我们包括了 #ifdef 块,以便如果您尝试将其包含在 C++ 代码中,该示例仍将有效。由于 C 与 C++ 的名称修饰规则,否则这将导致错误,但在这里我们将其设置为,如果使用 C++,则将其视为外部 C 函数。

  2. 现在将包含 {{{ SCRIPT }}} 作为内容的 html_template/shell_minimal.html 也添加到这个新目录中,只是为了方便(在实际的开发环境中,您显然会将它放在一个中心位置)。
  3. 现在让我们再次运行编译步骤。从最新的目录(以及在 Emscripten 编译器环境终端窗口中)内部,使用以下命令编译您的 C 代码。请注意,我们需要使用 NO_EXIT_RUNTIME 编译:否则,当 main() 退出时,运行时将被关闭,调用已编译的代码将无效。这对于正确的 C 模拟是必要的:例如,确保 atexit() 函数被调用。
    bash
    emcc -o hello3.html hello3.c --shell-file html_template/shell_minimal.html -s NO_EXIT_RUNTIME=1 -s "EXPORTED_RUNTIME_METHODS=['ccall']"
    
  4. 如果您再次在浏览器中加载示例,您将看到与之前相同的输出!
  5. 现在,我们需要从 JavaScript 运行新的 myFunction() 函数。首先,在文本编辑器中打开您的 hello3.html 文件。
  6. 添加一个 <button> 元素,如下所示,位于第一个打开的 <script type='text/javascript'> 标签之上。
    html
    <button id="mybutton">Run myFunction</button>
    
  7. 现在,将以下代码添加到第一个 <script> 元素的末尾
    js
    document.getElementById("mybutton").addEventListener("click", () => {
      alert("check console");
      const result = Module.ccall(
        "myFunction", // name of C function
        null, // return type
        null, // argument types
        null, // arguments
      );
    });
    

这说明了如何使用 ccall() 调用导出的函数。

另请参阅