将新的 C/C++ 模块编译为 WebAssembly
当您使用 C/C++ 等语言编写了一个新的代码模块后,可以使用 Emscripten 等工具将其编译为 WebAssembly。让我们看看它是如何工作的。
Emscripten 环境设置
首先,让我们设置必要的开发环境。
预备知识
按照以下说明获取 Emscripten SDK:https://emscripten.cn/docs/getting_started/downloads.html
编译示例
环境设置完成后,让我们看看如何使用它将 C 示例编译为 Wasm。Emscripten 编译时有许多可用选项,但我们将介绍的两个主要场景是:
- 编译为 Wasm 并创建 HTML 来运行我们的代码,以及运行 Wasm 所需的所有 JavaScript “胶水”代码。
- 编译为 Wasm 并仅创建 JavaScript。
我们将在下面介绍这两种情况。
创建 HTML 和 JavaScript
这是我们将介绍的最简单的情况,即让 emscripten 生成您在浏览器中以 WebAssembly 形式运行代码所需的一切。
-
首先,我们需要一个示例来编译。复制以下简单的 C 示例,并将其保存在本地驱动器上的新目录中的一个名为
hello.c
的文件中。c#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 “胶水”代码,以便在 Web 环境中编译和实例化 Wasm 以便使用。
此时,在您的源目录中应该有:
- 二进制 Wasm 模块代码 (
hello.wasm
) - 一个 JavaScript 文件,包含用于在原生 C 函数、JavaScript/Wasm 之间进行转换的胶水代码 (
hello.js
) - 一个 HTML 文件,用于加载、编译、实例化您的 Wasm 代码,并在浏览器中显示其输出 (
hello.html
)
运行您的示例
现在,您只需在支持 WebAssembly 的浏览器中加载生成的 hello.html
即可。从 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 文件 — 有关更多信息,请参阅如何设置本地测试服务器?。
如果一切按计划进行,您应该会在网页上出现的 Emscripten 控制台以及浏览器的 JavaScript 控制台中看到“Hello world”的输出。恭喜,您刚刚将 C 编译为 WebAssembly 并在浏览器中运行了它!
使用自定义 HTML 模板
有时您会想使用自定义 HTML 模板。让我们看看如何做到这一点。
-
首先,将以下 C 代码保存在一个名为
hello2.c
的文件中,放在一个新的目录中。c#include <stdio.h> int main() { printf("Hello World\n"); return 0; }
-
在您的 emsdk 仓库中搜索
shell_minimal.html
文件。将其复制到您的新目录中的一个名为html_template
的子目录中。 -
现在导航到您的新目录(同样,在您的 Emscripten 编译器环境终端窗口中),然后运行以下命令:
bashemcc -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 模板的路径,您将通过该 HTML 运行您的示例。
- 我们指定了
-
现在让我们运行这个示例。上面的命令将生成
hello2.html
,它将与模板具有大致相同的内容,并添加了一些胶水代码来加载生成的 Wasm,运行它等。在浏览器中打开它,您将看到与上一个示例大致相同的输出。
编译为 JavaScript 模块
您可以指定仅输出 JavaScript “胶水”文件(Emscripten 需要大量 JavaScript “胶水”代码来处理内存分配、内存泄漏以及许多其他问题),而不是完整的 HTML,只需在 -o
标志中指定一个 .js 文件而不是 HTML 文件,如下所示:
emcc -o hello.js hello.c -O3
然后,您可以将此 JavaScript 文件合并到您的程序中,这在使用打包器并且不直接处理 HTML 时特别有用。例如,您可以导入生成的 JavaScript 胶水文件,使其作为副作用运行。在您的应用程序的入口模块中,添加:
import "./hello.js";
或者,您可以生成一个工厂模块,它允许您创建模块的多个实例(默认情况下,胶水代码会全局加载模块,导致多个实例发生冲突)。
emcc -o hello.mjs hello.c -O3 -sMODULARIZE
注意: 如果您的输出文件扩展名是 .js 而不是 .mjs,那么您必须添加 -sEXPORT_ES6
设置来输出 JavaScript 模块。
然后,在您的代码中导入工厂并调用它:
import createModule from "./hello.mjs";
createModule().then((Module) => {
console.log("Wasm ready", Module);
});
调用 C 中定义的自定义函数
如果您想从 JavaScript 调用 C 代码中定义的函数,您可以使用 Emscripten 的 ccall()
函数和 EMSCRIPTEN_KEEPALIVE
声明,它会将您的函数添加到已导出函数的列表中(请参阅为什么我 C/C++ 源代码中的函数在编译到 JavaScript 时会消失,以及/或者我收到“没有要处理的函数”?)。让我们看看它是如何工作的。
-
首先,将以下代码保存为
hello3.c
并放在一个新的目录中:c#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 函数。 -
现在,为了方便起见,也将
html_template/shell_minimal.html
与{{{ SCRIPT }}}
内容一起添加到这个新目录中(在您的真实开发环境中,您显然会将其放在一个中心位置)。 -
现在让我们再次运行编译步骤。从您的最后一个目录中(同时在您的 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="my-button">Run myFunction</button>
-
现在,在第一个
<script>
元素的末尾添加以下代码:jsdocument.getElementById("my-button").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 函数
- 为什么我 C/C++ 源代码中的函数在编译到 JavaScript 时会消失,以及/或者我收到“没有要处理的函数”?
- 将现有的 C 模块编译为 WebAssembly