WebAssembly入门(一)——Emscripten

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....

安装

bash 复制代码
# 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 的安装目录下打开终端,执行以下命令检查是否安装成功

bash 复制代码
./emcc --check

注:

  • windows系统 ,通过在文件中找到并打开emcmdprompt.bat来启动该提示符,在其他命令行中是找不到emcc的。
  • 在 Windows 系统中,使用emcc 而不是 ./emcc来调用该工具。

安装成功后check的输出如下:

入门实践

hello world

准备基础测试文件:

c 复制代码
// test.c
#include <stdio.h>
int main() {
    printf("Hello from Emscripten!\n");
    return 0;
}

执行编译

bash 复制代码
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文件:

c 复制代码
// calc.c
int sum(int a, int b)
{
    return a + b;
}
int minus(int a, int b)
{
    return a - b;
}

执行编译

bash 复制代码
emcc calc.c -o calc.wasm  -s EXPORTED_FUNCTIONS=_sum,_minus --no-entry

编写html和js加载wasm

html 复制代码
<!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
c 复制代码
// 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 的编译流程如下:

  1. 前端编译:Clang 将 C/C++ 源码编译为 LLVM IR。
  2. 中间优化:LLVM 与 Binaryen 对 IR 做代码精简、循环优化、死代码消除等。
  3. 后端生成:输出 Wasm 二进制模块(.wasm)与 JavaScript 胶水代码(处理 Wasm 加载、内存管理、API 绑定),可选直接生成可运行的 HTML。
javascript 复制代码
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 代码更小、更快、更高效

相关推荐
恋猫de小郭2 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
牛奔3 小时前
Go 如何避免频繁抢占?
开发语言·后端·golang
想用offer打牌8 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
KYGALYX9 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅10 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
爬山算法10 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate