WebAssembly初探

1,024 阅读9分钟

WebAssembly初探

WebAssembly?

自从互联网诞生以来,到今天,它已经成为了世界上最大的社区。随着互联网发展起来的还有各类脚本语言,其中JavaScript在几轮斗争中存活下来成为今天当之无愧的“霸主”。全球最大的包管理npm中数亿的包每天被无数人引用,撑起了互联网的半壁江山。

随着互联网的发展,原本简单的应用变得逐渐臃肿。这个时候,JavaScript显得比较力不从心了。带宽的增大和网络费用的下调,网络应用似乎越来越成为现实。从以往的经验来看,带宽已经基本满足网络应用所需,但是更大的问题是:什么样的应用可以运行在互联网上呢?

几乎是共识的一件事儿是:脚本语言的性能与编译型语言的是存在差距的,这一点儿在js上体现得比较明显。Google虽然引入了V8引擎,让js有点儿静态语言的意思,性能上也提升了不少,但是和标准静态语言相比相差甚远。

为了应对高速发展的互联网,解决传统脚本语言性能不足的缺点,互联网标准化组织在接受提议并经过讨论以后,发布了WebAssembly标准。WebAssembly很大程度上提升了性能,力图解决js性能不足以应对大量计算应用的缺点。

解决性能问题的方案有很多,例如asm.js也是个优秀的解决方案。

WebAssembly提出已经相当长一段时间了,也已经有了许多成熟的项目。其中我最为震撼的应该是DOOM3,它的成功移植也让人看到WebAssembly的巨大潜力。

WebAssembluy需要浏览器提供支持,你可以在这里查看各个浏览器的兼容性。令人振奋的是,除了IE以外,几乎所有浏览器都开始陆陆续续添加对WebAssembly的支持!

而且,无法预料的是,就目前看来,WebAssembly有可能替代js成为开发首选。很有可能在不久的将来,WebAssembly将成为js的替代者,这不是危言耸听,Web IDL草案已经开始陆续征集意见,这个草案的完成度极高。Web IDL能够提供浏览器的API接口给其他语言,这意味着操纵DOM不再是js专属,只要符合标准,都可以调用!而且,调用这些API的速度会比JS更快!

所以这车,得上!稳!

这个系列我会写几篇,把我对其的了解逐一通过文章的形式分享出来。注意,这篇文章只是初探,不会过分涉及代码和原理,请不要疑惑为什么这么简单。

WebAssembly暂时无法提供polyfill支持。

怎么coding?

WebAssembly不是一门语言,是一个标准集。经过解析后,它看起来和汇编很像,可以将它看作浏览器上运行的“汇编”。直接书写不太现实,更为常规的做法是将静态语言编译成WebAssembly,就像编译成链接文件一样。

编译就需要编译器。得益于emscripten项目,目前已经有多种语言支持编译成为WebAssembly。理论上,只要符合一定标准,都可以使用它编译编译成WebAssembly

本教程中,我们会选择一门比较前沿的语言:Rust

Rust的理念很先进,绝大部分错误都可以通过编译器检测出来,没有GC,这意味着它不需要runtime。同时Rust的抽象是零开销的,并且代码可优化程度很高。在通过LLVM编译优化以后,Rust的性能可以直逼C/C++的运行速度,这也使得它进入T1梯队。有得有失,Rust为了实现这些,加了很多条条框框,使得书写起来不是那么简单。但我认为,Rust应该成为每个有追求的程序员应该学习的语言。

工具准备

rust的安装比较傻瓜化,只需要运行一段命令,接着按照提示弄好环境变量就行了.

安装方法(*unix):

curl https://sh.rustup.rs -sSf | sh

上面方法是用于安装rustup的。运行完上面的脚本之后,通常需要把~/.cargo/bin加入$PATH里面的。运行下面的命令:

echo PATH="$PATH:\$HOME/.cargo/bin" >> you_profile && source your_profile && rustc --version

your profile根据你的shell环境不同而不同,通常大多数人使用的是bash的.bash_profile。我是使用的zsh,因此是.zshrc

值得注意的是,rust分为多个版本,对于支持WebAssembly的一些特性而言,需要nightly版本支持,因此一般情况下,我们都是在使用nightly。使用下面的命令切换默认配置为nightly

rustup default nightly

接着我们需要能够将Rust代码编译成WebAssembly的工具。这里推荐wasm-pack,它几乎是现在最佳的WebAssembly的编译器,上手几乎没有难度。而且它为了和npm生态联动,使用起来和一些库很相似,尤其是webpack。它会自动将Rust编译,并且产生js代码,这个js代码是对wasm调用的封装,这样对开发者而言,使用起来就像一个普通的js包一样。另外它还产生了ts的定义文件,方便IDE代码提示。

由于是初探,因此不要把东西弄复杂了。我们先看看官方的模板吧!

首先,我们下载一个cargo-generate。顾名思义,用于根据模板生成项目的工具,类似于create-react-app

接着,前端开发必备nodejs全家桶。

最后,浏览器,请使用最新版firefox或者chrome

初探

我们首先下载官方提供的例子**[1]**:

cargo generate --git https://github.com/rustwasm/wasm-pack-template

接着你会得到一个工程,它的目录看起来是这样的:

├── Cargo.toml
├── LICENSE_APACHE
├── LICENSE_MIT
├── README.md
├── src
│   ├── lib.rs
│   └── utils.rs
└── tests
    └── web.rs

这是一个标准的rust工程,我们得稍加改造,使它可以在浏览器运行。

npm init**[2]**运行一下,使该工程同样也成为一个nodejs工程。现在,目录看起来是:

├── Cargo.toml
├── LICENSE_APACHE
├── LICENSE_MIT
├── README.md
├── package.json
├── src
│   ├── lib.rs
│   └── utils.rs
└── tests
    └── web.rs

接着,添加js依赖。我们需要webpack。运行以下命令**[3]**:

npm install webpack webpack-cli webpack-dev-server --save-dev

然后添加配置文件**[4]**,webpack.config.js,并写入以下内容:

const path = require('path');

module.exports = {
    entry: "./bootstrap.js",
    output: {
        path: path.resolve(__dirname, "dist"),
        filename: "bootstrap.js",
    },
    mode: "development",
};

我们把bootstrap.js作为入口文件,因此在项目跟路径下创建它**[5]**,并在里面写入:

import('./pkg/webassembly')
    .then(wasm => {
				wasm.greet();
    })
    .catch(e => console.error(e));

注意,wasm模块目前只能通过异步调用!因此我们需要bootstrap.js文件来异步引入它,不能直接import导入。

'./pkg/webassembly'现在不存在,那么怎么得到呢?wasm-pack派上用场了,直接build**[6]**即可,他会产生的!

wasm-pack build --out-name webassembly

该过程可能会很慢甚至失败,这是由于众所周知的原因。请搜索中科大源,将rustup和rust的源都替换为中科大提供的镜像源。运行成功以后,项目里面会出现pkg目录,里面有该wasm项目的组成文件,我们通常不会去关注他们。我们只需要调用那个js文件就行了。

然后,创建index.html文件,并引用bootstrap.js文件**[6]**:

<html>
  <head>
    <script src="./bootstrap.js"></script>
  </head>
</html>

接着,我们直接启动webpack服务**[7]**,看看效果:

npx webpack-dev-server

如果一切顺利,那么你打开localhost:8080的时候,应该会出现一个Hello, XXX的弹窗。这意味着,你迈出了WebAssembly的第一步。现在,让我们揭开这个项目的神秘面纱。

代码

关于js的部分,我默认你已经很熟悉js了,因此不再解释。需要值得注意的是,看起来我们只是在bootstrap.js里面只是调用了import('./pkg/webassembly')来引入了wasm代码,但实际上真的这么简单吗?

当然不是。之所以我们能够这么轻松地使用,得归功于wasm-packwebpackwasm-pack为你生成了wasm代码的同时,也生成了相应的封装好的jsd.ts文件,这些js文件里为了自动导出了你在原生rust代码里面希望导出给js的一些方法等。

如果你去阅读其中的js代码,你会发现在里面也只是简单的使用import wasm from './webassembly.wasm'来导入WebAssembly代码。但真的是这样简单吗?wasm代码能够像普通js库一样导入吗?答案是否定的,熟悉js的你或许已经有答案了,那就是webpackwebpackimport语句转换成了对应的WebAssembly的API。这些API以后可能会出文介绍。如果你感兴趣,MDN上关于WebAssembly的主题或许可以给你提供帮助。

跳过js,我们来看看rust代码部分。这部分比较困难,因为它涉及的部分比较多。这里不会去帮你解读它的具体原理,也不会教你学Rust

Rust教程十分的棒!

src目录下两个文件lib.rsutils.rs,后者没什么用,至少现在是这样,因为根本没调用。实际上它是用来调试的wasm代码的,因为在wasm的运行环境下,常规的调试条件有点儿难用。

首先是tests目录,顾名思义,是用于测试的,rust自带测试功能。同样,这个目录并没有用,里面也没有测试代码。忽略它是目前最好的做法。

在代码里面,我们使用了一个叫做wasm_bindgencrate(rust把库/包叫做crate)。这个库是很有魔力的,它让WebAssembly变得更加容易,如同代码中:

#[wasm_bindgen]
extern {
    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet() {
    alert("Hello, webassemblu!");
}

它把js环境中的alert绑定到了rust环境中,同时把greet函数导出到js环境中,一切看起来如此简单。导出倒还好,导入就显得比较麻烦了。每次都得像书写头文件定义一样,这样也太烦人了。难道没有人把所有的函数都定义好吗?那当然有了,出自quote作者的另两个crateweb_sysjs_sys,他们分别提供了web环境和js环境的IDL绑定

emmmmn,事情似乎变得困难了起来,这都是些什么啊?实际上,这些东西是不需要我们关心的,我们只需要学会怎么去用就行了。至于怎么实现的,这个真的有点儿复杂了。不过如果感兴趣,可以去看看源代码。

本文到此结束,或许你会问,这啥啊,什么都没讲清楚。你得明白,WebAssembly这个标准十分庞大,涉及的点儿也十分多。其次,它需要一门静态语言的支持,选择rust意味着这个难度变得更高。如果妄想一篇文章讲清楚,有点儿痴人说梦了。所以正如前文所说,这只是初探,进一步的解析,后面的文章会陆续解释的。