杰瑞科技汇

em plant教程该怎么学?

Emscripten 教程:从 C/C++ 到 WebAssembly 的完整指南

目录

  1. 什么是 Emscripten?
  2. 为什么使用 Emscripten?
  3. 环境准备
    • 安装 Emscripten SDK
    • 验证安装
  4. 你的第一个 "Hello, World!" 程序
    • 编译 C 代码
    • 运行生成的 Web 应用
  5. 深入理解编译过程
    • 生成的文件 (*.js, *.wasm, *.html)
    • emcc 常用选项
  6. 与 JavaScript 交互
    • 从 C/C++ 调用 JavaScript
    • 从 JavaScript 调用 C/C++ 函数
  7. 处理 DOM 和浏览器 API
    • 使用 emscripten.h 提供的辅助函数
  8. 进阶主题
    • 使用 C++ 和 STL
    • 文件系统 (--embed-file--preload-file)
  9. 性能优化
    • 指定编译目标 (-O1, -O2, -O3)
    • 启用 SIMD
    • 内存模型 (ALLOW_MEMORY_GROWTH)
  10. 总结与资源

什么是 Emscripten?

Emscripten 是一个 LLVM 到 JavaScript 的编译器,它将用 C/C++ (以及其他可以被 LLVM 前端处理的语言) 编写的代码编译成 WebAssemblyJavaScript

em plant教程该怎么学?-图1
(图片来源网络,侵删)

你可以把它想象成一个特殊的“翻译官”,它能听懂 C/C++ 的语言,并将其翻译成浏览器能够理解和运行的 WebAssembly 代码,为了在浏览器中启动和运行 WebAssembly,它还会生成一个“胶水” JavaScript 文件。

核心组件:

  • emcc: 命令行工具,是使用 Emscripten 的主要入口。
  • LLVM: Emscripten 依赖 LLVM 来分析和优化 C/C++ 代码。
  • Clang: Emscripten 使用 Clang 作为其 C/C++ 前端,将代码解析成 LLVM 的中间表示。
  • Binaryen: 一个用于 WebAssembly 的优化器后端。

为什么使用 Emscripten?

将现有的 C/C++ 代码库(如游戏引擎、科学计算库、图像处理工具)带到 Web 上,而无需用 JavaScript 重写。

  • 性能: WebAssembly 提供了接近原生的性能,非常适合计算密集型任务,如游戏、物理模拟、视频/音频处理等。
  • 代码复用: 你可以为 Web、桌面、移动设备等多个平台使用同一套核心 C++ 代码库。
  • 利用现有生态: 可以轻松地将像 OpenCV、SQLite、TensorFlow 等成熟的 C++ 库带到浏览器中。
  • 类型安全与工具链: C++ 的静态类型系统和成熟的开发工具链可以带来更稳定、更易于维护的大型项目。

环境准备

Emscripten 依赖于 Python 3, Node.js, 和一些系统工具,最推荐的方式是使用官方提供的 SDK,它会自动处理所有依赖。

em plant教程该怎么学?-图2
(图片来源网络,侵删)

安装 Emscripten SDK

  1. 确保你有以下工具:

    • Python 3.6+
    • Node.js (推荐 v14+)
    • 一个现代的 C++ 编译器 (如 GCC/Clang 或 MSVC),虽然 Emscripten 主要使用自己的工具,但有时系统编译器用于构建某些依赖。
  2. 下载并设置 SDK: 打开你的终端 (Windows 上是 PowerShell 或 Git Bash),然后运行以下命令:

    # 克隆 Emscripten SDK 仓库
    git clone https://github.com/emscripten-core/emsdk.git
    # 进入 emsdk 目录
    cd emsdk
    # 下载最新的工具 (可能需要几分钟)
    ./emsdk install latest
    # 激活最新的工具
    ./emsdk activate latest
    # 设置环境变量 (非常重要!)
    # 在 Linux/macOS 上:
    source ./emsdk_env.sh
    # 在 Windows (PowerShell) 上:
    .\emsdk_env.ps1

验证安装

在同一个终端窗口中,运行以下命令来检查 emcc 是否可用:

emcc -v

如果看到版本信息和一堆编译器输出,说明安装成功了!

em plant教程该怎么学?-图3
(图片来源网络,侵删)

重要提示: 每次打开一个新的终端窗口时,都需要先进入 emsdk 目录并运行 source ./emsdk_env.sh (或 Windows 上的等效命令) 来设置环境变量,为了避免麻烦,你可以将这个命令添加到你 shell 的配置文件中 (如 ~/.bashrc, ~/.zshrc, 或 Windows 的环境变量)。

你的第一个 "Hello, World!" 程序

让我们从最简单的例子开始。

创建 C 源文件

创建一个名为 hello.c 的文件,内容如下:

// hello.c
#include <stdio.h>
int main() {
    printf("Hello, World from C!\n");
    return 0;
}

编译 C 代码

在终端中,使用 emcc 命令来编译它:

emcc hello.c -o hello.html

这会生成三个文件:

  • hello.html: 一个简单的 HTML 文件,用于加载和运行你的程序。
  • hello.js: 胶水 JavaScript 文件,负责加载 WebAssembly 模块并与浏览器交互。
  • hello.wasm: 你的 C 代码编译成的 WebAssembly 模块。

运行生成的 Web 应用

Emscripten 自带了一个简单的本地服务器,可以避免浏览器的 CORS (跨域资源共享) 限制。

在终端中运行:

emrun hello.html

它会提示你打开一个浏览器地址 (通常是 http://localhost:8080),在浏览器中访问这个地址,你将在开发者工具的控制台中看到输出:

Hello, World from C!

深入理解编译过程

生成的文件

  • hello.wasm: 核心,包含了你 C 代码编译后的机器码,浏览器可以直接执行。
  • hello.js: 运行时库和胶水代码,它负责:
    • 从服务器下载 hello.wasm 文件。
    • 编译和实例化 WebAssembly 模块。
    • 提供一个 Module 对象,让 JavaScript 可以与 Wasm 交互。
    • 处理像 printf 这样的函数,将 C 的标准输出重定向到浏览器的控制台。
  • hello.html: 一个基础的 HTML 模板,它会自动加载并运行 hello.js

emcc 常用选项

  • -o <output_file>: 指定输出文件名,Emscripten 会根据扩展名决定生成什么。
    • hello.js: 只生成 JS 文件 (用于 Node.js 或作为库)。
    • hello.html: 生成一个可立即在浏览器中运行的完整 Web 应用。
    • hello.wasm: 只生成 Wasm 文件 (不常用,因为无法独立运行)。
  • -s <OPTION_NAME>=<value>: 设置 Emscripten 的特殊标志,这是最强大的选项之一。
    • -s ALLOW_MEMORY_GROWTH=1 允许 Wasm 模块在运行时动态增加内存。
  • -O1, --O2, -O3: 优化级别,类似 GCC/Clang。-O3 性能最高,但编译时间最长,生成的文件也最大,对于发布版本,推荐使用 -O2-O3
  • --bind: 在编译 C++ 代码时使用,它会自动生成“胶水”代码,使 C++ 类和函数更容易被 JavaScript 调用。

与 JavaScript 交互

这是 Emscripten 的核心功能之一。

从 C/C++ 调用 JavaScript

使用 EM_ASM 宏,它允许你在 C 代码中直接嵌入 JavaScript 代码。

修改 hello.c:

// hello_interactive.c
#include <stdio.h>
#include <emscripten.h> // 必须包含此头文件
int main() {
    printf("C: Hello, World!\n");
    // 调用一段简单的 JavaScript 代码
    EM_ASM({
        console.log("JS: Hello from inside EM_ASM!");
    });
    return 0;
}

编译并运行:

emcc hello_interactive.c -o hello_interactive.html --bind
emrun hello_interactive.html

输出:

C: Hello, World!
JS: Hello from inside EM_ASM!

EM_ASM 可以接收 C/C++ 的变量作为参数:

int number = 42;
const char* text = "from C";
EM_ASM({
    console.log("The number is:", $0, "and the text is:", $1);
}, number, text);

$0, $1 等占位符会被 C/C++ 的变量替换。

从 JavaScript 调用 C/C++ 函数

这个过程稍微复杂一些,需要两步:

  1. 在 C/C++ 中使用 EMSCRIPTEN_KEEPALIVE 标记要导出的函数。
  2. 在 JavaScript 中通过 Module.cwrapModule.asm 来获取并调用该函数。

C 代码 (function.c):

#include <emscripten.h>
// 这个函数将被 JavaScript 调用
int add(int a, int b) {
    return a + b;
}
// EMSCRIPTEN_KEEPALIVE 告诉编译器不要优化掉这个函数
EMSCRIPTEN_KEEPALIVE
int multiply(int a, int b) {
    return a * b;
}

HTML (function.html):

<!DOCTYPE html>
<html>
<head>JS to C Call</title>
</head>
<body>
    <h1>Check the console</h1>
    <script src="function.js"></script>
    <script>
        // Module.cwrap 用于导出 C 函数,使其像普通的 JS 函数一样被调用
        // 参数: C函数名, 返回值类型, 参数类型数组
        const addFunction = Module.cwrap('add', 'number', ['number', 'number']);
        const multiplyFunction = Module.cwrap('multiply', 'number', ['number', 'number']);
        // 现在可以像调用普通 JS 函数一样调用它们
        const sum = addFunction(5, 7);
        const product = multiplyFunction(5, 7);
        console.log(`5 + 7 = ${sum}`);
        console.log(`5 * 7 = ${product}`);
    </script>
</body>
</html>

编译:

emcc function.c -o function.html --bind

运行后,在浏览器控制台可以看到:

5 + 7 = 12
5 * 7 = 35

处理 DOM 和浏览器 API

直接在 C 中调用 document.getElementById 是不可能的,你需要通过 EM_ASMemscripten.h 提供的辅助函数。

使用 emscripten.h 中的函数是更现代和推荐的方式:

#include <stdio.h>
#include <emscripten.h>
#include <emscripten/html5.h>
void printMessage() {
    printf("C: About to update the DOM.\n");
    // EM_ASM 可以直接操作 DOM
    EM_ASM({
        document.body.innerHTML += '<p>JS: This paragraph was added from C!</p>';
    });
}
int main() {
    printMessage();
    return 0;
}

进阶主题

使用 C++ 和 STL

Emscripten 完全支持 C++ 和 STL,你只需要编译 .cpp 文件即可。

vector_example.cpp:

#include <vector>
#include <iostream>
#include <emscripten.h>
extern "C" { // 使用 C 链接约定,方便 cwrap 调用
    EMSCRIPTEN_KEEPALIVE
    int sumVector(const int* array, int length) {
        std::vector<int> v(array, array + length);
        int sum = 0;
        for (int num : v) {
            sum += num;
        }
        return sum;
    }
}

vector.html:

<script src="vector.js"></script>
<script>
    const array = new Int32Array([1, 2, 3, 4, 5]);
    const sumFunction = Module.cwrap('sumVector', 'number', ['array', 'number']);
    const result = sumFunction(array, array.length);
    console.log(`The sum is: ${result}`); // 输出: The sum is: 15
</script>

编译:

emcc vector_example.cpp -o vector.html -s ALLOW_MEMORY_GROWTH=1 -O3

注意: 使用 STL 时,Wasm 文件会变大,因为需要包含 STL 的库代码。-s ALLOW_MEMORY_GROWTH=1 对于处理动态大小的 STL 容器(如 std::vector)非常有用。

文件系统

你可以将 C 程序需要的文件(如图片、文本、数据)打包进最终的 Wasm 文件中。

file.c:

#include <stdio.h>
#include <emscripten.h>
int main() {
    FILE *file = fopen("my_data.txt", "r");
    if (file) {
        char buffer[256];
        while (fgets(buffer, sizeof(buffer), file)) {
            printf("C read from file: %s", buffer);
        }
        fclose(file);
    } else {
        printf("C could not open my_data.txt\n");
    }
    return 0;
}

编译时嵌入文件:

emcc file.c -o file.html --embed-file my_data.txt

这会生成 file.html, file.js, file.wasmfile.js 会自动处理 my_data.txt 的加载,使其在 Wasm 文件内部可被 fopen 访问。

性能优化

  • 优化级别: 使用 -O2-O3 进行发布编译。
  • SIMD: 对于计算密集型任务,启用 SIMD (单指令多数据流) 可以显著提升性能。
    emcc ... -s USE_SAFESIMD=1
  • 内存增长: 如果你的应用需要动态分配大量内存(加载大文件或使用 STL),启用 ALLOW_MEMORY_GROWTH
    emcc ... -s ALLOW_MEMORY_GROWTH=1
  • 剥离调试信息: 发布时,剥离调试信息可以减小文件大小。
    emcc ... -g0

总结与资源

Emscripten 是一个功能极其强大的工具,它为 Web 开发者打开了利用整个 C/C++ 生态的大门,虽然初看起来可能有些复杂,但掌握了基本流程(编译、交互、优化)后,你就能将几乎任何高性能的桌面应用带到浏览器中。

官方资源

希望这份教程能帮助你顺利入门 Emscripten!祝你编码愉快!

分享:
扫描分享到社交APP
上一篇
下一篇