QtScript

Note

Japanese Version is here: QtScript

Note

This page contains the contents that I researched in work time. This page is sponsered by DeNA Co.,Ltd.

Qt has QtScript.

  • QtScript is compatible with ECMA-262. In other words, it is a JavaScript without DOM.
  • Performance is not bad. (Current version using JavaScriptCore and try moving to V8)
  • It is a Qt Component. Easy to use.
  • It is easy to add objects and functions from C++.
  • It is easy to access and get object from C++.
  • Debugger is available. We can open QtScript(JS) debugger from Qt application.
  • Editor is available. We can open QtScript editor from Qt application (I have not tried yet).

Using QtScript from C++

Note

I am using 4.7.4 on Mac.

QtScript Engine is implemented as Qt class. You can use several features from C++.

Using QtScript

To build the application that using QtScript, you should add lines about QtScript to your project file.

QT +=  \
    script \
    scripttools

First line script is QtScript, and Second line scripttools is QtScript Debugger. Next, add #include statement to your source code, like main.cpp.

#include <QtScript/QScriptEngine>
#include <QtScript/QScriptValue>
#ifndef QT_NO_SCRIPTTOOLS
#include <QtScriptTools/QScriptEngineDebugger>
#endif

After that, you can instantiate QtScript Engine.

QScriptEngine engine;
QScriptValue ret = engine.evaluate("return Math.pow(2, 10)");
qDebug() << ret.toInteger();

Launching Debugger

You can attach the debugger only adding three lines:

QScriptEngineDebugger* debugger = new QScriptEngineDebugger();
debugger->attachTo(&engine);
debugger->action(QScriptEngineDebugger::InterruptAction)->trigger();

You can launch the debugger from script by using debugger; statement. To realize that, you should write first two lines of above code sample.

Reading JavaScript source file

You can read external JavaScript source files by using Qt’s file API.

QString fileName = "file path of your file.js";
QFile scriptFile(fileName);
scriptFile.open(QIODevice::ReadOnly);
QTextStream stream(&scriptFile);
QString contents = stream.readAll();
scriptFile.close();

engine.evaluate(contents, fileName);

If you want to deploy your JavaScript files, you should add these files to your project file(.pro).

folder_01.source = js
folder_01.target = .
DEPLOYMENTFOLDERS = folder_01

On MacOSX, these files are copied to Contents/Resources/js in .app file. I have never tried other environment, but from other sample code, on Windows, these files are copied to the same folder of the executable file. On *nix OS, if the application is installed to /usr/local/bin, other resource files will be installed to /usr/loca/share/*application name*/ folder.

QString adjustPath(const QString &path)
{
    #ifdef Q_OS_UNIX
    #ifdef Q_OS_MAC
    if (!QDir::isAbsolutePath(path))
        return QCoreApplication::applicationDirPath()
               + QLatin1String("/../Resources/") + path;
    #else
    const QString pathInShareDir = QCoreApplication::applicationDirPath()
          + QLatin1String("/../share/")
          + QFileInfo(QCoreApplication::applicationFilePath()).fileName()
          + QLatin1Char('/') + path;
    if (QFileInfo(pathInShareDir).exists())
        return pathInShareDir;
    #endif
    #endif
    return path;
}

This function convert your deployed resource files’ path to actual path on your environment. If you want to use main.js, use this function like this:

QString fileName = adjustPath("main.js");

Adding functions and objects to QtScript Engine

Let’s implement console.log. Following methods are much used for adding element into QtScript Engine.

  • QScriptEngine::newFunction(): Creating function object.
  • QScriptEngine::newObject(): Creating object.
  • QScriptEngine::globalObject(): Getting global name space (global object).
  • QScriptValue::property(): Getting property from object.
  • QScriptValue::setProperty(): Setting property.

At first, creating function that is called from JavaScript in the C++ source file.

static QScriptValue consoleLogForJS(QScriptContext* context, QScriptEngine* engine)
{
    QStringList list;
    for(int i=0; i<context->argumentCount(); ++i)
    {
        QScriptValue param(context->argument(i));
        list.append(param.toString());
    }
    qDebug() << list.join(" ");
    return engine->undefinedValue();
}

You can get JS parameter from context. This function joins input parameters and passes to qDebug.

Let’s register this function to QScriptEngine.

QScriptEngine engine;
QScriptValue globalObject = engine.globalObject();
QScriptValue console = engine.newObject();
globalObject.setProperty("console", console);
QScriptValue consoleLog = engine.newFunction(consoleLogForJS);
console.setProperty("log", consoleLog);

At first, this code creates console empty object under global name space. Then append above function as log under console object.

I don’t try, but you can add object it inherits QObject easily.

Supporting require() of CommonJS

Writing large program on one file is difficult and complex and crazy. Let’s implement require() function, that is defined in common specification of server side JavaScript. It makes enable to support source file dividing.

Our require() function’s spec is following:

  • Adding require function under global. require() has paths array. It holds include lists.
  • $MODULES global variable holds all read modules.
  • Before reading child script, add exports object and then $MODULES stores it.

At first, let’s create the base C++ function of require(). This function searchs target files from paths array.

The return type of the property() method is QScriptValue. Note this is not a link to internal object but copy. You can’t change value in QtScript Engine before calling setProperty(). So appending new element to converted paths is nonsense. Take care!

static QScriptValue requireForJS(QScriptContext* context, QScriptEngine* engine)
{
    QString requiredPath;
    bool found = false;
    QString param = context->argument(0).toString();
    param.push_back(".js");
    QStringList paths = engine->globalObject().property("require").property("paths").toVariant().toStringList();

    QFileInfo requiredFileInfo(param);
    QScriptContextInfo contextInfo(context->parentContext());
    QString parentFileName(contextInfo.fileName());
    paths.push_front(QFileInfo(parentFileName).dir().path());

    foreach(QString includePath, paths)
    {
        qDebug() << includePath;
        QFileInfo includePathInfo(includePath);
        if (includePathInfo.exists())
        {
            QDir includePathDir(includePath);
            includePathDir.cd(requiredFileInfo.dir().path());
            requiredPath = QDir::cleanPath(includePathDir.absoluteFilePath(requiredFileInfo.fileName()));

            if(QFileInfo(requiredPath).exists())
            {
                qDebug() << requiredPath;
                found = true;
                break;
            }
        }
    }
    if (!found)
    {
        qDebug() << "require() file not found: " << param;
        return engine->undefinedValue();
    }
    QScriptValue modules = engine->globalObject().property("$MODULES");
    QScriptValue existedModule = modules.property(requiredPath);
    if (existedModule.isValid())
    {
        return existedModule;
    }
    else
        QFile requireFile(requiredPath);
        requireFile.open(QIODevice::ReadOnly);

        QScriptContext* newContext = engine->pushContext();
        QScriptValue exportsObject = engine->newObject();
        newContext->activationObject().setProperty("exports", exportsObject);
        engine->evaluate(requireFile.readAll(), requiredPath);
        exportsObject = newContext->activationObject().property("exports");
        engine->popContext();
        requireFile.close();
        modules.setProperty(requiredPath, exportsObject);
        return exportsObject;
    }
}

Register this function to global name space. If you want to add new place to search path, you can add in this code.

この処理はJavaScriptの配列の操作をする参考にもなると思います。

QScriptEngine engine;
QScriptValue modules = engine.newObject();
globalObject.setProperty("$MODULES", modules);

ScriptValue require = engine.newFunction(requireForJS);
gobalObject.setProperty("require", require);
ScriptValue paths = engine->newArray();
QScriptValue push = paths.property("push");
push.call(paths, QScriptValueList() << QScriptValue("/search/path/you/want/to/add"));
require.setProperty("paths", paths);

Building Qt binding for QtScript Engine

Note

I tried 4.7.4 on Mac.

Default QtScript object is just ECMA Script Engine. It doesn’t have any extra objects except embedded object (Array, Math, RegExp). You can’t show Qt UI form by using default QtScript engine. If you want to use Qt objects in QtScript, you have to register from C++.

You can register almost all (not all) Qt object by using QtScript Binding Generator. You can find many pages but the latest one is here. Other’s are old.

My Qt is installed at ~/QtSDK/. Let’s create JS binding for this Qt library. I refered this web site to build binding. You have to set enveironment variables, this is not described at README. Above page is described for Windows environment, but I can build on Mac too.

$ export QTDIR=~/QtSDK/Desktop/Qt/474/gcc/
$ export PATH=$PATH:$QTDIR/bin

Download QtScript Binding Generator. I downloaded from this page, but you can use git too. You get qt-labs-qtscriptgenerator folder.

$ cd qt-labs-qtscriptgenerator/generator
$ qmake
$ make
(wait...)
$ ./generator --include-paths=~/QtSDK/Desktop/Qt/474/gcc/include
Please wait while source files are being generated...
(wait...)
Classes in typesystem: 639
Generated:
- classes...: 609 (607)
- header....: 410 (408)
- impl......: 410 (408)
- modules...: 22 (21)
- pri.......: 11 (11)

Done, 6 warnings (1220 known issues)

If you forget setting environment vaiables, The classes become 7. If you can build successfully, You see the more than 600 here.

$ cd ../qtbindings/
$ qmake
$ make
(しばらく待つ)

It creates binding libraries. At the same time, it creates test JS interpreter that can use Qt classes. You can test the generated bindings:

$ qs_eval/qs_eval ../examples/AnalogClock.js
../_images/qtanalogclock.png

If you can see this dialog, congratulations!

Register Qt binding to QtScript Engine

At first, put QtScript binding on plugins/script folder. And register these files to project file to deploy.

folder_01.source = js
folder_01.target = .
folder_02.source = plugins
folder_02.target = .
DEPLOYMENTFOLDERS = folder_01 folder_02

To load QtScript binding, you call setLibraryPaths of Application object and importExtensions() method of QScriptEngine object. This is little long code, but what to do is simple.

QScriptEngine engine;

QDir pluginDir(QApplication::applicationDirPath());
#ifdef Q_OS_MAC
pluginDir.cdUp();
pluginDir.cd("Resources");
#endif
if (!pluginDir.cd("plugins")) {
    fprintf(stderr, "plugins folder does not exist -- did you build the bindings?\n");
    return(-1);
}
QStringList paths = app.libraryPaths();
paths << pluginDir.absolutePath();
app.setLibraryPaths(paths);

QStringList extensions;
extensions << "qt.core"
           << "qt.gui"
           << "qt.xml"
           << "qt.svg"
           << "qt.network"
           << "qt.sql"
           << "qt.opengl"
           << "qt.webkit"
           << "qt.xmlpatterns"
           << "qt.uitools";
foreach (const QString &ext, extensions) {
    QScriptValue ret = engine.importExtension(ext);
    if (ret.isError())
    {
        qDebug() << "Error occured to load extionsion: " << ret.toVariant() << ext;
    }
}

QtScriptでメニューを登録する

QtScriptからQWebViewのHTMLを表示させる

QWebViewのJavaScriptと連携する

Note

Reference: Lively for Qt: http://lively.cs.tut.fi/qt/installation.html

QtScript Information

History

  • 2011/11/08
    • First version (Japanese only).
  • 2011/12/20
    • English version.
    • Add loading JS binding

Navi

Table Of Contents

Latest Photos

www.flickr.com
This is a Flickr badge showing public photos and videos from shibukawa.yoshiki. Make your own badge here.

Blog entries

RSS表示パーツ
無料 無料 無料

Previous topic

Tech Memo

Next topic

QtScript

This Page