跳到主要内容

C/C++ 编译 WebAssembly

Emscripten 是将 C/C++ 代码编译为 WebAssembly 的主流工具链。本章介绍如何使用 Emscripten 开发 WebAssembly 应用。

Emscripten 简介

Emscripten 是一个开源的编译器工具链,可以将 C/C++ 代码编译为 WebAssembly。它提供了:

  • 完整的 C/C++ 支持:支持大部分 C/C++ 标准库
  • 自动生成胶水代码:生成 JavaScript 加载和运行代码
  • 模拟文件系统:支持标准文件 I/O 操作
  • OpenGL ES 支持:可以编译图形应用
  • 丰富的工具链:包括优化、调试等工具

安装 Emscripten

安装依赖

Emscripten 需要 Python 和 Git:

python --version
git --version

安装 Emscripten SDK

Linux/macOS:

git clone https://github.com/emscripten-core/emsdk.git
cd emsdk

./emsdk install latest
./emsdk activate latest

source ./emsdk_env.sh

Windows:

git clone https://github.com/emscripten-core/emsdk.git
cd emsdk

emsdk install latest
emsdk activate latest

emsdk_env.bat

验证安装

emcc --version

基本编译

Hello World

创建 hello.c

#include <stdio.h>

int main() {
printf("Hello, WebAssembly!\n");
return 0;
}

编译:

emcc hello.c -o hello.html

这会生成三个文件:

  • hello.html - HTML 页面
  • hello.js - JavaScript 胶水代码
  • hello.wasm - WebAssembly 二进制

编译选项

# 只生成 wasm 文件
emcc hello.c -o hello.wasm

# 生成 HTML 和 JS
emcc hello.c -o hello.html

# 指定输出文件名
emcc hello.c -s WASM=1 -o output.html

# 优化级别
emcc hello.c -O2 -o hello.html

# 调试模式
emcc hello.c -g -o hello.html

导出函数

使用 EMSCRIPTEN_KEEPALIVE

#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
return a + b;
}

EMSCRIPTEN_KEEPALIVE
int multiply(int a, int b) {
return a * b;
}

编译:

emcc math.c -o math.js -s EXPORTED_FUNCTIONS="['_add','_multiply']" -s EXPORTED_RUNTIME_METHODS="['ccall','cwrap']"

使用 ccall 和 cwrap

JavaScript 端调用:

// 使用 ccall 直接调用
const result = Module.ccall('add', 'number', ['number', 'number'], [1, 2]);

// 使用 cwrap 创建函数包装
const add = Module.cwrap('add', 'number', ['number', 'number']);
const multiply = Module.cwrap('multiply', 'number', ['number', 'number']);

console.log(add(1, 2)); // 3
console.log(multiply(3, 4)); // 12

函数签名

ccall 和 cwrap 的参数类型:

类型说明
number整数或浮点数
string字符串
array数组
boolean布尔值

内存操作

访问内存

#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
int* get_buffer() {
static int buffer[1024];
return buffer;
}

EMSCRIPTEN_KEEPALIVE
void set_value(int* buffer, int index, int value) {
buffer[index] = value;
}

EMSCRIPTEN_KEEPALIVE
int get_value(int* buffer, int index) {
return buffer[index];
}

JavaScript 端:

const buffer = Module._get_buffer();
const bufferIndex = buffer / 4; // 整数是 4 字节

Module._set_value(buffer, 0, 42);
console.log(Module._get_value(buffer, 0));

// 使用 HEAP32 直接访问
Module.HEAP32[bufferIndex] = 100;
console.log(Module.HEAP32[bufferIndex]);

内存视图

// 获取内存缓冲区
const memory = Module.wasmExports.memory;
const buffer = memory.buffer;

// 创建类型化数组视图
const int32View = new Int32Array(buffer);
const float64View = new Float64Array(buffer);
const uint8View = new Uint8Array(buffer);

字符串处理

传递字符串到 C

#include <emscripten.h>
#include <string.h>

EMSCRIPTEN_KEEPALIVE
int string_length(const char* str) {
return strlen(str);
}

EMSCRIPTEN_KEEPALIVE
void to_uppercase(char* str) {
for (int i = 0; str[i]; i++) {
if (str[i] >= 'a' && str[i] <= 'z') {
str[i] -= 32;
}
}
}

JavaScript 端:

// 分配内存并写入字符串
function allocateString(str) {
const length = Module.lengthBytesUTF8(str) + 1;
const ptr = Module._malloc(length);
Module.stringToUTF8(str, ptr, length);
return ptr;
}

// 释放内存
function freeString(ptr) {
Module._free(ptr);
}

// 使用
const strPtr = allocateString("Hello, World!");
const len = Module._string_length(strPtr);
Module._to_uppercase(strPtr);
const upper = Module.UTF8ToString(strPtr);
freeString(strPtr);

console.log(len); // 13
console.log(upper); // HELLO, WORLD

返回字符串

#include <emscripten.h>
#include <stdlib.h>
#include <string.h>

EMSCRIPTEN_KEEPALIVE
char* greet(const char* name) {
const char* prefix = "Hello, ";
const char* suffix = "!";
int len = strlen(prefix) + strlen(name) + strlen(suffix) + 1;

char* result = (char*)malloc(len);
strcpy(result, prefix);
strcat(result, name);
strcat(result, suffix);

return result;
}

EMSCRIPTEN_KEEPALIVE
void free_string(char* str) {
free(str);
}

JavaScript 端:

const namePtr = allocateString("WebAssembly");
const resultPtr = Module._greet(namePtr);
const greeting = Module.UTF8ToString(resultPtr);
Module._free_string(resultPtr);
freeString(namePtr);

console.log(greeting); // Hello, WebAssembly!

文件系统

Emscripten 提供了虚拟文件系统支持。

预加载文件

emcc main.c -o main.html --preload-file data.txt

使用 MEMFS

#include <stdio.h>
#include <emscripten.h>

int main() {
// 写入文件
FILE* f = fopen("/tmp/test.txt", "w");
fprintf(f, "Hello, File System!");
fclose(f);

// 读取文件
char buffer[100];
f = fopen("/tmp/test.txt", "r");
fgets(buffer, sizeof(buffer), f);
fclose(f);

printf("Content: %s\n", buffer);
return 0;
}

JavaScript 端操作文件系统:

// 写入文件
Module.FS.writeFile('/tmp/input.txt', 'Hello from JavaScript');

// 读取文件
const content = Module.FS.readFile('/tmp/input.txt', { encoding: 'utf8' });
console.log(content);

// 列出目录
const files = Module.FS.readdir('/tmp');

// 删除文件
Module.FS.unlink('/tmp/input.txt');

结构体和类

导出结构体

#include <emscripten.h>
#include <stdlib.h>

typedef struct {
int x;
int y;
} Point;

EMSCRIPTEN_KEEPALIVE
Point* create_point(int x, int y) {
Point* p = (Point*)malloc(sizeof(Point));
p->x = x;
p->y = y;
return p;
}

EMSCRIPTEN_KEEPALIVE
void free_point(Point* p) {
free(p);
}

EMSCRIPTEN_KEEPALIVE
int get_x(Point* p) {
return p->x;
}

EMSCRIPTEN_KEEPALIVE
int get_y(Point* p) {
return p->y;
}

EMSCRIPTEN_KEEPALIVE
void set_x(Point* p, int x) {
p->x = x;
}

EMSCRIPTEN_KEEPALIVE
void set_y(Point* p, int y) {
p->y = y;
}

JavaScript 端:

class Point {
constructor(x, y) {
this.ptr = Module._create_point(x, y);
}

get x() {
return Module._get_x(this.ptr);
}

set x(value) {
Module._set_x(this.ptr, value);
}

get y() {
return Module._get_y(this.ptr);
}

set y(value) {
Module._set_y(this.ptr, value);
}

destroy() {
Module._free_point(this.ptr);
}
}

const p = new Point(10, 20);
console.log(p.x, p.y);
p.x = 30;
console.log(p.x, p.y);
p.destroy();

C++ 类

#include <emscripten.h>
#include <emscripten/bind.h>

class Calculator {
private:
double result;

public:
Calculator() : result(0) {}

void add(double value) { result += value; }
void subtract(double value) { result -= value; }
void multiply(double value) { result *= value; }
void divide(double value) { result /= value; }
void clear() { result = 0; }
double get_result() const { return result; }
};

EMSCRIPTEN_BINDINGS(calculator) {
emscripten::class_<Calculator>("Calculator")
.constructor()
.function("add", &Calculator::add)
.function("subtract", &Calculator::subtract)
.function("multiply", &Calculator::multiply)
.function("divide", &Calculator::divide)
.function("clear", &Calculator::clear)
.function("getResult", &Calculator::get_result);
}

编译:

emcc calculator.cpp -o calculator.js --bind -s EXPORTED_RUNTIME_METHODS="['wasmExports']"

JavaScript 端:

const calc = new Module.Calculator();
calc.add(10);
calc.multiply(2);
calc.subtract(5);
console.log(calc.getResult()); // 15
calc.delete();

回调函数

从 C 调用 JavaScript

#include <emscripten.h>

typedef void (*Callback)(int);

EMSCRIPTEN_KEEPALIVE
void process_data(int value, Callback callback) {
int result = value * 2;
callback(result);
}

JavaScript 端:

const callbackPtr = Module.addFunction((result) => {
console.log('Result:', result);
}, 'vi');

Module._process_data(21, callbackPtr);
Module.removeFunction(callbackPtr);

使用 EM_ASM

#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
void log_message(const char* msg) {
EM_ASM(
console.log(UTF8ToString($0));
, msg);
}

EMSCRIPTEN_KEEPALIVE
int get_random() {
return EM_ASM_INT(
return Math.floor(Math.random() * 100);
);
}

优化选项

优化级别

# 无优化,用于调试
emcc main.c -O0 -o main.html

# 基本优化
emcc main.c -O1 -o main.html

# 中等优化
emcc main.c -O2 -o main.html

# 最大优化
emcc main.c -O3 -o main.html

# 优化体积
emcc main.c -Os -o main.html

# 优化体积并移除未使用代码
emcc main.c -Oz -o main.html

减小体积

# 移除未使用的函数
emcc main.c -s DEAD_FUNCTIONS_ELIMINATION=1

# 压缩输出
emcc main.c --closure 1

# 移除运行时代码
emcc main.c -s MINIMAL_RUNTIME=1

# 禁用异常处理
emcc main.c -s DISABLE_EXCEPTION_CATCHING=1

# 禁用文件系统
emcc main.c -s NO_FILESYSTEM=1

性能优化

# 启用 SIMD
emcc main.c -msimd128 -o main.html

# 启用多线程
emcc main.c -pthread -s PTHREAD_POOL_SIZE=4

# 内存增长
emcc main.c -s ALLOW_MEMORY_GROWTH=1

常用编译选项

选项说明
-s WASM=1输出 WebAssembly
-s EXPORTED_FUNCTIONS指定导出函数
-s EXPORTED_RUNTIME_METHODS导出运行时方法
-s MODULARIZE=1生成模块化代码
-s EXPORT_NAME模块名称
-s ENVIRONMENT目标环境
-s ALLOW_MEMORY_GROWTH允许内存增长
-s INITIAL_MEMORY初始内存大小
-s MAXIMUM_MEMORY最大内存大小
-s STACK_SIZE栈大小
--preload-file预加载文件
--bind启用 Embind

调试

启用调试信息

emcc main.c -g -o main.html

使用 console.log

#include <emscripten.h>

void debug_log(const char* message) {
emscripten_log(EM_LOG_CONSOLE, "%s", message);
}

源码映射

emcc main.c -gsource-map -o main.html

完整示例

图像处理

#include <emscripten.h>
#include <stdlib.h>
#include <stdint.h>

EMSCRIPTEN_KEEPALIVE
void grayscale(uint8_t* pixels, int width, int height) {
int size = width * height * 4;
for (int i = 0; i < size; i += 4) {
uint8_t r = pixels[i];
uint8_t g = pixels[i + 1];
uint8_t b = pixels[i + 2];
uint8_t gray = (r + g + b) / 3;
pixels[i] = gray;
pixels[i + 1] = gray;
pixels[i + 2] = gray;
}
}

EMSCRIPTEN_KEEPALIVE
void invert(uint8_t* pixels, int width, int height) {
int size = width * height * 4;
for (int i = 0; i < size; i += 4) {
pixels[i] = 255 - pixels[i];
pixels[i + 1] = 255 - pixels[i + 1];
pixels[i + 2] = 255 - pixels[i + 2];
}
}

EMSCRIPTEN_KEEPALIVE
void brightness(uint8_t* pixels, int width, int height, int value) {
int size = width * height * 4;
for (int i = 0; i < size; i += 4) {
pixels[i] = (pixels[i] + value > 255) ? 255 : pixels[i] + value;
pixels[i + 1] = (pixels[i + 1] + value > 255) ? 255 : pixels[i + 1] + value;
pixels[i + 2] = (pixels[i + 2] + value > 255) ? 255 : pixels[i + 2] + value;
}
}

编译:

emcc image.c -o image.js -s EXPORTED_FUNCTIONS="['_grayscale','_invert','_brightness']" -s EXPORTED_RUNTIME_METHODS="['ccall','cwrap','HEAPU8']"

JavaScript 端:

const grayscale = Module.cwrap('grayscale', null, ['number', 'number', 'number']);
const invert = Module.cwrap('invert', null, ['number', 'number', 'number']);
const brightness = Module.cwrap('brightness', null, ['number', 'number', 'number', 'number']);

function processImage(imageData, operation, value = 0) {
const ptr = Module._malloc(imageData.data.length);
Module.HEAPU8.set(imageData.data, ptr);

switch (operation) {
case 'grayscale':
grayscale(ptr, imageData.width, imageData.height);
break;
case 'invert':
invert(ptr, imageData.width, imageData.height);
break;
case 'brightness':
brightness(ptr, imageData.width, imageData.height, value);
break;
}

const result = new Uint8ClampedArray(Module.HEAPU8.buffer, ptr, imageData.data.length);
imageData.data.set(result);
Module._free(ptr);

return imageData;
}

下一步

掌握 C/C++ 编译 WebAssembly 后,你可以继续学习: