Github代码
我们需要先理解JS中的原型对象。因为C++这边的绑定和JS的原理是一致的。
JS类的一个典型古典定义如下:
1
2
3
4
5
6
7
8
9
10
| function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log("Hello, my name is " + this.name);
};
let alice = new Person("Alice");
alice.sayHello();
|
类本身是一个函数Person
。而其成员函数都是绑定在函数的原型prototype
中。当实例化的时候,会调用Person
函数初始化成员变量,并且拷贝prototype
到对象中以找到成员函数。
假设我们有个Person类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| struct Person {
static int ID;
char name[512] = {0};
float height;
float weight;
int age;
Person(const std::string& name, float height, int age, float weight)
: height{height}, age{age}, weight{weight} {
ChangeName(name);
}
void Introduce() const {
std::cout << "I am " << name << ", age " << age << ", height " << height
<< ", weight " << weight << std::endl;
}
float GetBMI() const { return weight / (height * height); }
void ChangeName(const std::string& name) {
strcpy(this->name, name.data());
}
};
|
绑定的过程如下:
首先,需要创建一份JSClassID
。QuickJS内部用JSClassID
唯一标识一个类:
1
2
3
4
| gClassID = JS_NewClassID(runtime, &gClassID);
if (gClassID == 0) {
std::cerr << "create class id failed" << std::endl;
}
|
然后需要一个类定义对象JSClassDef
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| JSClassDef def{};
// will call when value be freed
def.finalizer = +[](JSRuntime*, JSValue self) {
if (!JS_IsObject(self)) {
std::cerr << "in finalizer, self is not object" << std::endl;
}
Person* opaque = static_cast<Person*>(JS_GetOpaque(self, gClassID));
if (!opaque) {
std::cerr << "self is nullptr" << std::endl;
}
delete opaque;
};
def.class_name = class_name;
|
def.finalizer
是类的析构函数。当JSValue
被GC掉的时候会调用。我们需要在这里清理内存。
def.class_name
则是类名。
接下来需要将这个类定义注册到JSRuntime
中:
1
| JS_NewClass(runtime, gClassID, &def);
|
然后我们需要根据原型对象的原理组建一个原型对象:
1
| JSValue proto = JS_NewObject(ctx);
|
接下来需要为成员变量/函数创建对应的JSValue
。注意这里成员变量和函数在C++中都是函数(成员变量由getter/setter表示)。比如说getter/setter:
1
2
3
4
5
6
7
8
9
10
11
12
| JSValue NameGetter(JSContext* ctx, JSValue self) {
// I'm lazy to check type :-)
const Person* p = static_cast<const Person*>(JS_GetOpaque(self, gClassID));
return JS_NewString(ctx, p->name);
}
JSValue NameSetter(JSContext* ctx, JSValue self, JSValueConst param) {
// I'm lazy to check type :-)
Person* p = static_cast<Person*>(JS_GetOpaque(self, gClassID));
p->ChangeName(JS_ToCString(ctx, param));
return JS_UNDEFINED;
}
|
使用JS_GetOpaque
从JSValue
中拿到特定类的指针(注意gClassID
一定要对得上。如果class id是无效的会返回空指针,这也就意味着你必须先注册对应的类)。
然后绑定给prototype:
1
2
3
| JSAtom name = JS_NewAtom(ctx, "name");
JS_DefinePropertyGetSet(ctx, proto, NameGetterJSValue, NameSetterJSValue, nameAtom, 0);
JS_FreeAtom(ctx, atom);
|
其他成员函数同理。
然后定义构造函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| JSValue ConstructorBinding(JSContext* ctx, JSValue self, int argc,
JSValueConst* argv) {
// I'm lazy to check argv type :-)
const char* name = JS_ToCString(ctx, argv[0]);
double height;
JS_ToFloat64(ctx, &height, argv[1]);
int age;
JS_ToInt32(ctx, &age, argv[2]);
double weight;
JS_ToFloat64(ctx, &weight, argv[3]);
Person* person = new Person(name, height, age, weight);
JSValue result = JS_NewObjectClass(ctx, gClassID);
JS_SetOpaque(result, person);
return result;
}
|
注意创建类对象的方法:
- 使用
JS_NewObjectClass
创建类对象 - 使用
JS_SetOpaque
将C++对象传给JS
然后,告诉QuickJS我们需要将此prototype和哪个类相关联:
1
| JS_SetClassProto(ctx, gClassID, proto);
|
最后将构造函数注册给global_this
,我们就可以在JS中使用此类啦:
1
2
3
| JSValue global_var = JS_GetGlobalObject(ctx);
JS_DefinePropertyValueStr(ctx, global_var, "Person", constructor, JS_CFUNC_constructor);
JS_FreeValue(ctx, global_var);
|
在JS中使用:
1
2
3
4
5
6
| let person = new Person("QJSKid", 150, 15, 40)
console.log(person.name)
person.name = "John"
console.log(person.name)
console.log(person.bmi)
person.introduce()
|
静态函数和成员直接绑定在构造函数上即可,无需绑定在prototype上:
1
2
| JSValue id_value = JS_NewInt32(ctx, Person::ID);
JS_SetPropertyStr(ctx, constructor, "ID", id_value);
|
使用: