Qt Remote Objects
Remote Object Concepts
Qt Remote Objects (QtRO) is an inter-process communication (IPC) module developed for Qt. The idea is to extend existing Qt's functionalities to enable an easy exchange of information between processes or computers.
One of the key features of Qt to enable this is the distinction between an objects API (defined by its Q_Property, Signals and Slots) and the implementation of that API. The purpose of QtRO is to meet the expected API, even if the true QObject is in a different process. A Slot called on a copy of an object (called a Replica in QtRO) is forwarded to the true object (called a Source in QtRO) for handling. Updates to the Source (either property changes or emitted Signals) are forwarded to every Replica.
A Replica is a light-weight proxy for the Source object, but one that supports the same connections and behavior of QObjects, which makes them as easy to use as any other QObject provided by Qt. Everything needed for the Replica to look like the Source object is handled behind the scenes by QtRO.
Note that Remote Objects behave differently from traditional remote procedure call (RPC) implementations. In RPC, the client makes a request and waits for the response. In RPC, the server does not push anything to the client unless it is in response to a request. The design of RPC is often such that different clients are independent of each other (for instance, two clients can ask a mapping service for directions and get different results). While it is possible to implement this in QtRO (as Source without properties, and Slots that have return values), it is designed more to hide the fact that the processing is really remote. You let a node give you the Replica instead of creating it yourself, possibly use the status signals (isReplicaValid()), but then interact with the object like you would with any other QObject-based type.
Uses
Consider a sensor, a global positioning system (GPS) receiver for instance. In QtRO terms, the Source would be the process that directly interacts with the GPS hardware and derives your current location. The location would be exposed as QObject properties, and the periodic updates to the location would update the properties and emit property changed signals. Replicas would be created in other processes and would always know your current location, but would not need any of the logic to compute the location from the sensor data. Connecting to the location changed Signal on the Replica would work as expected, as the signal emitted from the Source would trigger the Signal emission on every Replica.
Another example of QtRO usage would be for a service (for example, access to a printer). The Source is again the process controlling the printer directly. Here you would again likely have properties monitoring the ink levels or whether the printer is currently busy. However, the key feature, being able to print something, needs to be passed back to the printer. This aligns with the Qt Slot mechanism, which QtRO uses as the way for Replicas to make calls on the Source. In effect, properties and signals go from Source to Replicas, while slots go from (a) Replica to the Source. Assuming a print request is accepted, the printer status would change, which would change the status property. This would then be reported to all Replicas.
Nodes
QtRO needs a helper class to make this work: the QRemoteObjectNode. QRemoteObjectNodes (let's shorten the name to Node for now) are what enables the passing of information between processes. All of the QtRO functionality is enabled by a small number of distinct packets passing the necessary data between nodes.
Each process that participates in QtRO's IPC will instantiate a Node-based type (QRemoteObjectNode, QRemoteObjectHost, or QRemoteObjectRegistryHost). The latter types of Nodes provide additional functionality. Both QRemoteObjectHost and QRemoteObjectRegistryHost support the enableRemoting() (and the corresponding disableRemoting()) methods, which are the key methods to expose Source objects to the network. In order to use the Registry functionality, there should be one QRemoteObjectRegistryHost on the network. All other nodes can then pass the RegistryHost's URL to the Node's registryAddress constructor parameter, or pass the URL to the setRegistryUrl() method.
QtRO works as a peer-to-peer network. That is, in order to acquire() a valid Replica, the Replica node needs a connection to the node that hosts its Source. A host node is a node that allows other nodes to connect to it, which is accomplished by giving hosts unique addresses (the address is provided to the QRemoteObjectHost constructor or set by the setHostUrl method). The node that a Replica is requested from must establish the connection to the host node in order to initialize the Replica and keep it up to date.
Connecting Nodes using QtRO URLs
Host Nodes use custom URLs to simplify connections. While the list will likely be extended, QtRO currently supports two types of connections. A "tcp" connection (using the standard tcp/ip protocol) supports connections between devices as well as between processes on the same device. The 2nd option is a "local" connection - which can have less overhead, depending on the underlying OS features - but does not support connectivity between devices.
When using a local connection, a unique name must be used. For tcp connections, a unique address and port number combination much be used.
There is currently no zeroconf facility included in QtRO. All processes or devices must therefore know beforehand how to connect to each other. A QRemoteObjectRegistry (see below) can be used to simplify the connection process for a network with multiple Host Nodes.
Connection types are summarized in the following table.
URL | Host Node | Connecting Node |
---|---|---|
QUrl("local:replica") | QLocalServer("replica") | QLocalSocket("replica") |
QUrl("tcp://192.168.1.1:9999") | QTcpServer("192.168.1.1",9999) | QTcpSocket("192.168.1.1",9999) |
Nodes have a couple of enableRemoting() methods that are used to share objects on the network (this will produce an error if the Node is not a Host Node however). Other processes/devices that want to interact with a shared object use one of the node's acquire() methods to instantiate a replica.
Source objects
A Remote Object Source is the QObject that is responsible for the implementation of the exposed API.
At a high level, you have a choice of using a QObject type directly as a source or defining the desired API in a .rep template for use with the repc compiler.
If you already have a fully defined QObject, it can become a Source simply by passing it to QRemoteObjectHostBase::enableRemoting(). This lets other processes/devices create a Replica of the object to interact with (see Remote Object Interaction). You can then instantiate QRemoteObjectDynamicReplicas of your object, or use the QOBJECT_REPLICA macro in your project file, which will use repc to create a header file describing the Replica for use in that process/on that device (and provides compile-time checks).
Letting repc generate a Source header file for your project (using the REPC_SOURCE macro) provides three options of implementing the desired API. If your class name was Foo, the options would be the following (and see The rep file format for help in creating a rep file)
- FooSimpleSource inheritance
- FooSource inheritance
- FooSourceAPI usage with your own QObject
There is a <Type>SimpleSource class defined in the header, which provides basic getter/setter methods for each property and implements data members of the correct property type in the header. Here "<Type>" represents the class name from the .rep file, so if your class is of type "MyType" in the .rep file, there will be a MyTypeSimpleSource class declared in the produced header file. This is a fast way to get started using the API. To use this class, you need to inherit from this class and implement any defined slots (which are pure virtual in the generated header file). Whatever logic is needed to manage the exposed properties and define when Signals need to be emitted would be added to the overriding class as well.
If you need to hide the implementation details, you can use the <Type>Source class instead, which is the 2nd class declared in the same resulting header file. This class definition does not provide data members, and makes the getter/setter functions pure virtual as well. You have more flexibility in how you implement the class, although you need to write more code. This defines the API for both the source and replica side from a single .rep template file.
Finally, there is the <Type>SourceAPI class generated in the header. This is a templated class, for use specifically by the templated version of QRemoteObjectHostBase::enableRemoting() function overload, which allows you to use any QObject that supports the desired API as the source. You will get compile-time warnings if the class does not provide the correct API, and using this class allows you to hide or convert properties or signal/slot parameters.
Note: The QObject API is never exposed. For instance, while a Replica will have a destroyed signal, the destroyed signal of the source is not propagated. The Source and each Replica are unique QObjects with their own connections. The API that is exposed is defined by the .rep template used by repc, or in the case of raw QObjects, all API elements defined in the inheritance chain from a specific ancestor. Unless you define Q_CLASSINFO("RemoteObject Type") in an ancestor, the QObject's parent is used. If Q_CLASSINFO("RemoteObject Type") is used, that class's API is the lowest level of API used.
Identifying Sources
Since more than one Source can be shared by a host node, each Source requires a name. All repc generated headers include a way for the node to determine the class name (Q_CLASSINFO for replica/simplesource/source types, or a static name() function for the SourceAPI type). If you pass your own QObject type to QRemoteObjectHostBase::enableRemoting(), the name will be determined using the following logic:
- If the object or any of its ancestors has Q_CLASSINFO of type "RemoteObject Type" defined, the provided name will be used.
- Otherwise, the QObject's objectName (if set) will be used.
- If neither is available, the call to QRemoteObjectHostBase::enableRemoting() will fail, returning False.
Replica objects
A remote object replica is a proxy object that has (approximately) the same API as the Source QObject it is replicating. There are a few additional properties and signals to make it possible to detect when the Replica is initialized or if it loses the connectivity to the Source object. There are a few other differences: a constant property on the source cannot be constant on the replica. The value will not be known at the time the replica is instantiated, it will only be known once the replica is initialized (see Remote Object Interaction).
A compiled replica is a QRemoteObjectReplica based type, where the derived class definition is automatically generated by the repc compiler. Only a header file is generated (and using the REPC_REPLICA macro in your .pro file can make generation part of the build process), but it is a complete type. There is no public constructor, you need to use the QRemoteObjectNode::acquire template function to create the Replica instance.
A QRemoteObjectDynamicReplica can be generated at runtime. To do so, you call the non-templated version of QRemoteObjectNode::acquire(), passing in as an argument the Source name (a QString). Dynamic replicas are a bit more verbose to use from C++, but do not require compilation and can be used easily in QML or (potentially) exposed to scripting languages such as Python. Dynamic replicas do not support initial property values, and do not support introspection until they have been initialized.
An important difference between these two ways of creating replicas is the behavior before the replica is initialized. Since a Dynamic replica only gets a metaObject after initialization, it basically has no API before initialization. No properties, and no Signals to connect slots to. Due to the compile-time creation of the metaObject for compiled replicas, their API is available when the replica is instantiated. You can even provide default values for Properties in the template file, which will be used until the replica is initialized with current values from the Source.
See QRemoteObjectReplica and QRemoteObjectDynamicReplica
Replica Initialization
A host node will share the list of sources it hosts and every other node that connects to it. It will send updates when sources are added or removed from the list. In this way, a connected node will always know what sources it can attach to. Changes to a specific Source are only propagated to nodes that have a replica of that source. This avoids unnecessary network traffic.
When a node acquires a replica for a known source, the replica node sends a request for that source to the host node. Upon receipt of this request, the host will create a reply packet with the current values of all properties of the source. If the requested replica is dynamic, it will include the API definition for the source. The replica node will be included in the list of connections that receive changes to that source from then on.
If a replica is instantiated but the node is not connected to the node that hosts the requested source (or that object lives in a host node process, but sharing/remoting has not been enabled for the QObject), the Replica will still be created, it will just remain uninitialized.
If, at a later time, the replica node gets notified that the requested source is available from a connected node, it will at that point request the source and start the initialization process.
If the connection to a host node is lost, the replica will transition to the invalid state. It will attempt to reconnect and will re-initialize if the connection is restored (this making sure all Properties are current).
The Registry
When you QRemoteObjectNode::acquire() a replica, the node URL is not passed as an argument. This means you do not need to specify the host node, but it does require you to have some other means of connecting to that host. Without the registry, it is necessary to manually call QRemoteObjectNode::connect(), from each node, to every host node that has Source objects it should replicate. This is fine for small or static networks, but does not scale.
The registry provides a simpler way to establish these connections. Every node that wants to be part of the registry's network connects to the registry. The registry is itself a specialized source object, and thus is hosted by a node. Connecting to the registry is simply a matter of passing the registry's URL to the QRemoteObjectNode or QRemoteObjectHost constructor, or passing the URL to the setRegistryUrl method.
The registry is tightly integrated with QtRO. Whenever a Source is added or removed, the name/URL is updated in the registry automatically. So once your node is connected to the registry, it is not necessary to connect to any other nodes manually. If you request an object on the network and you aren't connected to the hosting node, the registry will know what URL to connect to and will initiate the connection. Once connected (and the list of available objects is passed along, including the desired Source), the initialization process for the requested Replica will start automatically.
Remote Object Interaction
Source/replica interaction is directional. Property changes and signal emission happen on the source, and are propagated to all replicas. If a property is writable, you can call the setter function on a replica. This will be forwarded to the source, and if a change is made, it will be made on the source and subsequently forwarded to all replicas. To the replica, it is then an asynchronous call, with latency before the change takes effect.
Whereas you can emit a signal on a replica, this may have unexpected results and is discouraged for that reason. It will only trigger slots connected to the replica itself, no slots connected to the source or other replicas. Like property setters, slot invocations on a replica are forwarded to the Source to run.
The behavior above is implemented automatically by QtRO, there is no need to write any replica implementation code. It will be handled automatically at runtime for dynamic replicas, or at compile time for repc generated headers.
Replica Ownership
The acquire methods return a pointer to the replica QObject instantiated by the node. The node has no way of knowing the intended lifetime of the replica, so it is the responsibility of the calling program to delete the replica when it is no longer needed.
You can instantiate multiple copies of the same replica (this may be necessary in QML for instance). All replicas of the same source from a single node will share a private data member which handles the network communication. This means multiple instances of a Replica do not introduce additional network traffic, although there will be some additional processing overhead. Failing to delete replicas will prevent the reference count on this private object to be invalid, and cause unnecessary network communication until the calling process exits. For this reason, it is recommended that QScopedPointer or QSharedPointer be used to help track a replica lifetime.
Remote Object Public Classes
The following classes make up the public interface for Qt Remote Objects:
A class holding information about client backends available on the Qt Remote Objects network | |
A class holding information about server backends available on the Qt Remote Objects network | |
A dynamically instantiated Replica | |
A (Host) Node on a Qt Remote Objects network | |
Base functionality common to Host and RegistryHost classes | |
A node on a Qt Remote Objects network | |
Virtual class provides the methods for setting PROP values of a replica to value they had the last time the replica was used | |
A (Host/Registry) node on a Qt Remote Objects network | |
A class holding information about Source objects available on the Qt Remote Objects network | |
A class interacting with (but not implementing) a Qt API on the Remote Object network |