aboutsummaryrefslogtreecommitdiffstats
path: root/sources/pyside6/doc/developer/remoteobjects.md
blob: 7d4c29aa02db2da4942ae3bdb72a1aef29585699 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# Qt Remote Objects Overview

[Qt Remote Objects](https://doc.qt.io/qt-6/qtremoteobjects-index.html) (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_](https://doc.qt.io/qt-6/qtremoteobjects-replica.html) QObject, making
the Replica a surrogate for the real QOject in your program (called the
[_Source_](https://doc.qt.io/qt-6/qtremoteobjects-source.html)). 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_](https://doc.qt.io/qt-6/qtremoteobjects-node.html). Nodes handle the details of
connecting processes or devices. A Replica is created by calling
[acquire()](https://doc.qt.io/qt-6/qremoteobjectnode.html#acquire) on a Node, and Sources
are shared on the network using
[enableRemoting()](https://doc.qt.io/qt-6/qremoteobjecthostbase.html#enableRemoting).

## Replicas are _latent copies_

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

```python
# 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.

```python
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)](https://doc.qt.io/qt-6/qtremoteobjects-repc.html) 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:
```cpp
class Thermistat
{
    PROP(int temp)
}
```

The desired interface would be
```python
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:

```python
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.

```python
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.

```python
    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.