0
votes

I'm working on this open source project, which basically is a live QML interpreter. Since a user's main activity would be to write QML code, I'd like to embed all logs and warnings and such in the application to provide some basic IDE-like functionality. First, I installed a custom message handler via qInstallMessageHandler which receives all runtime messages from QML:

auto Instance::HandleMessage(QtMsgType type, const QMessageLogContext& context, const QString& msg) -> void {
    std::cout << "handle message: " << msg.toStdString() << std::endl;
    Logger::GetInstance()->addEntry(msg); 
}

The Logger's addEntry method simply appends the message to a QStringList property and emits the corresponding changed()-signal. On the QML side, I feed that property to a ListView:

ListView {
    anchors.fill: parent;
    model: App.logger.entries;
    delegate: Text {
        text: modelData;
    }
}

This works. But the problem appears in combination with dynamic QML object instantiation. The 'magic' part of my application is a call to Qt.createQmlObject which expects some QML source as string and returns the newly created object:

function compile() {
    try {
        qmlObject = Qt.createQmlObject(<qml_source>, container, "root");
    } catch (exc) {
        // handle *compile-time* errors
    }
}

If 'compilation' fails, control immediately shifts to the catch clause and no new QML object will be created. But if I try to create an object from the following source, the application crashes:

function compile() {
    try {
        qmlObject = Qt.createQmlObject("import QtQuick 2.6\nItem { anchors.fill: parent; Rectangle { width: parent.width; height: a.height; } }", container, "root");
    } catch (exc) {
        // handle *compile-time* errors
    }
}

The above source is valid, but generates a runtime error. For some reason, the HandleMessage function gets called twice in this specific scenario:

handle message: [...]App/root:2: ReferenceError: a is not defined
handle message: [...]App/root:2: ReferenceError: a is not defined

And creates a binding loop:

handle message: [...]/App/containerview.qml:76:9: QML Repeater: Binding loop detected for property "model"

Which is also processed by the custom message handler... You see where this is going: the application gets stuck in a loop and finally crashes.

My observations so far:

  1. Not forwarding incoming messages to the QStringList property (just cout'ing them) results in only one single invocation of the custom message handler.

  2. Creating the same error after construction time causes no trouble:

    qmlObject = Qt.createQmlObject("import QtQuick 2.6\nimport QtQuick.Controls 1.4\nItem { anchors.fill: parent; Rectangle { id: rect; width: parent.width; } Button { anchors.centerIn: parent; text: \"do something stupid\"; onClicked: { rect.height = a.height; } } }", container, "root");
    
  3. Not using a list-based property (for example storing all messages in a single QString object) also solves the problem but has some obvious disadvantages.

And finally the question: Can someone explain why this happens and how to solve this problem?

1
I wonder if it might not be better to have two QML engines, one that runs the IDE and one that runs the content being edited? - Kevin Krammer
@KevinKrammer That's actually a good point! I'll give it a try, thanks! - qCring

1 Answers

1
votes

You let the message handler re-enter. You should not:

class Instance {
  QThreadStorage<bool> handlerEntered;
  ...
};

QThreadStorage<bool> Instance::handlerEntered;

void Instance::HandleMessage(
    QtMsgType type, const QMessageLogContext& context, const QString& msg)
{
    if (handlerEntered.localData()) return;
    QScopedValueRollback<bool> roll(handlerEntered.localData(), true);
    std::cout << "handle message: " << msg.toStdString() << std::endl;
    Logger::GetInstance()->addEntry(msg); 
}

You must also ensure that the Logger::addEntry method doesn't re-enter the event loop nor emit any messages. Most likely, you need to queue the call to the changed signal there:

void Logger::addEntry(const QString & msg) {
    ...
    QMetaObject::invokeMethod(this, "listChanged(QStringList)",
       Qt::QueuedConnection, Q_ARG(QStringList, data));
}

Otherwise, any directly-connected slots, like the QML engine, might do a lot of stuff while the signal - and addEntry and HandleMessage are still on the call stack.