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 代码更小、更快、更高效

相关推荐
颜淡慕潇20 小时前
Spring Boot 3.3.x、3.4.x、3.5.x 深度对比与演进分析
java·后端·架构
贵州数擎科技有限公司20 小时前
一批优质 AI 域名转让(.ai)|适合 AI 创业 / 产品 / 公司品牌
前端
小二·20 小时前
微前端架构完全指南:qiankun 与 Module Federation 双方案深度对比(Vue 3 + TypeScript)
前端·架构·typescript
EndingCoder20 小时前
枚举类型:常量集合的优雅管理
前端·javascript·typescript
Electrolux21 小时前
[wllama]纯前端实现大语言模型调用:在浏览器里跑 AI 是什么体验。以调用腾讯 HY-MT1.5 混元翻译模型为例
前端·aigc·ai编程
sanra12321 小时前
前端定位相关技巧
前端·vue
起名时在学Aiifox21 小时前
从零实现前端数据格式化工具:以船员经验数据展示为例
前端·vue.js·typescript·es6
oMcLin21 小时前
如何在Manjaro Linux上配置并优化Caddy Web服务器,确保高并发流量下的稳定性与安全性?
linux·服务器·前端
码途潇潇21 小时前
JavaScript 中 ==、===、Object.is 以及 null、undefined、undeclared 的区别
前端·javascript