WebAssembly入门

Posted by wyj on January 25, 2020

这篇文章没有被放到Hello, world里面,是因为我暂且还不知道这个有什么用。

WebAssembly,是用来在网站上运行一些非JavaScript语言程序的方法。

准备开发环境

按照这篇文章里面说的做。除了那个sdk-incoming-64bit实在是找不到,我把这个参数替换成了emscripten-master-64bitlatest两个参数。

过程中你就顺带着装好了一个nodeJS和一个Clang。我发现LLVM这个东西的版本很奇怪,Ubuntu 18.04的默认软件仓库里面的emscripten中版本是3.4;默认源中clang中版本是6;LOJ上是7;我现在日常在用的是8;手机里的是9;这个sdk中版本是10,然而令人尴尬的是em++嫌版本10太了,它期望的是11。

C++部分

干任何事之前,请记得调用

source PATH/TO/EMSDK/emsdk_env.sh --build=Release

否则会使用你的默认编译器。

我的入门程序是参照网上的,写了一个求斐波那契数。然而我对那个$O(ans)$的实现很看不起,就写了个完全没卡过常的矩乘。

有两个注意点:一个是程序中所有不需要导出的变量和函数都要开成static;另一个是所有要导出的变量和函数都要开成extern "C",因为C++默认会对函数名称进行一些修饰,使得函数重载和名字空间可以正常工作,然而C不会。

clang++的头文件是没有bits/stdc++.h的,你平常可以使用是因为一般clang会用系统的默认头文件,GNU/Linux的默认头文件当然是g++的了。

先放C++代码:

#include<cstring>
#include<initializer_list>
using ll=long long;
static const ll mod=998244353;
static ll ma[2][2],mb[2][2],mc[2][2];
static void po(int x){
	if(!x){
		mb[0][0]=mb[1][1]=1;
		mb[1][0]=mb[0][1]=0;
		return;
	}
	po(x/2);
	memset(mc,0,sizeof mc);
	mc[0][0]=mc[0][1]=mc[1][0]=mc[1][1]=0;
	for(int i:{0,1})for(int k:{0,1})for(int j:{0,1})
		(mc[i][j]+=mb[i][k]*mb[k][j])%=mod;
	memcpy(mb,mc,sizeof mc);
	if(x&1){
		memset(mc,0,sizeof mc);
		mc[0][0]=mc[0][1]=mc[1][0]=mc[1][1]=0;
		for(int i:{0,1})for(int k:{0,1})for(int j:{0,1})
			(mc[i][j]+=mb[i][k]*ma[k][j])%=mod;
		memcpy(mb,mc,sizeof mc);
	}
}
extern"C"{
int fib(int x){
	ma[0][0]=0;
	ma[1][1]=ma[0][1]=ma[1][0]=1;
	po(x);
	return mb[1][0];
}
}

这里的fib声明是extern "C" int fib(int),不用long long是因为需要额外写一个setTempRet0函数接收返回值的高32位。

然后编译。如果生成的文件过大,这里可以把O3改成Os。

em++ 1.cpp -s WASM=1 -s SIDE_MODULE=1 -O3 -o 1.html -std=c++14

JS部分

JS的作用基本上是网页上的数据与C++函数通信的中介。这是真正痛苦的地方。网上的介绍貌似基本都不是很行。我主要是参考的这个实现。然而此实现有两个问题:一是导出的函数名前方貌似不需要下划线(应该是extern "C"的原因?);二是没有分配栈空间。所以会出现错误:LinkError: WebAssembly.Instance(): Import #0 module="env" function="stackSave" error: function import requires a callable

我把这个错误扔到Google上面搜,没有一个解决方案可以帮助我。仅有的几个实现都把这些函数声明成了abort("...")或者return 0;,都不能解决我的问题。最后我猜到这个应该是指定的栈空间大小,就随便填了个$1000$上去,运行成功了!

// 在声明imports.env之后加上这句
imports.env.stackSave = imports.env.stackRestore = function(){return 1000;};

写完之后肯定是要测试一下效果的,不过有一个奇怪的限制是这个html不能双击打开,必须要先启动一个服务器,比如python3 -m http.server,然后才能查看。

我就把这个实现扔到了本网站上。然后发现一旦把layout设置成page,就会默认显示在网站首页的右上角。我觉得很丑,就新增了一个hidden属性。

速度测试

这个”速度测试”是WebAssembly和本地c++编译运行的对比。WebAssembly肯定比js快得多这不用说了吧。

为了测速度,继续用矩乘实现貌似不是很行。于是我写了一个简单的$O(ans)$递归,当$n=40$时直接C++编译运行用了473ms,Chrome上用了811ms,Firefox上用了732ms。