引言
上一节我们讲到如何用Emscripten将一个C编译陈wasm,并导出可供Javascirpt调用的接口,以及C++导出类的函数接口、导出类的封装对象等。然而,编译的方式比较玛法,有没办法能更友好一点实现wasm的编译呢
WASM 相关文档:
WebAssembly编译之(1)-asm.js及WebAssembly原理介绍
WebAssembly编译之(2)-Ubuntu搭建WASM编译环境
WebAssembly编译之(3)-WASM编译实战之C/C++导出asm.js及wasm库
这一节我们继续给大家介绍wasm编译进阶教程,多个文件的编译,甚至多个接口的编译。
上一节前面我们知道,每次执行emcc
指令时,我们都需要在命令行中携带参数EXPORTED_FUNCTIONS=['_funsName1','_funsName2']
指定需要导出的函数接口。而然,当需要导出的接口变非常多的时候,这样操作显然不太明智。怎么办呢?
EMSCRIPTEN_KEEPALIVE
宏修饰C的函数我们可以使用Emscripten给我们提供的宏指令EMSCRIPTEN_KEEPALIVE
修饰。使用这个宏之前需要引入#include
,同时为了避免代码提示找不到include头文件的
错误信息,我们可以需要在IDE编辑器中,设置好include目录,include的目录位置一般在你的EMSDK
git项目目录emsdk/upstream/emscripten/system/include/emscripten
下
我们还是先上代码
// HelloToolFuns.c
#include
#include
EMSCRIPTEN_KEEPALIVE
int myAdd(int a,int b){
int res = a+b;
return res;
}
EMSCRIPTEN_KEEPALIVE
int myMutilp(int a, int b){
int s = a * b;
return s;
}
EMSCRIPTEN_KEEPALIVE
void sayHello() {
printf("Hello World!(HelloToolFuns.c)\n");
}
我们可以直接使用一个EMSCRIPTEN_KEEPALIVE
修饰在需要导出的函数接口即可。在编译时,不需要指定带导出的接口函数名,只需要直接使用emcc
编译即可,如下所示:
注意:cpp的函数导出也同样适用这个方法,只是,任然需要在函数的外层套一个
extern "C" { }
emcc HelloToolFuns.c -o ./test-html/HelloToolFuns.js
EMSCRIPTEN_KEEPALIVE
本意是保持函数的意思,怎么理解呢?Emscripten
在编译时,为了保证wasm足够小,其实还会做一层删除无用函数的操作。如对于内联函数、或是未调用的函数会做DCE清除操作。当我们采用了这个宏,表示这个函数无论如何将做保留操作。
那么在编译后,被保留下来的函数将会挂着到Module.asm
下,这样,我们就可以调用了。我们分析一下我们的HelloToolFuns.js
代码,确实myAdd
、myMutilp
、sayHello
这三个函数都不存在任何调用关系,很显然在编译时,必然会被Emscripten
优化掉。这也就是我们上一节课开头发现的,为啥直接编译不携带任何参数时,编译出来的wasm在Module.asm
下没有任何我们需要的接口函数
EMSCRIPTEN_BINDINGS
实现C++的函数及类的完美导出前面我们介绍了,使用cpp编译导出wasm可用的函数时,需要添加extern "C"
,同时,需要将C++的类及方法做一层转化,使C可调用的C++的类。
而在Javascript使用这种导出的wasm类时,用起来总感觉不符合面向对象的编程习惯,看起就是妥妥C的编程习惯。如下代码所示:
var hToolObj = Module.asm.HelloTools_OBJ_New(); // 创建对象
Module.asm.HelloTools_add(hToolObj,10,20) // 访问对象
有没办法能使C++导出的wasm,在javascirpt调用时,可以优雅一点来访问呢?比如这样:
var hToolObj = new Module.asm.HelloTools(); // 创建对象
hToolObj.add(10,20) // 访问对象
这样是不更爽一点呢?这就是我们这小节需要重点介绍的EMSCRIPTEN_BINDINGS
宏。
我们还是先看看用怎么通过它实现cpp编写的函数的导出,先上代码:
// HelloToolFuns2.cpp
#include
#include
int myAdd(int a,int b){
int res = a+b;
return res;
}
int myMutilp(int a, int b){
int s = a * b;
return s;
}
void sayHello() {
printf("Hello World!(HelloToolFuns2.cpp - EMSCRIPTEN_BINDINGS)\n");
}
// 这里是导出列表
EMSCRIPTEN_BINDINGS(my_module) {
emscripten::function("myAdd",&myAdd);
emscripten::function("myMutilp",&myMutilp);
emscripten::function("sayHello",&sayHello);
}
然后编译cpp
emcc --bind HelloToolFuns2.cpp -o ./test-html/HelloToolFuns2.js
注意,编译时,我们需要添加 --bind参数,否则会报错
通过使用EMSCRIPTEN_BINDINGS
导出列表,我们甚至可以不需要对cpp的函数添加extern "C" {}
来包裹,直接写在EMSCRIPTEN_BINDINGS
声明的即可;
而使用这种方式的编译出来的函数接口,是直接挂着到Javascript的Module._<函数名>
的,如Module._sayHello
,而Module.asm
下是找不到的。
如何使用EMSCRIPTEN_BINDINGS
导出类呢,我们还是直接上代码
// HelloTools.cpp
#include
#include
class HelloTools{
public:
HelloTools(int n);
void print(int a, int b);
int add(int a, int b);
int num=0;
int sum=0;
static void showHello();
};
HelloTools::HelloTools(int n){
num = n;
}
void HelloTools::print(int a, int b){
std::cout<<"a+b="<<a<<"+"<<b<<"="<<a+b<<std::endl;
}
int HelloTools::add(int a, int b){
int c = 0;
sum+= a+b;
return sum;
}
void HelloTools::showHello(){
std::cout<<"show Hello Emscripten!"<<std::endl;
}
// 导出类
EMSCRIPTEN_BINDINGS(my_class_example) {
emscripten::class_<HelloTools>("HelloTools")
.constructor<int>()
.function("print",&HelloTools::print)
.function("add",&HelloTools::add)
.property("num",&HelloTools::num)
.class_function("showHello",&HelloTools::showHello)
;
}
执行编译
emcc --bind HelloTools.cpp -o ./test-html/HelloTools.js
编译出来的类将直接挂着到
Module.<类名>
,即Module.HelloTools
在浏览器中测试一下
var hToolObj = new Module.HelloTools(10) // 实例化HelloTools
hToolObj.add(10,20) // 返回30
hToolObj.num // 返回10
hToolObj.print(100,200) // 输出 a+b=100+200=300
Module.HelloTools.showHello() //输出 show Hello Emscripten!
至此,我们实现了通过Emscripten提供的接口,使用EMSCRIPTEN_BINDINGS
宏,实现C++函数或类的完美导出,且至此Javascript实现友好的调用。
(多文档导出,待续)