Как стать автором
Обновить

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

Программирование *C++ *Qt *
Из песочницы
Речь пойдет об объектах, используемых в 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, а так же документация дает больше гарантий корректной работы этого метода.
Пример использования данного класса:
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
    } 
}

Инстанциировать класс и экспортировать объект класса в QML можно так же следующим образом:
QmlCppSmartPtr<SomeObject> object; // используются unique_ptr и SimpleOwnershipPolicy как параметры по умолчанию. 
...
QmlCppSmartPtr<SomeObject, std::shared_ptr> object; // использование shared_ptr
...
return object.get();



P.S.: Код протестирован с помощью Qt 5.5, gcc 4.9.1
Теги:
Хабы:
Всего голосов 15: ↑14 и ↓1 +13
Просмотры 9.6K
Комментарии Комментарии 14