【webAssembly系列】webAssembly初探究竟

一、前言

自从JavaScript诞生开始,到现在开始变成流行的编程语言,背后的是web发展所推动的。web应用的变得更多更复杂,但是渐渐暴露出JavaScript的问题:

(1)语法太灵活导致开发大型web项目困难;

(2)性能不足满足一些场景的需要。

 

二、为什么需要WebAssembly

针对以上的问题,JavaScript出现了一些代替语言,比如:

(1)微软的TypeScript通过JS加入静态类型检查机制来改进js松散的语法,提升代码健壮性。

(2)谷歌的Dart则是为浏览器引入新的虚拟机去直接运行Dart程序以提升性能。

(3)火狐的asm.js则是取JS的子集,JS引擎针对asm.js做性能优化。

以上尝试各有优缺点。其中:

(1)TypeScript只是解决JS语法松散的问题,最后还是需要编译成JS去运行,对性能没有提升。

(2)Dart只能在chrome预览版中运行,无主流浏览器支持,用Dart开发的人不多。

(3)asm.js语法太简单,有很大限制,开发现率低。

三大浏览器巨头分别提出了自己的解决方案,互补兼容,这违背了web的宗旨,是技术规范统一让web走到今天,因此如果有一套新的规范去解决JS面临的问题就太好了。

于是webAssembly出现了,webAssembly是一种新的字节码格式,主流的浏览器已经支持webAssembly。和JS需要解释执行不同的是,webAssembly字节码和底层机器码很相似可快速装载运行,因此性能相对于JS解释执行大大提升。也就是说webAssembly并不是一门编程语言,而是一份字节码标准,需要用高级语言编译出字节码放到webAssembly虚拟机中才能运行,浏览器厂商需要做的是根据webAssembly规范实现虚拟机。

 

三、webAssembly原理

要弄清楚webAssembly原理,需要先搞清计算机运行原理。

 

电子计算机是由电子元件组成,为了方便处理电子元件只存在开闭两种状态,对于1和0,也就是计算机只认识1和0,数据和逻辑都需要由1和0表示,也就是可以直接装载到计算机中运行的机器码。机器码可读性极差,因此人们通过高级语言c,c++,Java,Go等编写再编译成机器码。

 

由于不同的计算机CPU架构不同,机器码标准有所差别,常见的CPU架构包括X86,AMD64,ARM,因此在由高级编程语言编程成可自行代码时需要指定目标架构。

 

webAssembly字节码是一种抹平了不同架构的机器码,webAssembly字节码不能直接在任何一种CPU架构上运行,但是由于非常接近机器码,可以非常快的被翻译为对应架构的机器码,因此webAssembly运行速度接近机器码,这听上去非常像java字节码。

 

相对于JS,webAssembly有以下优点:

(1)体积小。由于浏览器运行时只加载编译成的字节码,一样的逻辑比字符串描述的JS文件体积小很多。

(2)加载快。由于文件体积小,再加上无需解释执行,webAssembly能更快的加载并实例化,减少运行前的等待时间。

(3)兼容问题少。webAssembly是非常底层的字节码规范,制定好以后很少变动,就算发生变化,只需要从高级语言编译成字节码过程做兼容。可能出现兼容问题是JS和webAssembly桥接的JS接口。

 

每一个高级语言都去实现源码到不同平台的机器码的转换工作是重复的,高级语言只需要生成底层虚拟机(LLVM)认识的中间语言(LLVM IR),LLVM能实现:

(1)LLVM IR到不同CPU架构机器码的生成。

(2)机器码编译时性能和大小优化。

 

除此之外LLVM还实现了LLVM IR到webAssembly字节码的编译功能,也就是说只要高级语言能转换成LLVM IR,就能编译成webAssembly字节码,目前能编译webAssembly字节码的高级语言有:

(1)AssemblyScript:语法和TypeScript一致,对前端来说学习成本低,为前端编写webAssembly最佳选择。

(2)c\c++:官方推荐的方式,详细见文档

(3)Rust:语法复杂,学习成本高,对于前端来说可能会不适应。详细见使用文档

(4)kotlin:语法和java,js很相似,语言学习成本低,详细见文档

(5)GoLang:语法简单,学习成本低。但是对于webAssembly还处于未正式发布阶段,详细见文档

 

通常负责把高级语言翻译到 LLVM IR 的部分叫做编译器前端,把 LLVM IR 编译成各架构 CPU 对应机器码的部分叫做编译器后端; 现在越来越多的高级编程语言选择 LLVM 作为后端,高级语言只需专注于如何提供开发效率更高的语法同时保持翻译到 LLVM IR 的程序执行性能。

 

四、编写webAssembly

4.1 为什么选择AssemblyScript作为webAssembly的开发语言

AssemblyScript相对于C,rust等其他语言去写webAssembly而言,好处就是:对于前端来说无需额外的新语言学习成本,还有对于不支持webAssembly的浏览器,可以通过TypeScript编译器编译成可以正常执行的JS代码。从而实现从JS到webAssembly的平滑迁移。

 

4.2接入webpack构建

任何新的web开发技术都少不了构建,为了提供一套流畅的webAssembly开发流程,接下来开始介绍webpack具体步骤。

1、安装依赖,以便TS源码被AssemblyScript编译成webAssembly。

{
  "devDependencies": {
    "assemblyscript": "github:AssemblyScript/assemblyscript",
    "assemblyscript-typescript-loader": "^1.3.2",
    "typescript": "^2.8.1",
    "webpack": "^3.10.0",
    "webpack-dev-server": "^2.10.1"
  }
}

 

2、修改webpack.config.js,加入loader。

module.exports = {
    module: {
        rules: [
            {
                test: /\.ts$/,
                loader: 'assemblyscript-typescript-loader',
                options: {
                    sourceMap: true,
                }
            }
        ]
    },
};

 

3、修改typeScript的编译器配置tsconfig.json,以便让typeScript编译器能支持AssemblyScript中引入内置类型和函数。

{
  "extends": "../../node_modules/assemblyscript/std/portable.json",
  "include": [
    "./**/*.ts"
  ]
}

 

4、配置直接继承自AssemblyScript内置的配置文件。

 

五、webAssembly相关工具

除了上面提到的webAssembly二进制工具箱,webAssembly社区还有以下常用工具:

(1)Emscripten:能把c,c++代码转换成wasm,asm.js。

(2)Binaryen:提供更简单的IR,把IR转换成wasm,并且提供wasm的编译时优化,wasm虚拟机,wasm压缩等功能,前面提到的AssemblyScript就是基于他的。

 

六、webAssembly  JS API

目前webAssembly只能通过js去加载和执行,但是未来浏览器中可以通过像加载JS那样去加载和执行webAssembly,下面介绍如何使用JS调用webAssembly。

JS 调 WebAssembly 分为 3 大步:加载字节码 > 编译字节码 > 实例化,获取到 WebAssembly 实例后就可以通过 JS 去调用了,以上 3 步具体的操作是:

1、对于浏览器可以通过网络请求去加载字节码,对于nodejs可以通过fs模块读取字节码文件;

2、在获取到字节码后都需要转换成ArrayBuffer后才能被编译,通过webAssembly通过JS API webAssembly.compile编译后会通过Promise resolve 一个webAssembly.module,这个module是不能直接被调用的需要。

3、在获取到module后需要通过webAssembly.instance API去实例化module,获取到instance后就可以像使用JS模块一个调用。

 

其中的第 2、3 步可以合并一步完成,前面提到的 WebAssembly.instantiate 就做了这两个事情。

WebAssembly.instantiate(bytes).then(mod=>{
  mod.instance.f(50);
})

 

七、webAssembly  调用 JS

之前的例子都是用 JS 去调用 WebAssembly 模块,但是在有些场景下可能需要在 WebAssembly 模块中调用浏览器 API,接下来介绍如何在 WebAssembly 中调用 JS。

WebAssembly.instantiate 函数支持第二个参数 WebAssembly.instantiate(bytes,importObject),这个 importObject 参数的作用就是 JS 向 WebAssembly 传入 WebAssembly 中需要调用 JS 的 JS 模块。举个具体的例子,改造前面的计算斐波那契序列在 WebAssembly 中调用 Web 中的 window.alert 函数把计算结果弹出来,为此需要改造加载 WebAssembly 模块的 JS 代码:

WebAssembly.instantiate(bytes,{
  window:{
    alert:window.alert
  }
}).then(mod=>{
  mod.instance.f(50);
})

对应的还需要修改 AssemblyScript 编写的源码:

// 声明从外部导入的模块类型
declare namespace window {
    export function alert(v: number): void;
}
 
function _f(x: number): number {
    if (x == 1 || x == 2) {
        return 1;
    }
    return _f(x - 1) + _f(x - 2)
}
 
export function f(x: number): void {
    // 直接调用 JS 模块
    window.alert(_f(x));
}

修改以上 AssemblyScript 源码后重新用 asc 通过命令 asc f.ts 编译后输出的 wast 文件比之前多了几行:

(import "window" "alert" (func $src/asm/module/window.alert (type 0)))
 
(func $src/asm/module/f (type 0) (param f64)
    get_local 0
    call $src/asm/module/_f
    call $src/asm/module/window.alert)

多出的这部分 wast 代码就是在 AssemblyScript 中调用 JS 中传入的模块的逻辑。

除了以上常用的 API 外,WebAssembly 还提供一些 API,你可以通过这个 d.ts 文件去查看所有 WebAssembly JS API 的细节。

 

八、不止于浏览器

webAssembly作为一种底层字节码,除了能在浏览器中运行外,还能在其他环境下运行。

1、直接执行wasm二进制文件

前面提到的 Binaryen 提供了在命令行中直接执行 wasm 二进制文件的工具,在 Mac 系统下通过 brew install binaryen 安装成功后,通过 wasm-shell f.wasm 文件即可直接运行

2、在nodejs中运行

目前 V8 JS 引擎已经添加了对 WebAssembly 的支持,Chrome 和 Node.js 都采用了 V8 作为引擎,因此 WebAssembly 也可以运行在 Node.js 环境中;

V8 JS 引擎在运行 WebAssembly 时,WebAssembly 和 JS 是在同一个虚拟机中执行,而不是 WebAssembly 在一个单独的虚拟机中运行,这样方便实现 JS 和 WebAssembly 之间的相互调用。

要让上面的例子在 Node.js 中运行,可以使用以下代码:

const fs = require('fs');
 
function toUint8Array(buf) {
    var u = new Uint8Array(buf.length);
    for (var i = 0; i < buf.length; ++i) {
        u[i] = buf[i];
    }
    return u;
}
 
function loadWebAssembly(filename, imports) {
    // 读取 wasm 文件,并转换成 byte 数组
    const buffer = toUint8Array(fs.readFileSync(filename));
    // 编译 wasm 字节码到机器码
    return WebAssembly.compile(buffer)
        .then(module => {
            // 实例化模块
            return new WebAssembly.Instance(module, imports)
        })
}
 
loadWebAssembly('../temp/assembly/module.wasm')
    .then(instance => {
        // 调用 f 函数计算
        console.log(instance.exports.f(10))
    });

在 Nodejs 环境中运行 WebAssembly 的意义其实不大,原因在于 Nodejs 支持运行原生模块,而原生模块的性能比 WebAssembly 要好。 如果你是通过 C、Rust 去编写 WebAssembly,你可以直接编译成 Nodejs 可以调用的原生模块。

 

九、webAssembly展望

从上面的内容可见 WebAssembly 主要是为了解决 JS 的性能瓶颈,也就是说 WebAssembly 适合用于需要大量计算的场景,例如:

(1)在浏览器中处理音视频,flv.js 用 WebAssembly 重写后性能会有很大提升;

(2)react的dom diff 中涉及到大量计算,用webAssembly重写react核心模块能提升性能。safari浏览器使用的JS引擎JavaScriptCore也已经支持webAssembly,RN应用性能也能提升。

(3)突破大型3D网页游戏性能瓶颈,白鹭引擎已经开始探索用 WebAssembly

 

十、总结

WebAssembly 标准虽然已经定稿并且得到主流浏览器的实现,但目前还存在以下问题:

(1)浏览器兼容性不好,只有最新版本的浏览器支持,并且不同浏览器对JS webAssembly互相调的API支持不一致。

(2)生态工具不完善不成熟,目前还不能找到一门体验流畅的编写webAssembly的语言,都还处于初步阶段。

(3)学习资料太少,还需要更多的人去探索去采坑。

总之现在的 WebAssembly 还不算成熟,如果你的团队没有不可容忍的性能问题,那现在使用 WebAssembly 到产品中还不是时候, 因为这可能会影响到团队的开发效率,或者遇到无法轻易解决的坑而阻塞开发。

 

十一、参考

1、http://www.ruanyifeng.com/blog/2017/09/asmjs_emscripten.html

2、https://www.ibm.com/developerworks/cn/opensource/os-cn-clang/index.html

3、https://developer.mozilla.org/zh-CN/docs/WebAssembly/Understanding_the_text_format

4、https://developer.mozilla.org/zh-CN/docs/WebAssembly/Using_the_JavaScript_API

 

posted @ 2019-06-28 20:58  saucxs  阅读(971)  评论(0编辑  收藏  举报