Emscripten教程之C++和JavaScript绑定(三)

翻译:云荒杯倾

Embind用于绑定C++函数和类到JavaScript,这样编译代码就能在js中以一种很自然的方式来使用。Embind也支持从C++调JavaScript的class。

Embind支持绑定大多数C++的结构,包括C++11和C++14中引入的。它只有一个明显的限制就是目前还不支持raw pointers with complicated lifetime semantics。

本文展示了如何使用EMSCRIPTEN_BINDINGS()块来创建函数、类、值类型、指针(包括原始和智能指针)、枚举和常量的绑定,以及如何为抽象类创建绑定,这些抽象类可以在JavaScript中被重写。它还简要介绍了如何管理传递给JavaScript的c++对象句柄的内存。

note:
Embind的灵感来自 Boost.Python,他们使用非常相似的方法定义绑定。

一个简单例子

下面的代码使用EMSCRIPTEN_BINDINGS()暴露了C++ lerp()函数给JavaScript。

    // quick_example.cpp
    #include <emscripten/bind.h>

    using namespace emscripten;

    float lerp(float a, float b, float t) {
        return (1 - t) * a + t * b;
    }

    EMSCRIPTEN_BINDINGS(my_module) {
        function("lerp", &lerp);
    }

为了使用embind编译上例,请调用emcc的bing选项:

emcc --bind -o quick_example.js quick_example.cpp

生成的quick_example.js文件可以作为node模块加载,也可以使用<script>加载:

     <!doctype html>
    <html>
      <script src="quick_example.js"></script>
      <script>
        console.log('lerp result: ' + Module.lerp(1, 2, 0.5));
      </script>
    </html>

当quick_example.js文件初始化加载后, EMSCRIPTEN_BINDINGS()中代码会运行。

所有通过Embind暴露的symblols都可以在Module对象获取。

暴露一个类给JavaScript需要比较复杂的绑定语句,比如:

    class MyClass {
    public:
      MyClass(int x, std::string y)
        : x(x)
        , y(y)
      {}

      void incrementX() {
        ++x;
      }

      int getX() const { return x; }
      void setX(int x_) { x = x_; }

      static std::string getStringFromInstance(const MyClass& instance) {
        return instance.y;
      }

    private:
      int x;
      std::string y;
    };

    // Binding code
    EMSCRIPTEN_BINDINGS(my_class_example) {
      class_<MyClass>("MyClass")
        .constructor<int, std::string>()
        .function("incrementX", &MyClass::incrementX)
        .property("x", &MyClass::getX, &MyClass::setX)
        .class_function("getStringFromInstance", &MyClass::getStringFromInstance)
        ;
    }

绑定块在一个临时class_对象上定义了成员函数调用链(Boost.Python也是同样风格)。

note:
你应该只绑定那些你实际需要的项(将它作为一个规则或原则),因为每个绑定会增加代码大小。比如,内部方法和私有变量可以很少绑定。

在JavaScript中定义和使用MyClass实例的代码如下:

    var instance = new Module.MyClass(10, "hello");
    instance.incrementX();
    instance.x; // 12
    instance.x = 20; // 20
    Module.MyClass.getStringFromInstance(instance); // "hello"
    instance.delete();

内存管理

因为JavaScript,尤其是ECMA-262 Edition 5.1,不支持 finalizers or weak references with callbacks,因此Emscripten没有办法调用C++对象的析构函数。

警告:
JavaScript代码必须明确删除C++对象的句柄,否则Emscripten堆会无限增长。
    var x = new Module.MyClass;
    x.method();
    x.delete();

    var y = Module.myFunctionThatReturnsClassInstance();
    y.method();
    y.delete();

值类型

对基本类型进行手动内存管理是麻烦的,所以embind对值类型提供了支持。包括Value arrays和 value objects,分别对应js的array和object。

示例:

    struct Point2f {
        float x;
        float y;
    };

    struct PersonRecord {
        std::string name;
        int age;
    };

    PersonRecord findPersonAtLocation(Point2f);

    EMSCRIPTEN_BINDINGS(my_value_example) {
        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);
    }

以下代码就不需要担心手动生命周期管理。

    var person = Module.findPersonAtLocation([10.2, 156.5]);
    console.log('Found someone! Their name is ' + person.name + ' and they are ' + person.age + ' years old');

高级类概念(todo)

重载函数

构造函数和函数可以根据参数数量重载,但embind不支持根据参数类型重载。当你指定一个重载,请使用select_overload()帮助函数选中合适的签名。

    struct HasOverloadedMethods {
        void foo();
        void foo(int i);
        void foo(float f) const;
    };

    EMSCRIPTEN_BINDING(overloads) {
        class_<HasOverloadedMethods>("HasOverloadedMethods")
            .function("foo", select_overload<void()>(&HasOverloadedMethods::foo))
            .function("foo_int", select_overload<void(int)>(&HasOverloadedMethods::foo))
            .function("foo_float", select_overload<void(float)const>(&HasOverloadedMethods::foo))
            ;
    }

枚举

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;

常量

向JavaScript暴露一个常量:

    EMSCRIPTEN_BINDINGS(my_constant_example) {
        constant("SOME_CONSTANT", SOME_CONSTANT);
    }

内存视图

在某些情况下,将原始二进制数据以一个类型化数组的形式直接暴露给JavaScript代码是有价值的。这对于直接从堆上上传大型WebGL纹理非常有用。

内存视图应该像指针一样对待;生命周期和有效性不由运行时管理的,如果底层对象被修改或重新分配,则很容易损坏数据。

    #include <emscripten/bind.h>
    #include <emscripten/val.h>

    using namespace emscripten;

    unsigned char *byteBuffer = /* ... */;
    size_t bufferLength = /* ... */;

    val getBytes() {
        return val(typed_memory_view(bufferLength, byteBuffer));
    }

    EMSCRIPTEN_BINDINGS(memory_view_example) {
        function("getBytes", &getBytes);
    }

下面JavaScript代码接收类型数组视图

    var myUint8Array = Module.getBytes()
    var xhr = new XMLHttpRequest();
    xhr.open('POST', /* ... */);
    xhr.send(myUint8Array);

使用val将JavaScript翻译为C++

Embind提供了一个c++类,emscripten::val,您可以使用它将JavaScript代码转换为c++。使用val,可以在c++中调用JavaScript对象,读取和写入它们的属性,或者强制它们成为c++值,比如bool、int或std::string。

下面代码展示了你可以通过val在C++中调用JavaScript的 Web Audio API。
首先看一下js的代码,展示js怎么用这个API:

    // Get web audio api context
    var AudioContext = window.AudioContext || window.webkitAudioContext;

    // Got an AudioContext: Create context and OscillatorNode
    var context = new AudioContext();
    var oscillator = context.createOscillator();

    // Configuring oscillator: set OscillatorNode type and frequency
    oscillator.type = 'triangle';
    oscillator.frequency.value = 261.63; // value in hertz - middle C

    // Playing
    oscillator.connect(context.destination);
    oscillator.start();

    // All done!

然后使用val将代码翻译成c++,如下:

    #include <emscripten/val.h>
    #include <stdio.h>
    #include <math.h>

    using namespace emscripten;

    int main() {
      val AudioContext = val::global("AudioContext");
      if (!AudioContext.as<bool>()) {
        printf("No global AudioContext, trying webkitAudioContext\n");
        AudioContext = val::global("webkitAudioContext");
      }

      printf("Got an AudioContext\n");
      val context = AudioContext.new_();
      val oscillator = context.call<val>("createOscillator");

      printf("Configuring oscillator\n");
      oscillator.set("type", val("triangle"));
      oscillator["frequency"].set("value", val(261.63)); // Middle C

      printf("Playing\n");
      oscillator.call<void>("connect", context["destination"]);
      oscillator.call<void>("start", 0);

      printf("All done!\n");
    }

首先使用global()取全局AudioContext对象(如果不存在就取webkitAudioContext对象),然后使用new_()创建实例,从实例我们可以创建oscillator,设置set()它的属性,然后播放。

内建类型转换

embind为许多标准C++类型提供类型转换

C++类型JavaScript类型
voidundefined
booltrue or false
charnumber
signed charnumber
unsigned charnumber
shortnumber
ungigned shortnumber
intnumber
unsigned intnumber
lognumber
unsigned longnumber
floatnumber
doublenumber
std::stringArrayBuffer, Uint8Array, Uint8ClampedArray, Int8Array, or String
std::wstringString (UTF-16 code units)
emscripten::valanything

为了方便,embind还提供了工厂函数用来注册std::vector<T>(register_vector())和std::map<K, V>(register_map())类型。

    EMSCRIPTEN_BINDINGS(stl_wrappers) {
        register_vector<int>("VectorInt");
        register_map<int,int>("MapIntInt");
    }

性能

在撰写本文时,还没有对标准基准测试或相对于WebIDL Binder的全面的embind性能测试。

简单函数的调用开销在200ns左右。虽然还有进一步优化的空间,但到目前为止,它在实际应用程序中的性能已经被证明是完全可以接受的。

Emscripten代码移植系列文章

Emscripten代码移植主题系列文章是emscripten中文站点的一部分内容。
本文是第三个主题第二篇文章。
第一个主题介绍代码可移植性与限制
第二个主题介绍Emscripten的运行时环境
第三个主题第一篇文章介绍连接C++和JavaScript
第三个主题第二篇文章介绍embind
第四个主题介绍文件和文件系统

    原文作者:cunzaizhuyi
    原文地址: https://segmentfault.com/a/1190000011229465
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞