将新的 C/C++ 模块编译为 WebAssembly
当您用 C/C++ 等语言编写新的代码模块时,可以使用 Emscripten 等工具将其编译为 WebAssembly。让我们来看看它是如何工作的。
Emscripten 环境设置
首先,让我们设置必要的开发环境。
先决条件
按照以下说明获取 Emscripten SDK:https://emscripten.webassembly.net.cn/docs/getting_started/downloads.html
编译示例
环境设置完毕后,让我们看看如何使用它将 C 示例编译为 Wasm。使用 Emscripten 编译时,有多种选项可用,但我们将介绍的两种主要情况是
- 编译为 Wasm 并创建 HTML 来运行我们的代码,以及运行 Wasm 在 Web 环境中所需的所有 JavaScript“粘合”代码。
- 编译为 Wasm 并仅创建 JavaScript。
我们将在下面介绍这两种方法。
创建 HTML 和 JavaScript
这是我们将介绍的最简单的情况,您可以让 emscripten 生成运行代码所需的一切,包括 WebAssembly 在浏览器中的运行方式。
- 首先,我们需要一个示例来编译。复制以下简单的 C 示例,并将其保存在本地驱动器上新目录中的名为
hello.c
的文件中cpp#include <stdio.h> int main() { printf("Hello World\n"); return 0; }
- 现在,使用您用来进入 Emscripten 编译器环境的终端窗口,导航到与您的
hello.c
文件相同的目录,并运行以下命令bashemcc 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 并将其在浏览器中运行!
使用自定义 HTML 模板
有时您会想要使用自定义 HTML 模板。让我们看看如何做到这一点。
- 首先,将以下 C 代码保存在名为
hello2.c
的文件中,并将该文件保存在新目录中cpp#include <stdio.h> int main() { printf("Hello World\n"); return 0; }
- 在您的 emsdk 存储库中搜索
shell_minimal.html
文件。将其复制到您先前的新目录中的名为html_template
的子目录中。 - 现在,导航到您的新目录(同样,在 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 模板的路径。
- 我们指定了
- 现在让我们运行这个示例。上面的命令将生成
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++ 源代码中的函数会消失,或者我收到“没有函数可处理”?)。让我们看看它是如何工作的。
- 首先,将以下代码保存在名为
hello3.c
的文件中,并将该文件保存在新目录中默认情况下,Emscripten 生成的代码始终只调用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"); }
main()
函数,其他函数将被视为死代码并被删除。在函数名前面加上EMSCRIPTEN_KEEPALIVE
可以阻止这种情况发生。您还需要导入emscripten.h
库才能使用EMSCRIPTEN_KEEPALIVE
。注意:我们包括了
#ifdef
块,以便如果您尝试将其包含在 C++ 代码中,该示例仍将有效。由于 C 与 C++ 的名称修饰规则,否则这将导致错误,但在这里我们将其设置为,如果使用 C++,则将其视为外部 C 函数。 - 现在将包含
{{{ SCRIPT }}}
作为内容的html_template/shell_minimal.html
也添加到这个新目录中,只是为了方便(在实际的开发环境中,您显然会将它放在一个中心位置)。 - 现在让我们再次运行编译步骤。从最新的目录(以及在 Emscripten 编译器环境终端窗口中)内部,使用以下命令编译您的 C 代码。请注意,我们需要使用
NO_EXIT_RUNTIME
编译:否则,当main()
退出时,运行时将被关闭,调用已编译的代码将无效。这对于正确的 C 模拟是必要的:例如,确保atexit()
函数被调用。bashemcc -o hello3.html hello3.c --shell-file html_template/shell_minimal.html -s NO_EXIT_RUNTIME=1 -s "EXPORTED_RUNTIME_METHODS=['ccall']"
- 如果您再次在浏览器中加载示例,您将看到与之前相同的输出!
- 现在,我们需要从 JavaScript 运行新的
myFunction()
函数。首先,在文本编辑器中打开您的 hello3.html 文件。 - 添加一个
<button>
元素,如下所示,位于第一个打开的<script type='text/javascript'>
标签之上。html<button id="mybutton">Run myFunction</button>
- 现在,将以下代码添加到第一个
<script>
元素的末尾jsdocument.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()
调用导出的函数。
另请参阅
- emscripten.org — 了解有关 Emscripten 及其众多选项的更多信息。
- 使用 ccall/cwrap 从 JavaScript 调用已编译的 C 函数
- 为什么当我编译到 JavaScript 时,我的 C/C++ 源代码中的函数会消失,或者我收到“没有函数可处理”?
- 将现有的 C 模块编译为 WebAssembly