学习webAssembly小记

1,418 阅读8分钟

简介

  • webAssembly(简称wasm)是一种新型的二进制代码格式,包含这种二进制代码格式的文件可以用类似加载模块的方式被浏览器快速、高效地解析和执行。Wasm模块可以通过javascript提供的上层web API来调用浏览器本身的功能,比如与浏览器在显示DOM内容上进行交互。同时,WebAssembly不仅可以应用在Web浏览器上,它还可以应用在一些非web相关的场景中,比如Node.js环境下的后端应用甚至是物联网(IOT)应用。只要这些场景能够提供一个完备的Wasm虚拟机便可以正常地解析Wasm模块。

目标

  • 希望浏览器能够以近似原生c/c++应用的运行速度,来调用wasm模块中包含的那些在各类型平台(处理器架构)上都可以使用的通用硬件功能

兼容性浏览器支持

  • 2017年3月,四大主流浏览器厂商宣布均已在最新的浏览器中支持 WebAssembly,分别为 Chrome 57、FireFox 52、Safari TP 及 Microsoft Edge 。
    • 如果浏览器不支持 WebAssembly,也可以通过 Emscripten SDK 将 C++ 代码发布为 asm.js 格式,这种格式可以在所有浏览器中运行,并且性能仍然不低于传统的 .js 文件

调试

Wasm标准针对Wasm的原始二进制格式设计了一种名为“WAT(WebAssembly Text Format)”的可读文本格式,对应于该可读文本格式的文本文件也同样以“.wat”作为后缀。WAT中的语法结构直接对应着模块的功能和业务逻辑,而这对于深入理解模块的运行机制和对模块的细节进行优化提供了很大的帮助。

编译(C/C++编译成webAssembly)

  • 在线的编译工具:WasmExplorer
  • 本地的编译工具:Emscripten,它基于LLVM(通用的编译器工具链来实现各类静态或动态编译语言的编译器前端,以及各类硬件平台上的LLVM编译器后段)
  1. 可以将C/C++编译成asm.js(作为C/C++编译的目标设计的,可以理解为一种中间表示层语法),然后通过Binaryen工具链中的asm2wasm工具将这些asm.js代码转译成一个标准的Wasm模块。
  2. 也可以直接生成webAssembly二进制文件(后缀.wasm)。注:在1.37以上版本才支持直接生成wasm文件
  3. 在线平台快速构建,开发、编译、运行环境WasmFiddle

应用场景

  • 图像/视频编辑器
  • 在线的休闲类小游戏,甚至是跨平台的大型网络游戏
  • 在线的点对点应用
  • 在线的流媒体应用
  • 在线的图片识别应用
  • 虚拟现实(AR)类应用
  • 开发者使用的在线编辑器、编译器及调试器
  • 虚拟机和平台模拟器
  • 远程桌面应用
  • VPN应用
  • 加密服务类应用

小demo

fetch("orginal.wasm").then(response=>{response.arrayBuffer()}).then((bytes)=>{
          //通过浏览器提供的标准WebAssembly接口来编译和初始化一个Wasm模块
          WebAssembly.compile(bytes).then(module=>
          console.log(WebAssembly.Module.exports(module))
          WebAssembly.instantiate.import(module,{}})
          ).then(instance => {
            // 取出从Wasm模块中导出的函数
            let exports = instance.exports;
            // 获取该Wasm模块实例使用的各种api
            const result = exports.api;
          })
        })

Api

WebAssembly.compile(bufferSource)

该方法接收一段标准的二进制格式的WebAssembly模块代码,编译并返回一个对应于模块的 WebAssembly.Module 对象。WebAssembly.compile 方法接收的模块代码必须是以类型数组TypedArray或二进制缓冲区ArrayBuffer结构表示的二进制Wasm模块代码。该方法会对这些二进制形式的代码进行编译处理,然后返回一个可以解析为 WebAssembly.Module 对象实例的Promise结构

WebAssembly.Module

在WebAssembly.Module对象中实际上包含有一些已经由浏览器编译好的无状态的Wasm模块代码。除了通过WebAssembly.compile方法来创建WebAssembly.Module对象实例,我们还可以直接通过WebAssembly.Module的构造函数来构造对象实例。这两种方法的唯一区别是,前者是以异步方式进行的,而后者则是以同步方式进行的。因此,在绝大部分情况下,我们更建议开发者使用 WebAssembly.compile 方法以异步返回 Promise 对象的方式来创建 WebAssembly. Module对象实例。

WebAssembly.Module.exports(module)

该方法会返回从Wasm模块实例导出到JavaScript环境中的所有对象的描述信息。

WebAssembly.Module.imports(module)

该方法会列出 Wasm 模块在进行实例化时需要从 JavaScript 环境中导入的对象信息。为了能够更灵活地在JavaScript环境与Wasm模块对象实例进行交互,我们可以直接从JavaScript环境向正在进行实例化的 Wasm 模块对象实例传递一些数据

WebAssembly.instantiate(bufferSource, importObject)

通过这种重载形式,我们可以直接通过含有Wasm模块二进制数据的TypedArray类型数组或ArrayBuffer二进制缓冲区,来编译并实例化一个标准的Wasm模块对象实例。 在这种重载形式下,该方法一共接收两个参数。第一个参数bufferSource表示包含有Wasm模块二进制数据的TypedArray或ArrayBuffer数据结构;第二个参数importObject是一个原生的JavaScript对象,在该对象中包含有需要在Wasm模块的实例化过程中导入到模块实例中的对象

WebAssembly.compileStreaming(soure)

WebAssembly.compileStreaming(fetch('original.wasm')).then(module=>{
    WebAssembly.instantiatemodule, importObject)
    
    ....
})

该方法可以直接从浏览器底层的数据流源来编译WebAssembly.Module对象实例。省去了compile

WebAssembly.instantiateStreaming(source,importObject)

WebAssembly.instantiateStreaming(fetch('original.wasm'),importObject).then(resultObject=>{})

该方法的功能与 WebAssembly.compileStreaming 类似,可用于直接从系统底层的数据流源来编译并实例化一个Wasm模块。在调用该方法时需要传入两个参数,其中第一个参数为用于加载Wasm模块数据的数据流源;第二个参数是Wasm模块实例化时需要导入的对象信息。(省去了 WebAssembly.instantiate)因此,这也是我们首先推荐使用的模块初始化方式。

链接c/c++与webAssembly

构建类型

单独使用Wasm模块并不能完全承载C/C++源代码描述的所有功能。比如对于一个使用了 OpenGL 技术的 C/C++源程序,在将其转译为Wasm目标代码时,Emscripten会直接使用Web浏览器上的WebGL引擎来代替OpenGL的工作流程。但是由于这部分调用了WebGL接口的JavaScript代码其具体执行过程涉及浏览器本身的WebGL接口实现,因此无法将这部分代码与独立的Wasm模块打包在一起。Emscripten对此的解决办法是:只将那部分不涉及浏览器层API接口,仅具有纯计算和方法调用过程的代码打包并放置在统一的 Wasm 模块中。而对于那些需要与浏览器进行特殊交互或 JavaScript 接口调用的代码,则将它们按照普通的JavaScript代码进行打包并交由浏览器执行。 因此根据C/C++源程序的具体类型,我们可以通过Emscripten工具链有选择地将其构建成以下两种类型的WebAssembly应用。

standalone

Standalone 类型的 Wasm 应用只适用于那些仅包含有纯计算和方法调用逻辑的 C/C++源程序,即在源程序中不能含有任何涉及需要与浏览器 API 进行交互、发送远程(HTTP/Socket)请求,以及与数据显示、输入等I/O相关的代码。Emscripten在构建该类型Wasm应用时只会编译生成独立的 Wasm 二进制模块,而不会帮助其构建任何用于连接该模块与上层 JavaScript环境的脚本文件。

emscript-standalone.cc
#include <emscript.h>
#ifdef __cplusplus
extern "C"{
#endif
//函数定义,利用Emscripten提供的宏来防止函数被DCE处理掉
EMSCRIPTEN_KEEPALIVE int add (int x,int y){
return x +y
}
#ifdef __cplusplus
}
#endif

emcc emscripten-standalone.cc -Os -s WASM=1 -o emscripten-standlone.js
"-s"参数为emcc指定了“WASM=1”标识符,以使其将WebAssembly作为编译目标的文件类型。而“-Os”参数则是优化的关键所在,该参数会告知编译器以“第4等级”的优化策略来优化目标代码,进而删除其中没有被使用到并且与Emscripten运行时环境相关的所有信息。这样,我们便得到了一个完全简化的独立型Wasm模块

Dependent

Dependent类型的Wasm应用与Standalone类型有所不同,在该类型应用中一般都包含着大量与浏览器特定功能相关的方法调用。比如在对应C/C++源代码中使用了IO标准库、OpenGL等需要与宿主环境本身进行交互的相关技术。另外,由于 Wasm模块本身无法直接与浏览器进行交互,因此,Emscripten 便需要通过某种具有类似“胶水”功能的JavaScript代码,来将Wasm模块与Web浏览器在功能交互和数据资源传输层面连接起来。