How to make C++ classes available in QtScript

It's been a while since I posted here. What have I been doing? Mostly working, and in my spare time trying to get my head around QtScript. What follows is a short introduction into QtScript. I'm sure that there are points of inaccuracy in this post - this is only to be expected, since I'm just starting out.

What I need for my game, is a single C++ class that can defer some of it's processing to a script, thereby changing the behavior of the class. For example, I want a "GameObject" class, that takes a script filename as a constructor parameter, and defers certain parts of the processing to the script. In the case of the game object, I may want to defer the following functionality to a script:

  • Graphics loading (so different game objects look different).
  • AI processing.
  • Generic event handling.
However, not all scripts will do all of the above - some may only customize a very small amount.

The easiest way to do this is to have signals in the C++ class, that the script can choose to connect to (or not, as the case may be). As the C++ class is executed, it can emit these signals, and the corresponding slots in the script file can be called.

The Code

The C++ code is relatively simple. Consider the following header file:


#ifndef TESTSCRIPTOBJECT_H
#define TESTSCRIPTOBJECT_H
//
#include <qobject>
#include <qscriptvalue>
#include <qscriptable>
#include <qscriptengine>
#include <qmessagebox>
//

class QScriptEngine;


//
// Test script object class - used to demonstrate the fundamentals of QtScript.
//
class testScriptObject : public QObject
{
Q_OBJECT
public:
// ctor for this class - pass in the script engine to bind to.
testScriptObject( QScriptEngine *pEngine, QObject *parent =0);

// attach ourselves to a script file - this could be done inside the ctor. I
// have chosen to use a separate method instead.
void runScript(const QString &strProgram);

signals:
// test signal - we can emit this, and have some QtScript code run.
void signal1();

public slots:
// test slot - just displays a message box.
void slot1();
// second test slot - displays the string in a combo box.
void displayMsg(QString strMsg);

private:
// store a pointer to the script engine.
QScriptEngine *m_pEngine;

// store the "this" object, so we can manually call script functions if we need to.
QScriptValue m_thisObject;
};
This should all be fairly easy stuff. If it's not, you should probably look at the Qt tutorials before going any further.

Before we can use this class in any sensible manner, we need to do two things:
  1. Create a QScriptValue object that represents the "this" pointer. We do this so that we can call a script function, and pass it the C++ class object as "this" (we'll see this later).
  2. Open a script file and execute a "create" function, the contents of which will set up any signal / slot connections we require.
Let's dive into the C++ code, and take a look at the constructor for the above class:


testScriptObject::testScriptObject( QScriptEngine *pEngine, QObject *parent)
: QObject(parent),
m_pEngine(pEngine)
{
// create this object in the scripting land:
m_thisObject = m_pEngine->newQObject(this);

}
This is also pretty simple - the only line of any significance is the one where we create "m_thisObject".

Now on to step 2 - running the script file. Let's take a look at the runScript method:


void testScriptObject::runScript(const QString &strAppName)
{
QFile file(strAppName + ".js");

if (! file.exists())
{
QMessageBox::critical(0, "Error", "Could not find program file!");
return;
}

if (! file.open(QIODevice::ReadOnly | QIODevice::Text))
{
QMessageBox::critical(0, "Error", "Could not open program file!");
return;
}

QString strProgram = file.readAll();

// do static check so far of code:
if (! m_pEngine->canEvaluate(strProgram) )
{
QMessageBox::critical(0, "Error", "canEvaluate returned false!");
return;
}

// actually do the eval:
m_pEngine->evaluate(strProgram);

// uncaught exception?
if (m_pEngine->hasUncaughtException())
{
QScriptValue exception = m_pEngine->uncaughtException();
QMessageBox::critical(0, "Script error", QString("Script threw an uncaught exception: ") + exception.toString());
return;
}

QScriptValue createFunc = m_pEngine->evaluate("create");

if (m_pEngine->hasUncaughtException())
{
QScriptValue exception = m_pEngine->uncaughtException();
QMessageBox::critical(0, "Script error", QString("Script threw an uncaught exception while looking for create func: ") + exception.toString());
return;
}

if (!createFunc.isFunction())
{
QMessageBox::critical(0, "Script Error", "createFunc is not a function!");
}

createFunc.call(m_thisObject);

if (m_pEngine->hasUncaughtException())
{
QScriptValue exception = m_pEngine->uncaughtException();
QMessageBox::critical(0, "Script error", QString("Script threw an uncaught exception while looking for create func: ") + exception.toString());
return;
}
// now emit our test signal:
emit signal1();
}
Again, this is all pretty simple stuff. This method does the following:
  • Looks for, and opens the script file specified.
  • Does a static check of the code (makes sure that it's syntactically correct, but does not ensure that the script will run without error).
  • Evaluates the script file.
  • Retrieves the function named "create" from the script file.
  • Executes this function, passing our previously created "m_thisObject" as the "this" object for the script to use. This function then binds our signals to slots in the script file.
  • Finally, we emit our test signal.
Finally, let's look at the ECMAScript file I'm using:



// set up the game object - the 'this' object will be an actual QObject
// derived class passed in from the application.
function create()
{
// test 1 - call a slot in the class:
this.slot1();

this.signal1.connect(this, testSlot);
}

// create our own slot:
function testSlot()
{
this.displayMsg("Testing testing.. 123");
}

...and that's it! The "create" method will first call the "test2" slot in our C++ class, then connect our test signal to a scripted function. When we emit the signal in our C++ class, the "testSlot" function will be called.


This is just a starting point. The more I use the QtScript module, the more I marvel at the possibilities created with this tool. I shall post follow-up articles with more information and techniques as I see fit. In the meantime, go add scripting extension support to your faviourite project. With tools this easy to use, there's no excuse not to!