Emscripten代码移植之embind(三)

翻译:云荒杯倾

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++对象句柄的内存。

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

一个简单例子

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

1
2
3
4
5
6
7
8
9
10
11
12
// 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选项:

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

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

1
2
3
4
5
6
7
<!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需要比较复杂的绑定语句,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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也是同样风格)。

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

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

1
2
3
4
5
6
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++对象的析构函数。

1
2
警告:
JavaScript代码必须明确删除C++对象的句柄,否则Emscripten堆会无限增长。
1
2
3
4
5
6
7
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。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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);
}

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

1
2
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()帮助函数选中合适的签名。

1
2
3
4
5
6
7
8
9
10
11
12
13
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枚举类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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调用方式如下:

1
2
Module.OldStyle.ONE;
Module.NewStyle.TWO;

常量

向JavaScript暴露一个常量:

1
2
3
EMSCRIPTEN_BINDINGS(my_constant_example) {
constant("SOME_CONSTANT", SOME_CONSTANT);
}

内存视图

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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代码接收类型数组视图

1
2
3
4
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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++,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#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类型
void undefined
bool true or false
char number
signed char number
unsigned char number
short number
ungigned short number
int number
unsigned int number
log number
unsigned long number
float number
double number
std::string ArrayBuffer, Uint8Array, Uint8ClampedArray, Int8Array, or String
std::wstring String (UTF-16 code units)
emscripten::val anything

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

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

性能

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

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


Emscripten代码移植系列文章

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