Alexander Giesler
Alexander Giesler

Reputation: 175

Returning a C++ pointer to QML that is updated in a different thread

I am fairly new to Qt / QtQuick and I have to develop an application, which uses some sensor data that is received in a different thread over network periodically. This data should be used inside c++ for calculations and the latest data should also be displayed in QML. Everything is setup to be thread-safe inside c++ by using a mutex for protection and the data is visibly updated inside QML. However, I have some concerns about thread-safety on the QML side and I cannot find information or an example on the web about this topic. Specifically I am concerned about returning a pointer (which was the only way to return a C++ object to QML I guess) instead of a value and therefore a copy of the object. Here is a minimal example demonstrating the concern:

// File data.h
#include <QObject>

class Data : public QObject {
    Q_OBJECT
    Q_PROPERTY(QString someData READ someData WRITE setSomeData NOTIFY someDataChanged)

public:
    explicit Data(QObject* parent = nullptr) 
        :QObject(parent)
    {
    }

    QString someData() const {
         return _someData;
    }

    void setSomeData(const QString& value) {
         if (_someData != value) {
             _someData = value;
             emit someDataChanged();
         }
    }

signals:
    void someDataChanged();

private:
    QString _someData;

}; // Data


// File: controller.h
#include <QObject>

#include <thread>

class Controller : public QObject {
    Q_OBJECT
    Q_PROPERTY(Data data READ data NOTIFY dataChanged)

public:
    explicit Controller(QObject* parent = nullptr) 
        :QObject(parent)
        ,_running(false)
        ,_data(nullptr)
    {
        _data = new Data();
    }

    virtual ~Controller() {
        delete _data;
    }

    void start() {
        _running = true;
        _thread = std::thread([this]() { _threadFunc(); });
    }

    void stop() {
        _running = false;

        if (_thread.joinable()) {
            _thread.join();
        }
    }

    Data* data() {
        return _data;
    }

signals:
    void dataChanged();

private:
    void _threadFunc() {
        while (_running) {
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
            _data.setSomeData("foo");
            emit dataChanged();
        }
    }

    bool _running;
    std::thread _thread;
    Data* _data;

}; // Controller

// File main.qml
import QtQuick 2.0

Rectangle {
    width: 100
    height: 100

    Text {
        anchors.centerIn: parent
        text: Controller.data.someData
    }
}

Data is a simple container holding a QString as property. The controller contains the property data and starts a thread that periodically updates data and emits changes as signal. The output will be displayed correctly, but it feels pretty unsafe to return a raw pointer. So my questions here are:

  1. What happens if the data is written too fast and the thread manipulates the data at the same time when QML is using the pointer to update the visuals?
  2. Are there alternatives to returning a raw pointer, e.g., something Qt offers for this purpose and something I have not found yet?
  3. Is my kind of thinking wrong when it comes to using Qt/QML? I first developed the C++ backend (without any Qt parts) and now I am trying to connect it to the GUI. Maybe I should better design the backend around Qt or QML-friendly from start respectively?

Upvotes: 3

Views: 1232

Answers (1)

Alexander Giesler
Alexander Giesler

Reputation: 175

Well, I think I found a solution to my problem: I am still sure that working on the same object will cause issues. I read a bit about QML ownership and found out that by using a Property, the ownership remains at the C++ side. By using a function returning a pointer, QML takes over ownership and will take care to delete the object later on. So what I did here was following if someone encounters the same issue some day:

// File data.h
#include <QObject>

class Data : public QObject {
    Q_OBJECT
    Q_PROPERTY(QString someData READ someData WRITE setSomeData NOTIFY someDataChanged)

public:
    explicit Data(QObject* parent = nullptr) 
        :QObject(parent)
    {
    }

    Data(const Data& data)
        :QObject(data.parent)
        ,_someData(data.someData)
    {
    }

    QString someData() const {
         return _someData;
    }

    void setSomeData(const QString& value) {
         if (_someData != value) {
             _someData = value;
             emit someDataChanged();
         }
    }

signals:
    void someDataChanged();

private:
    QString _someData;

}; // Data


// File: controller.h
#include <QObject>

#include <thread>
#include <mutex> // New

class Controller : public QObject {
    Q_OBJECT
    //Q_PROPERTY(Data data READ data NOTIFY dataChanged)  // Removed

public:
    explicit Controller(QObject* parent = nullptr) 
        :QObject(parent)
        ,_running(false)
        ,_data(nullptr)
    {
        _data = new Data();
    }

    virtual ~Controller() {
        delete _data;
    }

    void start() {
        _running = true;
        _thread = std::thread([this]() { _threadFunc(); });
    }

    void stop() {
        _running = false;

        if (_thread.joinable()) {
            _thread.join();
        }
    }

    Q_INVOKABLE Data* data() { // Modified to be an invokable function instead of a property getter
        std::lock_guard<std::mutex> lock(_mutex); // New
        return new Data(*_data); // New
    }

signals:
    void dataChanged();

private:
    void _threadFunc() {
        while (_running) {
            std::this_thread::sleep_for(std::chrono::milliseconds(10));

            std::lock_guard<std::mutex> lock(_mutex); // New
            _data.setSomeData("foo");
            emit dataChanged();
        }
    }

    bool _running;
    std::thread _thread;
    std::mutex _mutex; // New
    Data* _data;

}; // Controller

// File main.qml
// NOTE: Controller is registered as context property alias 'controller'
import QtQuick 2.0

// Import the datatype 'data' to be used in QML
//import ... 

Rectangle {
    id: myRect
    width: 100
    height: 100

    property Data data

    Connections {
        target: controller
        onDataChanged: {
            myRect.data = controller.data()
        }
    }

    Text {
        anchors.centerIn: parent
        text: data.someData
    }
}

Basically, I make sure to lock the object and to take a copy. This copy can then safely be used by QML and the QML engine will take care to delete the memory after usage. Furthermore, I create an instance of the Data object in QML and register the signal to fetch and assign the latest copy.

Upvotes: 2

Related Questions