本文目录 23 个章节
WebGL应用框架深入-WebAssembly编译与运行时框架
微信不支持wasm层面启用多线程。 能否通过详细介绍C++经过Emscripten编译成WebAssembly的机制、微信等宿主平台运行JavaSCript和WebAssembly的运行时框架、多线程的底层原理、微信不支持wasm的技术底层原因,帮我详细解释这个问题
1. Emscripten 将 C++ 编译为 WebAssembly 的流程
前端:Clang/LLVM 转译 Emscripten 首先调用 LLVM 的 Clang 前端,将 C++ 源码编译成 LLVM IR(中间表示)。
中间处理:Binaryen 优化与转码 LLVM IR 经由 Binaryen 工具链进一步优化、转换为 WebAssembly 文本格式(WAT),并最终生成二进制
.wasm模块。生成“胶水”代码(Glue Code) 同时,Emscripten 生成一份 JavaScript “胶水”文件(通常为
module.js),用于在宿主环境中加载、编译并实例化.wasm模块,实现 C++ 标准库函数、内存分配、文件系统等运行时支持 (emscripten.org)。运行时配置与参数
- 通过命令行参数(例如
-s USE_PTHREADS=1、-s ALLOW_MEMORY_GROWTH=1)启用或禁用特定功能。 - Emscripten 提供对 POSIX、SDL、OpenGL(转换为 WebGL)等 API 的兼容支持,便于移植复杂应用 (emscripten.org)。
- 通过命令行参数(例如
2. 宿主平台的 JavaScript 与 WebAssembly 运行时框架
浏览器中的标准 WebAssembly 运行时
- WebAssembly 对象:浏览器内置
WebAssembly命名空间,提供instantiate、instantiateStreaming、Memory、Table等接口。 - JavaScript 引擎集成:V8、SpiderMonkey 等引擎原生解析
.wasm,管理线性内存(ArrayBuffer/SharedArrayBuffer)并执行字节码指令。
- WebAssembly 对象:浏览器内置
微信小程序中的运行时差异
- WXWebAssembly 替代 WebAssembly
自微信 8.0 起,原生
WebAssembly对象被替换为WXWebAssembly(或WXAssembly),仅支持最基础的加载与实例化,不包含 SIMD、线程等扩展功能 (github.com)。 - 无标准 Worker 接口
小程序环境中通常不暴露标准的
Web WorkerAPI,无法启动独立线程来并行执行 WebAssembly 模块 (github.com)。 - 安全与资源限制 小程序对脚本执行上下文、安全策略(如 CSP)以及单包体积(2 MB 限制)有严格限制,进一步影响运行时能力。
- WXWebAssembly 替代 WebAssembly
自微信 8.0 起,原生
3. WebAssembly 多线程的底层原理
共享线性内存
- WebAssembly 引入可配置为
shared: true的Memory对象,其底层是 JavaScript 的SharedArrayBuffer。 - 所有线程(以 Web Worker 形式存在)共享同一块线性内存,无需数据拷贝,即可通过原子操作同步访问 (web.dev)。
- WebAssembly 引入可配置为
线程创建与同步
- Web Worker:宿主通过
new Worker()创建线程;Emscripten 将pthread_create映射为生成 Worker 并传递对等上下文。 - 原子操作与屏障:利用
Atomics接口完成互斥锁、条件变量等同步原语;必要时调用memory.atomic.fence保证内存屏障 (stackoverflow.com)。
- Web Worker:宿主通过
Emscripten 中的 USE_PTHREADS 支持
开启
-s USE_PTHREADS=1后,编译产物中包含对pthreadAPI 的封装:- 启动 Worker
- 将主内存转为可共享的
SharedArrayBuffer - 自动注入消息通信与同步代码
依赖浏览器对 SharedArrayBuffer 和跨源隔离(COOP/COEP)的完整支持。
4. 技术底层原因:为何微信小程序不支持 wasm 多线程
不支持 SharedArrayBuffer
- 出于安全(Spectre/Meltdown 防护)和资源限制考虑,小程序环境往往禁用
SharedArrayBuffer,导致无法构建共享内存模型。
- 出于安全(Spectre/Meltdown 防护)和资源限制考虑,小程序环境往往禁用
无 Web Worker 接口
- 由于不提供标准
WorkerAPI,无法从 JavaScript 层面创建独立执行上下文,无法并行运行多个 WebAssembly 实例。
- 由于不提供标准
WXWebAssembly 功能裁剪
WXWebAssembly旨在满足基础计算与逻辑移植,舍弃了多线程、SIMD 等高级特性,以保持运行时轻量与安全 (github.com)。
跨源隔离(Cross-Origin Isolation)欠缺
- WebAssembly 线程要求启用 COOP/COEP 头部以允许
SharedArrayBuffer,但小程序无法控制 HTTP 响应头,无法满足此安全策略。
- WebAssembly 线程要求启用 COOP/COEP 头部以允许
打包体积与性能考量
- 多线程运行时需增加数百 KB 的运行时库(Atomic、同步机制等),与小程序“单包 ≤ 2 MB”的限制相冲突。
结论:
微信小程序采用了自定义的 WXWebAssembly 运行时,并在安全、资源与包体积等多方面做了裁剪和限制,因此暂不支持 WebAssembly 多线程。若需在小程序中并行计算,可考虑基于单线程分时调度或服务器端多线程配合的混合方案。
能否通过具体的代码来进行介绍。我希望理解:Emscripten将C++编译成WebAssembly,在V8运行时上跑的框架是怎样的,宿主(微信)、运行时和WebAssembly、JavaScript之间的架构关系、互动流程是怎样的,Emscripten如何在编译过程中,对C++语言屏蔽硬件和驱动层接口,使得C++无法直接操作系统的网络、磁盘等驱动的? Emscripten和宿主如何使小程序运行在沙箱中,屏蔽网络、磁盘等接口,使这些接口只能经过宿主的API提供的?
1. Emscripten 将 C++ 编译为 WebAssembly 的完整流程
1.1 C++ 源码示例
// hello.cpp
#include <iostream>
int main() {
std::cout << "Hello from Emscripten!" << std::endl;
return 0;
}
1.2 编译命令
em++ hello.cpp \
-O2 \
-s WASM=1 \
-s MODULARIZE=1 \
-s EXPORT_NAME="createModule" \
-o hello.js
-O2:开启优化-s WASM=1:输出.wasm而非 asm.js-s MODULARIZE=1-s EXPORT_NAME:生成可通过 ES6import的模块化 Glue Code (developer.mozilla.org)
编译后会生成:
hello.wasm:二进制模块hello.js:加载、编译、实例化 Wasm 的 JavaScript “胶水”代码- (可选)
hello.html:一份示例 HTML,用于直接在浏览器中打开并运行
2. V8(浏览器/小程序)运行时加载与执行 WebAssembly 的框架示例
Emscripten 生成的 hello.js 内部典型逻辑(摘录):
// hello.js(节选)
var Module = {
onRuntimeInitialized: function() {
// Wasm 模块实例化后回调
Module._main(); // 调用 C++ 的 main()
}
};
(function() {
// 将 Wasm 二进制用 Streaming API 加载并实例化
WebAssembly.instantiateStreaming(fetch('hello.wasm'), Module)
.then(function(result) {
Module.asm = result.instance.exports;
})
.catch(function(err) {
console.error('WASM instantiate failed:', err);
});
})();
WebAssembly.instantiateStreaming:在下载时即时编译,节省启动时间Module.onRuntimeInitialized:Emscripten 运行时就绪后执行入口函数 (developer.mozilla.org)
在微信小程序环境中,需要用其专属接口 WXWebAssembly 替代标准对象:
// 小程序端加载示例
import createModule from './hello.js';
const module = await createModule({
locateFile: path => path === 'hello.wasm' ? 'hello.wasm' : path
});
module.onRuntimeInitialized = () => {
module._main();
};
// 内部会调用 WXWebAssembly.instantiate 而非 WebAssembly.instantiate
- 微信在基础库中用
WXWebAssembly代替全局WebAssembly,不支持高级扩展如线程、SIMD (github.com)
3. Emscripten 对 C++ 系统调用与硬件驱动的屏蔽机制
3.1 虚拟系统调用与文件系统
- 系统调用映射
所有对
open、read、write等 POSIX 接口的调用,均被编译时替换为对library_syscall.js中 JS 函数的调用,不再调用真实操作系统内核 (stackoverflow.com)。
// library_syscall.js(简化示意)
function __syscall_open(pathPtr, flags, mode) {
var path = UTF8ToString(pathPtr);
return FS.open(path, flags, mode).fd; // 委托给 Emscripten 的虚拟 FS
}
- 虚拟文件系统(MEMFS)
默认使用内存文件系统
MEMFS,它模拟了 POSIX API,但所有数据都驻留在浏览器内存中,C++ 无法直接访问宿主磁盘 (emscripten.org)。
3.2 网络接口屏蔽
- 默认环境下,C++ 标准库不包含 BSD Socket 支持,尝试编译
<sys/socket.h>会因缺失实现而报错。 - 如需网络通信,必须显式使用 Emscripten 提供的 Web API 封装(如
emscripten_fetch、EM_ASM调用fetch),否则无法链至 OS 网络驱动。
4. 微信小程序沙箱化:网络与文件接口的宿主侧隔离
4.1 网络请求仅能通过宿主 API
微信小程序禁止直接使用 XMLHttpRequest 或浏览器原生 fetch,所有网络通信必须调用:
wx.request({
url: 'https://api.example.com/data',
method: 'GET',
success(res) { console.log(res.data); }
});
- 只能请求在“小程序管理后台→服务器域名”中登记的 HTTPS 接口
- 无法跨域,必须走
wx.request并受次数及域名白名单限制 (juejin.cn)
4.2 文件存取通过 FileSystemManager
const fs = wx.getFileSystemManager();
// 保存临时文件
wx.downloadFile({
url: 'https://example.com/file.bin',
success(res) {
fs.saveFile({
tempFilePath: res.tempFilePath,
filePath: `${wx.env.USER_DATA_PATH}/file.bin`,
success() { console.log('保存成功'); }
});
}
});
// 读取本地文件
fs.readFile({
filePath: `${wx.env.USER_DATA_PATH}/file.bin`,
encoding: 'binary',
success({ data }) { console.log('文件长度:', data.length); }
});
- 小程序提供独立沙箱的用户文件区
wx.env.USER_DATA_PATH,与宿主系统彻底隔离 - C++ 在 Wasm 中若调用文件 I/O,会被映射到上述 JS 接口,无法越过沙箱直接读写磁盘 (blog.csdn.net)
4.3 定制 Emscripten 虚拟 FS 驱动(可选)
若需将虚拟 FS 持久化至小程序文件系统,可在 --post-js 中注册自定义设备:
// post.js
Module['onRuntimeInitialized'] = () => {
FS.registerDevice(FS.makedev(89, 0), {
read: (stream, buffer, offset, length, position) => {
// 通过 wx.getFileSystemManager().readFileSync 读取真实数据
// 并填充到 buffer
},
write: (stream, buffer, offset, length, position) => {
// 通过 wx.getFileSystemManager().writeFileSync 持久化
}
});
FS.mkdev('/persist', FS.makedev(89, 0));
};
这样,C++ 通过 open("/persist/data") 调用时,底层会落到小程序的文件存取 API 上,实现沙箱内持久化。
总结:
- 编译层面:Emscripten 利用 LLVM/Clang → Binaryen 生成
.wasm+ JS Glue,并通过library_syscall.js实现对所有系统调用的 JS 代理,彻底屏蔽原生驱动。 - 运行时层面:V8(或微信小程序的 WXWebAssembly)负责字节码解析与执行,Glue Code 注入生命周期回调,组织虚拟内存与调用导出函数。
- 宿主层面:微信小程序以沙箱模式运行 JS/Wasm,只开放
wx.request、wx.getFileSystemManager等 API,保证了网络、磁盘等能力只能通过宿主侧接口使用。
以上示例与原理剖析可帮助您完整掌握从 C++ 代码到微信小程序中 WebAssembly 的编译、加载、执行及沙箱化流程。
Emscripten技术发展历史
我想更深入理解WebGL应用,从浏览器发展到小程序的技术历史,以及架构方案。现在想了解的问题是,Emscripten是什么团队创建的,在什么历史情境下,基于什么样的需求创建的,推动该团队创建Emscripten的资源支持来自于哪里
以下回答分为三部分,分别介绍 Emscripten 的创建团队与主要开发者、历史情境与初始需求,以及项目所获的主要资源支持来源。
1. 创建团队与主要开发者
- 主要创建者:Emscripten 由 Mozilla 研究团队工程师 Alon Zakai(GitHub 名称 “kripken”)于 2010 年发起并创建,并于 2011 年在 OOPSLA 大会上正式对外发布。 (cs.tufts.edu, hacks.mozilla.org)
- 组织背景:项目最初作为 Alon Zakai 的个人尝试,旨在探索将 LLVM IR 编译为 JavaScript 以便在浏览器中运行非 JavaScript 代码。随后,Mozilla 基金会将其纳入 Games Program 和研究团队,并提供人力及基础设施支持。 (spectrum.ieee.org, hacks.mozilla.org)
2. 历史情境与初始需求
WebAssembly 之前的技术局限 在 2010 年代初期,浏览器原生并不支持 WebAssembly,JavaScript 是唯一的前端可运行语言,但其在高性能计算与现有 C/C++ 生态方面存在显著差距。
核心需求
- 跨语言移植:无需重写,即可将成熟的 C/C++ 应用(如游戏引擎、图形库、科学计算代码)迁移到浏览器环境。
- 性能优化:利用 LLVM 的中间优化能力和 asm.js 策略,实现接近原生的执行效率。
- 生态兼容:保持对 POSIX、OpenGL(映射为 WebGL)等常用 API 的支持,简化移植成本。 (cs.tufts.edu, zhuanlan.zhihu.com)
3. 资源支持来源
Mozilla 基金会
- 提供项目所需的研发人员工时与基础设施,包括代码仓库托管、持续集成、测试平台等。
- 将 Emscripten 纳入 Mozilla Games Program,推动浏览器游戏生态建设。 (spectrum.ieee.org, hacks.mozilla.org)
开源社区与行业贡献
- 项目托管于 GitHub(kripken/emscripten),吸引来自学术和工业界的贡献者不断完善编译器与运行时支持。
- 多款行业主流引擎(如 Unity、Defold、Godot)均集成 Emscripten 导出功能,说明其成熟度与生态价值。 (github.com, en.wikipedia.org)
学术与工业会议
- OOPSLA、LLVM 开发者大会、Mozilla Hacks 等技术会议为 Emscripten 提供交流、展示与反馈渠道,进一步促进项目改进与资源投入。 (llvm.org, qconnewyork.com)
以上即 Emscripten 的创始背景、初始需求与资源支持来源概览。如需更深入的技术细节或架构演进,请告知。
Emscripten对C++崩溃处理与堆栈信息丢失问题
Emscripten在设计层面,当C++层出现崩溃时,就会将abort抛给JavaScript层对吗? 从具体的机制上是如何做到的? 请通过实际的代码进行说明。 从这个角度看,C++崩溃时,JavaScript无法获取C++的崩溃堆栈踪迹,是因为Emscripten在这个问题上的设计本身有缺陷造成的对吗?
1. C++ 层 abort() 如何映射到 JavaScript
当 C++ 代码中执行 abort()(或因断言失败、调用 __builtin_trap() 等触发致命中断)时,Emscripten 在编译阶段会将该调用重定向为对 WebAssembly 模块中一个导入函数(通常名为 abort 或 _abort)的调用。
导入函数定义 在生成的 Glue Code 中,会有类似下面的映射(以
asmLibraryArg为例):var asmLibraryArg = { // C++ 调用 abort() 会编译为对 env.abort 的调用 abort: function(what) { /* JS 侧实现,见下文 */ }, _abort: function() { return asmLibraryArg.abort(); } // 其它导入… }; var asm = Module.asm({ 'env': asmLibraryArg, // … });这一点可见于社区针对“Duplicate functions abort()”问题的讨论,其中明确指出
abort和_abort都被导入到 Wasm 模块作用域: (github.com)
2. 在 JavaScript 侧 abort 的具体实现及抛出机制
Emscripten 的运行时(preamble.js + 各类 JS library)为 abort 提供了以下核心实现:
// 伪代码示意,基于 Emscripten 4.x runtime
function abort(what) {
// 构造错误信息
what = what ? 'abort(' + what + ')' : 'abort';
// 打印到 stderr
Module.printErr(what);
// 标记已中止,避免后续再运行
ABORT = true;
// 抛出 WebAssembly.RuntimeError,使其能穿过所有 wasm 调用帧
throw new WebAssembly.RuntimeError(what);
}
// C++ 层调用 _abort,会再次调用 abort()
function _abort() {
abort();
}
- 当执行
abort()时,底层直接调用上述 JSabort,并最终通过throw new WebAssembly.RuntimeError(...)在 JavaScript 上抛出一个运行时错误。 - 这使得整个 WebAssembly 实例停止执行,并将控制权交回到最外层的 JS 调用堆栈中。
这一设计由 Emscripten 核心团队在“如何终止执行”问题中确认:“当前实现中,abort() 会调用 JS,收集(有限的)堆栈信息,然后抛出一个异常,异常会穿过所有 wasm 帧一直飘到调用者。” (github.com)
3. 为何 JavaScript 层无法获取 C++ 崩溃时的原生堆栈
异常抛出点在 JS 实现层 由于真正的
throw发生在 JS 侧(new WebAssembly.RuntimeError),所以捕获到的堆栈信息仅限于 JS 调用栈,不包含 Wasm 内部的 C++ 调用帧。WebAssembly 无内建跨语言堆栈导出 WebAssembly 目前并不提供一种标准机制,让 JS 可以通过异常对象获得完整的 Wasm 调用链。尽管社区已在为 Wasm 异常处理(Wasm EH)推动堆栈跟踪支持,但它目前仅在特定调试模式(
-sASSERTIONS或-sEXCEPTION_STACK_TRACES)下为 Wasm EH 生效,而不覆盖abort()这类“陷阱”(trap)场景 (github.com, emscripten.org)。设计取舍
- 在早期 Web 平台尚未支持 Wasm EH 前,Emscripten 选择用最直接的方式模拟“进程终止”行为:将中止转为 JS 异常并停止执行,而非引入更重的运行时栈展开与解析开销。
- 本质上,这并非“设计缺陷”,而是在当时技术和性能约束下的一种折中。随着 Wasm EH 与更丰富的调试信息支持成熟,未来可通过开启调试选项来获取更完整的堆栈信息。
总结:
- 确认:C++ 层的
abort()最终会被映射为 JSabort,并在 JS 侧以WebAssembly.RuntimeError的形式抛出。 - 机制:通过预先在 Glue Code 中导入
abort函数,并在运行时实现其打印错误并抛出异常,来完成跨语言的中止传递。 - 堆栈不可见:由于真正的异常抛出发生在 JS 侧,且当前 Wasm 平台对于“陷阱”并无统一的跨语言堆栈导出机制,导致 JS 捕获时无法获得 C++ 调用链。这是早期设计的权衡结果,而非单一“缺陷”。
若需更丰富的崩溃堆栈,可关注并尝试启用 WebAssembly 异常处理(Wasm EH)及相关调试选项(如 -sASSERTIONS、-sEXCEPTION_STACK_TRACES),或利用后处理脚本在 JS 层包装 abort 来手动捕获及记录调用信息。