PDA

View Full Version : QtScript : Prototype-based inheritance, wrappers and setDefaultPrototype()



ixM
7th February 2012, 23:43
Hi everyone,

I'm trying to use QtScript to allow "users" (just me for now :P) to control the game-engine I'm creating (nothing complicated as I'm just doing that for fun).

I've decided to create wrappers for all the classes I want to expose to the script engine. Those wrappers inherit from QObject and QScriptable.

As an example, let's consider the following classes:


Drawable (abstract): contains all information that all drawable objects share (position, name, parent, children,...)
MeshDrawable (extends Drawable): implements pure virtual functions for loading and drawing a mesh
Wrapper_Drawable: wraps a Drawable
Wrapper_MeshDrawable (extends Wrapper_Drawable): wraps a MeshDrawable


Let's concentrate on the last two classes:


class Wrapper_Drawable : public QObject, public QScriptable
{
Q_OBJECT
public:
explicit Wrapper_Drawable(Drawable* drawable = 0);

virtual Drawable* drawable() const;

public slots:
QScriptValue getPosition() const;
QScriptValue setPosition();

protected:
Drawable* m_drawable;
};

Q_DECLARE_METATYPE(Wrapper_Drawable*)


class Wrapper_MeshDrawable : public Wrapper_Drawable
{
Q_OBJECT
public:
Wrapper_MeshDrawable(MeshDrawable* meshDrawable = 0);

virtual MeshDrawable* drawable() const;

public slots:
QScriptValue addTriangle();
};

Q_DECLARE_METATYPE(Wrapper_MeshDrawable*)

I declare those classes to the script engine:


Wrapper_Drawable* wrapper_drawable = new Wrapper_Drawable();
QScriptValue wrapper_drawable_obj = m_scriptEngine->newQObject(wrapper_drawable);
wrapper_drawable_obj.setPrototype(m_scriptEngine->newObject());
m_scriptEngine->setDefaultPrototype(qMetaTypeId<Wrapper_Drawable*>(), wrapper_drawable_obj);
m_scriptEngine->globalObject().setProperty("Drawable", wrapper_drawable_obj);

Wrapper_MeshDrawable* wrapper_meshDrawable = new Wrapper_MeshDrawable();
QScriptValue wrapper_meshDrawable_obj = m_scriptEngine->newQObject(wrapper_meshDrawable);
wrapper_meshDrawable_obj.setPrototype(wrapper_draw able_obj);
m_scriptEngine->setDefaultPrototype(qMetaTypeId<Wrapper_MeshDrawable*>(), wrapper_meshDrawable_obj);
m_scriptEngine->globalObject().setProperty("MeshDrawable", wrapper_meshDrawable_obj);

But it doesn't do what I'm expecting (these are my questions BTW):


If I skip the last two lines of the two blocks above, and do qDebug() << wrapper_meshDrawable_obj.instanceOf(wrapper_drawab le_obj);, the result is false. How is that possible? Am I setting the prototype chain wrong?
I have read somewhere (or think I've read) that even just using the 4th line of the two blocks above (setDefaultPrototype()) should let the engine know that when it creates a new QScriptValue from a Wrapper_MeshDrawable, its prototype is the one of Wrapper_Drawable. Isn't that correct?
When I print the properties of Drawable, it has none. When I print the properties of MeshDrawable, it has all the properties of Drawable but not his. It seems that there is a shift in the prototype chain.


I hope my questions are clear but I would be happy to give any further information if you need some to help me.


Thank you very much in advance!

Regards,


Maximilien

wysota
8th February 2012, 02:53
You are doing it wrong. You should be registering default prototypes for your types (i.e. Drawable* and MeshDrawable*) and not your prototypes. Then you should be exposing instances of your types and not the prototypes. Furthermore you'll have problems while casting objects containing virtual methods.


#include <QCoreApplication>
#include <QScriptEngine>
#include <QScriptable>
#include <QPoint>

class Drawable {
public:
int x() const { return pt.x(); }
int y() const { return pt.y(); }
void setX(int xx) { pt.setX(xx); }
void setY(int yy) { pt.setY(yy); }
private:
QPoint pt;
};

class MeshDrawable : public Drawable {
public:

};

class DrawablePrototype : public QObject, public QScriptable {
Q_OBJECT
public:
DrawablePrototype(QObject *parent = 0) : QObject(parent) {}
public slots:
void setX(int x) {
Drawable *drawable = qscriptvalue_cast<Drawable*>(thisObject());
drawable->setX(x);
}
void setY(int y) {
Drawable *drawable = qscriptvalue_cast<Drawable*>(thisObject());
drawable->setY(y);
}
public:
Q_INVOKABLE virtual int x() const {
Drawable *drawable = qscriptvalue_cast<Drawable*>(thisObject());
return drawable->x();
}
Q_INVOKABLE virtual int y() const {
Drawable *drawable = qscriptvalue_cast<Drawable*>(thisObject());
return drawable->y();
}

private:

};

class MeshDrawablePrototype : public QObject, public QScriptable {
Q_OBJECT
public:
MeshDrawablePrototype(QObject *parent = 0) : QObject(parent) {}
public:
Q_INVOKABLE virtual int x() const {
MeshDrawable *drawable = qscriptvalue_cast<MeshDrawable*>(thisObject());
return -drawable->x();
}
Q_INVOKABLE virtual int y() const {
MeshDrawable *drawable = qscriptvalue_cast<MeshDrawable*>(thisObject());
return -drawable->y();
}

};

//QScriptValue constructDrawable(QScriptContext *context, QScriptEngine *engine) {
// if (!context->isCalledAsConstructor())
// return context->throwError(QScriptContext::SyntaxError, "please use the 'new' operator");

// return engine->newVariant(context->thisObject(), qVariantFromValue(new Drawable));
//}


Q_DECLARE_METATYPE(Drawable*)
Q_DECLARE_METATYPE(MeshDrawable*)

#include "main.moc"
#include <QtDebug>

int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QScriptEngine engine;
DrawablePrototype drawableProto;
MeshDrawablePrototype meshProto;
QScriptValue dPValue = engine.newQObject(&drawableProto);
QScriptValue mPValue = engine.newQObject(&meshProto);
engine.setDefaultPrototype(qMetaTypeId<Drawable*>(), dPValue);
engine.setDefaultPrototype(qMetaTypeId<MeshDrawable*>(), mPValue);

mPValue.setPrototype(dPValue);

Drawable *drawable = new Drawable;
MeshDrawable *meshDrawable = new MeshDrawable;

QScriptValue drawableObj = engine.newVariant(qVariantFromValue<Drawable*>(drawable));
QScriptValue meshDrawableObj = engine.newVariant(qVariantFromValue<MeshDrawable*>(meshDrawable));
engine.globalObject().setProperty("drawable", drawableObj);
engine.globalObject().setProperty("meshDrawable", meshDrawableObj);



qDebug() << engine.evaluate("drawable.setX(7); drawable.x()").toString();
//qDebug() << engine.evaluate("meshDrawable.setX(7); meshDrawable.x()").toString();
delete drawable;
delete meshDrawable;
}

Uncommenting the second qDebug() line crashes the app, probably because the cast in setX() malforms the object.

Anyway, if the only thing you want is to indeed have wrappers for your classes then you can do so:


class Drawable_Wrapper : public QObject {
Q_OBJECT
public:
Drawable_Wrapper(Drawable *drawable, QObject *parent = 0) : QObject(parent) { m_drawable = drawable; }
public slots:
virtual void setX(int x) { m_drawable->setX(x); }
private:
Drawable *m_drawable;
};

class MeshDrawable_Wrapper : public Drawable_Wrapper {
Q_OBJECT
public:
MeshDrawable_Wrapper(MeshDrawable *meshDrawable, QObject *parent = 0) : Drawable_Wrapper(meshDrawable, parent) {}
public slots:
virtual void setX(int x) { Drawable_Wrapper::setX(-x); }
};

Drawable_Wrapper wrapper(new Drawable);
MeshDrawable_Wrapper wrapper2(new MeshDrawable);
engine.globalObject()->setProperty("drawable", engine.newQObject(&wrapper));
engine.globalObject()->setProperty("meshDrawable", engine.newQObject(&wrapper2));

Then you have inheritance on C++ side instead of using prototypes. Not exactly the same functionality though.

ixM
8th February 2012, 09:28
Hi wysota!

Thank you for your quick and detailed answer. Now that you show it to me, it is clear that I, in fact, missed something with this whole prototype thing.

Although I understand the first part of your answer, I'm actually more interested in the second part of it which is still unclear to me.

My goal is in fact to provide a Wrapper to my objects so that 1) I don't have to worry about them inheriting from QObject (to make them invokable through QtScript), 2) I can keep abstract classes abstract but still be able to use them as types in QtScript (for example, I have an array of Drawable but they all actually are MeshDrawable,...) and 3) I can easily control what is exposed without having to put Q_INVOKABLE everywhere or making my functions slots.

I don't care about the C++ from an inheritance point of view. It seems to me that it would be easier to benefit from this mechanism but it's not that important. In contrary, on the QtScript side, it is very important that an inheritance relationship is achievable. And I also want each of the wrappers' scriptvalue I create to have the correct prototype.

The second part of your answer seems to do that but I'm not sure of some things. When I will create a new QVariant for a Wrapper_MeshDrawable, will it have all of its method exposed without me having to set a prototype? Is it possible to make the QtScript "type" have another name than Wrapper_xxx (for example, just xxx)?

Thank you again!

Maximilien

wysota
8th February 2012, 11:01
My goal is in fact to provide a Wrapper to my objects so that 1) I don't have to worry about them inheriting from QObject (to make them invokable through QtScript), 2) I can keep abstract classes abstract but still be able to use them as types in QtScript (for example, I have an array of Drawable but they all actually are MeshDrawable,...) and 3) I can easily control what is exposed without having to put Q_INVOKABLE everywhere or making my functions slots.
The big question is whether you want to create such objects from within scripts or just expose existing C++ objects (not classes) to the script.


The second part of your answer seems to do that but I'm not sure of some things. When I will create a new QVariant for a Wrapper_MeshDrawable, will it have all of its method exposed without me having to set a prototype?
Yes, since it is a QObject.


Is it possible to make the QtScript "type" have another name than Wrapper_xxx (for example, just xxx)?
Since here we only expose instances of objects then the "type" doesn't really matter as the instance is the only one of its "type" (as you can't create more instances of it) so you can treat it as an "Object". If you want to expose the type and not only the instance (by providing a constructor) then controlling the name of the "type" boils down to changing the constructor name.

ixM
8th February 2012, 12:56
Once again, thank you!

I want to be able to expose objects from C++ but also create new instances in the QtScript. The only thing I should do therefore is to create a constructor function right?

I think I'm getting my head around it!

wysota
8th February 2012, 16:00
Everything depends on how your objects should behave. Certainly providing a constructor function is a must if you want to create new C++ objects from within script environment.