WebAssembly其实出现已经好多年了,是在浏览器端虚拟环境中运行汇编语言的一个方式,它编写的程序叫做wasm。WebAssembly 于 2015 年首次发布,主要的目标是让浏览器执行的速度追赶上原生应用程序,目前绝大多数浏览器都开始默认启用 WebAssembly。
编写WebAssembly,可以用很多语言,比如C,C++,RUST,.NET 等语言
下面我将演示用C++开发一个WebAssembly程序的完整过程。
开发系统环境:ubuntu16
1.开发环境安装
虽然WebAssembly可以用C++开发,但这并不意味着,使用GCC,或者cl等编译器编译出的程序,直接可以在浏览器中运行,要把C/C++或者其它语言编译为wasm文件,需要使用其它工具进行翻译。目前最标准的使用工具就是emscripten
emscripten安装
emscripten这个东西,它对运行环境的要求很严格,所以,安装过程要严格按照官方提供的方式操作
我的开发环境是ubuntu,在ubuntu上安装emscripten,首先你要安装git,你的系统有没有安装过git,在终端上,执行一下就行
git
如果安装过,输出的内容应该和下图输出的差不多
如果没有的话,就安装一个。虽然emscrpten也可以在github上下载,但还是建议安装.输入以下命令,等待安装就可以
sudo apt-get install git
安装完git以后,就可以按照官方安装教程一步步的来了
# 从git下载emsdk
git clone https://github.com/emscripten-core/emsdk.git
# 进入emsdk目录
cd emsdk
# 合并本地代码(第一次下载的话,不需要)
git pull
# 下载和站桩最新版本
./emsdk install latest
#把最新版本设置为活动版本
./emsdk activate latest
# 设置环境变量
source ./emsdk_env.sh
上面这些步骤很重要,因为目前可以参考的资料较少,最好按照上面的步骤执行,如果某一步执行错误的话,要多次执行,保证每一步都执行完成
执行完成后在emsdk文件夹中的内容,大概如下
emcc -v
python是编译WebAssembly最重要的工具
目前emscripten版本所需要的python,是需要python3.6以上,如果你安装3.6以下版本,编译会失败。
如果你用的ubuntu也是16,那么系统内置的是python2,不能使用现在emscripten,要安装python3.6以上的版本
python3.6版本的安装建议使用源码安装,因为通过apt-get,我没有安装成功
去python官方网站下载
https://www.python.org/downloads/source/
我下载的是3.6.9 如果你没有把握,也和我下载一样的版本,然后执行解压
tar -xvzf Python-3.6.9.tg
解压完成后,进入Python-3.6.9文件夹,然后执行
./configure
然后执行
make
make install
这样就开始安装python3.6了
安装完成后,删除原来的python2链接
rm -rf /usr/bin/python
然后吧python3作为默认链接
ln -s (你的py3安装地址) /usr/bin/python
都操作完成后,直接敲入python,看看操作对了没有
如果你不知道刚才python3安装到哪,执行以下命令查询,然后再执行以上步骤
whereis python3
cmake安装
emscripten可以执行cmake编译,如果你要编译点小东西玩,就不需要了,但还是建议安装,如果你make用的熟的话,也可以使用make
可以看看自己安装过cmake没有,执行如下命令
cmake
如果没有,就apt-get安装
apt-get install cmake
chrome浏览器
这个倒也不是必须的,除了chrome浏览器,edge等chrome内核浏览器也行,firefox也行,但是你安装的chrome,起码要75以后的版本才可以
以上东西都安装后,基本开发环境就完成了
2.用WebAssembly操作excel
下面来写个WebAssembly操作excel文件的demo
完整的代码可以到以下地址获取
https://github.com/ltframe/code/tree/main/wasm-excel-demo
https://download.csdn.net/download/weixin_44305576/85906590
操作execl.我采用的是C++编写的BasicExcel库,采用这个库的原因是,它虽然没有很强大功能,但足够小巧,使用简单,而且文件少,基本表格操作具备。我以前写过关于它的使用,可以去翻翻
实现的目标
1.读出内容,然后在C++中生成Json,输出到前端,再由前端程序显示出来
2.在前端修改内容,把修改后的json传回C++,然后生成新的文件,下载到浏览器
BasicExcel只能打开xls文件,xlsx不行
创建excel文档
创建一个excel文件,里面弄两个工作表,
然后在第一个工作表里输入内容
第二个工作表里也随便输入一点
编写C++文件
class myclass
{
public:
wstring outputstr = L"";
myclass()
{
}
void saveexcel(string value)
{
cJSON *json = cJSON_Parse(value.c_str());
cJSON *info = cJSON_GetObjectItem(json, "info");
cJSON *count = cJSON_GetObjectItem(info, "count");
cJSON *tablenames = cJSON_GetObjectItem(info, "tablenames");
int _count = count->valueint;
cJSON *data = cJSON_GetObjectItem(json, "data");
BasicExcel *e = new BasicExcel();
e->New(_count);
for (int i = 0; i < 1; i++)
{
BasicExcelWorksheet* sheet = e->GetWorksheet((unsigned)i);
cJSON *name = cJSON_GetArrayItem(tablenames, i);
sheet->Rename(StringToWString(name->valuestring).c_str());
cJSON *list = cJSON_GetObjectItem(data, name->valuestring);
for (int j = 0; j < cJSON_GetArraySize(list); j++)
{
cJSON *item = cJSON_GetArrayItem(list,j);
for (size_t k = 0; k < cJSON_GetArraySize(item); k++)
{
cJSON *c = cJSON_GetArrayItem(item, k);
BasicExcelCell* cell = sheet->Cell(j, k);
cell->SetWString(StringToWString(c->valuestring).c_str());
}
}
}
e->SaveAs("/data/demo.xls");
}
void readsheet(BasicExcelWorksheet* sheet1)
{
size_t rows = sheet1->GetTotalRows();
size_t cols = sheet1->GetTotalCols();
for (size_t r = 0; r < rows; r++) {
outputstr+=L"[";
for (size_t c = 0; c < cols; c++)
{
BasicExcelCell* cell = sheet1->Cell(r, c);
//判断单元格类型
switch (cell->Type())
{
case BasicExcelCell::WSTRING:
{
const wchar_t* ret = cell->GetWString();
outputstr += L"\"";
outputstr += ret;
outputstr += L"\"";
if (c != cols - 1) {
outputstr += L",";
}
}
break;
default:
break;
}
}
outputstr+=L"]";
if(r!=rows-1){
outputstr+=L",";
}
}
}
wstring loadexcel(string fb)
{
outputstr += L"{\"info\":{";
BasicExcel *e =new BasicExcel;
wprintf(L"%ls\r\n",L"start load...");
string fname = string("/data/")+fb;
if (!e->Load(fname.c_str()))
{
wprintf(L"%ls\r\n",L"load failed");
return wstring(L"load failed\n");
}
int sheetscount = e->GetTotalWorkSheets();
outputstr +=L"\"count\":"+ std::to_wstring(sheetscount);
outputstr+=L",\"tablenames\":[";
for (size_t i = 0; i < sheetscount; i++)
{
outputstr+=L"\"";
outputstr+=e->GetUnicodeSheetName(i);
outputstr+=L"\"";
if(i!=sheetscount-1){
outputstr+=L",";
}
}
outputstr+=L"]},\"data\":{";
for (size_t i = 0; i < sheetscount; i++) {
BasicExcelWorksheet* sheet1 = e->GetWorksheet(i);
if (sheet1) {
outputstr += L"\"";
outputstr += e->GetUnicodeSheetName(i);
outputstr += L"\":[";
readsheet(sheet1);
outputstr += L"]";
if(i!=sheetscount-1){
outputstr += L",";
}
}
}
outputstr += L"}}";
wprintf(L"%ls",outputstr.c_str());
return outputstr;
}
};
EMSCRIPTEN_BINDINGS(myclass) {
class_<myclass>("myclass")
.constructor()
.function("loadexcel", &myclass::loadexcel)
.function("saveexcel", &myclass::saveexcel)
;
}
void setup_IDBFS(){
EM_ASM(
FS.mkdir('/data');
FS.mount(IDBFS,{},'/data');
);
}
int main()
{
setup_IDBFS();
setlocale(LC_ALL, "chs");
return 0;
}
BasicExcel使用就不说了,想了解的话,可以去看我以前写的文章。
主要说说emscripten相关部分
EMSCRIPTEN_BINDINGS的作用是建立一个C++类和JS类之间的绑定,绑定后,在前台js代码里,可以像使用一个js类一样使用它
function(“loadexcel”, &myclass::loadexcel),意思是,导出这个函数给前台JS使用,第一个参数,是在JS里调用方法的名称,第二个参数就是你的C++成员函数引用
EM_ASM(
FS.mkdir('/data');
FS.mount(IDBFS,{},'/data');
);
这段代码表示创建一个虚拟的data文件夹,然后把他挂载到IDBFS系统中,以后对excel文件的操作都在这个文件夹中
setup_IDBFS函数的作用,是建立一个文件系统,采用IDBFS方式挂载虚拟文件的路径。因为WebAssembly终究还是运行在一个虚拟系统中,不可能访问你的硬盘系统,所以setup_IDBFS,意思就建立了一个JS的文件系统和你的C++代码文件系统的桥梁。
除了IDBFS,还有其它文件系统,每种文件系统的使用场景是不同的。可以自己去看看教程
编写cmake
CMAKE_MINIMUM_REQUIRED(VERSION 3.5)
PROJECT(cx)
AUX_SOURCE_DIRECTORY(. DIR_SOURCE)
SET(CMAKE_BUILD_TYPE "Debug")
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)
SET(CMAKE_C_COMPILER emcc)
SET(CMAKE_CPP_COMPILER 'em++')
set(CMAKE_CXX_FLAGS "--bind")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='[FS]' -s INITIAL_MEMORY=268435456 \
-s ASSERTIONS=1 \
-s STACK_OVERFLOW_CHECK=2 \
-s PTHREAD_POOL_SIZE_STRICT=2 \
-s ALLOW_MEMORY_GROWTH=1 \
-lidbfs.js")
ADD_EXECUTABLE(app ${DIR_SOURCE})
如果对CMAKE不太了解的话,可以先看看文档,简单了解一下cmake.
set(CMAKE_CXX_FLAGS "--bind")
表示要启用绑定功能,就是c++里写的EMSCRIPTEN_BINDINGS
CMAKE_EXE_LINKER_FLAGS 选项中lidbfs.js,表示要使用IDBFS文件方式
EXTRA_EXPORTED_RUNTIME_METHODS表示要导出FS文件系统前台使用
这样,C++部分就算完成了,就开始编译了,
在C++项目文件夹中执行如下命令,
mkdir build
进入build文件夹,执行
emcmake cmake ..
执行完成后,就出现了app.js app.wasm两个文件,这就是你的c++编译后的文件,app.wasm是c++编译后的文件,app.js是一个类似胶水的文件,把你的wasm和Js文件关联在一起,当然没有这个也可以,你也可以自己写调用模块
编写javascript
应为这个比较简单,本段只贴出操作的核心部分。其它部分省略
function writefile(e){
let result=reader.result;
const uint8_view = new Uint8Array(result);
FS.writeFile('/data/'+file.name, uint8_view)
}
function savetodisk()
{
var data=FS.readFile("/data/demo.xls");
var blob;
blob = new Blob([data.buffer], {type: "application/vnd.ms-excel"});
saveAs(blob, "demo.xls");
}
function myfun(e) {
let files = document.getElementById('file').files;
file=files[0];
reader.addEventListener('loadend', writefile);
reader.readAsArrayBuffer(file);
setTimeout(()=>{
let jsonstr = instance.loadexcel(file.name);
//code......
},1000)
}
writefile函数的功能,是打开本地的excel文件,在虚拟的路径/data/当中,创建一个文件,这样C++里就能读取到它了,这里的data就是C++文件中setup_IDBFS创建的文件夹
savetodisk是从虚拟文件路径中,读取demo.xls文件,然后在浏览器端下载,这个demo.xls是在C++文件中saveexcel函数创建的
编写HTML
这就简单的多,代码就不贴了,主要就是把你刚才生成的app.js引入就行了,wasm不用引入,app.js会加载它
<script type="text/javascript" src="app.js" async>script>
运行程序
wasm文件不能直接打开运行,所以需要一个服务器方式运行,我采用的是anywhere,用其它的iis,apache都可以
运行界面如下
打开调试工具,网络->wasm,可以看到app.wasm已经加载完了
接下来就可以打开excel文件了,点击选择,选中第一步创建好的excel文件,确定
显示的内容,就是excel文件的内容
然后随便改改单元格里的内容
点保存
提示下载新的文件,下载好后打开
就是刚才改完的内容
到此,全部功能开发完成
完整源代码和演示代码去下面的地址下载
https://github.com/ltframe/code/tree/main/wasm-excel-demo
https://download.csdn.net/download/weixin_44305576/85906590
WebAssembly在速度方面,确实比Javascript提升很多,加上有很多的底层语言编写的库,这样就使得web应用的功能提升很多,比如我们通常如果要读取excel的话,一般做法是把excel上传到服务器,然后服务器解析后,再返回前端,现在有了wasm这种方式,在前端就可以搞定,这样服务器的压力少了很多。
还有,以前浏览器不能实现的功能,通过wasm也能实现了。比如,如果你把ffmpeg用WebAssembly编译后,那么浏览器上将可以播放除了mp4以外更多的视频格式,这确实就突破了浏览器本身很多的限制
未来,会有更多WebAssembly应用出现,一些原本只能运行在桌面上的应用,会随着WebAssembly的发展,更多的出现在web应用中