基于Emscripten的语言关系绑定-Embind

简介:

基于Emscripten的语言关系绑定主要有俩种方式EmbindWebIDL Binder
我先学习第一种基于Embind

  • 函数关系绑定

1.C/C++源码

//引入相关的头文件 
#include <emscripten/bind.h>
//与Embind相关的宏参数被定义在emscripten命名空间中
using namespace emscripten;
//定义一个函数
int add(int x,int y){
    return x+y;
} 
//使用EMSCRIPTEN_BINDINGS宏参数进行函数绑定
//把c++的函数绑定到js环境中 
EMSCRIPTEN_BINDINGS(module){
    function("add",&add);//绑定块的别名
}

首先定义了个用于计算两个整数和的 add 函数,然后通过 Embind 中间件提供的EMSCRIPTEN_BINDINGS宏参数将该函数绑定到了JavaScript环境中。

2.附加js代码

__ATPOSTRUN__.push(() =>{
    //从c/c++环境绑定到js环境中的语法元素将会直接注册到胶水脚本文件的全局“module”对象上
    console.log('The result by calling "add":'+Module.add(1,2));
});

可以看到,这里并没有通过ccallcwrap方法来调用在C/C++代码中绑定的add函数。我们直接从Module全局对象中导出并使用了该函数,而所有需要处理的语法细节绑定信息则全部交由Embind中间件在JavaScript侧的脚本实现代码来完成。

运行结果

  • 简单类关系绑定

C++const复习:https://www.jianshu.com/p/37d6d70d0c66

1.C/C++源码

//引入相关的头文件 
#include <emscripten/bind.h>
#include <iostream>
#include <string> 
//与Embind相关的宏参数被定义在emscripten命名空间中
using namespace std;
using namespace emscripten;
//定义一个简单类
class xClass{
    private:
        int x;
        string y;
        static const int num = 6;
    public:
        //构造函数 
        xClass(int x,string y) : x(x),y(y){
        }
        //成员函数
        void incrementX(){
            x+=1;
        } 
        //Get Set函数
         int getValueX() const{
            return x;
         }
         void setValueX(int val){
            x=val;
         }
         //静态方法
         //在静态成员函数中,不能访问非静态成员变量,也不能调用非静态成员函数
         static string getStringValue(const xClass & instance){//这里用到引用的方法 
            return instance.y;
         } 
         static void PrintNum(){
             cout<<"num is value: ";
             cout<<num<<endl; 
         }
}; 
//使用EMSCRIPTEN_BINDINGS宏参数进行函数绑定
//把c++的函数绑定到js环境中 
EMSCRIPTEN_BINDINGS(module){
    //绑定类 
    class_<xClass>("xClass")
    //绑定构造函数 
    .constructor<int,string>()
    //绑定成员函数
    .function("incrementX",&xClass::incrementX)
    //绑定私有成员变量的setter和getter方法
    .property("x",&xClass::getValueX,&xClass::setValueX) 
    //绑定类的静态方法
    .class_function("getStringValue",&xClass::getStringValue)
    .class_function("PrintNum",&xClass::PrintNum); 
}

如上面代码所示,我们通过名为“class”的模板函数以“链式”的方式来实现C/C++中“类”类型的关系绑定过程。这里需要通过该模板函数所返回对象下的各成员函数来分别完成对“类”结构中各种语法元素的绑定过程。比如constructor模板函数用来绑定“类”的构造函数结构,其模板初始化参数为该构造函数各形式参数的类型标识符function函数用来绑定“类”结构的多个成员函数:property函数主要用来绑定类中私有成员变量的setter 和getter访问器方法(由于JavaScript并没有私有成员的概念,因此C/C++中定义的私有成员在经过关系绑定后其值将可以被修改);最后的class_function函数则专门用于绑定直接定义在“类”结构中的静态函数。
我们可以使用如下JavaScript脚本代码,来在上层JavaScript环境中使用这个经过关系绑定后的“类”结构。

2.附加的js源码

__ATPOSTRUN__.push(() =>{
    //创建一个xClass类对象
    var xClass = new Module['xClass'](100,"I love xingxing");
    //打印该对象
    console.log(xClass);
    //打印该对象的私有成员变量x的值
    console.log(xClass['x']);
    //通过set函数赋值
    xClass['x']=98;
    //再次打印该对象的私有成员变量x的值
    console.log(xClass['x']);
    //调用该对象的成员函数
    xClass.incrementX();
    //再次打印该对象的私有成员变量x的值
    console.log(xClass['x']);
    //调用定义在该对象下的静态方法
    console.log(Module['xClass']['getStringValue'](xClass));
    console.log(Module['xClass']['PrintNum']());
    //析构该对象实例,释放wasm共享线性内存空间
    xClass.delete();
});

需要注意的是,对于有在JavaScript环境中已经使用完毕的“类”实例对象,都需要通过调用其各自的delete方法释放它们在Wasm模块共享线性内存中占用的空间;否则,随着应用的不断运行,在应用当的内存空间中创建的类对象实例会越来越多,可能会引发如OOM和内存泄漏等问题。

命令

emcc index.cc --std=c++11 --bind -s WASM=1 --post-js post-script.js -o index.js

3.js运行的代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>我的wasm学习</title>
</head>
<body>
<h1 id="my_result"></h1>
<script>
    var Module = {};
    fetch("/cctest/index.wasm").then(
        response => response.arrayBuffer()
    ).then((bytes) => {
        Module.wasmBinary = bytes;
        var script = document.createElement('script');
        script.src = "/cctest/index.js";
        document.body.appendChild(script);
    })
</script>
</body>
</html>
  • 数组和对象类型

Embind提供了value_arrayvalue_object两个模板函数,可以帮助我们将C/C++中的结构体类型与JavaScript 语言中的数组和对象类型进行绑定,并且这个绑定的作用是双向的。如下为一个简单的应用示例。

1.C/C++源码

//引入相关的头文件 
#include <emscripten/bind.h>
#include <string> 
//与Embind相关的宏参数被定义在emscripten命名空间中
using namespace emscripten;
//定义俩个结构体
struct Point2f{
    int x;
    int y;
}; 
struct PersonRecord{
    std::string name;
    int age;
};
//绑定到js环境的方法,该方法在内部用到了俩种结构体类型
PersonRecord findPersonAtLocation(Point2f &p){
    PersonRecord pr;
    if(p.x == 0&&p.y==0){
        pr = {"xiao_hong",25};
    }else{
        pr = {"xiao_ming",18};
    }
    return pr;
} 
//绑定C/C++中的结构体
EMSCRIPTEN_BINDINGS(module){
    //绑定数组,定义结构体与数组的转换规则
    value_array<Point2f>("Point2f")
    .element(&Point2f::x)
    .element(&Point2f::y);
    //绑定对象,定义结构体与对象的转换规则
    value_object<PersonRecord>("PersonRecord")
    .field("name",&PersonRecord::name)
    .field("age",&PersonRecord::age);
    //绑定函数
    function("findPersonAtLocation",&findPersonAtLocation);//发现有funxtion就不能有using namespace std; 
}

可以看到,在findPersonAtIocation方法中使用了我们定义在C/C++源代码中的两种结构体类型Point2f/PersonRecord。 从整体上看,该方法接收一个结构体引用并同时返回一个新的结构体对象。我们在EMSCRIPTEN_BINDINGS宏参数内定义了两种结构体与JavaScript语法元素的具体绑定规则。通过value_ array方法,我们将Point2f结构体中名为“x”和“y”的两个元素绑定到了 JavaScript环境中的一个二元数组上;而通过value_object方法,将PersonRecord结构体内名为“name”和"age”的两个属性绑定到JavaSript环境中的一个同名对象结构上。

2.附加js源码

__ATPOSTRUN__.push(() =>{
    var person = Module.findPersonAtLocation([0,0]);
    console.log('Person: '+person.name+' - '+person.age);
});
  • 原始指针

在Embind的语言关系绑定过程中,如果需要使用原始指针在JavaScript环境中传递对象和值,则必须在绑定对应语法元素时,为绑定对象传递一个名为allow_raw_pointers 的策略属性对象(这里实际上需要传递的是与该策略同名函数的调用返回值),该参数将帮助我们开启Embind内部对C指针的使用支持。

1.C/C++源码

//引入相关的头文件 
#include <emscripten/bind.h>
//与Embind相关的宏参数被定义在emscripten命名空间中
using namespace emscripten;
class xClass{
    public:
        xClass(int x):x(x){}
        inline int getX() const{
            return x;
        }
        inline void setX(int val){
            x=val;
        }
    private:
        int x;
}; 
//该方法在内部使用原始指针来传递对象
xClass* passThrough(xClass* ptr){
    return ptr;
} 

//绑定C/C++中的类和方法 
EMSCRIPTEN_BINDINGS(module){
    class_<xClass>("xClass")
    .constructor<int>()
    .property("x",&xClass::getX,&xClass::setX);
    //需要添加allow_raw_ pointers()标志以允许使用原始指针
    function("passThrough", &passThrough, allow_raw_pointers()); 
}

2.附加js源码

__ATPOSTRUN__.push(() =>{
    var xClass = new Module['xClass'](100);
    console.log(xClass['x']);//100
    //为对象实体设置一个别名
    var yClass = Module['passThrough'](xClass);
    //修改指针所指向对象的属性值
    yClass['x']=0;
    console.log(xClass['x']);//0
    console.log(yClass['x']);//0
    xClass.delete();
    yClass.delete();
});

3.命令

emcc index.cc --bind -s WASM=1 --post-js post-script.js -o index.js
实际上,由该函数创建生成的yClass对象与xClass对象在内存中指向同一一个对象实体。因此,当我们改变这两个对象中任意一个对象的属性值时,相应的另一个对象的属性值也会随之发生改变。

  • 智能指针

常用的智能指针一共分为三种类型, 这里只介绍Embind内部已经支持进行语言关系绑定的std:share_ptr和std:unique_ptr两种。两者的区别是,前者是基于引用计数实现的智能指针,即可以有多个share_ptrr指针同时指向同一块动态分配的内存,并且只有当最后一 个指向该内存的share_ptr指针离开其有效作用域时,这块共享内存才会被完全释放掉:后者则与之相反,当单个unique_ptr指针离开作用域时,内存便会被释放。

使用智能指针来维护一个类对象的生命周期

C/C++源码
//引入相关的头文件 
#include <emscripten/bind.h>
#include <memory>
//与Embind相关的宏参数被定义在emscripten命名空间中
using namespace emscripten;

class xClass{
    public:
        xClass(int x):x(x){}
        inline int getX() const{
            return x;
        }
        inline void setX(int val){
            x=val;
        }
    private:
        int x;
};

//绑定智能指针 
EMSCRIPTEN_BINDINGS(module){
    class_<xClass>("xClass")
    //这里将智能指针与类对象的创建过程进行绑定 
    .smart_ptr_constructor("shared_ptr<xClass>",&std::make_share<xClass,int>)
    .property("x",&xClass::getX,&xClass::setX); 
}

在上面的代码中,我们使用smart_ptr_constructor方法来实现类xClass的构造函数关系绑定。通过该方法,可以让在JavaScript环境中新创建的每一个xClass类对象都能够以某种特定的共享指针形式来管理其自身的生命周期及相应的资源。该方法一共接收两个参数, 其中第一为自定义的共享指针类型名称,我们可以根据所使用的智能指针类型来设置;第二个参数为指向当前“类”类型的某种智能指针的地址,这里直接使用C++ 11中的make_share模板函数创建了类型为“shared_ptr"的共享指针。当然,我们也可以通过编写自定义smart_ptr_trait模板类的方式来实现自己的智能指针类型。
当这个类型的一个对象被构造(如,new Module.['xClass'](10)),就会返回一个智能指针std::shared_ptr.

  • 将一个非成员函数绑定到某个类上

C/C++源码
//引入相关的头文件 
#include <emscripten/bind.h>
//与Embind相关的宏参数被定义在emscripten命名空间中
using namespace emscripten;

class xClass{
    public:
        xClass(int x):x(x){}
        inline int getX() const{
            return x;
        }
        inline void setX(int val){
            x=val;
        }
    private:
        int x;
};
//所绑定的非成员函数 其第一个参数会自动接受一个绑定类的对象引用 
void add(xClass &i,int x){
    i.setX(i.getX()+x);
} 
//绑定 
EMSCRIPTEN_BINDINGS(module){
    class_<xClass>("xClass")
    .constructor<int>() 
    .property("x",&xClass::getX,&xClass::setX)
    .function("add",&add);
}
附加js源码
__ATPOSTRUN__.push(() =>{
    var xClass = new Module['xClass'](100);
    xClass['add'](100);
    console.log(xClass['x']);
});
  • 接口类

C/C++源码
//引入相关的头文件 
#include <emscripten/bind.h>
#include <string> 
//与Embind相关的宏参数被定义在emscripten命名空间中
using namespace emscripten;
//定义一个接口类,该接口需要由子类来实现
class MyInterface{
    public:
        virtual std::string invoke(const std::string &str) = 0;
}; 
//定义一个胶水类用来链接C/C++与js代码
class DerivedClass : public wrapper<MyInterface> {
    public:
        EMSCRIPTEN_WRAPPER(DerivedClass);
        std::string invoke(const std::string &str) override{
        //简介调用在js中实现的方法
            return call<std::string>("invoke",str); 
        }
};
//绑定 
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++接口的本地函数方法,即extendimplement方法。但使用这两个方法的前提是在绑定接口类时,需要通过allow_subclass方法显式地声明将要在JavaScript环境中完成接口类的具体实现过程。接下来,我们便可以借助这两个方法,在JavaScript环境中实现C/C++接口的具体逻辑。

附加js代码
__ATPOSTRUN__.push(() =>{
    //通过extend方法来实现子类
    var DerivedClass = Module["MyInterface"].extend("MyInterface",{
        //构造方法(可选)
        __construct:function(){
            //调用父类的构造函数
            this.__parent.__construct.call(this);
        },
        //析构函数(可选)
        __destruct:function(){
            //调用父类的析构函数
            this.__parent.__destruct.call(this);
        },
        //对接口中纯虚函数的具体实现
        invoke:function(str){
            return str + " - from xingxing";
        }
    });
    var instanceByExtend = new DerivedClass();
    console.log(instanceByExtend.invoke("hello"));
    //通过implement方法来构造子类
    var implementations = {
        invoke:function(str){
            return str + str + " - from zhozhou";
        }
    };
    var instanceByImpelement = Module["MyInterface"].implement(implementations);
    console.log(instanceByImpelement.invoke("hello"));
});

在这段代码中,首先使用extend方法完成了Interface接口类的子类实现过程。与C/C++中维承类的实现过程类似,这里也可以选择性地使用__construct__destruct方法来为该实体类添加相应的构造函数和析构函数。相对于extend方法而言,implement方法则更适用于不需要构造函数与析构函数的简单接口类。可以看到,这里只需要将与接口类中纯虚函数其签名完全一致的JavaScript函数以对象结构进行包裹,并传递给从绑定类对象中导出的implement方法,即可完成对接口类的实现过程。更为方便的是,该方法会直接返回一个已经实例化好的子类对象,这样同时也省去了需要另外再new的过程。

  • 覆写非纯虚函数

C/C++代码
//引入相关的头文件 
#include <emscripten/bind.h>
#include <string> 
//与Embind相关的宏参数被定义在emscripten命名空间中
using namespace emscripten;
//定义一个接口类,该接口需要由子类来实现
class MyInterface{
    public:
        //定义虚函数(抽象类),已有默认的函数实现 
        virtual std::string invoke(const std::string &str){
            return str + " - from 'c++'"; 
        };
}; 
//定义一个胶水类用来链接C/C++与js代码
class DerivedClass : public wrapper<MyInterface> {
    public:
        EMSCRIPTEN_WRAPPER(DerivedClass);
        std::string invoke(const std::string &str) override{
        //简介调用在js中实现的方法
            return call<std::string>("invoke",str); 
        }
};
//绑定 
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");
}
附加js代码
__ATPOSTRUN__.push(() =>{
    //通过extend方法来实现子类
    var DerivedClass = Module["MyInterface"].extend("MyInterface",{
        //构造方法(可选)
        __construct:function(){
            //调用父类的构造函数
            this.__parent.__construct.call(this);
        },
        //析构函数(可选)
        __destruct:function(){
            //调用父类的析构函数
            this.__parent.__destruct.call(this);
        },
        //对接口中纯虚函数的具体实现
        invoke:function(str){
            return str + " - from xingxing";
        }
    });
    var instanceByExtend = new DerivedClass();
    console.log(instanceByExtend.invoke("hello"));
    //通过implement方法来构造子类
    var implementations = {
        invoke:function(str){
            return str + str + " - from zhozhou";//不起作用
        }
    };
    var instanceByImpelement = Module["MyInterface"].implement(implementations);
    console.log(instanceByImpelement.invoke("hello"));
});

从整体上看,这段代码与前面代码唯一的差别是,当绑定抽象类的非纯虚函数时,我们不能直接向function方法传递对应函数的指针,而是需要通过optional _verride方法将函数的调用过程封装在个特殊的匿名函数中并整体传递给function方法。另外,不同于实现接口类的过程,我们可以在JavaScript环境中选择性地覆写或直接使用invoke函数的默认实现,覆写的具体过程只能以extend即继承的方式来实现。

  • 绑定派生类

将在C/C++代码中实现的子类以及其派生父类同时绑定到js环境中

C/C++源码

//引入相关的头文件 
#include <emscripten/bind.h>
#include <string> 
//与Embind相关的宏参数被定义在emscripten命名空间中
using namespace emscripten;
//定义一个基类(父类) 
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);
}

在上面的代码中,首先定义了一个名为MyBaseClass的基本类结构,并以该类作为基类(父类)又重新定义了一一个名为MyDerivedClass的子类(派生类)结构。实际上,将上述两种类结构绑定到JavaScript环境的方法与前面介绍的简单类结构类似,只不过对于子类来说, 需要在绑定的过程中通过base方法来指定该类所对应的父类,而其他元素的绑定过程则没有任何改变。使用这两个类对象的JavaScript代码如下。

附加js代码
__ATPOSTRUN__.push(() =>{
    //分别创建基类和子类对象
    var baseClassInstance = new Module["MyBaseClass"]();
    var derivedClassInstance = new Module["MyDerivedClass"]();
    //分别调用俩个对象的同一个方法
    console.log(baseClassInstance.invoke("hello"));
    console.log(derivedClassInstance.invoke("hello"));
});
  • 自动向下转型

Embind可以帮助我们自动将一个指向基类对象的多态指针C++指针向下转型到对应的某个子类对象

C/C++代码
//引入相关的头文件 
#include <emscripten/bind.h>
#include <string> 
//与Embind相关的宏参数被定义在emscripten命名空间中
using namespace emscripten;
//定义一个基类(父类) 
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'"; 
        };
};
//该函数的返回值类型将自动向下转型到 MyDerivedClass类型的指针
MyBaseClass* get_MyDerivedClass(){
    return new MyDerivedClass();
}
//绑定 
EMSCRIPTEN_BINDINGS(module){
    //绑定基类 
    class_<MyBaseClass>("MyBaseClass")
        .constructor<>()
        .function("invoke",&MyBaseClass::invoke);
    //绑定子类
    class_<MyDerivedClass,base<MyBaseClass>>("MyDerivedClass")
        .constructor<>()
        .function("invoke",&MyDerivedClass::invoke);
    function("get_MyDerivedClass",&get_MyDerivedClass,allow_raw_pointers());
}
附加js代码
__ATPOSTRUN__.push(() =>{
    var derivedClassInstance = new Module["get_MyDerivedClass"]();
    console.log(derivedClassInstance.invoke("hello"));
});
  • 重载函数

C/C++代码
//引入相关的头文件 
#include <emscripten/bind.h>
#include <string> 
//与Embind相关的宏参数被定义在emscripten命名空间中
using namespace emscripten;

class MyOverloadClass{
    public:
        MyOverloadClass() = default;
        //定义一系列重载函数
        std::string foo() const{
            return "XingXing: ()";
        } 
        std::string foo(int x) const{
            return "XingXing: (int x)";
        } 
        std::string foo(int x,int y) const{
            return "XingXing: (int x int y)";
        } 
};

//绑定 
EMSCRIPTEN_BINDINGS(module){
    class_<MyOverloadClass>("MyOverloadClass")
        .constructor<>()
        //通过select_overload方法为每一个重载函数指定用于函数关系绑定的别名 
        .function("foo_v",select_overload<std::string(void) const>(&MyOverloadClass::foo))
        .function("foo_i",select_overload<std::string(int) const>(&MyOverloadClass::foo))
        .function("foo_ii",select_overload<std::string(int,int) const>(&MyOverloadClass::foo));
}

可以看到,我们在名为MyOverloadClassC++类中定义了多个名称相同但具有不同参数列表的重载函数。这些函数在进行关系绑定时,需要我们依次通过select_overload模板方法为其指定唯一的可以在JavaScript环境中使用的函数名。在使用该方法时,需要为其提供对应的重载函数类型签名及函数指针,Embind内部会根据函数签名和函数指针所指向的函数位置依次找到每一个重载函数的实体,并为它们分别绑定我们在function方法中设置的别名。上述带有别名的重载函数在JavaScript 环境中的使用方法,与普通类型函数的使用方法没有任何区别。示例代码如下。

附加js代码
__ATPOSTRUN__.push(() =>{
    var instance = new Module["MyOverloadClass"]();
    console.log(instance["foo_v"]());
    console.log(instance["foo_i"](10));
    console.log(instance["foo_ii"](10,10));
});
  • 枚举类型

Embind可以同时对C++ 98的枚举类型及C++ 11中的枚举类提供支持。被绑定的枚举类型或枚举类会以对象([Object object])的形式在JavaScript环境中使用。

C/C++代码
//引入相关的头文件 
#include <emscripten/bind.h> 
//与Embind相关的宏参数被定义在emscripten命名空间中
using namespace emscripten;
//C++ 98 中的枚举
enum OldStyle{
    OLD_STYLE_ONE,
    OLD_STYLE_TWO
}; 
//C++ 11中的枚举
enum class NewStyle{
    ONE,
    TWO
}; 
//绑定 
EMSCRIPTEN_BINDINGS(module){
    //通过enum_模板方法来绑定枚举类型和枚举类
    enum_<OldStyle>("OldStyle") 
        .value("ONE",OLD_STYLE_ONE)
        .value("TWO",OLD_STYLE_TWO);
    enum_<NewStyle>("NewStyle") 
        .value("ONE",NewStyle::ONE)
        .value("TWO",NewStyle::TWO);
}
附加js代码
__ATPOSTRUN__.push(() =>{
    console.log(Module["OldStyle"]["ONE"].value);
    console.log(Module["NewStyle"]["TWO"].value);
});
命令

emcc index.cc --std=c++11 --bind -s WASM=1 --post-js post-script.js -o index.js

  • 基本类型

Embind可以将在C/C++代码中定义的常见基本类型变量值,通过名为constant的方法绑定到JavaScript环境中进行使用。

C/C++源码
//引入相关的头文件 
#include <emscripten/bind.h> 
#include <string> 
//与Embind相关的宏参数被定义在emscripten命名空间中
using namespace emscripten;
//布尔类型
bool val_bool = true;
//字符类型
char val_char = 'c';
signed char val_s_char = 'c';
unsigned char val_u_char = 'c';
//整数值类型
short val_short = 100;
signed short val_s_short = -100;
unsigned short val_u_short = 100;
int val_int =100;
signed int val_s_int = -100;
unsigned int val_u_int = 100;
long val_long = 100;
signed long val_s_long = -100;
unsigned long val_u_long = 100;
//浮点数类型
float val_float = 1.5;
double val_double = 1.6;
//字符串类型
std::string val_string = "xingxing";
std::wstring val_wstring = L"星星"; 
//绑定 
EMSCRIPTEN_BINDINGS(module){
    //通过constant方法来绑定枚上述基本类型变量
    constant("val_bool",val_bool); 
    constant("val_char",val_char); 
    constant("val_s_char",val_s_char); 
    constant("val_u_char",val_u_char); 
    constant("val_short",val_short); 
    constant("val_s_short",val_s_short); 
    constant("val_u_short",val_u_short); 
    constant("val_int",val_int); 
    constant("val_s_int",val_s_int); 
    constant("val_u_int",val_u_int); 
    constant("val_long",val_long); 
    constant("val_s_long",val_s_long); 
    constant("val_u_long",val_u_long); 
    constant("val_float",val_float); 
    constant("val_double",val_double); 
    constant("val_string",val_string); 
    constant("val_wstring",val_wstring); 
}
附加js代码
__ATPOSTRUN__.push(() =>{
    console.log(Module["val_bool"]);
    console.log(Module["val_char"]);
    console.log(Module["val_s_char"]);
    console.log(Module["val_u_char"]);
    console.log(Module["val_short"]);
    console.log(Module["val_s_short"]);
    console.log(Module["val_u_short"]);
    console.log(Module["val_int"]);
    console.log(Module["val_s_int"]);
    console.log(Module["val_u_int"]);
    console.log(Module["val_long"]);
    console.log(Module["val_s_long"]);
    console.log(Module["val_u_long"]);
    console.log(Module["val_float"]);
    console.log(Module["val_double"]);
    console.log(Module["val_string"]);
    console.log(Module["val_wstring"]);
});
  • 容器类型(map vector等)

C/C++代码

#include <emscripten/bind.h> 
#include <string> 
#include <vector> 
using namespace std;
using namespace emscripten;
class xClass{
    public:
        xClass (int x,string y):x(x),y(y){
        }
        //使用vector向量容器
        vector<int> returnVectorData(){
            vector<int> v(10,x);
            return v;
        } 
        //使用map字典容器
        map<int,string> returnMapData(){
            map<int,string> m;
            m.insert(pair<int,string>(x,y));
            return m;
        }
        private:
            int x;
            string y;
};
 
EMSCRIPTEN_BINDINGS(module){
    class_<xClass>("xClass")
    .constructor<int,string>()
    .function("returnVectorData",&xClass::returnVectorData)
    .function("returnMapData",&xClass::returnMapData);
    //注册在案底吗中使用的容器类型
    register_vector<int>("vector<int>");
    register_map<int,string>("map<int,string>"); 
    
}

在上面的代码中,我们分别在returnVectorDatareturnMapData两个函数中使用并返回了相应的vector向量容器和map字典容器数据。为了能够让Embind支持这些容器类型数据的关系绑定过程,我们需要在整段代码结尾处的EMSCRIPTEN_BINDINGS宏参数内部,通过对应的register_vectorregister_map模板方法来对它们进行注册。需要注意的是,这里的模板方法在被调用时所使用的初始化参数类型,需要与我们在C++代码中使用的“字典”与“向量"容器元素类型相对应,即有多少种容器类型就需要调用多少次注册过程。我们可以通过如下JavaScript 脚本代码来使用上述在C++函数中返回的vectormap数据结构。Embind会帮助我们自动地在相应的“胶水“脚本代码中封装好针对这两种容器数据的常用操作方法,比如通过push_back方法向一个返回到JavaScript环境中的向量容器尾部推入新的元素,或者通过set方法来设置一个返回到JavaScript环境中的字典容器在某个索引位置上的元素值。其他相关方法的使用可以参考如下代码。

附加js代码
__ATPOSTRUN__.push(() =>{
    var xClass = new Module["xClass"](10,"KEY");
    var retVector = xClass.returnVectorData();
    //获取向量容器的大小
    var vectorSize = retVector.size();
    //重新设置向量容器中某索引位置上的元素
    retVector.set(vectorSize-1,11);
    //往向量容器尾部推入元素
    retVector.push_back(12);
    //遍历向量容器
    for(var i =0;i<retVector.size();i++){
        console.log("Vector Value: ",retVector.get(i));
    }
    //扩容向量容器并设置默认值
    retVector.resize(20,1);
    
    var retMap = xClass.returnMapData();
    //获取字典容器的大小
    var mapSize = retMap.size();
    //获取字典容器某索引的值
    console.log("Map value:",retMap.get(10));
    //重新设置字典某索引的值
    retMap.set(10,"xingxing");
    xClass.delete();
});
  • 转译JS代码

Embind内部提供了一个名为emscripten:val的类结构,通过该类结构,我们可以在C/C++代码中直接使用暴露在JavaScript 全局环境内的数据对象和变量。当然,我们也可以直接将上一节介绍的C/C++代码中的基本类型变量,通过该类间接地传递到JavaScript环境中进行使用。示例代码如下。

C/C++代码
#include <emscripten/val.h>
#include <emscripten/bind.h> 
#include <string> 
#include <iostream> 
using namespace emscripten;

val getDefaultStringValue(void){
    //直接构造的字面量值 
    return val("YHSPY");
} 

val getDefaultIntValue(void){
    //间接通过变量进行构造 
    int _t = 10;
    return val(_t);
} 
//操作DOM对象
void manipulateDOM(val content){
    //获取js环境下的document全局对象
    val documentInstance = val::global("document");
    //判断对象是否存在
    if(!documentInstance.as<bool>()){
        std::cout <<"NO 'windows.document' object found!"<< std::endl;
        return;
    } 
    //调用该对象并向页面中打印的内容
     documentInstance.call<void>("write",content);
     return;
} 

void mountTime(){
    val moduleInstance = val::global("Module");
    val DateContext = val::global("Date");
    if(!moduleInstance.as<bool>()){
        std::cout<<"NO 'Module' object found!"<< std::endl;
        return;
    }
    //创建一个新的Date对象
    val dateInstance = DateContext.new_();
    //在Module全局对象中新建一个currentTimestamp属性,并设置该属性值为当前时间值
    moduleInstance.set("currentTimestamp",dateInstance.call<val>("getTime"));
    return; 
}

 
EMSCRIPTEN_BINDINGS(module){
    //绑定方法
    function("getDefaultStringValue",&getDefaultStringValue); 
    function("getDefaultIntValue",&getDefaultIntValue); 
    function("manipulateDOM",&manipulateDOM); 
    function("mountTime",&mountTime); 
}

在这段代码中,我们借助emscripten:val类完成了 多种类型的操作。

  • (1)我们在两个不同语言环境之间(C/C++与JavaScript环境)需要进行数据交换的地方使用emscripten:val类来包装需要传道的数据值。该类对象同时支持以变量或字面量的形式进行初始化。比如在上面代码的前两个绑定函数中,其返回的数据值通过该类进行了包装,并且函数的这回值也被设置为emscripten:val,而Embind则会在调用函数时对其参数和返回值自动进行适当的数据类型转换。

  • (2)通过emscripten:val类提供的global方法,我们可以在C++代码中直接引用暴露在JavaScipt全局环境中的变量(比如这里引用了上层Web浏览器环境中的window.document全局对象).所引用的变量将会在C/C++代码中以同样的emscripten:val类型进行表示,因此,我们可以进一步通过该类型对象提供的call ,new_等方法,在C/C++代码中间接地对引用的上层JavaScript全局变量进行如方法调用、属性绑定等操作。

    虽然我们可以通过C/C++代码间接地调用浏览器中的各类JavaScript 全局对象,但这并不意味着直接通过C/C++代码来操作这些对象会有更高的性能。如图6-5所示为两种不同的上层JavaScript对象调用方式。


    可以看到,相对于直接通过上层JavaScript代码来操作浏览器的行为,间接地通过C/C++代码来控制浏览器行为会增加很多无意义的代码处理开销,比如频繁在两种环境(JavaScript以及WebAssembly)之间切换产生的上下文开销、数据值类型转换开销,以及“胶水”脚本的执行开销等。因此,是否需要通过Embind中间件在C/C++代码中直接进行类似使用DOM对象等全局的JavaSript对象(变量),则需要我们对项目的整体定位进行评估,并从项目的使用性能与工程化角度进行权衡后才能做决定

附加js代码
__ATPOSTRUN__.push(() =>{
    console.log(Module.getDefaultStringValue());
    console.log(Module.getDefaultIntValue());
    Module.manipulateDOM("this is from c++");
    Module.mountTime();
    console.log(Module["currentTimestamp"]);
});
  • 内存视图

C/C++代码
#include <emscripten/val.h>
#include <emscripten/bind.h> 
using namespace emscripten;
//定义一个无符号的字符数组
unsigned char _t [] = {'a','b','c'};
unsigned char *byteBuffer = _t;
//获取数组的长度
size_t bufferLength = sizeof(_t)/sizeof(unsigned char);

val getBytes(){
    //通过typed_memory_view方法标识类型数组的信息
    return val(typed_memory_view(bufferLength,byteBuffer)); 
} 
 
EMSCRIPTEN_BINDINGS(module){
    //绑定方法
    function("getBytes",&getBytes); 
}
附加js代码
__ATPOSTRUN__.push(() =>{
    //这里从C/C++代码中返回的二进制数组将会以TypedArray的形式展现
    console.log(Module.getBytes());
});


这里在向JavaScript环境返回数组结构时,需要先通过typed_memory_view方法来标识数组的长度与起始地址,然后将该方法的返回值用emscripten:val进行包装,最后返回到JavaScript环境中。Embind会将在整个过程中传递的进制数组内容直接保存到 Wasm模块的共享线性内存段中。因此,对于多媒体类型的应用,这可以有效地节省其使用的内存空间。

全文参考:《深入浅出WebAssembly》 于航

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容