Автоматический контроль времени жизни общих C++-QML объектов

в 9:45, , рубрики: c++, QML, qt, smart pointers, Программирование

Речь пойдет об объектах, используемых в C++ и QML одновременно, верхушкой иерархии наследования которых является QObject. Насколько мне известно, реализации механизма автоматического контроля времени жизни таких объектов на уровне библиотеки не существует. Подобный механизм избавил бы от сложностей, возникающих при ручном контроле времени жизни объектов, а так же от потенциальных багов, утечек памяти и крешей приложения. В этой статье я опишу этапы реализации данного механизма, а так же проблемы, рассмотренные в процессе исследования данной проблемы.

Для C++ объектов в отдельности используются интеллектуальные указатели. Однако, в таком случае обращение к данным объектам из QML будет некорректным, т.к. после разрушения их интеллектуальными указателями, объекты станут невалидными. В QML время жизни объектов контролирует garbage collector, но при условии, что ownership-у объекта выставлена опция QQmlEngine::JavaScriptOwnership, объект, не имеющий на себя ссылок в коде, разрушится при первом срабатывании сборщика мусора и дальнейшее обращение к нему со стороны C++ приведет к неблагоприятным последствиям.

Проблема заключается в том, что каждая из сторон не берет на себя владение объектом в момент, когда другая сторона собирается удалить объект, т.к. первая не получает уведомление об этом.

Эта проблема может быть решена с помощью класса, который отдавал бы владение той стороне, в которой планируется использовать объект в дальнейшем. Идея в том, что данный класс дорабатывает стандартный интеллектуальный указатель, а для универсальности его использования наш класс будет расширять тот smart pointer, который мы ему укажем шаблонным параметром. Начальный скелет класса выглядит так:

template <class Object, template<class> class Container = std::shared_ptr>
class QmlCppSmartPtr : public Container<QObject> {
};

Переключение владения должно быть непосредственно перед попыткой удаления объекта стороной, на которой он не будет использоваться в дальнейшем. Для этого соответствующая сторона должна вызвать некий коллбэк, внутри которого, собственно, и будет происходить переключение владения.

Вызванный на стороне С++ коллбэк реализовать весьма просто, например, им может быть кастомный Deleter, переданный параметром в конструктор базового класса. Для вызова коллбэка со стороны QML нам понадобился бы сигнал о том, что QML собирается разрушить объект, однако подобный сигнал на данный момент в Qt не реализован. Единственный сигнал, который на первый взгляд заслуживает внимания — сигнал destroyed класса QObject, однако он нам не подходит, т.к. этот сигнал вызывается уже в процессе удаления объекта и в этот момент переключение владения может привести к неопределенному поведению приложения.

После некоторого раздумья и пары экспериментов оказалось, что сигнал, уведомляющий о том, что сборщик мусора собирается разрушить объект, нам не нужен.

Согласно документации управлять владением можно так же с помощью указания parent объекту (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);
    }
};

Выставляя объекту опцию QQmlEngine::JavaScriptOwnership, мы обязуем сборщик мусора следить за объектом, но при этом не удалять его, пока у него есть parent. После того, когда объект стал “сиротой”, garbage collector продолжает за ним следить, к тому же с этих пор появилась возможность и удалить его. Это он и сделает при первом же срабатывании, даже если на объект не осталось ссылок в QML/JS коде. Последнее утверждение является логичным, потому что меняя опцию на QQmlEngine::JavaScriptOwnership, пользователь фреймверка не должен и не может знать, используется ли до сих пор этот объект на Qml стороне. QQmlEngine обязан обрабатывать запросы изменения ownership, пока объект висит в памяти. Видимо, разработчики Qt позаботились об этом.

Учитывая утверждение о том, что сборщик мусора берет на себя обязанность контролировать объект на протяжении его времени жизни, независимо от того, обнулился ли счетчик ссылок на этот объект, возникло предположение, что переключение владения можно реализовать и без помощи 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)) {
        QQmlEngine::setObjectOwnership(object, QQmlEngine::CppOwnership);
    }
    
private:
    void deleteObject(Object* object) {
        QQmlEngine::setObjectOwnership(object, QQmlEngine::JavaScriptOwnership);
    }
};

После некоторых доработок механизма получилась реализация вида:

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;
};

Способы управления владением я вынес в стратегии для полноты выбора, оставив во внимании оба подхода. Код стратегии SimpleOwnershipPolicy более производителен, но ParentOwnershipPolicy, на мой взгляд, менее подвержена возможным изменениям внутри самого Qt, а так же документация дает больше гарантий корректной работы этого метода.

P.S.: Код протестирован с помощью Qt 5.5, gcc 4.9.1

Автор: sushinskiy

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js