Serializing Qt Objects
Here, I will be interested in how to serialize a Qt object, transfer it over the network and maintain a synchronizing state of the copy between the original and the copy. Used Qt 4.8.
For example, I needed an object containing several properties and signals.
Class Device
property name
property state
property number property
signal alarm ()
If I create an object of class Device , the same object should appear on the far side. If I change the name property of an object, then it changes on the remote side. And no additional steps are taken to synchronize.
To implement this object behavior, we need, obviously, a client-server bundle of arbitrary implementation (we will not consider this issue here) and some tricks of Qt.
1. We describe the class UserMetaObject
The object identifier is a unique number, thanks to which all the object parameters sent, such as signals and properties, reach the desired object on the remote side.
Macro Q_CONSTRUCTOR - serves for guaranteed registration of Qt-objects in the lists of metatypes and prohibits the copy constructor.
The rest is obvious, except for ArgMap - this is a list of initial properties of the object.
2. We describe the class UserMetaCall inherited from UserMetaObject
The template void init () function - registers the type of an object in the Qt metatype system, as well as
collectSignals () - the function "collects" free signals and creates a special object for each signal in the detectors list.
reg () - registers an object in the client if init is executed on the client side. This means that the client will send the server part full information about the object, type name, identifier, current property values.
3. Linking object signals.
Let's take a closer look at the work of the collectSignals function.
Here everything is built on games with metaObject, which every QObject descendant has
As you can see, we go through all the methods of the object, and if it is a signal that does not belong to any property of the object, create a SignalDetector object of a suitable signature and associate the signal with a suitable SignalDetector slot. However, if there is no suitable slot, we associate it with the default slot (other such MB behavior is debatable, yes).
The search for a suitable slot can happen like this
Here we go through the functions (slots) of the SignalDetector and just check the transmitted signature for compatibility, Qt does something similar when QObject :: connect is called. If a suitable slot is found, then we associate the lower-level function QMetaObject :: connect.
In the absence of a suitable slot (which should not be), we try to use the onSignal () slot.
This function determines whether a signal belongs to an object property. Since the property may belong to the get / set methods and the signal that is usually sent when this property changes.
In principle, there is no particular mistake when leaving the binding of signals belonging to the properties of an object to signal detectors. It will just double work when transferring properties over the network. And on the far side, the copy property itself will generate the desired signal. Plus, we recall that a signal cannot always carry information about a new property value, that is, not pass any arguments.
An exemplary implementation of a signal detector may take the form
Here, I think, everything is clear. The process function sends data through the client to the remote side.
4. Initial initialization of properties
As we mentioned above, the set function is responsible for this
Elementary, we look for properties for matching names and set values to them. (Note, this must be done before registering the object in the client so that it does not try to transfer changes in the properties of the object that has not yet been created on the opposite side).
There was also a super function, but it is virtual and has no implementation, to be honest. Each user of UserMetaCall classes can override it if he wants to work with properties in the "constructor" in some special way.
5. Passing properties
If you look closely at the QObject class, you can find such lines in the Q_OBJECT macro definition
We are interested in the line virtual void * qt_metacast (const char *);
It is through this function that such calls as pass:
QMetaObject :: WriteProperty
QMetaObject :: ReadProperty
We will only be interested in changing the property of an object, so we focus only on QMetaObject :: WriteProperty.
Since qt_metacast is a virtual function, we will make a “substitution” for it to catch all changes to the property.
In the UserMetaCall class, define a function
In this function, we pass the intercepted property change call and the parameters of this call through the client, then redirect this call to the parent function qt_metacall, which has a normal Qt implementation.
6. Do not forget about the removal!
Removing an object should entail its removal on the server side. However, this issue has its own nuances. You can’t just take it and delete the object! It's not safe. The object on the server side may be in processing, it may be used at the time when you decided to delete it here. Therefore, when the destructor is called, we transmit, of course, information that the object was destroyed by the server, but on the other side we must decide what to do with the copy without the original.
That's all. In general terms, of course.
You can describe your class like this
After processing the Qt moc file will be generated with the following contents
You see, first he calls the parent function UserMetaCall :: qt_metacall which we redefined.
The general scheme is as follows. We create a successor object UserMetaCall, it automatically receives an identifier and the client part communicates with the server part, a copy of the object with the corresponding identifier is created on the remote side. Further, any change in the property of the object immediately affects the copy, the call of the signal leads to a similar behavior for the copy. That is, our client imperceptibly for the user sends notifications with the identifier, the name of the property or signal and QVariant - the value of the property or the arguments of the signal.
The principle of operation of the server side, IMHO, is understandable. Receive information from the client and process correctly. MetaObject is also used, but no tricks.
This method works in Qt4, did not check on Qt5 but it seems that qt_metacall is in place there. But with Qt3 there were some difficulties, since the system there is a bit different, I had to play a bit with pro files to achieve a similar result. But, most likely, no one writes on Qt3 anymore, this is not interesting.
For example, I needed an object containing several properties and signals.
Class Device
property name
property state
property number property
signal alarm ()
If I create an object of class Device , the same object should appear on the far side. If I change the name property of an object, then it changes on the remote side. And no additional steps are taken to synchronize.
To implement this object behavior, we need, obviously, a client-server bundle of arbitrary implementation (we will not consider this issue here) and some tricks of Qt.
1. We describe the class UserMetaObject
#define Q_CONSTRUCTOR(type) \
public: \
type(const ArgMap& arg = ArgMap()) { set(arg); super(arg); init(); } \
private: \
type(const type& dev) { Q_UNUSED(dev); }
class SERIALOBJECT_EXPORT UserMetaObject: public QObject
{
Q_OBJECT
int objectID; // идентификатор объекта
protected:
static int objectIDCounter; // статический счетчик для идентификаторов
QString cname; // имя класса создаваемого объекта
public:
UserMetaObject();
virtual ~UserMetaObject();
void setObjectID(int value);
int getObjectID() const;
QString className() const;
};
The object identifier is a unique number, thanks to which all the object parameters sent, such as signals and properties, reach the desired object on the remote side.
Macro Q_CONSTRUCTOR - serves for guaranteed registration of Qt-objects in the lists of metatypes and prohibits the copy constructor.
The rest is obvious, except for ArgMap - this is a list of initial properties of the object.
2. We describe the class UserMetaCall inherited from UserMetaObject
class UserMetaCall: public UserMetaObject
{
QList detectors;
void reg();
protected:
void set(const ArgMap &arg);
public:
explicit UserMetaCall();
virtual ~UserMetaCall();
int qt_metacall(QMetaObject::Call call, int id, void ** arg);
bool findSuitDetector(SignalDetector *det, const QMetaMethod& met);
bool isPropertySignal(int signalID);
bool setDefaultDetector(SignalDetector *det);
void collectSignals();
template
void init()
{
cname = metaObject()->className();
if(!QMetaType::type(cname.toStdString().c_str()))
qRegisterMetaType(cname.toStdString().c_str());
collectSignals();
reg();
}
virtual void super(const ArgMap& arg = ArgMap());
};
The template void init () function - registers the type of an object in the Qt metatype system, as well as
collectSignals () - the function "collects" free signals and creates a special object for each signal in the detectors list.
reg () - registers an object in the client if init is executed on the client side. This means that the client will send the server part full information about the object, type name, identifier, current property values.
3. Linking object signals.
Let's take a closer look at the work of the collectSignals function.
Here everything is built on games with metaObject, which every QObject descendant has
void UserMetaCall::collectSignals()
{
for(size_t i=metaObject()->methodOffset(); i(metaObject()->methodCount()); ++i)
{
QMetaMethod met = metaObject()->method(i);
int signalId = metaObject()->indexOfSignal(met.signature());
if(isPropertySignal(signalId)) continue;
if(signalId>0)
{
SignalDetector *det = new SignalDetector(met.signature());
det->setObjectID(getObjectID());
detectors.append(det);
if(!findSuitDetector(det, met))
{
setDefaultDetector(det);
}
}
}
}
As you can see, we go through all the methods of the object, and if it is a signal that does not belong to any property of the object, create a SignalDetector object of a suitable signature and associate the signal with a suitable SignalDetector slot. However, if there is no suitable slot, we associate it with the default slot (other such MB behavior is debatable, yes).
The search for a suitable slot can happen like this
bool UserMetaCall::findSuitDetector(SignalDetector *det, const QMetaMethod &met)
{
if(!det) return false;
for(size_t method=det->metaObject()->methodOffset();
method(det->metaObject()->methodCount()); ++method)
{
if(this->metaObject()->checkConnectArgs(met.signature(), det->metaObject()->method(method).signature()))
{
int sigID = metaObject()->indexOfMethod(det->getSignature().toStdString().c_str());
if(QMetaObject::connect(this, sigID, det, method)) return true;
}
}
return false;
}
Here we go through the functions (slots) of the SignalDetector and just check the transmitted signature for compatibility, Qt does something similar when QObject :: connect is called. If a suitable slot is found, then we associate the lower-level function QMetaObject :: connect.
bool UserMetaCall::setDefaultDetector(SignalDetector *det)
{
if(!det) return false;
int defaultMethId = det->metaObject()->indexOfMethod("onSignal()");
int sigID = metaObject()->indexOfMethod(det->getSignature().toStdString().c_str());
if(!QMetaObject::connect(this, sigID, det, defaultMethId))
{
qWarning()<<"connect fail";
return false;
}
return true;
}
In the absence of a suitable slot (which should not be), we try to use the onSignal () slot.
bool UserMetaCall::isPropertySignal(int signalID)
{
for(size_t i=metaObject()->propertyOffset();
i(metaObject()->propertyCount()); ++i)
{
if(metaObject()->property(i).notifySignalIndex()==signalID)
{
return true;
}
}
return false;
}
This function determines whether a signal belongs to an object property. Since the property may belong to the get / set methods and the signal that is usually sent when this property changes.
In principle, there is no particular mistake when leaving the binding of signals belonging to the properties of an object to signal detectors. It will just double work when transferring properties over the network. And on the far side, the copy property itself will generate the desired signal. Plus, we recall that a signal cannot always carry information about a new property value, that is, not pass any arguments.
An exemplary implementation of a signal detector may take the form
class SignalDetector: public QObject
{
Q_OBJECT
QString signature;
int objectID;
void process(const QVariant &value);
public:
SignalDetector(const QString &signame, QObject *parent =0):
QObject(parent),signature(signame){}
int getObjectID() const;
void setObjectID(int value);
QString getSignature();
public Q_SLOTS:
void onSignal(QString value) { process(QVariant(value)); }
void onSignal(int value) { process(QVariant(value)); }
void onSignal(bool value) { process(QVariant(value)); }
void onSignal(float value) { process(QVariant(value)); }
void onSignal(double value) { process(QVariant(value)); }
void onSignal(char value) { process(QVariant(value)); }
void onSignal() { process(QVariant()); }
};
Here, I think, everything is clear. The process function sends data through the client to the remote side.
4. Initial initialization of properties
As we mentioned above, the set function is responsible for this
void UserMetaCall::set(const ArgMap &arg)
{
for(QPropertyList::const_iterator i=arg.begin(); i!=arg.end(); ++i)
{
int propNumber = metaObject()->indexOfProperty(i.key().toStdString().c_str());
if(propNumber>=0)
{
setProperty(i.key().toStdString().c_str(), i.value());
}
}
}
Elementary, we look for properties for matching names and set values to them. (Note, this must be done before registering the object in the client so that it does not try to transfer changes in the properties of the object that has not yet been created on the opposite side).
There was also a super function, but it is virtual and has no implementation, to be honest. Each user of UserMetaCall classes can override it if he wants to work with properties in the "constructor" in some special way.
5. Passing properties
If you look closely at the QObject class, you can find such lines in the Q_OBJECT macro definition
#define Q_OBJECT \
public: \
Q_OBJECT_CHECK \
static const QMetaObject staticMetaObject; \
Q_OBJECT_GETSTATICMETAOBJECT \
virtual const QMetaObject *metaObject() const; \
virtual void *qt_metacast(const char *); \
QT_TR_FUNCTIONS \
virtual int qt_metacall(QMetaObject::Call, int, void **); \
private: \
Q_DECL_HIDDEN static const QMetaObjectExtraData staticMetaObjectExtraData; \
Q_DECL_HIDDEN static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **);
...
We are interested in the line virtual void * qt_metacast (const char *);
It is through this function that such calls as pass:
QMetaObject :: WriteProperty
QMetaObject :: ReadProperty
We will only be interested in changing the property of an object, so we focus only on QMetaObject :: WriteProperty.
Since qt_metacast is a virtual function, we will make a “substitution” for it to catch all changes to the property.
In the UserMetaCall class, define a function
int UserMetaCall::qt_metacall(QMetaObject::Call call, int id, void **arg)
{
if(call == QMetaObject::WriteProperty)
{
QVariant var = (*reinterpret_cast(arg[0]));
if(клиент есть)
{
клиент->sendProperty(getObjectID(), QString(metaObject()->property(id).name()), var);
}
}
return this->UserMetaObject::qt_metacall(call, id, arg);
}
In this function, we pass the intercepted property change call and the parameters of this call through the client, then redirect this call to the parent function qt_metacall, which has a normal Qt implementation.
6. Do not forget about the removal!
Removing an object should entail its removal on the server side. However, this issue has its own nuances. You can’t just take it and delete the object! It's not safe. The object on the server side may be in processing, it may be used at the time when you decided to delete it here. Therefore, when the destructor is called, we transmit, of course, information that the object was destroyed by the server, but on the other side we must decide what to do with the copy without the original.
That's all. In general terms, of course.
You can describe your class like this
class Device: public UserMetaCall
{
Q_OBJECT
Q_PROPERTY(QString name READ name WRITE setname NOTIFY nameChanged)
Q_PROPERTY(bool state READ state WRITE setstate NOTIFY stateChanged)
Q_PROPERTY(int number READ number WRITE setnumber NOTIFY numberChanged)
Q_CONSTRUCTOR(Device)
public:
void super(const ArgMap &arg);
QString name() const;
bool state() const;
int number() const;
void callAlarm();
public slots:
void setname(QString arg);
void setstate(bool arg);
void setnumber(int arg);
signals:
void nameChanged(QString arg);
void stateChanged(bool arg);
void numberChanged(int arg);
void alarm();
private:
QString m_name;
bool m_state;
int m_number;
};
After processing the Qt moc file will be generated with the following contents
int Device::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
_id = UserMetaCall::qt_metacall(_c, _id, _a);
if (_id < 0)
return _id;
if (_c == QMetaObject::InvokeMetaMethod) {
if (_id < 7)
qt_static_metacall(this, _c, _id, _a);
_id -= 7;
}
#ifndef QT_NO_PROPERTIES
else if (_c == QMetaObject::ReadProperty) {
...
You see, first he calls the parent function UserMetaCall :: qt_metacall which we redefined.
The general scheme is as follows. We create a successor object UserMetaCall, it automatically receives an identifier and the client part communicates with the server part, a copy of the object with the corresponding identifier is created on the remote side. Further, any change in the property of the object immediately affects the copy, the call of the signal leads to a similar behavior for the copy. That is, our client imperceptibly for the user sends notifications with the identifier, the name of the property or signal and QVariant - the value of the property or the arguments of the signal.
The principle of operation of the server side, IMHO, is understandable. Receive information from the client and process correctly. MetaObject is also used, but no tricks.
This method works in Qt4, did not check on Qt5 but it seems that qt_metacall is in place there. But with Qt3 there were some difficulties, since the system there is a bit different, I had to play a bit with pro files to achieve a similar result. But, most likely, no one writes on Qt3 anymore, this is not interesting.