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 后,你可以继续学习: