Qt Remote Objects Overview

Qt Remote Objects (or QtRO) is described as an IPC module. That puts the focus on the internal details. It should be looked at more as a Connected Framework.

QtRO lets you easily take an existing Qt application and interact with it from other devices. QtRO allows you to create a Replica QObject, making the Replica a surrogate for the real QOject in your program (called the Source). You interact with the Replica the same way you would the Source (with one important difference) and QtRO ensures those interactions are forwarded to the source for handling. Changes to the Source are cascaded to any Replicas.

The mechanism Qt Remote Objects provides for enabling these objects to connect to each other are a network of Nodes. Nodes handle the details of connecting processes or devices. A Replica is created by calling acquire() on a Node, and Sources are shared on the network using enableRemoting().

Replicas are latent copies

Qt Remote Object interactions are inherently asynchronous. This can lead to confusing results initially

# Assume a replica initially has an int property `i` with a value of 2
print(f"Value of i on replica = {replica.i}") # prints 2
replica.iChanged.connect(lambda i: print(f"Value of i on replica changed to {i}"))
replica.i = 3
print(f"Value of i on replica = {replica.i}") # prints 2, not 3

# When the eventloop runs, the change will be forwarded to the source instance,
# the change will be made, and the new i value will be sent back to the replica.
# The iChanged signal will be fired
# after some delay.

Note: To avoid this confusion, Qt Remote Objects can change setters to “push” slots on the Replica class, making the asynchronous nature of the behavior clear.

replica.pushI(3)  # Request a change to `i` on the source object.

How does this affect PySide?

PySide wraps the Qt C++ classes used by QtRO, so much of the needed functionality for QtRO is available in PySide. However, the interaction between a Source and Replica are in effect a contract that is defined on a per object basis. I.e., different objects have different APIs, and every participant must know about the contracts for the objects they intend to use.

In C++, Qt Remote Objects leverages the Replica Compiler (repc) to generate QObject header and C++ code that enforce the contracts for each type. REPC uses a simplified text syntax to describe the desired API in .rep files. REPC is integrated with qmake and cmake, simplifying the process of leveraging QtRO in a C++ project. The challenges in PySide are

  1. To parse the .rep file to extract the desired syntax

  2. Allow generation of types that expose the desired API and match the needed contract

  3. Provide appropriate errors and handling in cases that can’t be dynamically handled in Python. For example, C++ can register templated types such as a QMap<double, MyType> and serialize such types once registered. While Python can create a similar type, there isn’t a path to dynamically serialize such a type so C++ could interpret it correctly on the other side of a QtRO network.

Under the covers, QtRO leverages Qt’s QVariant infrastructure heavily. For instance, a Replica internally holds a QVariantList where each element represents one of the exposed QProperty values. The property’s QVariant is typed appropriately for the property, allows an autogenerated getter to (for instance with a float property) return return variant.value<float >();. This works well with PySide converters.

RepFile PySide type

The first challenge is handled by adding a Python type RepFile can takes a .rep file and parses it into an Abstract Syntax Tree (AST) describing the type.

A simple .rep might look like:

class Thermistat
{
    PROP(int temp)
}

The desired interface would be

from pathlib import Path
from PySide6.QtRemoteObjects import RepFile

input_file = Path(__file__).parent / "thermistat.rep"
rep_file = RepFile(input_file)

The RepFile holds dictionaries source, replica and pod. These use the names of the types as the key, and the value is the PyTypeObject* of the generated type meeting the desired contract:

Source = rep_file.source["Thermistat"]    # A Type object for Source implementation of the type
Replica = rep_file.replica["Thermistat"]  # A Type object for Replica implementation of the type

Replica type

A Replica for a given interface will be a distinct type. It should be usable directly from Python once instantiated and initialized.

Replica = rep_file.replica["Thermistat"]  # A Type object matching the Replica contract
replica = node.acquire(Replica)           # We need to tell the node what type to instantiate
# These two lines can be combined
replica_instance = node.acquire(rep_file.replica["Thermistat"])

# If there is a Thermistat source on the network, our replica will get connected to it.
if replica.isInitialized():
    print(f"The current tempeerature is {replica.temp}")
else:
    replica.initialized.connect(lambda: print(f"replica is now initialized. Temp = {replica.temp}"))

Source type

Unlike a Replica, whose interface is a passthrough of another object, the Source needs to actually define the desired behavior. In C++, QtRO supports two modes for Source objects. A MyTypeSource C++ class is autogenerated that defines pure virtual getters and setters. This enables full customization of the implementation. A MyTypeSimpleSource C++ class is also autogenerated that creates basic data members for properties and getters/setters that work on those data members.

The intent is to follow the SimpleSource pattern in Python if possible.

    Thermistat = rep_file.source["Thermistat"]
    class MyThermistat(Thermistat):
        def __init__(self, parent = None):
            super().__init__(parent)
            # Get the current temp from the system
            self.temp = get_temp_from_system()

Realizing Source/Replica types in python

Assume there is a RepFile for thermistat.rep that defines a Thermistat class interface.

ThermistatReplica = repFile.replica["Thermistat"] should be a Shiboken.ObjectType type, with a base of QRemoteObjectReplica’s shiboken type.

ThermistatSource = repFile.source["Thermistat"] should be a abstract class of Shiboken.ObjectType type, with a base of QObject’s shiboken type.

Both should support new classes based on their type to customize behavior.