绑定类
JS中的原型对象(prototype)
我们需要先理解JS中的原型对象。因为C++这边的绑定和JS的原理是一致的。
JS类的一个典型古典定义如下:
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
到对象中以找到成员函数。
C++绑定类的例子
假设我们有个Person类:
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
唯一标识一个类:
gClassID = JS_NewClassID(runtime, &gClassID);
if (gClassID == 0) {
std::cerr << "create class id failed" << std::endl;
}
然后需要一个类定义对象JSClassDef
:
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
中:
JS_NewClass(runtime, gClassID, &def);
然后我们需要根据原型对象的原理组建一个原型对象:
JSValue proto = JS_NewObject(ctx);
接下来需要为成员变量/函数创建对应的JSValue
。注意这里成员变量和函数在C++中都是函数(成员变量由getter/setter表示)。比如说getter/setter:
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:
JSAtom name = JS_NewAtom(ctx, "name");
JS_DefinePropertyGetSet(ctx, proto, NameGetterJSValue, NameSetterJSValue, nameAtom, 0);
JS_FreeAtom(ctx, atom);
其他成员函数同理。
然后定义构造函数:
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和哪个类相关联:
JS_SetClassProto(ctx, gClassID, proto);
最后将构造函数注册给global_this
,我们就可以在JS中使用此类啦:
JSValue global_var = JS_GetGlobalObject(ctx);
JS_DefinePropertyValueStr(ctx, global_var, "Person", constructor, JS_CFUNC_constructor);
JS_FreeValue(ctx, global_var);
在JS中使用:
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上:
JSValue id_value = JS_NewInt32(ctx, Person::ID);
JS_SetPropertyStr(ctx, constructor, "ID", id_value);
使用:
console.log(Person.ID)