QQmlListProperty erratic crashes with ownership transferred to JavaScript

514 Views Asked by At

I'm experiencing some serious nasal demons with Qt5 (5.8, Arch Linux) and QML. Basically, the code is exposing a QQmlListProperty with its members created on the fly and transferring their ownership to JavaScript. When the itemChanged() signal is emitted thousands of times (in my actual code only a couple of times is enough) the program segfaults.

According to Valgrind + GDB, it looks like 'A' is freed by QObject::event and then read by QML, causing the segfault.

Weirdly, even changing the name of the variable 'game' to something else changes the number of times I have to click the button for the program to crash. Removing the repeater or the rectangle or using a different expression for the color of the rectangle affects whether the program crashes or not. Also, whenever the ownership is not transferred to JS, there is no problem at all. Sadly, that also causes memory leaks since nothing is freeing the 'A' objects.

Because of those details I believe there's undefined behavior somewhere in the code, but I have no idea where.

This is the shortest example I could create:

main.cpp:

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QObject>
#include <QQmlListProperty>

template <typename T>
T* make_js() {
    T* t = new T();
    QQmlEngine::setObjectOwnership(t, QQmlEngine::JavaScriptOwnership);
    return t;
}

class A: public QObject {
    Q_OBJECT
    Q_PROPERTY(int id READ id CONSTANT)
public:
    int id() const {return 1;}
};

class B: public QObject {
    Q_OBJECT
    Q_PROPERTY(A* item READ item NOTIFY itemChanged)
    Q_PROPERTY(QQmlListProperty<A> list READ list CONSTANT)
public:

    A *item() const { return make_js<A>(); }

    Q_INVOKABLE void change() { emit itemChanged(); }

    QQmlListProperty<A> list() {
        return QQmlListProperty<A>(
            this, nullptr,
            [](QQmlListProperty<A> *) {return 1;},
            [](QQmlListProperty<A> *, int ) {return make_js<A>();});
    }
signals:
    void itemChanged();
};

#include "main.moc"

int main(int argc, char *argv[]) {
    QGuiApplication a(argc, argv);

    B* game = new B();

    QQmlApplicationEngine engine;

    qmlRegisterType<B>("example", 1, 0, "B");
    qmlRegisterType<A>("example", 1, 0, "A");
    engine.rootContext()->setContextProperty("thing", game);

    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    a.exec();

    delete game;
    return 0;
}

main.qml:

import QtQuick 2.0
import QtQuick.Layouts 1.0
import QtQuick.Window 2.0
import QtQuick.Controls 2.0
import example 1.0

Window {
    visible: true
    width: 1280
    height: 800
    title: qsTr("Example")

    ColumnLayout {
        anchors.fill: parent

        Repeater {
            model: thing.list

            Rectangle {
                color: thing.item.id === 1 ? "red" : "blue"
            }
        }

        Item {
            Layout.fillWidth: true
            Layout.fillHeight: true

            Button {
                text: "Click me a bunch of times to crash!"
                anchors.fill: parent
                onClicked: {for(var i=0;i<1000;++i) thing.change();}
            }
        }
    }
}

What is the reason for this behavior? Do you happen to know of a workaround for this? Could it be that QQmlListProperty is somehow not compatible with JavaScript ownership?

EDIT: I found a workaround which is good enough for my use. Changing

Q_PROPERTY(QQmlListProperty<A> list READ list CONSTANT)

into

Q_PROPERTY(QQmlListProperty<A> list READ list NOTIFY listChanged)

and emitting listChanged() in change() along with itemChanged() fixes it. With additional testing, I noticed that Qt only calls the indexing lambda from list() once and later frees the returned 'A' -object. It doesn't call the lambda again and seems to try to reuse the already freed 'A'.

However, I'd still love to know what is causing this in the first place.

0

There are 0 best solutions below