Cute   Learning Hub


Custom Qt Types

Cute allows custom Qt types as arguments in remote signals and slots.

To use custom types as arguments to remote signals and slots, developers have to:

Cute supports all basic Qt types and types from the C++ standard library. Custom types are registered using the Cute::registerType template function. The main difference from local signal-slot connections is that Cute uses the DataStream class for streaming data. A custom data streamer is required mainly for security reasons.

As servers process data coming from the wire, they must be able to deal with rogue messages sent by malicious clients trying to abuse message processing. The QDataStream class optimizes data streaming by reserving memory prior to processing the data. For example, when streaming a QVector, this reservation uses the value of a 4-byte unsigned integer without any checks/restrictions to allocate the array. Thus, for example, a malicious user can send a message pretending to be a streamed QVector, but with a size of 4,294,967,295 bytes. Note that the message can be small. Just the size of the message must have the maximum allowed value for an unsigned 4-byte integer to force an excessive memory allocation. Other allocations limit memory growth to 1024**2 bytes (1MB) per step, which can also be used by malicious clients to abuse memory allocation.

DataStream supports most types natively supported by QVariant. The exception comes from types belonging to GUI, such as QEasingCurve, from types represented by other types (for example, instead of sending a QJsonDocument, a QByteArray of the UTF-8 representation can be sent instead), and from types that ceased to exist in Qt6 like QRegExp, QLinkedList, and QPair.

DataStream also supports C++ standard types (std::pair, std::vector, std::list, std::deque, std::unordered_set, std::queue, std::stack, std::map, std::multimap, std::unordered_map, std::unordered_multimap, std::string, std::u16string and std::u32string). DataStream abstracts away implementation details regarding Qt containers allowing them to be exchanged by peers running different major Qt versions (5/6).

If CustomType is a class used as a custom type, then Cute requires stream operators with the following signatures to be defined:

DataStream &operator<<(DataStream &out, const CustomType &myObj);
DataStream &operator>>(DataStream &in, CustomType &myObj);

Custom types must be registered using the Cute::registerType function. This function allows QVariant to hold variables of the registered type and DataStream to stream those QVariants. Supported template-based types from Qt and the C++ standard library (QVector, QList, std::list, ...) must also be registered. However, users do not need to provide stream operators for these classes because template-based stream operators exist for them, and the Cute::registerType function automatically generates them upon registration.

In Qt5, users must declare std::queue and std::stack using the Q_DECLARE_METATYPE macro with the instantiated class, like Q_DECLARE_METATYPE(std::queue<uint>). Qt6 does not require std::queue and std::stack to be declared. Note that when declaring std::queue or std::stack when using Qt5, the template type must have the name used by the Qt meta-type system. For example, the Qt meta-type system identifies quint16 as ushort. Thus, in this case, std::stack should be declared as Q_DECLARE_METATYPE(std::stack<ushort>) instead of Q_DECLARE_METATYPE(std::stack<quint16>).

Custom types must be registered both on the client and server sides.

On GitHub, there is the source code of an example that defines the Calculator class and exposes it to the Cute server. The Calculator class adds integers and informs whenever the sum overflows. In the example, clients interact with remote objects of the Calculator class. The Calculator class uses a custom integer type to showcase how the Cute server and SDKs define and use custom types.

Below is shown the Integer class header (the source code is available on GitHub):

//
// Integer.h
//
// #include <CuteClientSdk.h> or #include <CuteServerSdk.h>
#if defined(SERVER_SIDE)
#include <CuteServer.h>
#elif defined(CLIENT_SIDE)
#include <CuteClient.h>
#endif

using namespace Cute;

#include <QMetaType>

class Integer
{
public:
    Integer();
    explicit Integer(const qint32 &value);
    Integer(const Integer &other);
    ~Integer() = default;
    Integer &operator=(const Integer &other);
    void setValue(const qint32 &value) {m_value = value;}
    [[nodiscard]] qint32 value() const {return m_value;}

private:
    qint32 m_value = 0;
    static int m_metaTypeId;
};

DataStream &operator<<(DataStream &stream, const Integer &i);
DataStream &operator>>(DataStream &stream, Integer &i);

Q_DECLARE_METATYPE(Integer)

And the Integer class is implemented as follows:

//
// Integer.cpp
//
#include "Integer.h"

int Integer::m_metaTypeId = Cute::registerType<Integer>();


Integer::Integer()
{
    if (m_metaTypeId <= 0)
        qFatal("Failed to register custom Qt type.");
}

Integer::Integer(const qint32 &value)
        : m_value(value)
{
    if (m_metaTypeId <= 0)
        qFatal("Failed to register custom Qt type.");
}

Integer::Integer(const Integer &other)
{
    *this = other;
}

Integer &Integer::operator=(const Integer &other)
{
    if (this == &other)
        return *this;
    m_value = other.m_value;
    return *this;
}

DataStream &operator<<(DataStream &stream, const Integer &i)
{
    stream << i.value();
    return stream;
}

DataStream &operator>>(DataStream &stream, Integer &i)
{
    qint32 readValue;
    stream >> readValue;
    i = Integer(readValue);
    return stream;
}

The Calculator class uses the Integer class as shown below:

#include "Integer.h"
#include <CuteServer.h>
#include <QSharedPointer>

class Calculator : public QObject
{
Q_OBJECT
public:
    explicit Calculator(QSharedPointer<Cute::IConnectionInformation> ci)
    {Q_UNUSED(ci)}
    ~Calculator() override = default;

public slots:
    REMOTE_SLOT qint32 addIntegers(qint32 a, qint32 b);

signals:
    REMOTE_SIGNAL void overflow(Integer a, Integer b);
};

And the Calculator class is implemented as follows:

#include "Calculator.h"

REGISTER_REMOTE_OBJECT("/calculator", Calculator);


qint32 Calculator::addIntegers(qint32 a, qint32 b)
{
    if ((a > 0 && b > 0 && (a+b) < 0)
        || (a < 0 && b < 0 && (a+b) > 0))
    {
        emit overflow(Integer(a), Integer(b));
        return -1;
    }
    else
        return a+b;
}

Clients interact with remote objects from the Calculator class as shown below:

#include "Integer.h"
#include <CuteClient.h>
#include <QCoreApplication>

using namespace Cute::Client;

class CalculatorClient : public QObject
{
Q_OBJECT
public:
    CalculatorClient(qint32 a, qint32 b)
            : m_calculator("Calculator",
                           QUrl("cute://127.0.100.125:1234/calculator"))
    {
        // Establish remote signal-slot connection
        RemoteObject::connect(&m_calculator,
                              SIGNAL(overflow(Integer,Integer)),
                              this,
                              SLOT(onOverflow(Integer,Integer)));
        // Call the remote slot directly
        m_slotResponse = m_calculator.callRemoteSlot("addIntegers", a, b);
        QObject::connect(m_slotResponse.data(),
                         &IRemoteSlotResponse::responded,
                         [](const QVariant &response) {
                             qWarning("Sum is %d.", response.value<qint32>());
                             QCoreApplication::quit();});
    }

public slots:
    // Remote signal-slot connections require public slots.
    void onOverflow(const Integer &a, const Integer &b)
    {
        qWarning("Adding %d to %d overflows.", a.value(), b.value());
    }

private:
    RemoteObject m_calculator;
    QSharedPointer<IRemoteSlotResponse> m_slotResponse;
};

The calculator example is available on GitHub.