QtScripting madness

Published March 15, 2017
Advertisement

tl;dr: health much improved, wrapped [font='courier new']QtScript[/font] in a command line application with a DLL plugin system.

When I was messing around getting normal mapping working recently, I decided to use the alpha channel of the normal map as a specular value to allow for per-pixel specularity in my game. I decided to write a little Qt app that would allow me to load the normal map, then load a red-black image and import the red values into the alpha channel of the base image and save it.

I started this as a GUI application but it felt a bit heavy so I figured out how to set up a Qt console application and started writing a command-line app to do the task. It occurred to me that I could generalise this a bit and make the app extendable to support various image manipulations I might require in the future.

Then I started thinking, why don't I write a command line wrapper around the depreciated but still working [font='courier new']QtScript[/font]? I could expose a [font='courier new']QImage[/font] to the script easily enough, and implement [font='courier new']getPixel()[/font] and [font='courier new']setPixel()[/font] methods and so on, and loading and saving methods.

So once this was working, it then occurred to me - why make this specific to image processing? Why not have a generic wrapper around [font='courier new']QtScript[/font] that I can invoke on any script file, and gradually create a library of classes I expose to the script? Why not indeed.

The final bit of insanity was the decision to write a compleletly agnostic wrapper around [font='courier new']QtScript[/font] that loads DLLs that add classes to the script. And that is what we now have.

[font='courier new']Qsc.exe[/font] is the product of the [font='courier new']QscDriver[/font] project. It is a pretty trivial program:

int main(int argc, char *argv[]){ QCoreApplication app(argc, argv); if(app.arguments().count() < 2) { std::cout << "QSC Copyright(C)2017 Aardvajk Software Ltd\n"; std::cout << "USAGE: QSC script [arguments...]\n"; return 0; } QScriptEngine *engine = new QScriptEngine(&app); QStringList args; for(int i = 2; i < app.arguments().count(); ++i) args.push_back(app.arguments()); engine->globalObject().setProperty("args", qScriptValueFromSequence(engine, args)); QString bin = QCoreApplication::applicationDirPath(); foreach(const QString &plugin, QDir(bin).entryList(QStringList() << "*.dll" << "*.qsc", QDir::Files)) { if(QFileInfo(plugin).suffix() == "dll" && plugin != "QscApi.dll") { if(!loadDllPlugin(bin, plugin, engine)) { continue; } } else if(QFileInfo(plugin).suffix() == "qsc") { if(!loadQscPlugin(bin, plugin, engine)) { continue; } } } QFile file(app.arguments()[1]); if(!file.open(QIODevice::ReadOnly | QIODevice::Text)) { std::cerr << "QSC: Unable to open - " << app.arguments()[1].toStdString() << "\n"; return -1; } QScriptValue result = engine->evaluate(QString::fromUtf8(file.readAll())); if(result.isError()) { std::cerr << result.toString().toStdString() << "\n"; return -1; }}Bascially it just initailises a [font='courier new']QScriptEngine[/font], exposes an array of strings called [font='courier new']args[/font] to the script containing any command line parameters passed to [font='courier new']Qsc.exe[/font] (after the script file itself).

Then it gets the directory that [font='courier new']Qsc.exe[/font] is in and searches that directory, loading any DLLs it finds and calling a [font='courier new']QscInit(QScriptEngine*)[/font] method on them.

bool loadDllPlugin(const QString &bin, const QString &plugin, QScriptEngine *engine){ QString path = bin + QDir::separator() + plugin; HMODULE module = LoadLibraryA(path.toStdString().c_str()); if(module == NULL) { std::cerr << "Warning: unable to load plugin - " << plugin.toStdString() << "\n"; return false; } typedef void(*InitFunc)(QScriptEngine*); InitFunc func = reinterpret_cast(GetProcAddress(module, "QscInit")); if(func == NULL) { std::cerr << "Warning: unable to locate QscInit in plugin - " << plugin.toStdString() << "\n"; return false; } func(engine); return true;}Each DLL then just implements this call to add whatever classes it wants to the engine's [font='courier new']globalObject()[/font].

extern "C" __declspec(dllexport) void QscInit(QScriptEngine *engine){ engine->globalObject().setProperty("Color", engine->newFunction(colorConstructor)); engine->globalObject().setProperty("Image", engine->newFunction(imageConstructor));}It also searches for any files in the exe directory with a .QSC extension, which it treats as text files containing script that are just then evaluated by the [font='courier new']QScriptEngine[/font] prior to executing the actual script. This allows us to have a library of script functions and so on automatically loaded as well.

bool loadQscPlugin(const QString &bin, const QString &plugin, QScriptEngine *engine){ QFile file(bin + QDir::separator() + plugin); if(!file.open(QIODevice::ReadOnly | QIODevice::Text)) { std::cerr << "Warning: unable to load plugin - " << plugin.toStdString() << "\n"; return false; } QScriptValue result = engine->evaluate(QString::fromUtf8(file.readAll())); if(result.isError()) { std::cerr << "Warning: error evaluating plugin - " << plugin.toStdString() << "\n"; std::cerr << result.toString().toStdString() << "\n"; return false; } return true;}To aid the creation of the DLLs, there is a [font='courier new']QscApi[/font] project. This yields [font='courier new']QscApi.h[/font], [font='courier new']libQscApi.a[/font] and [font='courier new']QscApi.dll[/font] which is statically linked to each DLL and provides some helper methods.

#include namespace Qsc{template T objectData(const QScriptValue &object){ return qvariant_cast(object.data().toVariant());}template T objectData(QScriptContext *ctx){ return objectData(ctx->thisObject());}template T scriptCast(const QScriptValue &value){ return T(); }template<> int scriptCast(const QScriptValue &value){ return value.toInteger(); }template<> float scriptCast(const QScriptValue &value){ return value.toNumber(); }template<> QString scriptCast(const QScriptValue &value){ return value.toString(); }template QScriptValue getset(QScriptContext *ctx, QScriptEngine *engine, Get get, Set set){ T t = qvariant_cast(ctx->thisObject().data().toVariant()); if(ctx->argumentCount() == 1) { (t.*set)(scriptCast(ctx->argument(0))); ctx->thisObject().setData(engine->newVariant(t)); } return QScriptValue((t.*get)());}int checkArguments(QScriptContext *ctx, const QString &method, const QStringList &codes, QList &args);}Mental though some of this may look, it makes implementing the script classes a lot more concise, especially when combined with our wonderful new lambdas.

The [font='courier new']getet[/font] method is slightly interesting. It takes as its template arguments the type of the object represented by the script value, the type of the property of the object we are getting/setting, and types that end up being pointer-to-member-function pointers that are then invoked on the object.
The [font='courier new']scriptCast[/font] template and its specialisations then allow us to generically convert the argument from the [font='courier new']QScriptContext[/font] (a [font='courier new']QScriptValue)[/font] to the correct property type based on the template.
For example, here we set a property on a [font='courier new']Color[/font] object that contains a [font='courier new']QColor[/font] in its [font='courier new']QVariant[/font] data, using [font='courier new']int QColor::red()[/font] and [font='courier new']void QColor::setRed(int value)[/font].

void f(){ object.setProperty("red", engine->newFunction([](QScriptContext *c, QScriptEngine *e){ return Qsc::getset(c, e, &QColor::red, &QColor::setRed); } ), QScriptValue::PropertyGetter | QScriptValue::PropertySetter);}The ImageClass DLL implements both an Image and a Color class.

QScriptValue makeColor(QScriptContext *ctx, QScriptEngine *engine, QColor color){ QScriptValue object = engine->newObject(); object.setData(engine->newVariant(color)); object.setProperty("red", engine->newFunction([](QScriptContext *c, QScriptEngine *e){ return Qsc::getset(c, e, &QColor::red, &QColor::setRed); } ), QScriptValue::PropertyGetter | QScriptValue::PropertySetter); object.setProperty("green", engine->newFunction([](QScriptContext *c, QScriptEngine *e){ return Qsc::getset(c, e, &QColor::green, &QColor::setGreen); } ), QScriptValue::PropertyGetter | QScriptValue::PropertySetter); object.setProperty("blue", engine->newFunction([](QScriptContext *c, QScriptEngine *e){ return Qsc::getset(c, e, &QColor::blue, &QColor::setBlue); } ), QScriptValue::PropertyGetter | QScriptValue::PropertySetter); object.setProperty("alpha", engine->newFunction([](QScriptContext *c, QScriptEngine *e){ return Qsc::getset(c, e, &QColor::alpha, &QColor::setAlpha); } ), QScriptValue::PropertyGetter | QScriptValue::PropertySetter); return object;}QScriptValue colorConstructor(QScriptContext *ctx, QScriptEngine *engine){ QList ps; switch(Qsc::checkArguments(ctx, "Color", { "", "nnn", "nnnn" }, ps)) { case 0: return makeColor(ctx, engine, Qt::black); case 1: return makeColor(ctx, engine, QColor(ps[0].toInteger(), ps[1].toInteger(), ps[2].toInteger())); case 2: return makeColor(ctx, engine, QColor(ps[0].toInteger(), ps[1].toInteger(), ps[2].toInteger(), ps[3].toInteger())); } return QScriptValue();}QScriptValue imageConstructor(QScriptContext *ctx, QScriptEngine *engine){ QScriptValue object = engine->newObject(); QImage image; QList ps; switch(Qsc::checkArguments(ctx, "Image", { "nn", "s" }, ps)) { case 0: { image = QImage(ps[0].toInteger(), ps[1].toInteger(), QImage::Format_ARGB32); image.fill(Qt::black); } break; case 1: { if(!image.load(ps[0].toString())) { ctx->throwError(QString("Unable to open Image - %1").arg(ps[0].toString())); return QScriptValue(); } } break; default: return QScriptValue(); } object.setData(engine->newVariant(image)); object.setProperty("width", engine->newFunction([](QScriptContext *ctx, QScriptEngine*){ return QScriptValue(Qsc::objectData(ctx).width()); }), QScriptValue::PropertyGetter); object.setProperty("height", engine->newFunction([](QScriptContext *ctx, QScriptEngine*){ return QScriptValue(Qsc::objectData(ctx).height()); }), QScriptValue::PropertyGetter); object.setProperty("pixel", engine->newFunction(imagePixel)); object.setProperty("setPixel", engine->newFunction(imageSetPixel)); object.setProperty("save", engine->newFunction(imageSave)); return object;}[font='courier new']Qsc::checkarguments[/font] returns the index of the matching character code string (n = number, s = string, o = object etc) or -1 if none match, in which case it calls [font='courier new']QScriptContext::throwError()[/font] with an appropriate message.

We can take advanatage of Qt's [font='courier new']QVariant[/font] system to store the [font='courier new']QImage[/font] in the [font='courier new']QScriptValue[/font] here. [font='courier new']QImage[/font], like most of Qt's heavy classes, is copy-on-write so the amount of copying going on isn't as bad as it looks.
So finally, loading a red-black image into the alpha channel of a base image can be implemented as a [font='courier new']QtScript[/font] function:


var source = Image(args[0]);var alpha = Image(args[1]);for(var y = 0; y < source.height; ++y){ for(var x = 0; x < source.width; ++x) { var s = source.pixel(x, y); var c = alpha.pixel(x, y); s.alpha = c.red; source.setPixel(x, y, s); }}source.save(args[2]);Or something similar. Or this could be defined as a script function in a .QSC file in the exe directory and we can then just write a script that calls the function.

I've also added a [font='courier new']TextFile[/font] class (via another DLL of course) that loads a text file into a script array, and provides a [font='courier new']save()[/font] method as well. This is quite simple so will list this one in full.

#include "C:/Projects/Qsc/QscApi/QscApi/QscApi.h"#include QScriptValue textFileSave(QScriptContext *ctx, QScriptEngine *engine){ QScriptValue object = ctx->thisObject(); QList ps; switch(Qsc::checkArguments(ctx, "save", { "s" }, ps)) { case 0: break; default: return QScriptValue(); } QFile file(ps[0].toString()); if(!file.open(QIODevice::WriteOnly | QIODevice::Text)) { ctx->throwError(QString("Unable to create - %1").arg(ps[0].toString())); return QScriptValue(); } int length = object.property("length").toInteger(); for(int i = 0; i < length; ++i) { file.write(object.property(i).toString().toUtf8()); if(i < length - 1) { file.write("\n"); } } return QScriptValue();}QScriptValue textFileConstructor(QScriptContext *ctx, QScriptEngine *engine){ QScriptValue object = engine->newArray(); QList ps; switch(Qsc::checkArguments(ctx, "TextFile", { "", "s" }, ps)) { case 0: break; case 1: { QFile file(ps[0].toString()); if(!file.open(QIODevice::ReadOnly | QIODevice::Text)) { ctx->throwError(QString("Unable to open - %1").arg(ps[0].toString())); return QScriptValue(); } QString content = QString::fromUtf8(file.readAll()); int line = 0; foreach(const QString &text, content.split('\n', QString::KeepEmptyParts)) { object.setProperty(line++, text); } } } object.setProperty("save", engine->newFunction(textFileSave)); return object;}extern "C" __declspec(dllexport) void QscInit(QScriptEngine *engine){ engine->globalObject().setProperty("TextFile", engine->newFunction(textFileConstructor));}So I can now use [font='courier new']Qsc[/font] to manipulate text files in pretty much any way I want - something that, particularly for work, I often found myself setting up and writing temporary C++ projects which is always a pain and total overkill.

Few more bits to do - need to be able to invoke a function from the command line to avoid having to write a script if I just want to call one of my library methods on some arguments for example, and I should probably set up a system to provide more control over which DLLs and QSCs are loaded on startup.

So there you go. A scripting language I already know and am comfortable with, thoroughly extensible and should save me a great deal of time in the future.
We'll get back to the game next update. Thanks for stopping by.

Previous Entry Various
2 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement