Saturday, January 31, 2015

QQuickItem (Cannot assign object to list)

Introduction

I find myself with a problem that the Qt forums can't solve. The only way to fix it is to dive into the Qt source code and figure it out myself. This blog post is all about the internals of QtQuick2 and is meant for an intermediate audience. I apologize about this blogpost as it's mainly about how I'm fixing this one issue; but I assure the reader that once finished they'll have more intimate knowledge of QtQuick2.

Prerequisites for this entry include basic knowledge about QQuickItem and customization using C++. Before writing this article I was familiar with the class predecessor, QDeclarativeItem. The two are very similar yet very very different. That might be the subject of another post.

The first section is all about detailing the crime scene or in other words describing the problem. The second section documents some useful information about import's in Qt5/QtQuick2 (including some really general information about qmldir). The third section after that documents the "default property" in QML and what it is all about.The fourth section is all about where and how the definitions of QQuickItems are stored.

Problem Description

During a port of QtQuick1 to QtQuick2 code I got the error:

types.qml: 48:1: Cannot assign object to list

We've all been there- a problem we're seeing in a third party library. This is a kind of problem that no one else sees. We didn't want to deal with branching our code for the two distributions. Using some typedefs along with a hand rolled preprocessor the whole thing works great. Except when I saw the following error when loading some QML:

types.qml: 48:1: Cannot assign object to list

This was a strange error indeed. My QML program looked like this:

import QtQuick 2.0
import blah 1.0

TypesCreator {
    id: root
    ...

    Connections { // Line 48
        target: __start__ // __start__ is an object I bound a QObject to.
        onStart: {
            // Do something.
        }
    }
}

The TypesCreator element is part of the blah import that is custom to the application. In the interest of treating this article as a "whodunnit" type of story you can assume that TypesCreator's object looks like this in C++:

class TypesCreator : public QQuickItem
{
    Q_OBJECT
    public:

    private:
};

This obviously does nothing except acts as a container for other objects. In any crime scene you have to know the area. In this case I needed to know what was imported and what wasn't.

Where/What Modules Are/Exist?

The first theory I had was that the Connections object wasn't registered. Eventually it was read that Connections was technically not part of QtQuick 2 but rather was part of QtQml [1]. Upon further reading, QtQml components are automatically included as part of QtQuick. This theory is a dead end- however it led me to trying to solve the riddle of "what objects belong to what import"?

The answer is:

http://doc.qt.io/qt-5/modules-qml.html

As a sidenote to this section: the Qt5 documentation is quite irritating. While being much more comprehensible it fails to be searchable. It took me over 5 minutes to find the module list.

The module source code for the imports is organized under:

qtdeclarative/src/imports/

The easiest way to decipher the imports is to go under a directory and read the qmldir file. The qmldir file has all of the information about what types of classes for QML the module contains. For example I'm looking at:

qtdeclarative/src/imports/qtquick2/qmldir

This is what it reads:

module QtQuick
plugin qtquick2plugin
classname QtQuick2Plugin
typeinfo plugins.qmltypes
designersupported

Essentially it is a hacked together file format that attaches some extra information onto a module. In this case the first line defines what the module name is. The second line tells me that this is a collection of C++ objects that make up this module. The rest I'll skip for now- I just want to know where the basic information is at this point.

Default QML Properties

One of the biggest questions I had when trying to solve this problem was what property could this not assign an object to? I put that in bold mainly to get back to the original error message:

types.qml: 48:1: Cannot assign object to list

So what was this property? The only way I could actually solve this was to get a breakpoint into the code that was printing the error message and then work backwards. To achieve this purpose I:
  1. Performed a grep to find the string "Cannot assign object to list" in the Qt Source code. It turned up in qtdeclarative/src/qml/compiler/qqmltypecompiler.cpp.
  2. When I found the message I noticed that it passed in a propertyName as a parameter to the function, I outputted this using qDebug(). 
  3. Finally to really throw a wrench into things I throw a standard exception. This allows Visual Studio to trip up each time it encounters this error.
When running I discovered the property name being set was "data". This was confusing since no where in my code did a "data" object exist. That's not true though! Inside of the source code for QQuickItem it defines the property "data" as:

Q_PRIVATE_PROPERTY(QQuickItem::d_func(), QQmlListProperty<QObject> data READ data DESIGNABLE false)

So every item in QML has a data property and apparently it's a list of QObject's. That's something new- however I'm never using the "data" property. As I was about to find out, this is also not true.

There's a feature in QML where the developer can specify the "default property" [3]. Inside of QQuickItem it has the following macro also added in:

Q_CLASSINFO("DefaultProperty", "data")

This means that any sort of unbound object created will be added to "data". Since every QML object has the default property set to "data" all of what would appear to be the child items are added in via the data property. It is mentioned in the documentation that the data elements when added are also added to the children list.

This explains a lot; it explains why the property it cannot add Connections to is called "data", it explains why the type it is trying to add to is referenced as a list. The only thing it doesn't solve is why Connections isn't registered as a QObject. It should be; something isn't adding up. The next logical step is to observe how the Connections object is added to a QML Engine.

The How and Where QML Types Are Stored

The final bit of the puzzle for this entry is determining why the QML types cannot be found. This involved stepping into Qt. I found out the following:

1) For each type defined for QML, there is a QQmlType object. According to my notes the declaration can be found in qqmlmetatype_p.h. [4]
2) There's an object of class QQmlMetaTypeData that contains a map of all the created types. When the developer calls qmlRegisterType, the type created, its name, and version information all end up in a map here.The declaration is found in qqmlmetatype.cpp [5].
3) The map itself holds a hash of type identifier to QQmlType (line 76 in the source).

My best guess is that the type identifier for a well known type is getting overwritten in this map. The basic type I'm trying to use was replaced with some garbage type. To understand this, I tracked down where the QML numeric type was defined. If walking up the call stack, the QML Type identifier is found to map to the QMetaTypeIdHelper<T>::qt_metatype_id() where T is the type of the class.

The summary to this section is that any time a registration occurs with the meta type system the identifier that is produced is also used in QML to map type information. The numbers are mostly guaranteed to be unique since they are using a basic atomic integer mechanism so it makes sense. Having this information in hand I could now surmise that somewhere I was doing something stupid when registering types. This guess proved to be right since I found this little gremlin in the code:

qRegisterMetaType<QObject*>("LayerItem");

Looking at it now I'm kicking myself. This was using the type id from QObject* to map to the string "LayerItem".

Conclusion

While searching for why the runtime error "Cannot assign object to list" in QML I learned a lot about how the language parses, interprets, and stores its programs:

  • Modules in QtQuick2 are very similar to QtQuick1 modules with the exception they can store more meta information in qmldir.
  • QObject based classes have the ability to store a default parameter which can be used to store "child" information in a custom way.
  • QML Types are stored in a map inside of QQmlMetaTypeData. The id's used in the map come from the Qt Meta Object System.
In the process of learning about all of these facets of the engine I found the solution to my problem which was to get rid of a call to qRegisterMetaType<QObject*>. 


REFERENCES

[1] - http://doc.qt.io/qt-5/qtqml-qmlmodule.html

[2] - http://qt-project.org/doc/qt-4.8/declarative-cppextensions-referenceexamples-default.html

[3] - http://doc.qt.io/qt-5/qtqml-syntax-objectattributes.html#property-attributes

[4] - https://qt.gitorious.org/qt/qtdeclarative/source/ccd69247eaa6289bc31115f863ba4737b14fe34e:src/qml/qml/qqmlmetatype_p.h

[5] - https://qt.gitorious.org/qt/qtdeclarative/source/e2ea0a83cc876fb54a2a8bf6f1350dbfa52f596b:src/qml/qml/qqmlmetatype.cpp


No comments:

Post a Comment