背景
WebAssembly
简单来说,WebAssembly可以帮助我们将C++编译为可被JS调用的.wasm二进制格式,且在Web端运用,以在Web端调用C++模块,发挥C++高效的语言优势。
安装Emscripten3.1.0
Emscripten 是一个完整的 WebAssembly 编译器工具链。
官方网站:https://emscripten.org/index.html
官方安装文档:https://emscripten.org/docs/getting_started/downloads.html
按照官方文档中的安装步骤即可顺利完成Emscripten安装,且官方文档对Emscripten的用法有较为详细的说明,建议有需要时先在官方文档中搜索和查看。
另外附参考文档如下:
https://www.cntofu.com/book/150/zh/ch1-quick-guide/readme.md
C++与Javascript交互
绑定
原始方式
可对C++中方法以 extern "C"
EMSCRIPTEN_KEEPALIVE
修饰,即可在html中调用。
extern "C"
——转化为C接口,
EMSCRIPTEN_KEEPALIVE
宏——表明此方法需一直保留不被优化。
调用时的方法前面加“_”,且emcc生成的胶水代码中可搜索到changeColor函数名。
//example.cpp
#include <emscripten.h>
extern "C" void EMSCRIPTEN_KEEPALIVE changeColor(){return 0;}
//emcc
emcc --bind example.cpp -o index.js -O3 -s WASM=1
<script type='text/javascript'>
var canv = document.getElementById('canvas');
var Module = {canvas: canv};
</script>
<!-- Call the javascript glue code (index.js) as generated by Emscripten -->
<script src="index.js"></script>//调用index.js
<!-- Allow the javascript to call C++ functions -->
<script type='text/javascript'>
canv.addEventListener('click', _changeColor, false);//使用index.js中方法
</script>
Embind
Embind 用于绑定 C++ 函数和类到 JavaScript ,这样编译代码就能在 js 中以一种很自然的方式来使用:
- 需要在 C/C++ 代码中添加
#include <emscripten/bind.h>
头文件。 - 使用
EMSCRIPTEN_BINDINGS()
块来创建函数、类、值类型、指针(包括原始和智能指针)、枚举和常量的绑定 - 编译时加入–bind参数
绑定类、函数、属性、结构体可参考如下示例:
//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;
}
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;}
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;
};
EMSCRIPTEN_BINDINGS(my_module) { //my_module可以随意填写
//绑定结构体
value_object<Point>("Point")
.field("x", & Point::x)
.field("y", & Point::y);
//绑定函数
function("_getPoint", &getPoint);
//绑定类、类中函数、属性
class_<MyClass>("MyClass")
.constructor<int>() //构造函数
.function("CompareBig", &MyClass::CompareBig) //普通类成员函数
.class_function("getNum", &MyClass::getNum) //静态类成员函数
.property("m_num", &MyClass::getNumValue, &MyClass::setNum)//绑定属性,将私有变量暴露
;
}
emcc --bind -o index.js example.cpp -O3 -s WASM=1
//html中的js代码
//调用函数
var oPoint = Module._getPoint();
var ix = oPoint.x;
var iy = oPoint.y;
//调用类
var instance = new Module.MyClass(10); //声明类的对象
instance.CompareBig(1,2); //类函数
Module.MyClass.getNum(instance); //静态函数
instance.m_num = 20; //绑定属性后可直接对变量赋值
instance.delete(); //使用后需释放,否则Emscripten堆将无限增长
智能指针
1.普通智能指针的绑定
//绑定智能指针的两种方式
EMSCRIPTEN_BINDINGS(module) {
class_<Class>("Class")
.constructor<int>()
.smart_ptr<std::shared_ptr<Class>>("shared_ptr<Class>")//智能指针
.property("x", &Class::getX, &Class::setX);
}
EMSCRIPTEN_BINDINGS(module) {
class_<Class>("Class")
//这里将智能指针与类对象的创建过程进行绑定
.smart_ptr_constructor("shared_ptr<Class>", &std::make_shared<Class, int>)
.property("x", &Class::getX, &Class::setX);
};
2.自定义智能指针的绑定
2.1.编写自定义smart_ptr_trait
模板类,实现自己的智能指针类型。
//编写自定义smart_ptr_trait模板类,实现自己的智能指针类型
template<typename PointeeType>
struct smart_ptr_trait<dan::Smart_Object<PointeeType>> {
typedef dan::Smart_Object<PointeeType> PointerType;
typedef typename PointerType::element_type element_type;
static element_type* get(const PointerType& ptr) {
return ptr.get();
}
static sharing_policy get_sharing_policy() {
return sharing_policy::BY_EMVAL;
}
static dan::Smart_Object<PointeeType>* share(PointeeType* p, EM_VAL v) {
return new dan::Smart_Object<PointeeType>(
p,
val_deleter(val::take_ownership(v)));
}
static PointerType* construct_null() {
return new PointerType;
}
private:
class val_deleter {
public:
val_deleter() = delete;
explicit val_deleter(val v)
: v(v)
{}
void operator()(void const*) {
v();
// eventually we'll need to support emptied out val
v = val::undefined();
}
private:
val v;
};
};
2.2.在智能指针类中添加element_type
,要构造smart_ptr_trait
,智能指针中须有此变量。
using element_type = typename remove_extent<T>::type;
![image.png](https://img-blog.csdnimg.cn/img_convert/f5bfd22713555967e83677aded7a96da.png#crop=0&crop=0&crop=1&crop=1&height=181&id=Li64e&margin=[object Object]&name=image.png&originHeight=181&originWidth=707&originalType=binary&ratio=1&rotation=0&showTitle=false&size=12542&status=done&style=none&title=&width=707)
2.3.在EMSCRIPTEN_BINDINGS
中绑定。
EMSCRIPTEN_BINDINGS(my_module) {
class_<SGGeoPoint>("SGGeoPoint")
.constructor()
.smart_ptr<SGGeoPointPtr>("SGGeoPointPtr")//自定义智能指针
.function("point",&SGGeoPoint::point)
.function("getX",&SGGeoPoint::getX)
.function("getY",&SGGeoPoint::getY);
};
接口类
以下示例了接口类的绑定方法。
//定义一个接口类,该接口需要由子类来实现
class MyInterface{
public:
virtual void invoke(const std::string &str) = 0; //纯虚函数
};
//定义一个胶水类用来链接C/C++与js代码
class DerivedClass : public wrapper<MyInterface> {
public:
EMSCRIPTEN_WRAPPER(DerivedClass);
void invoke(const std::string &str) override{
return call<void>("invoke",str);} //间接调用在js中实现的方法
};
//绑定
EMSCRIPTEN_BINDINGS(module){
class_<MyInterface>("MyInterface")
//纯虚函数:绑定父类中的抽象接口
.function("invoke",&MyInterface::invoke,pure_virtual())
//通过allow_subclass方法向绑定的接口添加俩个js方法extend和inplement,用于实现定义在c++代码中的接口
.allow_subclass<DerivedClass>("DerivedClass");
}
上面的代码中,通过**wrapper**
模板类构建了一个用于连接C/C++
代码与JavaScript
环境的**“胶水”类**。在该类内部,通过调用在JavaScript
代码中实现的子类接口这种方式来间接地绑定C++
代码中的接口类与JavaScript
环境中的子类实现过程。而在EMSCRIPTEN_BINDINGS
内部绑定接口类中定义的抽象方法时,需要为function
方法提供一个名为**pure_virtual()**
的策略标志,该标志会标识纯虚函数的绑定过程,并为其提供相应的异常捕获能力。
Embind
为我们提供了两个可用于在JavaScript
代码中实现C/C++接口的本地函数方法,即**extend**
和**implement**
方法。但使用这两个方法的前提是在绑定接口类时,需要通过**allow_subclass**
方法显式地声明将要在JavaScript
环境中完成接口类的具体实现过程。接下来,便可以借助这两个方法,在JavaScript
环境中实现C/C++
接口的具体逻辑。
//通过extend方法来实现子类
var DerivedClass = Module.MyInterface.extend("MyInterface",
{
//构造方法(可选)
__construct: function(){
this.__parent.__construct.call(this); //调用父类的构造函数
},
//析构函数(可选)
__destruct: function(){
this.__parent.__destruct.call(this); //调用父类的析构函数
},
//对接口中纯虚函数的具体实现
invoke: function(str){
console.log("js_invoke_ing" + str);
},
});
//调用子类方法
var instanceExtend = new DerivedClass;
instanceExtend.invoke("i'm extend");
//通过implement方法来构造子类
var x = {
invoke:function(str){
console.log("invoking with:"+ str);
}
};
var interfacePbject = Module.MyInterface.implement(x);
//调用子类方法
interfacePbject.invoke("i'm implement");
extend:在这段代码中,首先使用**extend**
方法完成了Interface
接口类的子类实现过程。与C/C++
中维承类的实现过程类似,这里也可以选择性地使用__construct
或__destruct
方法来为该实体类添加相应的构造函数和析构函数。
**implement:**相对于extend
方法而言,**implement**
方法则更适用于不需要构造函数与析构函数的简单接口类。可以看到,这里只需要将与接口类中纯虚函数其签名完全一致的JavaScript
函数以对象结构进行包裹,并传递给从绑定类对象中导出的implement
方法,即可完成对接口类的实现过程。更为方便的是,该方法会直接返回一个已经实例化好的子类对象,这样同时也省去了需要另外再new
的过程。
覆写非纯虚函数
//定义一个接口类,该接口需要由子类来实现
class MyInterface{
public:
//非纯虚函数
virtual void invokeN(const std::string &str){
std::cout << str + " - from 'c++'"<<std::endl;}
};
//定义一个胶水类用来链接C/C++与js代码
class DerivedClass : public wrapper<MyInterface> {
public:
EMSCRIPTEN_WRAPPER(DerivedClass);
void invokeN(const std::string &str) override{
return call<void>("invokeN",str);} //间接调用在js中实现的方法
};
//绑定
EMSCRIPTEN_BINDINGS(module){
class_<MyInterface>("MyInterface")
//非纯虚函数:需要通过optional_override方法来创建特殊的Lambda函数,防止js代码与Wrapper函数之间产生循环递归调用问题
.function("invoke",optional_override([](MyInterface &self,const std::string &str){
return self.MyInterface::invoke(str);
}))
//通过allow_subclass方法向绑定的接口添加俩个js方法extend和inplement,用于实现定义在c++代码中的接口
.allow_subclass<DerivedClass>("DerivedClass");
}
从整体上看,这段代码与前面代码唯一的差别是,当绑定抽象类的非纯虚函数时,不能直接向function 方法传递对应函数的指针,而是需要通过optional _verride
方法将函数的调用过程封装在个特殊的匿名函数中并整体传递给 function 方法。另外,不同于实现接口类的过程,我们可以在 JavaScript 环境中选择性地覆写或直接使用 invoke 函数的默认实现,覆写的具体过程只能以**extend**
即继承的方式来实现。
//通过extend方法来实现子类
var DerivedClass = Module.MyInterface.extend("MyInterface",
{
//选择性地对接口中非纯虚函数的具体实现
invokeN: function(str){
console.log("js_invokeN_ing" + str);
}
});
//调用子类方法
var instanceExtend = new DerivedClass;
instanceExtend.invokeN("i'm extend");
C++中派生类
//定义一个基类(父类)
class MyBaseClass{
public:
MyBaseClass() = default;
virtual std::string invoke(const std::string &str){
return str + " - from 'MyBaseClass'"; };
};
//定义继承的子类
class MyDerivedClass : public MyBaseClass{
public:
MyDerivedClass() = default;
std::string invoke(const std::string &str) override{
return str + " - from 'MyDerivedClass'"; };
};
//绑定
EMSCRIPTEN_BINDINGS(module){
//绑定基类
class_<MyBaseClass>("MyBaseClass")
.constructor<>()
.function("invoke",&MyBaseClass::invoke);
//绑定子类
class_<MyDerivedClass,base<MyBaseClass>>("MyDerivedClass")
.constructor<>()
.function("invoke",&MyDerivedClass::invoke);
}
重载函数
使用select_overload()
帮助函数选中合适的签名。
//示例说明
struct Example {
void foo();
void foo(int i);
void foo(float f) const;
};
EMSCRIPTEN_BINDING(overloads) {
class_<Example>("Example")
.function("foo", select_overload<void()>(&Example::foo))
.function("foo_int", select_overload<void(int)>(&Example::foo))
.function("foo_float", select_overload<void(float)const>(&Example::foo))
;
}
下面演示了 SGGeoPoint 的构造函数和 createObject重载函数。
class_<SGGeoPoint>("SGGeoPoint")
.constructor()
.constructor<double,double>()//此行对应参数3有默认值的C++构造函数
.constructor<double,double,double>()
.smart_ptr<SGGeoPointPtr>("SGGeoPointPtr")
.class_function("createObject",select_overload<SGGeoPointPtr()>(&SGGeoPoint::createObject))
.class_function("createObjectByXYZ",select_overload<SGGeoPointPtr(double,double,double)>(&SGGeoPoint::createObject))
;
函数参数默认值
对于构造函数来说,可以较简单的实现默认参数的绑定,而普通函数可通过optional_override
方法来创建特殊的Lambda函数。
对于下面的带参函数
//SGGeoPoint类构造函数、静态函数的参数有默认值
class SMART_GEOMETRY_EXPORT SGGeoPoint :public SGAbstractGeometry
{
public:
SGGeoPoint();
SGGeoPoint(double x, double y, double z = 0.0);//带默认值
static dan::Smart_Object<SGGeoPoint> createObject();
static dan::Smart_Object<SGGeoPoint> createObject(double x, double y, double z = 0.0);//带默认值
}
对应绑定写法:
class_<SGGeoPoint>("SGGeoPoint")
.constructor()
.constructor<double,double>()//构造函数直接这样写即可
.constructor<double,double,double>()
.smart_ptr<SGGeoPointPtr>("SGGeoPointPtr")
.class_function("createObject",select_overload<SGGeoPointPtr()>(&SGGeoPoint::createObject))
.class_function("createObject_1",select_overload<SGGeoPointPtr(double,double,double)>(&SGGeoPoint::createObject))
//普通函数通过lamda函数可达到需求
.class_function("createObject_2",optional_override([](double x,double y){
return SGGeoPoint::createObject(x,y);
}))
;
枚举
embind支持C++98枚举和C++11枚举类。
enum OldStyle {
OLD_STYLE_ONE,
OLD_STYLE_TWO
};
enum class NewStyle {
ONE,
TWO
};
EMSCRIPTEN_BINDINGS(my_enum_example) {
enum_<OldStyle>("OldStyle")
.value("ONE", OLD_STYLE_ONE)
.value("TWO", OLD_STYLE_TWO);
enum_<NewStyle>("NewStyle")
.value("ONE", NewStyle::ONE)
.value("TWO", NewStyle::TWO);
}
JavaScript中调用形式如下:
Module.OldStyle.ONE;
Module.NewStyle.TWO;
常量
EMSCRIPTEN_BINDINGS(my_constant_example) {
constant("SOME_CONSTANT", SOME_CONSTANT);
}
Embind参考链接:
https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html
https://www.osheep.cn/3952.html
https://www.jianshu.com/p/a03444bf9e97
https://www.cnblogs.com/catwin/p/13337074.html
画布
Emscripten提供了对EGL和OpenGL的支持。详情可参考官方文档:https://emscripten.org/docs/porting/multimedia_and_graphics/index.html
Emscripten画布
Emscripten中只有唯一画布,可通过画布id为其指定大小。
EGL创建上下文
1.获取对象的句柄
EGLDisplay eglGetDisplay(EGL_DEFAULT_DISPLAY)
2.在显示器上初始化
EGLBoolean eglInitialize()
3.查找渲染目标参数
eglGetConfigs()/eglChooseConfig()
4.创建主渲染目标表面
EGLSurface eglCreateWindowSurface()
5.创建 GLES2渲染上下文,创建时可指定版本为ES2/ES3
EGLContext eglCreateContext()
6.激活渲染上下文
eglMakeCurrent()
OpenGLES3
若使用OpenGLES3,需指定参数-s FULL_ES3=1
。
注意,程序中opengles3所用着色器的第一行应指定版本**#version 300 es**
。
JS绑定画布
为JS的画布元素canvas通过以下方式绑定C++中默认画布。
<body>
<!-- Create the canvas that the C++ code will draw into -->
<canvas id="canvas" oncontextmenu="event.preventDefault()"></canvas>
<!-- Allow the C++ to access the canvas element -->
<script type='text/javascript'>
var canv = document.getElementById('canvas');
var Module = {
canvas: canv
};
</script>
<script type='text/javascript' src="smartgis.3dexample.js"></script>
</body>
虚拟文件系统
Emscripten文件系统
Emscripten提供了MEMFS内存文件系统,供fopen()
/fread()
/fwrite()
等libc/libcxx文件访问函数调用。
参考链接:
https://www.cntofu.com/book/150/zh/ch3-runtime/ch3-03-fs.md
https://emscripten.org/docs/porting/files/packaging_files.html
emcc preload
文件打包可以在emcc命令行中完成,有 preload(预加载)以及embed(嵌入)两种方式。其中preload方式效率更高,打包后文件体积更小,打包后生成与.js同名的.data数据文件,其中包含了所有文件的二进制数据,同时在.js文件的胶水代码中包含对文件包的下载、装载操作。
emcc中使用示例:
--preload-file hello.txt //指定打包文件
--preload-file filedir //指定打包文件夹下所有文件,包含下级文件夹中的文件
CMAKE打包文件
需要打包的文件:
![image.png](https://img-blog.csdnimg.cn/img_convert/7f3a9b3c37f91b01e25f66487c29aa73.png#clientId=u275b0154-1d4f-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=59&id=F2zgW&margin=[object Object]&name=image.png&originHeight=118&originWidth=1180&originalType=binary&ratio=1&rotation=0&showTitle=false&size=36560&status=done&style=none&taskId=ued147289-c81b-4c43-a57f-43612e5ca5b&title=&width=590)
- CMakeLists.txt 中
--preload-file
指定打包文件/文件夹filedir
set_target_properties(test PROPERTIES LINK_FLAGS "--bind --preload-file shaders")
-
被打包的文件/文件夹需放在该项目cmake目录中
-
编译后程序将能调用到打包文件中的着色器。
内存
设置内存
若Em编译时内存不足,我们可以修改emsdk/upstream/emscripten/src目录里的settings.js中的INITIAL_MEMORY
值来设置更大的内存。
Web端运行时也可能出现内存不足的错误,但运行时数据大小的波动是巨大的,在初始编译时指定超大内存会非常浪费。可以通过可变内存参数指定在运行时扩大内存容量。
可变内存
使用-s ALLOW_MEMORY_GROWTH
模式,当编译目标是asm.js时,可变内存会影像性能。但当编译目标是wasm时,使用可变内存模式非常高效,不会影响性能。
调试
编译工程
CMake工程
代码
- 设置渲染模式为OpenGLES3
SGERenderer::setCurrentRenderer(“OpenGLES3”);
- emscripten_set_main_loop设置em消息循环
#include <emscripten.h>
std::function<void()> loop;
void main_loop() { loop(); }
int main()
{
//.....
loop = [&]
{
//draw......
};
emscripten_set_main_loop(main_loop, 0, true);
return 0;
}
- 着色器指定版本
在着色器文件的顶部(第一行),应声明版本:
# version 300 es
库依赖
在cmake工程编译中,需要将依赖到的库加入CMakelists.txt文件中。如果库A依赖了库B,且代码中使用到了库B的类或方法,则需要将库B也加入依赖:
link_directories(/mnt/hgfs/smartgis.all/smartgis.all/bin/x64/Release)
link_directories(/home/czw/3rd_a)
target_link_libraries(testtwo stdc++ dl smartgis.core.a smartgis.common.a
smartgis.data.a smartgis.geometry.a geos.a m)
set_target_properties(testtwo PROPERTIES LINK_FLAGS "--bind --experimental-wasm-simd")
编译后只有.wasm文件和.js文件,是对工程内代码的封装,不包含第三方库包,文件体积不会过大。
Em参数
以下是一些emscripten编译参数,可按需指定编译参数。
--bind #执行C++到JS地绑定
--experimental-wasm-simd #SIMD
-std=c++11
-s WASM=1 #生成.wasm而不是asm.js
-s FULL_ES3=1 #使用gles3.0
--preload-file shaders #打包文件
-s LLD_REPORT_UNDEFINED #指定对undefined类型错误更详细地输出
-s ALLOW_MEMORY_GROWTH #允许程序运行时内存增长
-s ASSERTIONS=1 #断言,指定后Web运行时可输出更多错误信息
-v #编译时输出更多信息
-O3 #优化编译
CMAKE
对程序依赖的各个库进行静态库编译。
windows:
emsdk_env.bat //注册em环境
emcmake cmake .. //cmake
emmake make ///make
linux:
#1.注册环境:进入emsdk目录后执行emsdk_env.sh
source ./emsdk_env.sh
#2.cmake
emcmake cmake ..
#3.make
emmake make
最终编译出smartgis.3dexample.js、smartgis.3dexample.wasm、smartgis.3dexample.data三个文件。
启动Web
在运行目录中新建index.html文件,引入smartgis.3dexample.js,指定canvas连接,执行以下操作:
emrun --no_browser --port 8080 .
将命令窗输出的网址在浏览器打开,即可在浏览器查看效果。若无连接,将地址中0.0.0.0修改为localhost刷新即可。
除了emrun外,也可以使用以下方式开启服务。
python -m http.server 8080
//如果是python2,则使用下面代码
//python -m SimpleHTTPServer 8080
OpenGL呈现效果
目前已初步验证了自主引擎启用OpenGLES3渲染模式在Web端呈现。
绘制三角形
以下是绘制三角形后在Web端显示的效果。着色器在打包文件中进行了加载。
加载IFC
以下是自主引擎渲染ifc模型在web端的显示。受不断打印信息的影响,截图中帧数较小。但帧数确实是引擎后续需优化的问题。