Skip to content

Commit 19abd81

Browse files
stottleFriedemannKleint
authored andcommitted
Make Remote Objects usable beyond Models
While present, the Qt Remote Objects bindings to Python have not been very useful. The only usable components were those based on QAbstractItemModel, due to the lack of a way to interpret .rep files from Python. This addresses that limitation. Fixes: PYSIDE-862 Change-Id: Ice57c0c64f11c3c7e74d50ce3c48617bd9b422a3 Reviewed-by: Friedemann Kleint <[email protected]> Reviewed-by: Brett Stottlemyer <[email protected]>
1 parent 3c66c45 commit 19abd81

31 files changed

+3337
-5
lines changed

sources/pyside6/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ if(Qt${QT_MAJOR_VERSION}Qml_FOUND)
2525
add_subdirectory(libpysideqml)
2626
endif()
2727

28+
if(Qt${QT_MAJOR_VERSION}RemoteObjects_FOUND)
29+
add_subdirectory(libpysideremoteobjects)
30+
endif()
31+
2832
if(Qt${QT_MAJOR_VERSION}UiTools_FOUND)
2933
add_subdirectory(plugins/uitools)
3034
find_package(Qt6 COMPONENTS Designer)

sources/pyside6/PySide6/QtRemoteObjects/CMakeLists.txt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,23 @@ ${QtRemoteObjects_GEN_DIR}/qtroserveriodevice_wrapper.cpp
2929
${QtRemoteObjects_GEN_DIR}/qtremoteobjects_module_wrapper.cpp
3030
)
3131

32+
find_package(Qt6 REQUIRED COMPONENTS Core)
33+
3234
set(QtRemoteObjects_include_dirs ${QtRemoteObjects_SOURCE_DIR}
3335
${QtRemoteObjects_BINARY_DIR}
3436
${Qt${QT_MAJOR_VERSION}RemoteObjects_INCLUDE_DIRS}
37+
${libpysideremoteobjects_SOURCE_DIR}
3538
${SHIBOKEN_INCLUDE_DIR}
3639
${libpyside_SOURCE_DIR}
3740
${SHIBOKEN_PYTHON_INCLUDE_DIR}
3841
${QtCore_GEN_DIR}
3942
${QtNetwork_GEN_DIR})
4043

41-
set(QtRemoteObjects_libraries pyside6
42-
${Qt${QT_MAJOR_VERSION}RemoteObjects_LIBRARIES})
43-
4444
set(QtRemoteObjects_deps QtCore QtNetwork)
4545

46+
set(QtRemoteObjects_libraries pyside6 pyside6remoteobjects
47+
${Qt${QT_MAJOR_VERSION}RemoteObjects_LIBRARIES})
48+
4649
create_pyside_module(NAME QtRemoteObjects
4750
INCLUDE_DIRS QtRemoteObjects_include_dirs
4851
LIBRARIES QtRemoteObjects_libraries

sources/pyside6/PySide6/QtRemoteObjects/typesystem_remoteobjects.xml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
<load-typesystem name="templates/core_common.xml" generate="no"/>
99
<load-typesystem name="QtCore/typesystem_core.xml" generate="no"/>
1010
<load-typesystem name="QtNetwork/typesystem_network.xml" generate="no"/>
11+
<inject-code class="native" position="beginning">
12+
#include "pysideremoteobjects.h"
13+
</inject-code>
1114

1215
<rejection class="QRemoteObjectStringLiterals"/>
1316
<rejection class="*" function-name="getTypeNameAndMetaobjectFromClassInfo"/>
@@ -26,6 +29,10 @@
2629
</object-type>
2730
<object-type name="QRemoteObjectNode">
2831
<enum-type name="ErrorCode"/>
32+
<add-function signature="acquire(PyTypeObject*, PyObject* @name@ = 0)"
33+
return-type="PyTypeObject*">
34+
<inject-code class="target" file="../glue/qtremoteobjects.cpp" snippet="node-acquire"/>
35+
</add-function>
2936
</object-type>
3037
<object-type name="QRemoteObjectPendingCall">
3138
<enum-type name="Error"/>
@@ -35,7 +42,12 @@
3542
<object-type name="QRemoteObjectRegistryHost"/>
3643
<object-type name="QRemoteObjectReplica">
3744
<enum-type name="State"/>
38-
<!-- protected: <enum-type name="ConstructorType"/> -->
45+
<enum-type name="ConstructorType" python-type="IntEnum"/> <!-- Needed even though protected -->
46+
<modify-function signature="QRemoteObjectReplica(QRemoteObjectReplica::ConstructorType)">
47+
<modify-argument index="1">
48+
<replace-default-expression with="{}"/>
49+
</modify-argument>
50+
</modify-function>
3951
</object-type>
4052
<object-type name="QRemoteObjectSettingsStore"/>
4153
<value-type name="QRemoteObjectSourceLocationInfo"/>
@@ -53,4 +65,7 @@
5365
<!-- QtNetwork is pulled in via QtRemoteObjectsDepends. -->
5466
<suppress-warning text="^Scoped enum 'Q(Ocsp)|(Dtls).*' does not have a type entry.*$"/>
5567

68+
<inject-code class="target" position="end"
69+
file="../glue/qtremoteobjects.cpp" snippet="qtro-init"/>
70+
5671
</typesystem>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (C) 2024 Ford Motor Company
2+
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3+
4+
// @snippet qtro-init
5+
PySide::RemoteObjects::init(module);
6+
// @snippet qtro-init
7+
8+
// @snippet node-acquire
9+
auto *typeObject = reinterpret_cast<PyTypeObject*>(%PYARG_1);
10+
if (!PySide::inherits(typeObject, SbkPySide6_QtRemoteObjectsTypeStructs[SBK_QRemoteObjectReplica_IDX].fullName)) {
11+
PyErr_SetString(PyExc_TypeError, "First argument must be a type deriving from QRemoteObjectReplica.");
12+
return nullptr;
13+
}
14+
15+
static PyObject *pyConstructWithNode = Shiboken::Enum::newItem(
16+
Shiboken::Module::get(SbkPySide6_QtRemoteObjectsTypeStructs[SBK_QRemoteObjectReplica_ConstructorType_IDX]),
17+
1 /* protected QRemoteObjectReplica::ConstructorType::ConstructWithNode */
18+
);
19+
20+
Shiboken::AutoDecRef args;
21+
if (pyArgs[1])
22+
args.reset(PyTuple_Pack(3, %PYSELF, pyConstructWithNode, pyArgs[1]));
23+
else
24+
args.reset(PyTuple_Pack(2, %PYSELF, pyConstructWithNode));
25+
26+
PyObject *instance = PyObject_CallObject(%PYARG_1, args.object());
27+
if (!instance)
28+
return nullptr; // Propagate the exception
29+
30+
%PYARG_0 = instance;
31+
// @snippet node-acquire

sources/pyside6/cmake/Macros/PySideModules.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ macro(create_pyside_module)
297297
set(ld_prefix_list "")
298298
list(APPEND ld_prefix_list "${pysidebindings_BINARY_DIR}/libpyside")
299299
list(APPEND ld_prefix_list "${pysidebindings_BINARY_DIR}/libpysideqml")
300+
list(APPEND ld_prefix_list "${pysidebindings_BINARY_DIR}/libpysideremoteobjects")
300301
list(APPEND ld_prefix_list "${SHIBOKEN_SHARED_LIBRARY_DIR}")
301302
if(WIN32)
302303
list(APPEND ld_prefix_list "${QT6_INSTALL_PREFIX}/${QT6_INSTALL_BINS}")

sources/pyside6/cmake/PySideSetup.cmake

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,9 @@ endforeach()
199199
# Whether to add libpysideqml
200200
find_package(Qt6 COMPONENTS Qml)
201201

202+
# Whether to add libpysideremoteobjects
203+
find_package(Qt6 COMPONENTS RemoteObjects)
204+
202205
string(REGEX MATCHALL "[0-9]+" qt_version_helper "${Qt${QT_MAJOR_VERSION}Core_VERSION}")
203206

204207
list(GET qt_version_helper 0 QT_VERSION_MAJOR)

sources/pyside6/doc/developer/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,4 @@ many features and implementation details that the project has:
3636
signature_doc.rst
3737
mypy-correctness.rst
3838
feature-motivation.rst
39+
remoteobjects.md
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Copyright (C) 2025 Ford Motor Company
2+
# SPDX-License-Identifier: BSD-3-Clause
3+
4+
if (NOT CMAKE_MINIMUM_REQUIRED_VERSION)
5+
cmake_minimum_required(VERSION 3.18)
6+
cmake_policy(VERSION 3.18)
7+
endif()
8+
9+
project(libpysideremoteobjects LANGUAGES CXX)
10+
11+
if (NOT libpyside_SOURCE_DIR) # Building standalone
12+
message(STATUS "Building standalone. Setting C++ standard and build type.")
13+
set(CMAKE_CXX_STANDARD 17)
14+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
15+
if(NOT CMAKE_BUILD_TYPE)
16+
set(CMAKE_BUILD_TYPE Release)
17+
endif()
18+
find_package(Python3 REQUIRED COMPONENTS Interpreter Development)
19+
find_package(Shiboken6 REQUIRED)
20+
find_package(libpyside REQUIRED)
21+
get_target_property(pyside6_SOURCE_DIR PySide6::pyside6 INTERFACE_INCLUDE_DIRECTORIES)
22+
endif()
23+
24+
find_package(Qt6 REQUIRED COMPONENTS Core RepParser RemoteObjects)
25+
26+
set(libpysideremoteobjects_HEADERS
27+
pysidecapsulemethod_p.h
28+
pysidedynamicclass_p.h
29+
pysidedynamiccommon_p.h
30+
pysidedynamicenum_p.h
31+
pysidedynamicpod_p.h
32+
pysiderephandler_p.h
33+
)
34+
35+
set(libpysideremoteobjects_SRC
36+
pysiderephandler.cpp
37+
pysidecapsulemethod.cpp
38+
pysidedynamiccommon.cpp
39+
pysidedynamicclass.cpp
40+
pysidedynamicpod.cpp
41+
pysidedynamicenum.cpp
42+
${libpysideremoteobjects_HEADERS}
43+
)
44+
45+
list(GET Qt6RepParser_INCLUDE_DIRS 0 REPPARSER_DIR)
46+
47+
include(QtTargetHelpers)
48+
include(QtTestHelpers)
49+
include(QtLalrHelpers)
50+
add_library(pyside6remoteobjects STATIC ${libpysideremoteobjects_SRC})
51+
52+
target_include_directories(pyside6remoteobjects PRIVATE
53+
${REPPARSER_DIR}
54+
${Qt${QT_VERSION_MAJOR}Core_PRIVATE_INCLUDE_DIRS}
55+
${Qt${QT_MAJOR_VERSION}RemoteObjects_INCLUDE_DIRS}
56+
${Qt${QT_MAJOR_VERSION}RemoteObjects_PRIVATE_INCLUDE_DIRS}
57+
${pyside6_SOURCE_DIR} # Added internally by the create_pyside_module function
58+
${SHIBOKEN_INCLUDE_DIR}
59+
${libpyside_SOURCE_DIR}
60+
${SHIBOKEN_PYTHON_INCLUDE_DIR}
61+
${CMAKE_CURRENT_BINARY_DIR} # Include the component-specific build directory
62+
)
63+
64+
target_link_libraries(pyside6remoteobjects PRIVATE
65+
Shiboken6::libshiboken # Added internally by the create_pyside_module function
66+
Qt6::Core
67+
Qt6::RemoteObjectsPrivate
68+
)
69+
70+
qt_process_qlalr(
71+
pyside6remoteobjects
72+
"${REPPARSER_DIR}/parser.g"
73+
""
74+
)
75+
76+
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D QT_NO_CAST_FROM_ASCII -D QT_NO_CAST_TO_ASCII")
77+
78+
#
79+
# install stuff
80+
#
81+
82+
install(FILES ${libpysideremoteobjects_HEADERS}
83+
DESTINATION include/${BINDING_NAME}${pyside6remoteobjects_SUFFIX})
84+
85+
install(TARGETS pyside6remoteobjects EXPORT PySide6RemoteObjectsTargets
86+
LIBRARY DESTINATION "${LIB_INSTALL_DIR}"
87+
ARCHIVE DESTINATION "${LIB_INSTALL_DIR}"
88+
RUNTIME DESTINATION bin)

0 commit comments

Comments
 (0)