Wiki

On the Fast Track to Application Scripting

by Kent Hansen

With the introduction of Qt Script as a standard part of Qt in version 4.3, Qt/C++ application developers have a seamlessly integrated solution for adding scripting to their applications. Qt Script is a simple, yet powerful way of providing script authors---who don't necessarily know C++---with an interface for working in the context of your Qt application. In this article, we give an overview of the main steps that are involved when embedding Qt Script into an application, and use a small example to showcase the key techniques.

The Embedding Python into Qt Applications article in Qt Quarterly issue 23 gives a good overview of the benefits of scripting, as well as application scripting and the creation of such APIs in general. Like PythonQt (the solution described in that article), the purpose of Qt Script is to provide a scripting environment that application authors can embed into their Qt applications; it is not geared towards "pure" script-based application development.

In embedded scripting, you typically make one or more "host objects" (comprising your application's scripting API) available to the scripting environment; you then provide one or more mechanisms for scripts to be executed in this environment. As we shall see, Qt Script's tight integration with Qt's meta-object system makes both the configuration process and the subsequent communication between C++ and scripts straightforward.

Getting Started

In order to use Qt Script in your application, you must first add the following line to your qmake project (.pro) file:

    QT += script

You may then include the main QtScript header in your C++ code to gain access to the full Qt Script API:

    #include <QtScript>

The QScriptEngine class provides your application with an embedded scripting environment. You can have as many script engines as you want in your application; each engine is in effect a lightweight, self-contained virtual machine. To execute scripts, you call the engine's evaluate() function, as in the following simple example:

    QScriptEngine engine;
    QScriptValue result = engine.evaluate("(1+2)*3");
    qDebug() << "Result as float:" << result.toNumber();

Thankfully, the Qt Script language is capable of more than just adding and multiplying constants. The scripting language is based on the ECMAScript 3 standard (ECMA-262), and thus sports the same core language features as JavaScript 1.x, a language that is widely used in Web-based development.

Scripting Qt Objects

With Qt Script, scripting QObjects is easy. The following C++ code shows how to make an instance of a standard QPushButton widget available to scripts:

    QPushButton button;
    QScriptValue scriptButton = engine.newQObject(&button);
    QScriptValue global = engine.globalObject();
    global.setProperty("button", scriptButton);

QScriptEngine::newQObject() returns a proxy script object for the given Qt C++ object.

In the above example, we chose to make the proxy object available to scripts as a global variable. Scripts can subsequently manipulate properties of the button and call its slots through the proxy, as in the following script snippet:

    button.checkable = true;
    button.show();

Additionally, scripts can connect to the button's signals:

    function clickHandler(checked) {
      print("button checked: " + checked);
    }
 
    button.clicked.connect(clickHandler);

To define our own custom scripting API, we design QObject-based classes, expressing the API in terms of Qt properties, signals and slots. To configure the Qt Script environment, we just need to wrap the scripting-related objects using newQObject() and make the resulting proxy objects available to scripts, like we did with the QPushButton above.

In the remainder of this article, we will study a small example application, CubbyHolistic, and see how we can make it scriptable. This will illustrate the main aspects of how Qt Script and Qt/C++ play together.

The CubbyHolistic Application

The original CubbyHolistic C++ application is a simple implementation of the producer-consumer pattern. There is a resource, a CubbyHole object, that is shared between two threads: a ProducerThread and a ConsumerThread. The CubbyHole class declaration looks like this:

    class CubbyHole : public QObject
    {
    public:
        CubbyHole(QObject *parent = 0);
 
        void depositValue(const QVariant &value);
        QVariant withdrawValue();
 
    private:
    // implementation details ...
    };

The CubbyHole class uses standard synchronization mechanisms in Qt to implement the depositValue() and withdrawValue() functions; i.e., blocking the producer and consumer when necessary.

The run() reimplementations of ProducerThread and ConsumerThread typically look like this:

    void ProducerThread::run()
    {
      for (int i = 0; i < 10; ++i)
        cubbyHole->depositValue(i * 4);
    }
 
    void ConsumerThread::run()
    {
        for (int i = 0; i < 10; ++i) {
            QVariant v = cubbyHole->withdrawValue();
            /* Do something with the value */
        }
    }

The application's main() function constructs a CubbyHole object and gets the producer and consumer going:

    int main(int argc, char **argv)
    {
        QCoreApplication app(argc, argv);
 
        CubbyHole hole;
        ProducerThread producer(&hole);
        ConsumerThread consumer(&hole);
 
        producer.start();
        consumer.start();
        producer.wait();
        consumer.wait();
        return 0;
    }

Scripting CubbyHolistic

As a fun little experiment, let's change CubbyHolistic so that the producer and consumer can be provided as scripts, rather than C++ classes; that is, C++ code will no longer have to be (re)compiled in order to support different producers and consumers that use the common CubbyHole interface.

The CubbyHole object itself will remain wholly implemented in C++, but we will expose a scripting API to it. To do this, we can modify the CubbyHole declaration [1] so that depositValue() and withdrawValue() become slots:

    class CubbyHole : public <a href="http://www.crossplatform.ru/documentation/qtdoc4.3/qobject.php">QObject</a>
    {
        Q_OBJECT
    ...
    public slots:
        void depositValue(const QVariant &value);
        QVariant withdrawValue();
    ...
    };

This will allow scripts to call these two functions. Next, we declare a QThread subclass that will be used to evaluate a script in a separate thread, in the context of a given CubbyHole object:

    class ScriptThread : public QThread
    {
    public:
        ScriptThread(const QString &fileName,
                     CubbyHole *hole);
    protected:
        void run();
 
    private:
        QString m_fileName;
        CubbyHole *m_hole;
    };

The producer and consumer will both be represented as instances of ScriptThread; it's merely a matter of passing the appropriate file name to the constructor. All the work happens in the run() reimplementation:

    void ScriptThread::run()
    {
      QScriptEngine engine;
 
      QScriptValue scriptHole = engine.newQObject(m_hole);
      QScriptValue global = engine.globalObject();
      global.setProperty("cubbyHole", scriptHole);
 
      QFile file(m_fileName);
      file.open(QIODevice::ReadOnly);
      QString contents = QTextStream(&file).readAll();
      file.close();
 
      engine.evaluate(contents);
 
      if (engine.hasUncaughtException()) {
        QStringList bt = engine.uncaughtExceptionBacktrace();
        qDebug() << bt.join("\n");
      }
    }

Each thread has its own script engine. The shared CubbyHole C++ object is exposed as a global variable in the engine, and the contents of the script file are read and passed to evaluate(). There's also a check to see if the evaluation caused a script exception (e.g., due to a syntax error in the script); in that case, we print a backtrace to aid in debugging the problem.

The final step C++-wise is to change the application's main() function to use the ScriptThread class to provide the producer and consumer (for simplicity, fixed file names are used).

    int main(int argc, char **argv)
    {
      QCoreApplication app(argc, argv);
      CubbyHole hole;
      ScriptThread producer("./producer.qs", &hole);
      ScriptThread consumer("./consumer.qs", &hole);
 
      producer.start();
      consumer.start();
      producer.wait();
      consumer.wait();
      return 0;
    }

Now we are ready to start writing scripts! This is the original ProducerThread behavior expressed as a script in producer.qs:

    for (var i = 0; i < 10; ++i)
      cubbyHole.depositValue(i * 4);

And here is the equivalent code in consumer.qs:

    for (var i = 0; i < 10; ++i) {
      var v = cubbyHole.withdrawValue();
      /* Do something with the value */
    }

Here's a producer script that will generate a random sequence of Fibonacci numbers:

    function fibonacci(n) {
      if (n <= 1)
        return n;
      return fibonacci(n-1) + fibonacci(n-2);
    }
 
    for (var i = 0; i < 10; ++i) {
      var arg = Math.round(Math.random() * 20);
      var value = fibonacci(arg);
      cubbyHole.depositValue(value);
    }

With a little more effort, the scripts can become "pluggable"; for example, the application could scan a folder of scripts and have the user pick or create the producer and consumer he or she wants to use.

Further Reading

Qt Script has features that haven't been discussed in this article, but which can be useful in more complex applications---such as the ability to script non-QObject types. That being said, the Qt Script C++ API itself is quite lightweight thanks to its tight integration with Qt as a whole. The Qt documentation and examples cover the use of Qt Script in detail, including interesting use cases like how to combine Qt Script and Qt Designer forms.


[1] Modifying the C++ class API directly has been done here for the sake of simplicity; in your application, you may want to have separate C++ and scripting APIs.

Обсудить...