PDA

View Full Version : Call Slot from QtScript with Qt::QueuedConnection



lukasfischer
16th July 2015, 19:22
I have to provide a "linear" scripting interface that runs infinitely next to the GUI, which would block my eventloop. So I am running a QScriptEngine in a seperate thread (beeing aware of the consequences). There is one thing that keeps me puzzled: Usually using multiple threads the communication is safe as long as you use signals and slots, passing objects by value and connecting with Qt::QueuedConnection, right?
So now I want to call a slot of an object which I made available within the scriptengine. The Documentation states:


Emitting Signals from Scripts

To emit a signal from script code, you simply invoke the signal function, passing the relevant arguments:


myQObject.somethingChanged("hello");
It is currently not possible to define a new signal in a script; i.e., all signals must be defined by C++ classes.


But in the Debugger I can see, that the calls are executed on the scriptengine thread instead of the object's owner thread. I found the workaround by calling
QMetaObject::invokeMethod(object,"slotname",Qt::QueuedConnection) in the slot, but i don't want to modify all objects i want to make accessible from within the scriptengine.
Is there a way to force the scriptengine to use QueuedConnection for calling slots?

I have also tried setProcessEventsInterval() for the engine without starting another thread, but couldn't get it to work. Further i don't want the Eventloop to be pumped by the script engine, feels creepy.
Thanks, Lukas

anda_skoa
17th July 2015, 10:13
Can you describe your setup a bit more?

That "myQObject", was it created by the script or created in C++ and passed into the script?
Is it owned by the thread running the script engine?
Where did you connect the signal?
Which thread owns the receiver object?

Cheers,
_

lukasfischer
17th July 2015, 11:36
the myObject line is from the Documentation.

in my case it is a QTextEdit (inherited by Console) created in the GUI thread and registered in the scriptEngine (which is in the script thread).

Mainwindow.cpp:


console = new Console(this);
script = new ScriptThread(this);
script->attachConsole(console);


console.h:


#ifndef CONSOLE_H
#define CONSOLE_H

#include <QWidget>
#include <QTextEdit>

Q_DECLARE_METATYPE(QTextCursor)
Q_DECLARE_METATYPE(QTextCharFormat)

class Console : public QTextEdit
{
Q_OBJECT

public:
explicit Console(QWidget *parent = 0);
signals:

public slots:
void write(QString string);
void write(QString string, QString color);
void clr();

};

#endif // CONSOLE_H

scriptthread.h:


#ifndef SCRIPTTHREAD_H
#define SCRIPTTHREAD_H

#include <QThread>
#include <QtScript/QScriptEngine>
#include <QPushButton>
#include "console.h"

class ScriptThread : public QThread
{
Q_OBJECT
public:
explicit ScriptThread(QObject *parent = 0);

void run() Q_DECL_OVERRIDE;

void addScriptableObject(QString name, QObject* obj);
void attachConsole(Console* console);

signals:
void set(QString name, QVariant value);
void scriptFinished();

public slots:

private:
QScriptEngine* scriptEngine;
QString code;
Console* m_console;
QScriptContext* ctx;
};

#endif // SCRIPTTHREAD_H


console.cpp:


Console::Console(QWidget *parent) :
QTextEdit(parent)
{
qRegisterMetaType<QTextCursor>("QTextCursor");
qRegisterMetaType<QTextCharFormat>("QTextCharFormat");
setReadOnly(true);
setFontFamily("monospace");
setStyleSheet("QTextEdit { background-color: 'black'; }");
setTextColor(QColor("white"));
}

void Console::write(QString string)
{
setTextColor(QColor("White"));
append(string);
//QMetaObject::invokeMethod(this,"setTextColor",Qt::QueuedConnection,Q_ARG(QColor,QColor("white")));
//QMetaObject::invokeMethod(this,"append",Qt::QueuedConnection,Q_ARG(QString, string));
}

void Console::write(QString string, QString color)
{
setTextColor(QColor(color));
append(string);
//QMetaObject::invokeMethod(this,"setTextColor",Qt::QueuedConnection,Q_ARG(QColor,QColor(color))) ;
//QMetaObject::invokeMethod(this,"append",Qt::QueuedConnection,Q_ARG(QString, string));
}

void Console::clr()
{
//textEdit->clear();
QMetaObject::invokeMethod(this,"clear",Qt::QueuedConnection);
}


script.cpp:


ScriptThread::ScriptThread(QObject *parent) :
QThread(parent)
{
code = "console.write(\"testing console color capabilities\");\n"
"button.text = \"This is a scriptable Button!\";\n"
"button.show();\n"
"for(i=0;i<100;i++){\n"
"time.sleep_ms(20);console.write(i+\"%\")}\n"
"console.write(\"OK\",\"red\");\n"
"time.sleep_ms(1000);\n"
"//console.clr();\n"
"button.hide();"
"console.write(\"exiting\");\n";
scriptEngine = new QScriptEngine();

ctx = scriptEngine->pushContext();
}

void ScriptThread::run()
{
if (m_console != NULL)
{
if (scriptEngine->canEvaluate(code))
{
m_console->write("Script seems to be ok, starting...\n");
scriptEngine->evaluate(code);
if (scriptEngine->hasUncaughtException())
{
m_console->write("Error at: " + scriptEngine->uncaughtException().toString());
}
} else {
m_console->write("Script contains errors!\n");
}
scriptEngine->popContext();
scriptEngine->collectGarbage();
}
}

void ScriptThread::addScriptableObject(QString name, QObject *obj)
{
ctx->activationObject().setProperty(name, scriptEngine->newQObject(obj));

}

void ScriptThread::attachConsole(Console *console)
{
m_console = console;
ctx->activationObject().setProperty("console",scriptEngine->newQObject(m_console));
}



In the console.cpp I set breakpoints inside the Console::write() slot. As far as I understood slots are directly called from the script as mentioned in the documentation, there is no "emit" command in javascript.
As a workaround I changed the thread on which the slot is called by manually invoking the slot with:

QMetaObject::invokeMethod(this,"setTextColor",Qt::QueuedConnection,Q_ARG(QColor,QColor("white")));
which works as expected. But i would assume that methods declared as slots are automatically invoked by the scriptengine instead of being called directly, especially since there is no emit function! Did i understand something wrong about the idea of the scriptengine?

Thanks, Lukas

anda_skoa
17th July 2015, 13:25
Emit is not a function in C++ either, it is a preprocessor macro that expands to nothing. I.e. it is only there for improved readability, to distinguish between method calls and signal emits.

So you are not emitting any signal at all, you are directly calling a slot.
The feature you've described in your first post, i.e. the automatic Qt::QueuedConnection on cross-thread signal/slot connections, is for exactly that: signal/slot connections.

Your setup is quite problematic actually:

Your script engine object belongs to the main thread but you are calling its evaluate() method (and others) from another thread.
Better have the script engine and all objects it works on be part of the thread that runs it.

You are also calling methods on a widget (m_console) from a non-main-thread.
Either call the methods via QMetaObject::invoke using Qt::QueuedConnection or emit a signal and let the cross-thread signal/slot connect handle that.

Cheers,
_

lukasfischer
17th July 2015, 13:35
Thanks, I think I am starting to understand the problem. I will create the engine in the script thread and try again. Can the objects registered in the engine be from another thread and is their registering threadsafe (at least while the engine is not evaluating anything)? Will their methods be properly invoked then or will the engine do direct calls when accessing member functions within the script?
Thanks for the explainations.

anda_skoa
17th July 2015, 14:04
Thanks, I think I am starting to understand the problem. I will create the engine in the script thread and try again. Can the objects registered in the engine be from another thread and is their registering threadsafe (at least while the engine is not evaluating anything)?

I personally wouldn't risk registering objects belonging to another thread but it might work if you do it beforehand.



Will their methods be properly invoked then or will the engine do direct calls when accessing member functions within the script?

I don't think that script method invocation would stop working, they should be called just like when you call them from C++.

Cheers,
_