|
| 1 | +# Qt Remote Objects Overview |
| 2 | + |
| 3 | +[Qt Remote Objects](https://doc.qt.io/qt-6/qtremoteobjects-index.html) (or QtRO) |
| 4 | +is described as an IPC module. That puts the focus on the internal details. |
| 5 | +It should be looked at more as a Connected Framework. |
| 6 | + |
| 7 | +QtRO lets you easily take an existing Qt application and interact with it from |
| 8 | +other devices. QtRO allows you to create a |
| 9 | +[_Replica_](https://doc.qt.io/qt-6/qtremoteobjects-replica.html) QObject, making |
| 10 | +the Replica a surrogate for the real QOject in your program (called the |
| 11 | +[_Source_](https://doc.qt.io/qt-6/qtremoteobjects-source.html)). You interact with |
| 12 | +the Replica the same way you would the Source (with one important difference) and QtRO |
| 13 | +ensures those interactions are forwarded to the source for handling. Changes to the |
| 14 | +Source are cascaded to any Replicas. |
| 15 | + |
| 16 | +The mechanism Qt Remote Objects provides for enabling these objects to connect to each |
| 17 | +other are a network of |
| 18 | +[_Nodes_](https://doc.qt.io/qt-6/qtremoteobjects-node.html). Nodes handle the details of |
| 19 | +connecting processes or devices. A Replica is created by calling |
| 20 | +[acquire()](https://doc.qt.io/qt-6/qremoteobjectnode.html#acquire) on a Node, and Sources |
| 21 | +are shared on the network using |
| 22 | +[enableRemoting()](https://doc.qt.io/qt-6/qremoteobjecthostbase.html#enableRemoting). |
| 23 | + |
| 24 | +## Replicas are _latent copies_ |
| 25 | + |
| 26 | +Qt Remote Object interactions are inherently asynchronous. This _can_ lead to |
| 27 | +confusing results initially |
| 28 | + |
| 29 | +```python |
| 30 | +# Assume a replica initially has an int property `i` with a value of 2 |
| 31 | +print(f"Value of i on replica = {replica.i}") # prints 2 |
| 32 | +replica.iChanged.connect(lambda i: print(f"Value of i on replica changed to {i}")) |
| 33 | +replica.i = 3 |
| 34 | +print(f"Value of i on replica = {replica.i}") # prints 2, not 3 |
| 35 | + |
| 36 | +# When the eventloop runs, the change will be forwarded to the source instance, |
| 37 | +# the change will be made, and the new i value will be sent back to the replica. |
| 38 | +# The iChanged signal will be fired |
| 39 | +# after some delay. |
| 40 | +``` |
| 41 | + |
| 42 | +Note: To avoid this confusion, Qt Remote Objects can change setters to "push" |
| 43 | +slots on the Replica class, making the asynchronous nature of the behavior |
| 44 | +clear. |
| 45 | + |
| 46 | +```python |
| 47 | +replica.pushI(3) # Request a change to `i` on the source object. |
| 48 | +``` |
| 49 | + |
| 50 | +## How does this affect PySide? |
| 51 | + |
| 52 | +PySide wraps the Qt C++ classes used by QtRO, so much of the needed |
| 53 | +functionality for QtRO is available in PySide. However, the interaction between |
| 54 | +a Source and Replica are in effect a contract that is defined on a _per object_ |
| 55 | +basis. I.e., different objects have different APIs, and every participant must |
| 56 | +know about the contracts for the objects they intend to use. |
| 57 | + |
| 58 | +In C++, Qt Remote Objects leverages the |
| 59 | +[Replica Compiler (repc)](https://doc.qt.io/qt-6/qtremoteobjects-repc.html) to |
| 60 | +generate QObject header and C++ code that enforce the contracts for each type. |
| 61 | +REPC uses a simplified text syntax to describe the desired API in .rep files. |
| 62 | +REPC is integrated with qmake and cmake, simplifying the process of leveraging |
| 63 | +QtRO in a C++ project. The challenges in PySide are |
| 64 | +1) To parse the .rep file to extract the desired syntax |
| 65 | +2) Allow generation of types that expose the desired API and match the needed |
| 66 | + contract |
| 67 | +3) Provide appropriate errors and handling in cases that can't be dynamically |
| 68 | + handled in Python. |
| 69 | +For example, C++ can register templated types such as a QMap<double, MyType> |
| 70 | +and serialize such types once registered. While Python can create a similar |
| 71 | +type, there isn't a path to dynamically serialize such a type so C++ could |
| 72 | +interpret it correctly on the other side of a QtRO network. |
| 73 | + |
| 74 | +Under the covers, QtRO leverages Qt's QVariant infrastructure heavily. For |
| 75 | +instance, a Replica internally holds a QVariantList where each element |
| 76 | +represents one of the exposed QProperty values. The property's QVariant is |
| 77 | +typed appropriately for the property, allows an autogenerated getter to (for |
| 78 | +instance with a float property) return `return variant.value<float >();`. This |
| 79 | +works well with PySide converters. |
| 80 | + |
| 81 | +## RepFile PySide type |
| 82 | + |
| 83 | +The first challenge is handled by adding a Python type RepFile can takes a .rep |
| 84 | +file and parses it into an Abstract Syntax Tree (AST) describing the type. |
| 85 | + |
| 86 | +A simple .rep might look like: |
| 87 | +```cpp |
| 88 | +class Thermistat |
| 89 | +{ |
| 90 | + PROP(int temp) |
| 91 | +} |
| 92 | +``` |
| 93 | +
|
| 94 | +The desired interface would be |
| 95 | +```python |
| 96 | +from pathlib import Path |
| 97 | +from PySide6.QtRemoteObjects import RepFile |
| 98 | +
|
| 99 | +input_file = Path(__file__).parent / "thermistat.rep" |
| 100 | +rep_file = RepFile(input_file) |
| 101 | +``` |
| 102 | + |
| 103 | +The RepFile holds dictionaries `source`, `replica` and `pod`. These use the |
| 104 | +names of the types as the key, and the value is the PyTypeObject* of the |
| 105 | +generated type meeting the desired contract: |
| 106 | + |
| 107 | +```python |
| 108 | +Source = rep_file.source["Thermistat"] # A Type object for Source implementation of the type |
| 109 | +Replica = rep_file.replica["Thermistat"] # A Type object for Replica implementation of the type |
| 110 | +``` |
| 111 | + |
| 112 | +## Replica type |
| 113 | + |
| 114 | +A Replica for a given interface will be a distinct type. It should be usable |
| 115 | +directly from Python once instantiated and initialized. |
| 116 | + |
| 117 | +```python |
| 118 | +Replica = rep_file.replica["Thermistat"] # A Type object matching the Replica contract |
| 119 | +replica = node.acquire(Replica) # We need to tell the node what type to instantiate |
| 120 | +# These two lines can be combined |
| 121 | +replica_instance = node.acquire(rep_file.replica["Thermistat"]) |
| 122 | + |
| 123 | +# If there is a Thermistat source on the network, our replica will get connected to it. |
| 124 | +if replica.isInitialized(): |
| 125 | + print(f"The current tempeerature is {replica.temp}") |
| 126 | +else: |
| 127 | + replica.initialized.connect(lambda: print(f"replica is now initialized. Temp = {replica.temp}")) |
| 128 | +``` |
| 129 | + |
| 130 | +## Source type |
| 131 | + |
| 132 | +Unlike a Replica, whose interface is a passthrough of another object, the |
| 133 | +Source needs to actually define the desired behavior. In C++, QtRO supports two |
| 134 | +modes for Source objects. A MyTypeSource C++ class is autogenerated that |
| 135 | +defines pure virtual getters and setters. This enables full customization of |
| 136 | +the implementation. A MyTypeSimpleSource C++ class is also autogenerated that |
| 137 | +creates basic data members for properties and getters/setters that work on |
| 138 | +those data members. |
| 139 | + |
| 140 | +The intent is to follow the SimpleSource pattern in Python if possible. |
| 141 | + |
| 142 | +```python |
| 143 | + Thermistat = rep_file.source["Thermistat"] |
| 144 | + class MyThermistat(Thermistat): |
| 145 | + def __init__(self, parent = None): |
| 146 | + super().__init__(parent) |
| 147 | + # Get the current temp from the system |
| 148 | + self.temp = get_temp_from_system() |
| 149 | +``` |
| 150 | + |
| 151 | +## Realizing Source/Replica types in python |
| 152 | + |
| 153 | +Assume there is a RepFile for thermistat.rep that defines a Thermistat class |
| 154 | +interface. |
| 155 | + |
| 156 | +`ThermistatReplica = repFile.replica["Thermistat"]` should be a Shiboken.ObjectType |
| 157 | +type, with a base of QRemoteObjectReplica's shiboken type. |
| 158 | + |
| 159 | +`ThermistatSource = repFile.source["Thermistat"]` should be a abstract class of |
| 160 | +Shiboken.ObjectType type, with a base of QObject's shiboken type. |
| 161 | + |
| 162 | +Both should support new classes based on their type to customize behavior. |
0 commit comments