Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

绑定类

Github代码

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_GetOpaqueJSValue中拿到特定类的指针(注意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;
}

注意创建类对象的方法:

  1. 使用JS_NewObjectClass创建类对象
  2. 使用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)