QML Engine Deletes C++ Objects Still In Use

Correction: In the original post, I stated that ownership is transferred from C++ to QML by READ functions of Q_PROPERTYs. This is wrong. Ownership is only transferred by Q_INVOKABLE functions and slots, which return a QObject pointer. I corrected my post and my code example. Now, the simple code example crashes as desired. Many thanks to Richard and Simon for pointing out my mistake.

I recently spent three days on a customer project to figure out why my QML application crashed with a segmentation fault. The crash happened after a long sequence of interactions, which was hard to reproduce. I finally managed to reproduce the crash reliably after going through the same six-step interaction four times.

After many hours of debugging and scrutinising my code, I had an epiphany about the stack trace leading to the crash. The stack trace originated from deep inside the QML engine and ended in calling the destructor of a C++ object, which was still in use on the C++ side. The trace never touched any of the application’s C++ code in between.

So far, I had only asked myself the question: Where in my C++ code do I corrupt the memory? After my little epiphany, I changed my question: Why does the QML engine delete a C++ object still in use? What do I overlook in the interaction between QML and C++?

Reproducing the Crash

The example application, which you can download from here, contains the flaw and crashes after three clicks. Things are more difficult in real life.

On Mac OS X, I can reproduce the crash by repeating the following steps a couple of times.

  • Press the Open button. The application shows one of three customer names picked randomly.
  • Press the Close button. The application shows a blank screen.

The application usually crashes when I click the Open button for the second time. It may take more iterations for you. On Linux, I can’t reproduce the crash easily. This situation shows that running an application on different platforms is always a good idea.

Understanding the Code

After startup, the application shows the yellowish screen at the left. When users press the Open button, the onClicked handler of the Button in main.qml dynamically creates a CustomerInfo item and initialises the property customer of this item with the value returned by the function g_customerMgr.randomCustomer(). When users press the Close button in the screen on the right, the onClicked handler destroys the CustomerInfo item again. The QML code of the handler looks as follows.

    onClicked: {
        if (!view.isCustomerInfoShown) {
            loader.setSource("CustomerInfo.qml",
                {"customer": g_customerMgr.randomCustomer() })
        }
        else {
            loader.setSource("", {})
        }
        view.isCustomerInfoShown = !view.isCustomerInfoShown
    }

CustomerInfo.qml accesses the first and last name of the customer through the properties customer.firstName and customer.lastName and shows them on the screen.

Rectangle {
    property Customer customer
    Text {
        text: customer.firstName + " " + customer.lastName
    }
}

Customer is a simple C++ class derived from QObject and is registered with the QML engine in main().

    qmlRegisterType("Customer.Models", 1, 0, "Customer");

The setSource() call in the onClicked handler

    loader.setSource("CustomerInfo.qml",
        {"customer": g_customerMgr.randomCustomer() })

assigns the return value of the Q_INVOKABLE function randomCustomer() to the property customer of the CustomerInfo item. g_customerMgr is a C++ object of type CustomerManager, which is added to the root context of the QML engine in main() and is globally available in QML.

    engine.rootContext()->setContextProperty("g_customerMgr", 
                                             &customerMgr);

The function randomCustomer() selects one of three customers randomly and returns the selected customer.

    Q_INVOKABLE Customer *randomCustomer() const
    {
        return m_customers[qrand() % 3];
    }

Finding the Reason for the Crash

The stack trace of the crash, a segmentation fault, doesn’t tell us more than what we see. The crash happens when we open the second screen with the customer name. The crash is a consequence of the statement

    loader.setSource("CustomerInfo.qml", {"customer": g_customerMgr.randomCustomer() })

As the C++ class CustomerManager creates some Customer objects on the heap and passes them to QML, we can reasonably suspect that the Customer objects may cause the crash. The line

    text: customer.firstName + " " + customer.lastName

in CustomerInfo.qml is the only place where QML accesses a Customer object. Even if we change the above line to text: "Juhu", the application still crashes.

Next, we don’t pass a Customer object in the call to setSource() any more.

    loader.setSource("CustomerInfo.qml")

The application doesn’t crash any more. That’s a big step forward! We now know that the second argument

    {"customer": g_customerMgr.randomCustomer() }

in the original call to setSource() causes the problem. We undo the last two changes to the source code and go back to the original version.

We may suspect that randomCustomer() returns a dangling pointer, which was deleted earlier. So, we put a debug message in the destructor of Customer or set a breakpoint there.

The stack trace is illuminating. The destructor is called out of the blue. The destructor is not called by our own code. This is when I had my epiphany. If my code doesn’t call the constructor, it must be the QML engine – probably while perfoming garbage collection!

So far my guiding question had been: Where does my code corrupt the memory? Now the guiding question became: Why does the QML engine think that it can destroy the Customer object? Or rephrased: Where does the QML engine take over the ownership from C++?

The only possible place was the second argument of the call to setSource():

    loader.setSource("CustomerInfo.qml", {"customer": g_customerMgr.randomCustomer() })

The Customer pointer returned by the C++ function randomCustomer() is assigned to the QML property customer. After some searching in the Qt documentation, I found the following paragraph about transferring ownership from C++ to QML (emphasis mine).

When data is transferred from C++ to QML, the ownership of the data always remains with C++. The exception to this rule is when a QObject is returned from an explicit C++ method call: in this case, the QML engine assumes ownership of the object, unless the ownership of the object has explicitly been set to remain with C++ by invoking QQmlEngine::setObjectOwnership() with QQmlEngine::CppOwnership specified.

Additionally, the QML engine respects the normal QObject parent ownership semantics of Qt C++ objects, and will never delete a QObject instance which has a parent.

Of course, the function randomCustomer() is the exception of the rule. The QML engine assumes ownership of the returned Customer object. Neither of the two exceptions of the exception applies, because the ownership of the returned object is not set to QQmlEngine::CppOwnership explicitly and because the returned object does not have a parent – as a look at the constructor of CustomerManager reveals.

    CustomerManager(QObject *parent = nullptr)
        : QObject(parent)
    {
        m_customers.append(new Customer("Joe", "Smith"));
        m_customers.append(new Customer("Jack", "Miller"));
        m_customers.append(new Customer("Harold", "Beck"));
    }

When the user closes the customer info screen, the QML engine destroys the CustomerInfo item. Hence, the QML engine regards the Customer object stored in the customer property of CustomerInfo as unused. When the QML engine performs its next garbage collection, it will destroy this unused Customer object. As the CustomerManager object and the Customer object live longer than the CustomerInfo item, newly created CustomerInfo items will eventually access an already destroyed Customer object. The application crashes with a segmentation fault.

Fortunately, the Qt documentation tells us how to fix the problem. We set the parent of the newly created Customer to CustomerManager.

    CustomerManager(QObject *parent = nullptr)
        : QObject(parent)
    {
        m_customers.append(new Customer("Joe", "Smith", this));
        m_customers.append(new Customer("Jack", "Miller", this));
        m_customers.append(new Customer("Harold", "Beck", this));
    }

Additionally, we remove the line delete m_currentCustomer; from the destructor of CustomerManager – to avoid a double destruction.

The crucial point with this ownership transfer from C++ to QML is that we may not notice it for a long time. We don’t know when the QML engine triggers the garbage collection and destroys objects that are not used any more in QML but are still used in C++. No matter how much we test, we may not see the crash. We can increase the probability of the crash by reducing the available RAM. The crash is more likely to happen on an embedded system with 256 MB of RAM than on our development computer with 16 GB of RAM.

Conclusion

My advice is to perform the following three actions to mitigate the risk of a crash.

  1. Thoroughly search your code base for C++ functions returning a QObject or a class derived from QObject to QML and make sure that the returned objects have a parent. The obvious candidates are Q_INVOKABLE functions. Slots should not return anything. But if they do, they are candidates, too.
  2. In code reviews, pay special attention to these problematic functions.
  3. Add a rule to your coding guidelines addressing these problematic functions and how to fix them.

Note that READ functions of Q_PROPERTYs do not transfer ownership from C++ to QML as Richard pointed out in the comments (see Richard’s link to the Qt documentation) and Simon (@tr0nical) on Twitter. So, that’s one less candidate to look at.

6 thoughts on “QML Engine Deletes C++ Objects Still In Use

  1. YOU SAVED MY LIFE !

    Had exactly the same issue. Thanks to your blog I “only” spend 4 hours on it…

    Many, many, many thanks

  2. So you’re saying that the transfer of pointer ownership to the QML engine also occurs with Q_PROPERTY properties with READ accessor functions? Reading the Qt documentation I’d interpreted it as only occurring with Q_INVOKABLE functions.

    Sounds like there might be a couple of landmines buried in my code…

    • In my real-life project, the ownership transfer occurred with a property like

      Q_PROPERTY(Customer *customer READ customer WRITE setCustomer NOTIFY customerChanged)

      So, yes, Q_PROPERTY properties are potential “landmines”.

  3. What version of Qt did you see this with?

    In the process of clearing the above-mentioned landmines I came across some more documentation (http://doc.qt.io/qt-5/qqmlengine.html#ObjectOwnership-enum)

    “Objects not-created by QML have CppOwnership by default. The exception to this are objects returned from C++ method calls; their ownership will be set to JavaScriptOwnership. This applies only to explicit invocations of Q_INVOKABLE methods or slots, but not to property getter invocations.”

    This statement is present in the documentation as far back as 5.5, and to my reading very clearly states that what you’ve seen shouldn’t be happening.

    Fancy raising a bug report?

  4. Richard,
    You are actually right. READ functions of Q_PROPERTYs do not transfer ownership from C++ to QML. Only Q_INVOKABLE functions and slots do. The real-life example actually used a Q_INVOKABLE. In the meantime, I correct my post and code example. The example now crashes 🙂
    Thanks,
    Burkhard

Comments are closed.