WebAssembly开发者引导
这个教程将一步一步指导你将一个简单的程序编译成 WebAssembly。
https://www.wasm.com.cn/getting-started/developers-guide/
前置条件
想要编译成WebAssembly,你首先需要先编译 LLVM。这是运行后续工具的先决条件。
- Git。Linux 和 OS X 系统中好像已经默认装好了,在 Windows 上需要在这里安装 Git。
- CMake。在 Linux 和 OS X系统中,你可以使用包管理工具 apt-get 或 brew 来安装。如果是 Windows 系统,你可以点击这里。
- 系统编译工具。Linux上,安装 GCC。OS X 上,安装 Xcode。Windows 上安装 Visual Studio 2015 Community with Update 3 或更新版本。
- Python 2.7.x,在 Linux 和 OS X上,很可能已经装好了。看这里。
安装完毕后,确认 git,cmake 和 python 已经在你的环境变量里,可以使用。
错误处理
Error: No tool or SDK found by name ‘sdk-incoming-64bit’
执行./emsdk install latest
按照提示配置环境变量即可
./emsdk activate latest
编译 Emscripten
通过 Emscripten SDK 构建 Emscripten 是自动的,下面是步骤。
$ git clone https://github.com/juj/emsdk.git
$ cd emsdk
$ ./emsdk install sdk-incoming-64bit binaryen-master-64bit
$ ./emsdk activate sdk-incoming-64bit binaryen-master-64bit
这些步骤完成以后,安装完成。将 Emscripten 的环境变量配置到当前的命令行窗口下。
$ source ./emsdk_env.sh
这条命令将相关的环境变量和目录入口将会配置在当前的命令行窗口中。
在 Windows中,./emsdk 使用 emsdk 代替,source ./emsdk_env.sh 使用 emsdk_env 代替。
编译并运行一个简单的程序
现在,我们已经有了一个完整的工具链,将简单的程序编译成 WebAssembly。不过,这里有一些值得提醒的地方:
在使用 emcc 命令时,要带着 -s WASM=1 参数(不然,默认将会编译成asm.js)。
如果我们想让 Emscripten 生成一个我们所写程序的HTML页面,并带有 wasm 和 JavaScript 文件,我们需要给输出的文件名加 .html 后缀名。
最后,当我们运行程序的时候,我们不能直接在浏览器中打开 HTML 文件,因为跨域请求是不支持 file 协议的。我们需要将我们的输出文件运行在HTTP协议上。
下面这些命令可能让你创建一个简单的“hello word”程序,并且编译它。
$ mkdir hello
$ cd hello
$ echo '#include <stdio.h>' > hello.c
$ echo 'int main(int argc, char ** argv) {' >> hello.c
$ echo 'printf("Hello, world!\n");' >> hello.c
$ echo '}' >> hello.c
$ emcc hello.c -s WASM=1 -o hello.html
我们可以使用 emrun 命令来创建一个 http 协议的 web server 来展示我们编译后的文件。
$ emrun --no_browser --port 8080 .
HTTP 服务开启后,您可以在浏览器中打开。如果你看到了“Hello,word!”输出到了 Emscripten 的控制面板,恭喜你!你的 WebAssembly 程序编译成功了!
编译wasm更多的例子
首先我们知道wasm是目标语言,是一种新的V-ISA标准,所以编写wasm应用,正常来说不会直接使用WAT可读文本格式,更不会用wasm字节码;而是使用其他高级语言编写源代码,经过编译后得到wasm应用。课程中使用了C++来编写源代码,所以这里我也用C++来编写demo。
wasm的运行环境主要分为两类,一类是Web浏览器,另一类就是out-of-web环境,运行于Web浏览器的wasm应用主要使用Emscripten来编译得到,因为它会在编译过程中,为所编译代码在Web平台的功能适配性进行一定的调整。
针对Web平台的编译
对于功能适配性的调整,可以从下面这个例子中得到体现。
编码
首先我们编写一段功能简单的C++源代码:
#include <iostream>
extern "C" {
// 防止Name Mangling
int add(int x, int y) {
return x + y;
}
}
int main(int argc, char **argv) {
std::cout << add(10, 20) << std::endl;
return 0;
}
这段代码里,声明了一个函数“add”,它的定义被放置在“extern “C” {}”结构中,以防止函数名被C++的Name Mangling机制更改,从而确保在宿主环境中调用该函数时,可以用与C++源码中保持一致的函数名,来直接调用这个函数。
这段代码中还定义了主函数main,其内部调用了add函数,并且通过std::cout 来将该函数的调用结果输出到stdout。
编译
现在我们可以用Emscripten这个工具集中最为重要的编译器组件emcc,来编译这段源代码。命令如下所示:
emcc main.cc -s WASM=1 -O3 -o main.html
通过“-s”参数,为emcc指定了编译时选项“WASM=1”,这样emcc就会将输入的源代码编译为wasm格式目标代码,“-o”参数则指定了产出文件的格式为“.html”,这样Emscripten就会生成一个可以直接在浏览器中使用的Web应用。
这个自动生成的应用中,包含了wasm模块代码、JavaScript代码以及HTML代码。
运行
现在我们可以尝试在本地运行这个简单的Web应用。首先自行准备一个简单的Web服务器:
const http = require('http');
const url = require('url');
const fs = require('fs');
const path = require('path');
const PORT = 8888;
const mime = {
"html": "text/html;charset=UTF-8",
"wasm": "application/wasm" // 遇到".wasm"格式文件的请求时,返回特定的MIME
}
http.createServer((req, res) => {
let realPath = path.join(__dirname, `.${url.parse(req.url).pathname}`);
// 检查所访问文件是否存在并且可读
fs.access(realPath, fs.constants.R_OK, err => {
if (err) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end();
} else {
fs.readFile(realPath, "binary", (err, file) => {
if (err) {
// 文件读取失败时返回500
res.writeHead(500, { 'Content-Type': 'text/plain' });
end();
} else {
// 根据请求的文件返回相应的文件内容
let ext = path.extname(realPath);
ext = ext ? ext.slice(1) : 'unknow';
let contentType = mime[ext] || 'text/plain';
res.writeHead(200, { 'Content-Type', contentType });
res.write(file, "binary");
res.end();
}
});
}
});
}).listen(PORT);
console.log("Server is running at port: " + PORT + ".");
这段代码中最为重要的一个地方,就是对wasm格式文件请求的处理。
通过返回特殊的MIME类型“application/wasm”,我们明确告诉浏览器,这是一个wasm格式的文件,这样浏览器就可以允许应用使用针对wasm文件的“流式编译”方式,来加载和解析该文件。
现在我们通过8888端口来访问刚刚编译生成的main.html文件。可以看到,Emscripten将C++源码中使用std::cout将数据输出到stdout,模拟为输出到页面上指定的textarea区域。这就是Emscripten针对Web平台的功能适配性调整。
再继续看,Emscripten自动生成的完整wasm Web应用,不管是js文件还是html文件,体积都偏大,这是因为Emscripten自动生成的“胶水代码”中,包含有通过JavaScript模拟出的POSIX运行时环境的完整代码,而大多数情况下,我们不需要这些。
仅生成wasm模块
那怎样可以使得Emscripten仅生成wasm模块,而js胶水代码和Web API这两部分的代码由我们自己编写呢?
答案就是调整编译时的命令行参数。那么我们要如何去编写JS来调用wasm模块导出的函数呢?
课程里有个图像处理的例子,这里就来整个小例子。
首先编写我们的HTML页面:
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DEMO</title>
</head>
<body>
<div>
<h1>Counter: </h1>
<span>0</span>
<button id="increaseButton">点我+1</button>
</div>
<script src="index.js"></script>
</body>
</html>
这里想要实现一个功能,点击按钮后,span内的数字加1,当然这个功能JavaScript也能做,但现在作为练习,我们要通过调用wasm函数来实现。
然后就是重要的JavaScript代码,如下:
// index.js
document.addEventListener('DOMContentLoaded', async () => {
let response = await fetch('./index.wasm');
let bytes = await response.arrayBuffer();
let {instance} = await WebAssembly.instantiate(bytes);
let {
increase
} = instance.exports;
const span = document.querySelector('span');
const button = document.querySelector('#increaseButton');
let count = 0;
button.addEventListener('click', () => {
count = increase(count);
span.innerText = count;
});
});
首先,通过fetch获取wasm模块,并获取fetch方法返回的Response对象;
然后,调用response对象上的arrayBuffer()方法,将内容解析为ArrayBuffer的形式,这个ArrayBuffer将作为WebAssembly.instantiate方法的实际调用参数;这是一个用于实例化wasm模块的方法。
接着,WebAssembly.instantiate将实例化对应的wasm模块,我们就可以获得模块的实例对象,在instance变量中,可以获得从wasm模块导出的所有方法。
此时,我们就可以调用wasm模块的方法了,假设instance上有个increase方法,就可以这样调用。
现在,我们编写对应的C++代码并进行编译。
// index.cc
#include <emscripten.h>
extern "C" {
EMSCRIPTEN_KEEPALIVE int increase(int x) {
return x+1;
}
}
此处我们需要引入<emscripten.h>,因为需要使用其中定义的宏EMSCRIPTEN_KEEPALIVE,因为这个文件中我们不声明主函数main,也不在文件内部调用这个increase函数,为了防止在编译过程中被DCE(Dead Code Elimination)处理掉,需要使用这个宏来标记函数。
现在我们来编译这个文件。
$ emcc index.cc -s WASM=1 -O3 --no-entry -o index.wasm
仅生成wasm模块文件的编译方式,通常称为”standalone模式”。
“-o”参数为我们指定了输出的文件格式为“.wasm”,这就是告诉Emscripten以“standalone”的方式来编译C++源码。
“–no-entry”参数则告诉编译器,这个wasm模块没有声明“main”函数。
上述命令执行完毕后,就会得到一个名为“index.wasm”的二进制模块文件。
此时我们就可以尝试去运行这个Web应用,可以看到和期待的效果一致。
当然这个demo很简单,目前要发挥wasm的优势,更适合将其应用在计算密集的功能。
调试应用
当我们编写完应用时,少不了要调试。那么如何针对wasm应用进行调试呢,Emscripten也提供了一些方式。
编译阶段
首先是针对编译阶段,当使用emcc编译项目时,可以通过为命令添加“EMCC_DEBUG”环境变量的方式,来让emcc以“调试模式”来编译项目。
$ EMCC_DEBUG=1 emcc index.cc \
> -s WASM=1 \
> -O3 \
> --no-entry -o index.wasm
可以看到编译时输出了很多的信息,这是因为我们将EMCC_DEBUG这个环境变量的值设置为1,EMCC_DEBUG的值可以设置为3个值,分别是0、1、2。
0表示关闭调试模式,这和不加这个环境变量是一样的效果;1表示输出编译时的调试性信息,同时生成包含有编译器各个阶段运行信息的中间文件;可用于编译流程的调试。
可以通过ls命令查看生成了哪些文件;调试性信息中包含了各个编译阶段所实际调用的命令行信息,通过对这些信息分析,能够辅助开发者查找编译失败的原因。
当EMCC_DEBUG的值设置为2时,可以得到更多的调试性信息。
运行阶段
当我们成功地编译了wasm应用,但在实际运行时发生了错误,就需要在运行时进行调试。Emscripten也提供了一定的支持,我们可以在编译时设定参数“-g“以保留与调试相关的信息。
当设置为”-gsource-map“时,emcc会生成可用于在Web浏览器中进行“源码级”调试的特殊DWARF信息;通过这些特殊格式的信息,使我们可以直接在浏览器中对wasm模块编译之前的源代码进行诸如“设置断点”、“单步跟踪”等调试手段。
这里我们尝试调试之前编写的index.cc。
$ emcc index.cc -gsource-map -s WASM=1 -O3 --no-entry -o index.wasm
此时重新加载Web应用并打开“开发者面板”的“sources”Tab,就可以通过“操作”C++源代码的方式,来为应用所使用的wasm模块设置断点。(wasm模块的加载方式需要改为“流式编译”)。
通过这种方式,开发者就可以方便地在wasm Web应用的运行过程中,调试发生在wasm模块内部的“源码级”错误。
WebAssembly作为一种相对较新的技术,可以先保持一点了解。
把c++编译出的.js和.wasm引入vue
把wasm放到vue里,要修改:
- 在vue.config.js中添加配置
const CopyWebpackPlugin = require('copy-webpack-plugin');
plugins: [
new CopyWebpackPlugin([
{
from: "./src/wasm/out/sig_handler.wasm",
to : "./static/js/sig_handler.wasm"
},
])
]
意思大概是把wasm文件放到打包后的static/js/位置,这样编译wasm过程中生成的胶水js文件就能访问到wasm文件了
- 然后在生成的胶水js文件末尾加上
export default Module;
- 这样就能在main.js中导入了
const OriginalVueWasm = import('@/wasm/out/sig_handler')
导入之后使用
async function waitwasm() {
const wasmmodule = await OriginalVueWasm;
wasmmodule.default.onRuntimeInitialized = () => {
Vue.prototype.$wasm = wasmmodule.default;
//new Vue ...
}
}
(async () => {
waitwasm()
})()
这样在vue里就能通过this.$wasm来访问了
webpack5开启实验性参数引入WASM
emcc是webassemble的编译器,类似于gcc的作用。
emcc编译C/C++代码的流程:
C/C++代码通过emcc编译为字节码,然后根据不同的目标编译为asm.js或wasm。emcc和gcc编译选项类似,例如-s OPTIONS=VALUE、-O等。另外为了适应Web环境,emcc增加了一些特有的选项,如–pre-js 、–post-js 等。
setting.js里边有编译关键参数。
WebAssembly之emcc编译命令 - 云+社区 - 腾讯云cloud.tencent.com/developer/article/1695218
setting.jsemsettings.surma.technology/
emcc hello.c -O3 -s SIDE_MODULE=1 -o hello.wasm
需要的库一般这么构建出wasm就可以直接被浏览器API使用。
但是,webpack5是直接支持导入的,不过要添加实验性参数。
重要的是,如果你用的cra,需要在file-loader中去掉wasm后缀的文件,否则会被file-loader捕获。
总的来讲就是这个不能被任何loader捕获,webpack5自带解析。
experiments: {
asyncWebAssembly: true,
syncWebAssembly: true
},
编译后直接像普通module一样import就行了。
import * as main from './hello.wasm'
监听print事件
在wasm自动生成的胶水代码.js中,stdout和stderr分别绑定的语句为:
var out = Module['print'] || console.log.bind(console);
var err = Module['printErr'] || console.error.bind(console);
要修改输入输出的监听事件,可以重定义Module:
var Module = {
preRun: [],
postRun: [],
print: function(text) {
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
console.warn(text);
},
printErr: function(text) {
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
console.error(text);
},
或者直接修改胶水代码中的out和err变量。
文件读写
跨平台的C/C++程序常使用fopen()、fread()、fwrite()等Libc/LibCXX提供的同步文件访问函数。通常在文件系统方面,JavaScript程序与C/C++本地程序有巨大的差异,主要体现在:
运行在浏览器中的JavaScript程序无法访问本地文件系统;
在JavaScript中,无论ajax()还是fetch(),都是异步操作。
Emscripten提供了一套虚拟文件系统,以兼容Libc/LibCXX提供的同步文件访问函数。
在最底层,Emscripten提供了3种文件系统,分别为MEMFS(内存文件系统)、NODEFS、IDBFS。它们各自的特点:
MEMFS系统的数据完全存储于内存中,程序运行时写入的数据在页面刷新或程序重载后将丢失;
NODEFS是Node.js文件系统,可以访问本地文件系统,可以持久化存储数据,但只能用于Node.js环境。
IDBFS是IndexedDB文件系统,是基于浏览器的IndexedDB对象,可以持久化存储数据,但只能用于浏览器环境。
基于MEMFS的打包文件系统
文件导入MEMFS系统之前,需要先将其打包。文件打包有两种方式:
emcc命令:embed和preload。
单独的文件打包工具file_packager.py。
1、embed模式
文件数据被转为JavaScript代码;
(对于embed模式,其需要将数据文件化编码,所产生的文件包体积大于preload模式下产生的文件包体积,因此除非需要打包的文件总数据量非常小,否则尽可能使用preload模式)
2、preload模式
除了生成.js文件外,还会额外生成同名的.data文件。其中,.data文件包含所有文件的二进制数据,.js文件包含.data文件包下载、装载操作的胶水代码。
int main()
{
FILE* fp = fopen("hello.txt","rt");
if(fp)
{
while(!feof(fp))
{
char c = fgetc(fp);
if(c!=EOF)
{
putchar(c);
}
}
fclose(fp);
}
return 0;
}
emcc pack.c -o pack.js --preload-file hello.txt
–preload-file参数不仅可以打包单个文件,还可以打包整个目录。
emcc pack.c -o pack.js --preload-file dat_dir
生成的打包文件pack.data包括dat_dir内的所有内存
3、file_packager.py打包
步骤一:
python /home/hyde/emsdk/upstream/emscripten/tools/file_packager.py fp.data --preload file --js-output=fp.js
将file目录内的文件打包成fp.data和fp.js文件。
步骤二:
使用外挂文件包时,主程序编译必须增加 -s FORCE_FILESYSTEM=1参数以强制启用文件系统。
emcc test.c -o test.js -s FORCE_FILESYSTEM=1
步骤三:
在网页中,必须先引入外挂文件包.js,再引入主程序.js
<script src="fp.js"></script>
<script src="test.js"></script>
虽然下载文件包是异步的,但是Emscripten可以确保运行时准备就绪时,文件系统初始化完成,因此在Module.onRuntimeInitialized()回调函数中使用文件系统是安全的。
NODEFS文件系统
void setup_nodefs(){
EM_ASM(
FS.mkdir('/data');
FS.mount(NODEFS,{root:'.'},'/data');
);
}
int main()
{
setup_nodefs();
FILE* fp = fopen("/data/nodefs_data.txt","r+t");
if(fp == NULL)
{
fp = fopen("/data/nodefs_data.txt","w+t");
}
int count =0 ;
if(fp)
{
fscanf(fp,"%d",&count);
count++;
fseek(fp,0,SEEK_SET);
...
}
else{
printf("fopen failed\n");
}
return 0;
}
FS.mkdir(‘/data’);//在虚拟文件系统中创建了‘/data’目录
FS.mount(NODEFS,{root:‘.’},‘/data’);//把当前的本地目录挂接到了/data目录。
IDBFS文件系统
EMSCRIPTEN_KEEPALIVE
void test()
{
FILE* fp = fopen("/data/nodefs.txt","r+t");
...
}
int main(){
EM_ASM(
FS.mkdir('/data');
FS.mount(IDBFS,{},'/DATA');
FS.syncfs(true,function(err){
assert(!err);
ccall('test','v');
});
);
return 0;
}
IDBFS的挂接是通过FS.mount()方法完成的。
事实上在运行时,IDBFS仍然是使用内存来存储虚拟文件系统,只不过IDBFS可以通过FS.syncfs()方法进行内存数据与IndexedDB的双向同步,达到数据持久化存储的目的。
FS.syncfs()是异步操作,因此,例子中的读写文件的test()函数必须在FS.syncfs()的回调函数中调用。
转载自:https://blog.csdn.net/qq_34754747/article/details/120651937
Embind绑定普通函数和类
Embind用于将 C++ 函数和类绑定到 JavaScript,以便编译后的代码可以被“正常”的 JavaScript 以自然的方式使用。Embind还支持从 C++ 调用 JavaScript 类。
Embind 支持绑定大多数 C++ 结构,包括 C++11 和 C++14 中引入的结构。它唯一重要的限制是它目前不支持具有复杂生命周期语义的原始指针。
所有通过Embind暴露的symblols都可以在Module对象获取
只绑定那些实际需要的项目,因为每次绑定都会增加代码大小
JavaScript,特别是 ECMA-262 版本 5.1,不支持终结器 或带有回调的弱引用。因此,Emscripten 无法自动调用 C++ 对象上的析构函数。
JavaScript 代码必须明确删除它收到的任何 C++ 对象句柄,否则 Emscripten 堆将无限增长。
- 添加头文件#include <emscripten/bind.h>
- 使用EMSCRIPTEN_BINDINGS()进行绑定,将关键方法进行映射,
- 编译时加入–bind参数
不使用EMSCRIPTEN_BINDINGS()
在之前初步的使用WebAssebmly时,通常使用如下:
将方法强制为C接口,并加上宏EMSCRIPTEN_KEEPALIVE表明此方法需一直保留不被优化
#ifdef __cplusplus
extern "C" {
#endif
void EMSCRIPTEN_KEEPALIVE myFunction(void) {
printf("我的函数已被调用\n");
}
#ifdef __cplusplus
}
#endif
在调用的时候,方法前必须要加上’_’
绑定普通函数
接下来看一个简单的例子
#include <emscripten/emscripten.h>
#include <emscripten/bind.h>
using namespace emscripten;
float compareBig(int a, int b) {
return a > b ? a : b;
}
EMSCRIPTEN_BINDINGS(my_module) {
function("compareBig", &compareBig);
}
使用命令
emcc --bind -o quick_example.js quick_example.cpp
此时,就可以用Module进行直接调用
绑定类
class MyClass {
public:
MyClass(int num){ m_num = num; };
void CompareBig(int x, int y)
{
printf("Big one is %d\n", x > y ? x : y);
}
static int getNum(const MyClass& instance) {
return instance.printfNum;
}
private:
int printfNum()
{
return m_num;
}
private:
int m_num;
};
// Binding code
EMSCRIPTEN_BINDINGS(my_class_example) {
class_<MyClass>("MyClass")
.constructor<int>() //构造函数
.function("CompareBig", &MyClass::CompareBig) //普通类成员函数
.class_function("getNum", &MyClass::getNum) //静态类成员函数
;
}
使用后记得需要释放:instance.delete()
新建一个example.cpp文件,代码如下:
#include <emscripten/bind.h>
using namespace emscripten;
struct Point {
int x;
int y;
};
Point getPoint() {
Point point = {0};
point.x = 100;
point.x = 200;
return point;
}
EMSCRIPTEN_BINDINGS(my_module) {
value_object<Point>("Point")
.field("x", & Point::x)
.field("y", & Point::y)
;
function("_getPoint", &getPoint);
}
使用embind编译上例,请调用emcc的bind选项,编译指令如下:
$ emcc --bind -o example.js example.cpp -O3 -s WASM=1
在JavaScript中调用如下:
var oPoint = Module._getPoint();
var ix = oPoint.x;
var iy = oPoint.y;
绑定属性
假如上个例子里面打算直接给m_num赋值,而它本身又是一个私有变量,我们可以直接设置属性将其暴露
class MyClass {
public:
MyClass(int num){ m_num = num; };
void CompareBig(int x, int y)
{
printf("Big one is %d\n", x > y ? x : y);
}
static int getNum(MyClass& instance) {
return instance.printfNum();
}
int getNumValue() const
{
printf("getNumvalue:%d\n", m_num);
return m_num;
}
void setNum(int num)
{
printf("setNum:%d\n", num);
m_num = num;
}
private:
int printfNum()
{
return m_num;
}
private:
int m_num;
};
// Binding code
EMSCRIPTEN_BINDINGS(my_class_example) {
class_<MyClass>("MyClass")
.constructor<int>() //构造函数
.function("CompareBig", &MyClass::CompareBig) //普通类成员函数
.property("m_num", &MyClass::getNumValue, &MyClass::setNum)
.class_function("getNum", &MyClass::getNum) //静态类成员函数
;
}