diff options
Diffstat (limited to 'sources/pyside6/doc/developer')
| -rw-r--r-- | sources/pyside6/doc/developer/index.rst | 1 | ||||
| -rw-r--r-- | sources/pyside6/doc/developer/remoteobjects.md | 162 |
2 files changed, 163 insertions, 0 deletions
diff --git a/sources/pyside6/doc/developer/index.rst b/sources/pyside6/doc/developer/index.rst index bddd39d91..0260ad272 100644 --- a/sources/pyside6/doc/developer/index.rst +++ b/sources/pyside6/doc/developer/index.rst @@ -36,3 +36,4 @@ many features and implementation details that the project has: signature_doc.rst mypy-correctness.rst feature-motivation.rst + remoteobjects.md diff --git a/sources/pyside6/doc/developer/remoteobjects.md b/sources/pyside6/doc/developer/remoteobjects.md new file mode 100644 index 000000000..7d4c29aa0 --- /dev/null +++ b/sources/pyside6/doc/developer/remoteobjects.md @@ -0,0 +1,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. |
