📜 ⬆️ ⬇️

Automatic control of the lifetime of common C ++ - QML objects

The discussion deals with objects used in C ++ and QML at the same time, the top of which inheritance hierarchy is QObject. As far as I know, there is no implementation of the mechanism for automatic control of the lifetime of such objects at the library level. Such a mechanism would eliminate the difficulties arising from the manual control of the lifetime of objects, as well as potential bugs, memory leaks and application crashes. In this article I will describe the stages of the implementation of this mechanism, as well as the problems considered in the process of researching this problem.

Intelligent pointers are used for C ++ objects separately. However, in this case, accessing these objects from QML will be incorrect, since after being destroyed by their smart pointers, the objects will become invalid. In QML, the lifetime of objects is controlled by the garbage collector, but provided that the QQmlEngine :: JavaScriptOwnership option is set to ownership of the object, an object that does not have references in the code will collapse when the garbage collector first triggers and will be accessed by C ++. to adverse effects.

The problem is that each side does not assume ownership of the object at the moment when the other side is going to delete the object, since The first does not receive notification of this.

This problem can be solved with the help of a class that would give ownership of the side in which you plan to use the object in the future. The idea is that this class refines the standard intellectual pointer, and for the universality of its use, our class will expand the smart pointer, which we indicate to it with a template parameter. The initial class skeleton looks like this:
')
template <class Object, template<class> class Container = std::shared_ptr> class QmlCppSmartPtr : public Container<QObject> { }; 

The transfer of ownership must be immediately before attempting to remove an object with a side on which it will not be used further. To do this, the corresponding party must call a kollbek, inside which, in fact, there will be a switch of ownership.

Called on the C ++ side, the callback is quite simple to implement, for example, it can be a custom Deleter, passed as a parameter to the constructor of the base class. To call a callback from the QML side, we would need a signal that QML is going to destroy the object, however, such a signal is not currently implemented in Qt. The only signal that at first glance deserves attention is the signal destroyed of the QObject class, but it does not suit us, since this signal is already being called in the process of deleting an object, and at this point the transfer of ownership can lead to an indefinite application behavior.

After some thought and a couple of experiments, it turned out that we didn’t need a signal notifying that the garbage collector was going to destroy the object.

According to the documentation, ownership can also be controlled by specifying the parent object (parent ownership semantics).

 template <typename Object, template<class> class Container = std::shared_ptr> class QmlCppSmartPtr : public Container<Object> { public: explicit QmlCppSmartPtr(Object* object) : Container<Object>(object, std::bind(&QmlCppSmartPtr::deleteObject, this, std::placeholders::_1)) { object->setParent(new QObject()); QQmlEngine::setObjectOwnership(object, QQmlEngine::JavaScriptOwnership); } private: void deleteObject(Object* object) { object->parent()->deleteLater(); object->setParent(nullptr); } }; 

By setting the object to the QQmlEngine :: JavaScriptOwnership option, we commit the garbage collector to keep an eye on the object, but not to delete it as long as it has a parent. After the object became an “orphan”, the garbage collector continues to monitor it, and from that time it was possible to remove it. He will do this at the first actuation, even if the object has no references in the QML / JS code . The last statement is logical, because by changing the option to QQmlEngine :: JavaScriptOwnership, the user of the framework should not and cannot know whether this object is still used on the Qml side. QQmlEngine is required to process ownership change requests while the object is in memory. Apparently, the Qt developers took care of this.

Considering the assertion that the garbage collector assumes the obligation to control an object during its lifetime, regardless of whether the reference count for this object has been reset, it has been assumed that the transfer of ownership can be implemented without parental ownership semantics:

 template <typename Object, template<class> class Container = std::shared_ptr> class QmlCppSmartPtr : public Container<Object> { public: explicit QmlCppSmartPtr(Object* object) : Container<Object>(object, std::bind(&QmlCppSmartPtr::deleteObject, this, std::placeholders::_1)) { QQmlEngine::setObjectOwnership(object, QQmlEngine::CppOwnership); } private: void deleteObject(Object* object) { QQmlEngine::setObjectOwnership(object, QQmlEngine::JavaScriptOwnership); } }; 

After some improvements of the mechanism, the implementation of the form turned out

 template <class Object> struct SimpleOwnershipPolicy { static void init(Object* object) { QQmlEngine::setObjectOwnership(object, QQmlEngine::CppOwnership); } static void destroy(Object* object) { QQmlEngine::setObjectOwnership(object, QQmlEngine::JavaScriptOwnership); } }; template <class Object> struct ParentOwnershipPolicy { void init(Object* object) { object->setParent(new QObject()); QQmlEngine::setObjectOwnership(object, QQmlEngine::JavaScriptOwnership); } void destroy(Object* object) { object->parent()->deleteLater(); object->setParent(nullptr); } }; template <typename Object, template<class, class...> class Container> struct SmartPointer { using type = Container<Object>; }; template <typename Object> struct SmartPointer<Object, std::unique_ptr> { using type = std::unique_ptr<Object, std::function<void(Object*)>>; }; template <typename Object, template<class, class...> class Container = std::unique_ptr, template<class> class OwnershipPolicy = SimpleOwnershipPolicy> class QmlCppSmartPtr : public SmartPointer<Object, Container>::type { public: explicit QmlCppSmartPtr(Object* object, OwnershipPolicy<Object> && ownershipPolicy = OwnershipPolicy<Object>()) : SmartPointer<Object, Container>::type(object, std::bind(&QmlCppSmartPtr::deleteObject, this, std::placeholders::_1)) , m_ownershipPolicy(std::move(ownershipPolicy)) { m_ownershipPolicy.init(object); } private: void deleteObject(Object* object) { m_ownershipPolicy.destroy(object); } OwnershipPolicy<Object> m_ownershipPolicy; }; 

I took the methods of property management into strategies for completeness, leaving both approaches in mind. The SimpleOwnershipPolicy strategy code is more productive, but the ParentOwnershipPolicy, in my opinion, is less susceptible to possible changes within Qt itself, and the documentation gives more guarantees for the correct operation of this method.
An example of using this class:
 class SomeObject : public QObject //  C++-QML  { Q_OBJECT public: Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged) SomeObject(QString const& name, int val); virtual ~SomeObject() override; explicit SomeObject(QObject *parent = 0); QString name() const; void setName(QString const& name); int value() const; void setValue(int val); signals: void valueChanged(); void nameChanged(); private: QString m_name; int m_val; }; class QmlObjectProvider : public QObject { Q_OBJECT public: Q_INVOKABLE SomeObject* createObject(QString const& name, int value) { m_cppObjects.emplace_back(new SomeObject(name, value)); return m_cppObjects.back().data(); } Q_INVOKABLE void removeObjectsFromCppSide() { m_cppObjects.clear(); } private: std::vector<QmlCppSmartPtr<SomeObject, QSharedPointer, ParentOwnershipPolicy>> m_cppObjects; }; ... //    QML qmlRegisterType<QmlObjectProvider>("com.provider", 1, 0, "QmlObjectProvider"); qmlRegisterType<SomeObject>("com.provider", 1, 0, "SomeObject"); engine.rootContext()->setContextProperty("qmlObjectProvider", &qmlObjectProvider); 

QML code:
 ... import com.provider 1.0 Button { onClicked: { var obj = qmlObjectProvider.createObject("SomeObjectName", 42); qmlObjectProvider.removeObjectsFromCppSide(); //    C++  text = obj.name; //   QML    ,        gc } } 

You can instantiate a class and export a class object to QML as follows:
 QmlCppSmartPtr<SomeObject> object; //  unique_ptr  SimpleOwnershipPolicy    . ... QmlCppSmartPtr<SomeObject, std::shared_ptr> object; //  shared_ptr ... return object.get(); 



PS: Code tested with Qt 5.5, gcc 4.9.1

Source: https://habr.com/ru/post/274915/


All Articles