以前也尝试过学WASM,但由于当时没有应用场景所以安装完环境就弃坑了,这次来好好学习一下用法。这篇笔记是边学边写的,如果有错误或者需要补充的地方请留言。
WASM通常需要使用其他高级语言编写后编译为wasm二进制文件交给运行时去运行,但如果你的头够铁,也可以尝试手写wasm指令,或者通过wasm的文本格式理解它的底层运行原理。
本笔记在Windows平台上使用C++编写源码编译为wasm,所以需要安装emscripten工具包。除了C++以外,Rust也是一个推荐选项,而其它语言目前还没有非常完备的支持。
安装Emscripten
这是用于把C/C++编译到wasm的工具包。
Emscripten的项目地址在:https://github.com/emscripten-core/emscripten
文档地址在:https://emscripten.org/index.html
安装方式官方手册在:https://emscripten.org/docs/getting_started/downloads.html,这里我简单介绍一下,但还以官方手册为准,我不会同步更新。
首先把emscripten仓库克隆到任意位置,然后进入该目录
git clone --depth 1 https://github.com/emscripten-core/emsdk.git cd emsdk
然后依次执行以下命令进行安装
emsdk.bat install latest emsdk.bat activate latest --permanent
上面的` –permanent`参数是为了让emsdk的命令在全局可用,否则你需要在每个命令环境中都执行一次activate。执行完了之后可以关闭当前的命令窗口,然后找个地方开始准备写我们的代码了。
编写前的准备
新建一个test目录,在里面新建一个test.cpp文件(用于编写源码)和一个start.js文件(用于导入并运行wasm)以及一个空的dist目录(用于存放编译结果),我将在test.cpp中编写我们所有的测试代码。
首先进行一些说明:通过emcc编译出来的wasm程序默认会自带一个js文件作为wasm文件的加载器,把这个生成的js文件引入网页或者使用nodejs执行都可以得到它的执行结果,但现在开发项目通常都会把各种外部库作为模块导入,而不是直接引入到全局作用域,因此你需要添加一些编译选项来让emcc导出一个模块化的加载器,使其可以被其它代码import,下面会讲。
再补充一些概念说明:正是因为Emscripten帮你创建了用于加载wasm文件的加载器js,运行这个加载器你将直接得到一个emscripten的wasm实例对象,因此你无需关心这个wasm文件最终是如何被加载和初始化的,也无需关心wasm实例的初始化过程需要哪些配置以及跨环境如何进行通信,但如果你想了解手动加载wasm需要做些什么,可以看这个MDN文档。
编译参数:为了符合通常的开发习惯,因此接下来所有的编译过程都会添加以下编译选项:
- -sMODULARIZE=1,作用是让生成的js文件作为模块导出,之后我们会编写一个start.js作为执行入口来导入我们的编译结果再执行。
- -sEXPORT_ES6 (自动)指定该参数表示强制生成es6格式的模块,不指定时根据输出文件名自动判断, .js 后缀默认会生成UMD格式的模块, .mjs 后缀默认是es6模块,为了浏览器和node之间测试的通用性,我们均导出es6模块,之后的测试中输出文件均为mjs,此选项是自动启用的,所以该选项有时我会省略。
-
–sASSERTIONS,作用是显示更详细的错误信息,以便我们调试。在编译发布版本时不用加这个,也不应该加这个。
- 关于更多的编译选项,可以查看官方文档:https://emscripten.org/docs/tools_reference/settings_reference.html。
有一个不使用的编译参数:-sWASM=0|1|2,在很多地方会出现却没说明为什么要设置这个值, 所以我觉得需要特别说明一下,这个参数有三个值可选:
- -sWASM=0:编译器将不会生成wasm文件,仅仅把C++编译为等效的Javascript代码。没错,emscripten不光可以把c编译到wasm,也可以直接编译成等效的js。
- -sWASM=1(默认):编译器将生成一个wasm文件和一个js加载器文件,生成的两个文件搭配使用,缺一不可。由于其默认值就是1,所以之后的测试中不会去指定它。
- -sWASM=2:编译器同样会生成一个wasm文件和一个js文件,但这个js文件除了能够加载wasm文件以外也包含了从c++编译为js的等效代码(相当于0和1选项混合),用于在运行环境无法加载wasm时自动以js替代执行。
还有一个常用的可选编译参数: -sSINGLE_FILE,可以让导出的wasm文件以base64的形式嵌入到js文件中,避免分为两个文件加载,这个选项适合用于发布版本,在之后的测试中我们不使用它。
输出优化:在添加了各种库之后输出js文件和wasm文件的大小会很大,这时可以使用-O
参数(这是O不是零)来开启编译优化,这个O参数和gcc本身的O参数含义有些区别,它不光影响输出的二进制wasm,也会影响js文件,具体用法可以看这个文档,在学习的过程中我也不会去使用它,该参数可以在输出最终发布版本的时候使用以减小文件大小。
为了之后方便编译,建议把编译命令编写成 脚本文件,或者写进 package.json的scripts条目 里以便直接调用。
第一个WASM程序
打开test.cpp文件,在里面写入以下测试代码:
#include <iostream> int main() { std::cout << "Hello, World!" << std::endl; return 0; }
然后执行命令编译:emcc -sMODULARIZE=1 -sASSERTIONS -sEXPORT_ES6 -o dist/test.mjs test.cpp
执行完后应当在dist目录中生成了两个文件,一个test.mjs和一个test.wasm(注意,这两个文件是搭配使用的,需要始终放在同一个目录里),如果没有或者编译过程出错了的话就说明前面的自己根据错误搜搜,这里就当已经成功编译了。
然后打开start.js,写入以下内容:
(async ()=>{ const {default:wasm}=await import('./dist/test.mjs'); const test=await wasm(); console.log(test); })();
都保存后,直接执行node start.js
,你将能在结果中看到”Hello, World!”以及wasm实例对象的内容。其中`_main`是emcc导出的cpp文件中对应的main函数,加载器js文件会自动执行该方法,如果你不需要自动执行任何函数,可以在cpp文件中不写main函数,编译器的默认选项会自动忽略它。
到这能够成功跑起来就已经算是能够使用wasm了,接下来我会继续写几篇笔记以完成更复杂的任务。
本文发布于 https://luojia.me
本站文章未经文下加注授权不得拷贝发布。