其他
记一次完整 C++ 项目编译成 WebAssembly 的实践
作者| 张翰(门柳)
出品|阿里巴巴新零售淘系技术部
本文知识点提炼: 1、把复杂的 C++ 框架编译成 WebAssembly。 2、在 wasm 模块里调用 DOM API ! 3、在 js 和 wasm 之间传递复杂数据结构。 4、对 WebAssembly 技术发展的期待。
项目背景
简单来讲,现在这个 C++ 框架已经能运行在各种原生渲染引擎之上了,我想保持同一份 C++ 源码,让它能运行在干净的浏览器中。
需求分析
▐ 要实现的目标
// 1. 自定义一个组件
class HelloWorld extends ReactiveElement { /* ... */ }
// 2. 向环境中注册组件,给定一个名称
customElements.define('hello-world', HelloWorld)
// 3. 定义组件的模板
customElements.defineTemplate(HelloWorld, {
type: 'h1',
// 添加数据绑定,表示 h1 的 innerText 是由表达式 message 计算出来的
innerText: { '@binding': '`Say: ${message}`' }
})
// 4. 创建组件,传递初始数据
const app = new HelloWorld({ message: 'Hi~' })
// 5. 把组件挂载到 #root
customElements.mount('#root', app)
// 6. 更新组件的数据,会自动触发 UI 的更新
app.setState({
message: "What's up!"
})
看起来用 JS 写个 polyfill 就可以搞定。但是模板的运算和数据绑定怎么实现?里面是可以包含循环、分支和表达式的,前端框架的做法是把它编译成 js 代码,如果写 polyfill,很可能又写出了一个前端框架,或者基于现有前端框架做封装,但是这样就和原生框架的行为不一致了。
▐ 遇到的问题
demo.js <-----> [响应式框架] <-----> DOM API
WebAssembly 是一种编译目标,虽然运行时是 wasm 和 JS 之间互相调用,但是写代码的时候感知到的是 C++ 和 JS 之间的互相调用。文中说的 JS 和 C++ 的调用,实际意思是 JS 和 wasm 模块之间的调用。
在 C++ 和 JS 之间传递复杂数据结构。(数据通信)
实现 C++ 和 JS 复杂数据类型的一对一绑定。(类型绑定)
技术方案
▐ 如何编译代码
使用 wasi-sdk 的编译链路我也在尝试,可以编译出来独立的 wasm 包,但是目前还没把它运行起来,需要在运行时注入响应式框架依赖的渲染接口。
<script src="path/to/wasm-framework.js"></script>
<script>
initializeWasmAPIs().then(exports => {
// wasm 初始化完成后,动态插入 demo 的脚本
const $script = document.createElement('script')
$script.setAttribute('src', `path/to/demo.js`)
document.body.appendChild($script)
})
</script>
▐ 接口的互相调用
(A) (B) (C) (D)
demo.js <-----> [js <---> wasm <---> js] <-----> DOM API
除了上面提到的接口封装方式以外, Emscripten 还提供了 Embind ② 和 Web IDL Binder ③ 的方式绑定 JS 接口,原理和上面方法相同,我觉得封装太厚了就没选择使用。
▐ 搞定数据通信
传递内存 buffer
const wasmMemory = new WebAssembly.Memory({ initial: 256 })
WebAssembly.instantiate(wasmBinary, {
env: {
memory: wasmMemory
}
})
// 把想要传递的数据转成 ArrayBuffer (假设是 Uint8Array)
const dataBuffer = encodeDataByJS({ /* my data */ })
// 向 wasm 申请一段内存,由 wasm 代码负责实现并返回内存内存起始地址
const offset = applyMemoryFormWasm(dataBuffer.length)
// 以 unit8 的格式操作 wasm 的内存 (格式应该与 dataBuffer 的格式相同)
const heapUint8 = new Uint8Array(wasmMemory.buffer, offset, dataBuffer.length)
// 把数据写入 wasm 内存
heapUint8.set(dataBuffer)
传递复杂数据结构
使用 JSON 字符串来传递复杂类型仅仅适用于我这个项目,并不是个通用方案,如何高效实现 wasm 数据通信还要具体情况具体分析。
功能的实现
▐ 代码实现
▐ 对接效果
▐ 性能对比
使用 WebAssembly 调用 DOM API 并不是一个合适的使用场景,和 JS 进行频繁的调用和传值也不是 WebAssembly 的强项,这个测试用例放大了通信开销。
对 WebAssembly 的期待
我能用 WebAssembly 做点什么?
我能为 WebAssembly 做的什么?
在工程层面解决 WebAssembly 研发链路的问题。现在无论是开发、编译还是调试都会遇到很多问题,开发体验和开发效率都比较低。目前我个人觉得比较靠谱的三种开发语言是 C++、Rust 和 AssemblyScript,分别面向不同类型的开发者。
在平台侧解决 WebAssembly 模块的管理问题。解决 wasm 在真正使用时的加载、分发、依赖管理、复用等问题,要是能构建出 npm 这样丰富的生态就好了。
在客户端/服务端解决 WebAssembly 独立运行时的问题。能够把丰富的平台原生能力,高效的、标准的透出到 wasm 模块中,并且解决性能、稳定性、安全性等问题。
性能优化!性能优化!性能优化! 无需多说,性能优化永无止境。
写在最后
END