WebAssembly最详教程
WebAssembly 是一种新的编码方式,可以在现代的 Web 浏览器中运行——它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如 C/C++、C# 和 Rust 等语言提供编译目标,以便它们可以在 Web 上运行。它也被设计为可以与 JavaScript 共存,允许两者一起工作。
它提供了一条使得以各种语言编写的代码都可以接近原生的速度在 Web 中运行的途径,使得以前无法在 Web 上运行的客户端应用程序得以在 Web 上运行,使得我们有可能将桌面应用(如 AutoCAD)甚至电子游戏移植到 Web。
WebAssembly 被设计为可以和 JavaScript 一起协同工作——通过使用 WebAssembly 的 JavaScript API,你可以把 WebAssembly 模块加载到 JavaScript 应用中并且在两者之间共享功能。这允许你在同一个应用中利用 WebAssembly 的性能和能力以及 JavaScript 的表达力和灵活性,即使你可能并不知道如何编写 WebAssembly 代码。
JavaScript存在的问题
JavaScript 是解释型语言,也是动态类型语言。如果变量类型是在运行时决定的,那么就是动态类型语言。对于 JavaScript 中的操作,每次执行程序时,引擎都必须检查它是整数还是浮点数,或者任何其他有效的数据类型。所以 JavaScript 中的每条指令都要经过几次类型检查和转换,这会影响到它的执行速度。
相对于动态类型语言,还有静态类型语言,C++就是一种静态类型语言,其变量类型是在定义的时候就决定了的。同样的操作通过一条指令,编译器就能知道变量 x 的类型和内存位置。
JavaScript 在运行代码时花费的时间:
WebAssembly 花费的时间:
WebAssembly解决的问题
- 一种新型的代码,可以运行在 Web 浏览器,提供一些新特性并主要专注于高性能
- 主要不是用于写,而是 C/C++、C#、Rust 等语言编译的目标,所以你即使不知道如何编写 WebAssembly 代码也能利用它的优势
- 其他语言编写的代码也能以近似于原生速度运行,客户端 App 也能在 Web 上运行
- 在浏览器或 Node.js 中可以导入 WebAssembly 模块,JS 框架能够使用 WebAssembly 来获得巨大的性能优势和新的特性的同时在功能上易于使用
WebAssembly的优势
- 快、高效、便利 -- 通过利用一些通用的硬件能力,能够跨平台以近乎于原生的速度执行
- 可读、可调试 -- WebAssembly 是一种低层次的汇编语言,但是它也有一种人类可读的文本格式,使得人们可编写代码、查看代码、可调试代码。
- 确保安全 -- WebAssembly 明确运行在安全、沙箱的执行环境,类似其他 Web 的代码,它会强制开启同源和一些权限策略。
- 不破坏现有的 Web -- WebAssembly 被设计与其他 Web 技术兼容运行,并且保持向后兼容性。
WebAssembly 如何适应网络平台?
网络平台可以被想象成拥有两个部分:
- 一个运行网络程序(Web app)代码的虚拟机VM(比如给你的程序提供能力的 JavaScript)
- 一系列网络程序能够调用API从而控制网络浏览器/设备功能,并且能够让事物发生变化的 Web API(DOM、CSSOM、WebGL、IndexedDB、Web Audio API 等)。
当试图把 JavaScript 应用到诸如 3D 游戏、虚拟现实、增强现实、计算机视觉、图像/视频编辑以及大量的要求原生性能的其他领域的时候,我们遇到了性能问题
同时,下载、解析和编译大体积的 JS 应用是很困难的,在一些资源更加受限的平台上,如移动设备等,则会更加放到这种性能瓶颈。
WebAssembly 是一种与 JavaScript 不同的语言,它不是为了替代 JS 而生的,而是被设计为与 JS 互为补充并能协作,使得 Web 开发者能够重复利用两种语言的优点:
- JS 是高层次的语言,灵活且极具表现力,动态类型、不需要编译步骤,并且有强大的生态,非常易于编写 Web 应用。
- WebAssembly 是一种低层次、类汇编的语言,使用一种紧凑的二级制格式,能够以近乎原生的性能运行,并提供了低层次的内存模型,是 C++、Rust 等语言的编译目标,使得这类语言编写的代码能够在 Web 上运行(需要注意的是,WebAssembly 将在未来提供垃圾回收的内存模型等高层次的目标)
随着 WebAssembly 的出现,上述提到的 VM 现在可以加载两种类型的代码执行:JavaScript 和 WebAssembly。
JavaScript 和 WebAssembly 可以互操作,实际上一份 WebAssembly 代码被称为一个模块,而 WebAssembly 的模块与 ES2015 的模块在具有很多共同的特性。
WebAssembly 关键概念
- 模块:表示一个已经被浏览器编译为可执行机器码的 WebAssembly 二进制代码。一个模块是无状态的,并且像一个二进制大对象(
Blob
)一样在 Window 和 Worker 之间进行共享(通过 postMessage() 函数)。一个模块能够像一个 ES 的模块一样声明导入和导出。 - 内存:一个可变长的 ArrayBuffer。本质上是连续的字节数组,WebAssembly 的低级内存存取指令可以对它进行读写操作。
- 表格:一个可变长的类型化数组。表格中的项存储了不能作为原始字节存储在内存里的对象的引用(为了安全和可移植性的原因)。
- 实例:一个模块及其在运行时使用的所有状态,包括内存、表格和一系列导入值。一个实例就像一个已经被加载到一个拥有一组特定导入的特定的全局变量的 ES 模块。
如何在应用里使用 WebAssembly?
WebAssembly 给 Web 平台添加了两块内容:二进制格式代码,以及一系列可用于加载和执行二进制代码的 API。
- 使用 EMScripten 来移植 C/C++ 应用
- 在汇编层面直接编写和生成 WebAssembly 代码
- 编写 Rust 应用,然后将 WebAssembly 作为它的输出
- 使用 AssemblyScript,它是一门类似 TypeScript 的语言,能够编译成 WebAssembly 二进制
编译C/C++为WebAssembly
Emscripten 环境安装
git clone https://github.com/juj/emsdk.git
cd emsdk
# 在 Windows 上
# 安装依赖项,这个过程比较长,会下载并自动安装node、Java、python等环境包,耐心等待
emsdk install latest
# 激活上一步下载的环境变量临时,一旦关闭cmd窗口,环境变量就失效了。
emsdk activate --global --build=Release sdk-incoming-64bit binaryen-master-64bit
# 注意:Windows 版本的 Visual Studio 2017 已经被支持,但需要在 emsdk install 需要追加 --vs2017 参数。
global 标识会让 PATH 变量在全局被设置,所以接下来所打开的终端或者命令行窗口都会被设置。如果你仅仅想让 Emscripten 在当前窗口生效,就删掉这个标识。
每当你想要使用 Emscripten 时,尝试从远程更新最新的 emscripten 代码是个很好的习惯(运行 git pull)。如果有更新,重新执行 install 和 activate 命令。这样就可以确保你使用的 Emscripten 一直保持最新。
现在让我们进入 emsdk 文件夹
输入以下命令来让你进入接下来的流程
# on Windows
# 把emsdk加到环境变量中,方便后续执行命令
emsdk_env.bat
安装时遇到的问题:
emcc : 无法将“emcc”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。
重新执行下命令 ./emsdk.bat activate latest --permanent
编译一个样例 C 程序到 asm.js 或者 wasm。
#include <stdio.h>
int main(int argc, char ** argv) {
printf("Hello World\n");
}
在emsdk文件夹下执行
emcc E:\selfWork\html\hello.c
执行后,会在当前cmd所在目录下生成a.out.js
和 a.out.wasm
编译为html格式的demo文件
emcc E:\selfWork\html\hello.c -o test.html
将html文件发布到web容器中,或者使用下面命令发布
python -m http.server
# 也可以使用下面的命令,直接打开默认浏览器运行
emrun test.html
也可以转到一个已经配置过 Emscripten 编译环境的终端窗口中,进入刚刚保存 hello.c 文件的文件夹中,然后运行下列命令:
emcc hello.c -s WASM=1 -o hello.html
命令中选项的细节:
-s WASM=1
— 指定我们想要的 wasm 输出形式。如果我们不指定这个选项,Emscripten 默认将只会生成 asm.js。-o hello.html
— 指定这个选项将会生成 HTML 页面来运行我们的代码,并且会生成 wasm 模块,以及编译和实例化 wasm 模块所需要的“胶水”js 代码,这样我们就可以直接在 web 环境中使用了。
这个时候在你的源码文件夹应该有下列文件:
hello.wasm
二进制的 wasm 模块代码hello.js
一个包含了用来在原生 C 函数和 JavaScript/wasm 之间转换的胶水代码的 JavaScript 文件hello.html
一个用来加载,编译,实例化你的 wasm 代码并且将它输出在浏览器显示上的一个 HTML 文件
只生成某个文件
使用 WebAssembly JavaScript API
调用自定义方法
默认情况下,Emscripten 生成的代码只会调用 main()
函数,其他的函数将被视为无用代码。在一个函数名之前添加 EMSCRIPTEN_KEEPALIVE
能够防止这样的事情发生。你需要导入 emscripten.h
库来使用 EMSCRIPTEN_KEEPALIVE
。
#include <stdio.h>
#include <emscripten/emscripten.h>
int main(int argc, char ** argv) {
printf("Hello World\n");
}
#ifdef __cplusplus
extern "C" {
#endif
int EMSCRIPTEN_KEEPALIVE myFunction(int argc, char ** argv) {
printf("我的函数已被调用\n");
}
#ifdef __cplusplus
}
#endif
备注:为了保证万一你想在 C++ 代码中引用这些代码时代码可以正常工作,我们添加了
#ifdef
代码块。由于 C 与 C++ 中名字修饰规则的差异,添加的代码块有可能产生问题,但目前我们设置了这一额外的代码块以保证你使用 C++ 时,这些代码会被视为外部 C 语言函数。
//调用一个定义在 C、C++ 中的自定义方法
const importObject={
wasi_snapshot_preview1:{proc_exit:(arg)=>console.log(arg)},
}
let funcpp
fetch("WebAssemblyTest2.wasm")
.then((res)=>res.arrayBuffer())
.then((bytes)=>WebAssembly.instantiate(bytes,importObject))
.then((results)=>{
console.log(results)
funcpp = results.instance.exports._Z3fibi
})
针对以下函数,WebAssembly对于性能的提升对比
function funj(n){
if(n<=1){
return n
}
return funj(n-1)+funj(n-2)
}