Речь пойдет об объектах, используемых в 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

Комментарии (14)


  1. tzlom
    12.01.2016 14:05

    А pimpl не проще в этом случае?


    1. sushinskiy
      12.01.2016 15:21

      Что именно по вашему можно было бы вынести в приватную имплементацию? На мой взгляд реализации стратегий — возможно. Других мыслей не приходит. В плане архитекруты класс далек от идеала и его можно реструктурировать и расширять на свой «вкус». В этой стать сделан основной упор на автоматические контроль жизни объектов, расшаренных в QML-C++.


      1. tzlom
        12.01.2016 15:39

        Статья однозначно полезная и интересная
        Я бы вынес в закрытую имплементацию весь класс, а во внешний бы засадил умный указатель, тогда умный указатель становится доступен и внутри GC JS и он уже обеспечивает нам корректное освобождение при разрушении последнего объекта.


        1. sushinskiy
          12.01.2016 15:44

          Я думал о идее, чтоб сам умный указатель представлял тот объект, который мы хотим экспортировать, однако столкнулся с подводными камнями, когда объест на стороне C++ сам себя разрушает. Не могли бы вы написать пример(хотя бы псевдокод)? Возможно я что-то упустил…


          1. tzlom
            12.01.2016 16:28

            class ApproxPrivate {
            //....
            };
            
            class Approx: public QObject {
              Q_OBJECT
            // ...  
              Approx() {
              // ...
              QQmlEngine::setObjectOwnership(this, QQmlEngine::JavaScriptOwnership); // нормально ли что QML знает обо всех объектах? Нет ли способа передать владение только при передаче внутрь QML ?
              }
              
            private:
              QSharedPointer<ApproxPrivate> d_ptr;
              // По идее должно работать Q_DECLARE_PRIVATE(Approx)
              // И если работает тогда доступно Q_D(Approx)
            };
            
            

            Я не проверял эту идею, но скорее всего сработает.
            Вообще у меня не праздный интерес, может быть у вас уже есть тесты на которых можно проверять утечки и сегфолты?


            1. sushinskiy
              12.01.2016 16:35

              И отдавать в QML указатель класса Approx? Но тода при выходе из области видимости объект класса Approx разрушится, разрушив d_ptr и QML в дальнейшем будет иметь доступ к уже невалидному объекту.


              1. tzlom
                13.01.2016 13:55

                Так если мы в QML отдаём владение, то Approx должен быть на куче в любом случае, с чего ему разрушаться то?


                1. sushinskiy
                  13.01.2016 14:44

                  В случае если мы его создадим в куче и передадим в QML во владение, то Qml разрушит обеъкт, когда его ссылки закончатся. После этого C++ обратится уже к разрушенному объекту.


                  1. tzlom
                    13.01.2016 15:15

                    Всё так, потому что мы отдаём владение в QML и с этим конкретным экземпляром больше не должны работать, а если хотим — делаем копию, копии такого объекта очень дешёвые

                    Approx *toQML = new Approx();
                    Approx toC = *toQML;  // а можно и в кучу если надо
                    


                    1. sushinskiy
                      13.01.2016 16:35

                      Я так понял, Вы имеете в виду, что Approx будет оберткой над общим объектом и будет делигировать вызовы функций/свойств/слотов/сигналов этому объекту. Это можно было бы реализовать с помощью переопределения функции QMetaObject::qt_metaCall(). Этот похдход имеет место, но в нем есть два недостатка:

                      1.Фунции, которые экспортируют объект в QML, должны иметь возвращаемый тип QObject*, а не тот тип, который реально хотим экспортировать
                      Например, уже не получиться реализовать функцию подобным образом:

                      Q_INVOKABLE SomeObject* createObject(QString const& name, int value) {
                       Approx *toQML = new Approx();
                       return toQML;
                      }
                      

                      только так:
                      Q_INVOKABLE QObject* createObject(QString const& name, int value) {
                       Approx *toQML = new Approx();
                       return toQML;
                      }
                      

                      Если реализовывать такой подход, теряется типобезопасность, ведь подобные функции могут вызываться так же из C++ кода.

                      2. второй недостаток не столь существенный, однако он есть: Qt Creator не сможет авктопомплитить свойства, методы этого объекта в QML коде.


            1. sushinskiy
              12.01.2016 16:36

              Тесты пока нет возможности выложить. Как появится — обязательно выложу


  1. QtRoS
    12.01.2016 15:34
    +2

    А пользоваться-то как?
    Я как-то недавно с подобной проблемой сталкивался, поэтому можете считать меня эталонным представителем целевой аудитории. И пока не особо понятно, как следует применять и насколько будет удобно.


    1. sushinskiy
      12.01.2016 15:39

      Спасибо, за ценное замечание по поводу использования. В ближайшее время допишу пример использования.


      1. sushinskiy
        12.01.2016 16:21

        Статья обновлена.