WebAssembly介绍
WebAssembly,简单来说,是一种浏览器可执行的代码格式,诸如 C、C++ 和 Rust 等源语言能够编译为WebAssembly代码,并暴露可供js调用的函数,使得js能够利用接近原生的速度完成一些逻辑。
关于WebAssembly的介绍,MDN已经足够详细了,developer.mozilla.org/zh-CN/docs/… ,不再赘述。
Emscripten 介绍和安装
Emscripten 是一个基于 LLVM/Clang 的开源编译器工具链,核心是将 C/C++ 等原生代码编译为 WebAssembly(Wasm)和 JavaScript “胶水代码”,让高性能原生程序与库能在浏览器、Node.js 及其他 Wasm 运行时高效执行,性能接近原生,是原生代码向 Web 迁移的核心工具。更多信息可访问官网:emscripten.org/docs/index.…
安装
# Get the emsdk repo
git clone https://github.com/emscripten-core/emsdk.git
# Enter that directory
cd emsdk
# Fetch the latest version of the emsdk (not needed the first time you clone)
git pull
# Download and install the latest SDK tools.
./emsdk install latest
# Make the "latest" SDK "active" for the current user. (writes .emscripten file)
./emsdk activate latest
# Activate PATH and other environment variables in the current terminal
source ./emsdk_env.sh
注:windows系统用emsdk.bat替换上述命令中的emsdk,用emsdk_env.bat替换上述命令中的source ./emsdk_env.sh。
安装验证
在 Emscripten 的安装目录下打开终端,执行以下命令检查是否安装成功
./emcc --check
注:
-
windows系统,通过在文件中找到并打开emcmdprompt.bat来启动该提示符,在其他命令行中是找不到emcc的。
-
在 Windows 系统中,使用emcc而不是 ./emcc来调用该工具。
安装成功后check的输出如下:

入门实践
hello world
准备基础测试文件:
// test.c
#include <stdio.h>
int main() {
printf("Hello from Emscripten!\n");
return 0;
}
执行编译
emcc test.c -o test.html # 生成test.html + test.js + test.wasm
启动本地服务器,打开test.html

生成文件介绍
test.html
test.html 是 Emscripten 自动生成的完整 HTML 页面,本质是一个开箱即用的前端载体,核心作用是:
-
作为 Wasm/JS 代码的运行容器:内置了加载
test.js(胶水代码)的逻辑,无需你手动写 HTML 引入脚本;
-
提供基础的交互界面:默认包含一个简单的页面结构,以及控制台输出区域(用于显示 C 代码中
printf/puts 等函数的输出);
-
简化测试流程:直接在本地服务器打开就可以运行,无需额外编写 HTML 代码。
test.js
test.js 是整个编译产物的核心,也是 Emscripten 最关键的输出文件之一,被称为 “胶水代码”,作用是连接 JavaScript 环境和 Wasm 模块,填补两者的差异。
它包含以下核心功能(按执行顺序):
(1)Wasm 模块的加载与实例化
自动处理 test.wasm(编译时同步生成的 Wasm 文件)的加载、解析和实例化,无需你手动调用 WebAssembly.instantiate。
(2)Emscripten 运行时初始化
初始化 Wasm 运行所需的环境:
- 线性内存(Memory)管理;
- 虚拟文件系统(MEMFS/IDBFS)(模拟 C 的文件操作,如
fopen/fwrite);
- 系统调用模拟(如
exit/time);
- 错误处理和日志输出。
(3)C/JS 互操作桥梁
- 封装 C 函数的调用逻辑:把 Wasm 中的函数(如下划线前缀的
_main)映射为更友好的 JS 调用方式;
- 处理 C 标准库输出:把 C 代码中
printf/puts 的输出重定向到 HTML 页面的控制台区域;
- 暴露运行时 API:如
ccall/cwrap(用于 JS 调用 C 函数)、FS(文件系统操作)等。
(4)自动执行 C 的 main 函数
默认情况下,test.js 初始化完成后会自动调用 C 代码中的 main 函数,这也是为什么你编译的 C 程序能直接在浏览器中运行。
在实践中,我们不一定需要使用编译生成的胶水代码,很多时候自己手动管理wasm更方便。
test.wasm
执行 C 代码编译后的指令(如 main 函数、sum 函数)
js调用wasm函数
这个demo中我们不生成胶水代码,使用原生api调用wasm。
准备c文件:
// calc.c
int sum(int a, int b)
{
return a + b;
}
int minus(int a, int b)
{
return a - b;
}
执行编译
emcc calc.c -o calc.wasm -s EXPORTED_FUNCTIONS=_sum,_minus --no-entry
编写html和js加载wasm
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>WASM Calc Demo</title>
</head>
<body>
<h1>WASM Calculator</h1>
<div id="output"></div>
<script>
(async () => {
// 加载 wasm 文件
const response = await fetch("calc.wasm");
const buffer = await response.arrayBuffer();
const wasmModule = await WebAssembly.instantiate(buffer);
// 导出的方法都在 instance.exports 里
const { sum, minus } = wasmModule.instance.exports;
console.log(wasmModule);
// 调用并显示结果
const a = 7,
b = 3;
document.getElementById("output").innerHTML = `
sum(${a}, ${b}) = ${sum(a, b)}<br>
minus(${a}, ${b}) = ${minus(a, b)}
`;
})();
</script>
</body>
</html>
效果如下:

注:
- 在 C 语言的 ABI(应用二进制接口)规范里,大多数平台的链接器会为 C 全局符号加上一个下划线
_ 前缀。
Emscripten 也遵循了这个传统。你写的 int sum(int a, int b),编译后实际导出的符号是 _sum
- 一般情况下编译时会找main函数,如果不需要main,可以加参数
--no-entry,否则编译会报错
- EXPORTED_FUNCTIONS参数不是必须,编译时可以根据文件内容生成导出的函数。但如果函数既没有被 C 代码调用,也没有被正确导出,Emscripten 可能会认为它没用,直接去掉它。因此可以修改代码,添加
EMSCRIPTEN_KEEPALIVE
// calc.c
#include <emscripten/emscripten.h>
EMSCRIPTEN_KEEPALIVE
int sum(int a, int b)
{
return a + b;
}
EMSCRIPTEN_KEEPALIVE
int minus(int a, int b)
{
return a - b;
}
执行emcc calc.c -o calc.wasm -s --no-entry一样能得到两个函数
工作原理概览和LLVM/Clang介绍
Emscripten 的编译流程如下:
-
前端编译:Clang 将 C/C++ 源码编译为 LLVM IR。
-
中间优化:LLVM 与 Binaryen 对 IR 做代码精简、循环优化、死代码消除等。
-
后端生成:输出 Wasm 二进制模块(.wasm)与 JavaScript 胶水代码(处理 Wasm 加载、内存管理、API 绑定),可选直接生成可运行的 HTML。
C/C++ 源码
↓
Clang(LLVM 前端) → 生成 LLVM IR
↓
LLVM 优化器 → 优化 LLVM IR
↓
LLVM 后端 → 生成 原始 WebAssembly 模块
↓
Binaryen(wasm-opt) → 优化 Wasm
↓
最终输出:优化后的 .wasm
这里出现了一些名词:前端/后端/Clang/LLVM/IR,下面简单介绍下这些概念。
1. IR(Intermediate Representation,中间表示)
IR 是编译器在前端(源码解析) 和后端(目标代码生成) 之间的 “中间语言”,是连接不同源码语言和不同目标平台的桥梁。核心特点如下:
- 与源码语言无关:不管是 C/C++、Rust、Go 还是 Swift,只要能被 LLVM 前端编译,最终都会转换成统一的 IR。
- 与目标平台无关:IR 不包含任何 CPU 架构、操作系统的特有指令,只描述 “计算逻辑”。
- 分层设计:LLVM IR 分为 LLVM IR(文本 / 二进制形式) 和更底层的 Machine IR,前者用于跨平台优化,后者用于针对具体架构生成机器码。
2. LLVM(Low Level Virtual Machine)
LLVM(Low Level Virtual Machine)是一个开源的编译器基础设施项目,
它本质上是一套编译器前端和后端的集合,支持多种编程语言和目标平台。
-
前端:如 Clang,把 C/C++ 转成 LLVM IR(中间表示)
-
后端:把 LLVM IR 转成目标机器码(如 x86、ARM、WebAssembly)
Clang
Clang 是 LLVM 项目的一个核心子组件,本质是 LLVM 生态的C/C++/Objective-C 前端编译器,二者是框架与组件的关系 ——LLVM 提供通用的编译基础设施,Clang 负责将 C 系源码转换成 LLVM IR,再由 LLVM 的优化器和后端完成后续流程。
3. Binaryen
Binaryen 是一个专注于 WebAssembly(WASM)的工具链库和优化器,
由 WebAssembly 核心团队成员开发和维护(主要用 C++ 实现,带有 JS/Python 接口)。
它的目标是让 WebAssembly 代码更小、更快、更高效。