aboutsummaryrefslogtreecommitdiffstats
path: root/sources/pyside6/libpysideremoteobjects
diff options
context:
space:
mode:
Diffstat (limited to 'sources/pyside6/libpysideremoteobjects')
-rw-r--r--sources/pyside6/libpysideremoteobjects/CMakeLists.txt88
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysidecapsulemethod.cpp230
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysidecapsulemethod_p.h87
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysidedynamicclass.cpp506
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysidedynamicclass_p.h15
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysidedynamiccommon.cpp124
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysidedynamiccommon_p.h89
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysidedynamicenum.cpp158
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysidedynamicenum_p.h15
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysidedynamicpod.cpp260
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysidedynamicpod_p.h15
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysideremoteobjects.h16
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysiderephandler.cpp459
-rw-r--r--sources/pyside6/libpysideremoteobjects/pysiderephandler_p.h35
14 files changed, 2097 insertions, 0 deletions
diff --git a/sources/pyside6/libpysideremoteobjects/CMakeLists.txt b/sources/pyside6/libpysideremoteobjects/CMakeLists.txt
new file mode 100644
index 000000000..f73eba6ee
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/CMakeLists.txt
@@ -0,0 +1,88 @@
+# Copyright (C) 2025 Ford Motor Company
+# SPDX-License-Identifier: BSD-3-Clause
+
+if (NOT CMAKE_MINIMUM_REQUIRED_VERSION)
+ cmake_minimum_required(VERSION 3.18)
+ cmake_policy(VERSION 3.18)
+endif()
+
+project(libpysideremoteobjects LANGUAGES CXX)
+
+if (NOT libpyside_SOURCE_DIR) # Building standalone
+ message(STATUS "Building standalone. Setting C++ standard and build type.")
+ set(CMAKE_CXX_STANDARD 17)
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
+ if(NOT CMAKE_BUILD_TYPE)
+ set(CMAKE_BUILD_TYPE Release)
+ endif()
+ find_package(Python3 REQUIRED COMPONENTS Interpreter Development)
+ find_package(Shiboken6 REQUIRED)
+ find_package(libpyside REQUIRED)
+ get_target_property(pyside6_SOURCE_DIR PySide6::pyside6 INTERFACE_INCLUDE_DIRECTORIES)
+endif()
+
+find_package(Qt6 REQUIRED COMPONENTS Core RepParser RemoteObjects)
+
+set(libpysideremoteobjects_HEADERS
+ pysidecapsulemethod_p.h
+ pysidedynamicclass_p.h
+ pysidedynamiccommon_p.h
+ pysidedynamicenum_p.h
+ pysidedynamicpod_p.h
+ pysiderephandler_p.h
+)
+
+set(libpysideremoteobjects_SRC
+ pysiderephandler.cpp
+ pysidecapsulemethod.cpp
+ pysidedynamiccommon.cpp
+ pysidedynamicclass.cpp
+ pysidedynamicpod.cpp
+ pysidedynamicenum.cpp
+ ${libpysideremoteobjects_HEADERS}
+)
+
+list(GET Qt6RepParser_INCLUDE_DIRS 0 REPPARSER_DIR)
+
+include(QtTargetHelpers)
+include(QtTestHelpers)
+include(QtLalrHelpers)
+add_library(pyside6remoteobjects STATIC ${libpysideremoteobjects_SRC})
+
+target_include_directories(pyside6remoteobjects PRIVATE
+ ${REPPARSER_DIR}
+ ${Qt${QT_VERSION_MAJOR}Core_PRIVATE_INCLUDE_DIRS}
+ ${Qt${QT_MAJOR_VERSION}RemoteObjects_INCLUDE_DIRS}
+ ${Qt${QT_MAJOR_VERSION}RemoteObjects_PRIVATE_INCLUDE_DIRS}
+ ${pyside6_SOURCE_DIR} # Added internally by the create_pyside_module function
+ ${SHIBOKEN_INCLUDE_DIR}
+ ${libpyside_SOURCE_DIR}
+ ${SHIBOKEN_PYTHON_INCLUDE_DIR}
+ ${CMAKE_CURRENT_BINARY_DIR} # Include the component-specific build directory
+)
+
+target_link_libraries(pyside6remoteobjects PRIVATE
+ Shiboken6::libshiboken # Added internally by the create_pyside_module function
+ Qt6::Core
+ Qt6::RemoteObjectsPrivate
+)
+
+qt_process_qlalr(
+ pyside6remoteobjects
+ "${REPPARSER_DIR}/parser.g"
+ ""
+)
+
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D QT_NO_CAST_FROM_ASCII -D QT_NO_CAST_TO_ASCII")
+
+#
+# install stuff
+#
+
+install(FILES ${libpysideremoteobjects_HEADERS}
+ DESTINATION include/${BINDING_NAME}${pyside6remoteobjects_SUFFIX})
+
+install(TARGETS pyside6remoteobjects EXPORT PySide6RemoteObjectsTargets
+ LIBRARY DESTINATION "${LIB_INSTALL_DIR}"
+ ARCHIVE DESTINATION "${LIB_INSTALL_DIR}"
+ RUNTIME DESTINATION bin)
diff --git a/sources/pyside6/libpysideremoteobjects/pysidecapsulemethod.cpp b/sources/pyside6/libpysideremoteobjects/pysidecapsulemethod.cpp
new file mode 100644
index 000000000..d5a5454f0
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysidecapsulemethod.cpp
@@ -0,0 +1,230 @@
+// Copyright (C) 2025 Ford Motor Company
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "pysidecapsulemethod_p.h"
+
+extern "C"
+{
+
+// This struct is used for both CapsuleMethod and CapsuleProperty
+struct CapsuleDescriptor
+{
+ PyTypeObject base;
+ PyObject *capsule;
+ PyMethodDef methodDef;
+
+ void configure(PyObject *capsule, PyMethodDef *method)
+ {
+ this->capsule = capsule;
+ Py_INCREF(capsule);
+ // We make a copy of the input name and doc strings so they can be temporary on
+ // the input.
+ if (method->ml_name)
+ methodDef.ml_name = strdup(method->ml_name);
+ if (method->ml_doc)
+ methodDef.ml_doc = strdup(method->ml_doc);
+ methodDef.ml_meth = method->ml_meth;
+ methodDef.ml_flags = method->ml_flags;
+ }
+};
+
+static PyObject *CapsuleDescriptor_tp_new(PyTypeObject *type, PyObject * /* args */, PyObject * /* kwds */);
+static void CapsuleDescriptor_free(PyObject *self);
+static PyObject *CapsuleMethod_descr_get(PyObject *self, PyObject *instance, PyObject * /* owner */);
+static PyObject *CapsuleProperty_descr_get(PyObject *self, PyObject *instance, PyObject * /* owner */);
+static int CapsuleProperty_descr_set(PyObject *self, PyObject *instance, PyObject * /* owner */);
+
+/**
+ * We are creating two related types, CapsuleMethod and CapsuleProperty, that are
+ * used to enable lambda-like behavior. The difference is in usage, where
+ * CapsuleMethod's __get__ function returns a Callable (i.e., method-like usage:
+ * obj.capsuleMethodName(args)) and only supports the __get__ method.
+ * CapsuleProperty on the other hand is used for properties, and supports both
+ * __get__ and __set__ methods (i.e., obj.capsulePropertyName = value or val =
+ * obj.capsulePropertyName).
+ */
+static PyTypeObject *createCapsuleMethodType()
+{
+ PyType_Slot CapsuleMethodType_slots[] = {
+ {Py_tp_new, reinterpret_cast<void *>(CapsuleDescriptor_tp_new)},
+ {Py_tp_descr_get, reinterpret_cast<void *>(CapsuleMethod_descr_get)},
+ {Py_tp_free, reinterpret_cast<void *>(CapsuleDescriptor_free)},
+ {0, nullptr}
+ };
+
+ PyType_Spec CapsuleMethodType_spec = {
+ "2:PySide6.QtRemoteObjects.CapsuleMethod",
+ sizeof(CapsuleDescriptor),
+ 0,
+ Py_TPFLAGS_DEFAULT,
+ CapsuleMethodType_slots};
+
+ PyObject *type = PyType_FromSpec(&CapsuleMethodType_spec);
+ if (!type) {
+ PyErr_Print();
+ return nullptr;
+ }
+ return reinterpret_cast<PyTypeObject*>(type);
+}
+
+PyTypeObject *CapsuleMethod_TypeF(void)
+{
+ static auto *type = createCapsuleMethodType();
+ return type;
+}
+
+static PyTypeObject *createCapsulePropertyType(bool isWritable)
+{
+ PyType_Slot WritablePropertyType_slots[] = {
+ {Py_tp_new, reinterpret_cast<void *>(CapsuleDescriptor_tp_new)},
+ {Py_tp_descr_get, reinterpret_cast<void *>(CapsuleProperty_descr_get)},
+ {Py_tp_descr_set, reinterpret_cast<void *>(CapsuleProperty_descr_set)},
+ {Py_tp_free, reinterpret_cast<void *>(CapsuleDescriptor_free)},
+ {0, nullptr}
+ };
+
+ PyType_Slot ReadOnlyPropertyType_slots[] = {
+ {Py_tp_new, reinterpret_cast<void *>(CapsuleDescriptor_tp_new)},
+ {Py_tp_descr_get, reinterpret_cast<void *>(CapsuleProperty_descr_get)},
+ {Py_tp_free, reinterpret_cast<void *>(CapsuleDescriptor_free)},
+ {0, nullptr}
+ };
+
+ PyType_Spec CapsulePropertyType_spec = {
+ "2:PySide6.QtRemoteObjects.CapsuleProperty",
+ sizeof(CapsuleDescriptor),
+ 0,
+ Py_TPFLAGS_DEFAULT,
+ isWritable ? WritablePropertyType_slots : ReadOnlyPropertyType_slots};
+
+ PyObject *type = PyType_FromSpec(&CapsulePropertyType_spec);
+ if (!type) {
+ PyErr_Print();
+ return nullptr;
+ }
+ return reinterpret_cast<PyTypeObject*>(type);
+}
+
+PyTypeObject *CapsuleProperty_TypeF(bool isWritable=false)
+{
+ if (isWritable) {
+ static auto *type = createCapsulePropertyType(true);
+ return type;
+ }
+ static auto *type = createCapsulePropertyType(false);
+ return type;
+}
+
+static PyObject *CapsuleDescriptor_tp_new(PyTypeObject *type, PyObject * /* args */, PyObject * /* kwds */)
+{
+ auto *self = reinterpret_cast<CapsuleDescriptor *>(PyType_GenericAlloc(type, 0));
+ if (self != nullptr) {
+ self->capsule = nullptr;
+ self->methodDef = {nullptr, nullptr, METH_NOARGS, nullptr}; // Initialize methodDef
+ }
+ return reinterpret_cast<PyObject *>(self);
+}
+
+static void CapsuleDescriptor_free(PyObject *self)
+{
+ auto *d = reinterpret_cast<CapsuleDescriptor *>(self);
+ Py_XDECREF(d->capsule);
+ free(const_cast<char*>(d->methodDef.ml_name));
+ free(const_cast<char*>(d->methodDef.ml_doc));
+}
+
+static PyObject *CapsuleMethod_descr_get(PyObject *self, PyObject *instance, PyObject * /* owner */)
+{
+ if (instance == nullptr) {
+ // Return the descriptor object if accessed from the class
+ Py_INCREF(self);
+ return self;
+ }
+
+ auto *d = reinterpret_cast<CapsuleDescriptor *>(self);
+ CapsuleDescriptorData *data = new CapsuleDescriptorData{instance, d->capsule};
+ PyObject *payload = PyCapsule_New(data, "Payload", [](PyObject *capsule) {
+ delete reinterpret_cast<CapsuleDescriptorData *>(PyCapsule_GetPointer(capsule, "Payload"));
+ });
+ if (!payload)
+ return nullptr;
+
+ Py_INCREF(payload);
+ return PyCFunction_New(&d->methodDef, payload);
+}
+
+bool add_capsule_method_to_type(PyTypeObject *type, PyMethodDef *method, PyObject *capsule)
+{
+ if (PyType_Ready(type) < 0) {
+ PyErr_Print();
+ return false;
+ }
+ auto *descriptor = reinterpret_cast<CapsuleDescriptor *>(
+ PyObject_CallObject(reinterpret_cast<PyObject *>(CapsuleMethod_TypeF()), nullptr));
+ if (!descriptor) {
+ PyErr_Print();
+ return false;
+ }
+ descriptor->configure(capsule, method);
+
+ auto *descr = reinterpret_cast<PyObject *>(descriptor);
+ if (PyObject_SetAttrString(reinterpret_cast<PyObject *>(type), method->ml_name, descr) < 0) {
+ PyErr_Print();
+ return false;
+ }
+ return true;
+}
+
+static PyObject *CapsuleProperty_descr_get(PyObject *self, PyObject *instance, PyObject * /* owner */)
+{
+ if (instance == nullptr) {
+ // Return the descriptor object if accessed from the class
+ Py_INCREF(self);
+ return self;
+ }
+
+ auto *d = reinterpret_cast<CapsuleDescriptor *>(self);
+ CapsuleDescriptorData *data = new CapsuleDescriptorData{instance, d->capsule};
+ PyObject *payload = PyCapsule_New(data, "Payload", [](PyObject *capsule) {
+ delete reinterpret_cast<CapsuleDescriptorData *>(PyCapsule_GetPointer(capsule, "Payload"));
+ });
+ if (!payload)
+ return nullptr;
+
+ return PyObject_CallFunctionObjArgs(PyCFunction_New(&d->methodDef, payload), nullptr);
+}
+
+static int CapsuleProperty_descr_set(PyObject *self, PyObject *instance, PyObject *value)
+{
+ auto *d = reinterpret_cast<CapsuleDescriptor *>(self);
+ CapsuleDescriptorData *data = new CapsuleDescriptorData{instance, d->capsule};
+ PyObject *payload = PyCapsule_New(data, "Payload", [](PyObject *capsule) {
+ delete reinterpret_cast<CapsuleDescriptorData *>(PyCapsule_GetPointer(capsule, "Payload"));
+ });
+ if (!payload)
+ return -1;
+
+ Py_INCREF(payload);
+ PyObject *result = PyObject_CallFunctionObjArgs(PyCFunction_New(&d->methodDef, payload),
+ value, nullptr);
+ if (!result)
+ return -1;
+
+ Py_DECREF(result);
+ return 0;
+}
+
+// Returns a new CapsuleProperty descriptor object for use with PySideProperty
+PyObject *make_capsule_property(PyMethodDef *method, PyObject *capsule, bool isWritable)
+{
+ auto *type = CapsuleProperty_TypeF(isWritable);
+ auto *descriptor = PyObject_CallObject(reinterpret_cast<PyObject *>(type), nullptr);
+ if (!descriptor)
+ return nullptr;
+
+ reinterpret_cast<CapsuleDescriptor*>(descriptor)->configure(capsule, method);
+
+ return descriptor;
+}
+
+} // extern "C"
diff --git a/sources/pyside6/libpysideremoteobjects/pysidecapsulemethod_p.h b/sources/pyside6/libpysideremoteobjects/pysidecapsulemethod_p.h
new file mode 100644
index 000000000..7b6abc54b
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysidecapsulemethod_p.h
@@ -0,0 +1,87 @@
+// Copyright (C) 2025 Ford Motor Company
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef PYSIDE_CAPSULEMETHOD_P_H
+#define PYSIDE_CAPSULEMETHOD_P_H
+
+#include <sbkpython.h>
+
+extern "C"
+{
+
+/**
+ * This code is needed to solve, in C++ and adhering to the stable API,
+ * creating what are in effect lambda functions as instance methods on custom
+ * types. The goal is to be able to add methods to a dynamic type. If the .rep
+ * file defines a slot `mySlot`, it need to be added to the dynamic type. For
+ * Source types, this should be an abstract method that raises a
+ * NotImplementedError unless defined in the Python subclass. For Replica
+ * types, this should include an implementation that forwards the request
+ * through the underlying QRemoteObjectReplica instance.
+ *
+ * The stable API doesn't currently provide a way define a method that can
+ * receive both the `self`, `args`, and runtime (but constant per method, i.e.,
+ * lambda like) data using Py_tp_methods. Possibly post 3.13 when METH_METHOD is
+ * part of the stable API. But for now, it is not.
+ *
+ * The solution is to create a custom descriptor
+ * (https://docs.python.org/3/howto/descriptor.html) that can hold the runtime
+ * data and then when called, will return a PyCFunction_New generated PyObject
+ * that is passed both class instance `self` and the runtime data (a PyCapsule)
+ * together as a tuple as a new `self` for the method. The static method
+ * definition needs to expect and handle this, but when combined in C++, we can
+ * define a single handler that receives both the original `self` of the instance
+ * and the runtime capsule with data for handling.
+ */
+
+/**
+ * The CapsuleDescriptorData struct is what will be passed as the pseudo `self`
+ * from a CapsuleMethod or CapsuleProperty to the associated handler method. The
+ * handler method (which should look like a standard PyMethodDef method) should
+ * parse it into the payload (the "lambda variables") and the actual instance
+ * (the "self").
+ */
+struct CapsuleDescriptorData
+{
+ PyObject *self;
+ PyObject *payload;
+};
+
+/**
+ * The new type defining a descriptor that stores a PyCapsule. This is used to
+ * store the runtime data, with the __get__ method returning a new Callable.
+ */
+PyTypeObject *CapsuleMethod_TypeF(void);
+
+/**
+ * The new type defining a descriptor that stores a PyCapsule. This is used to
+ * store the runtime data, with the __get__ (and __set__ if isWritable) providing
+ * property behavior.
+ */
+PyTypeObject *CapsuleProperty_TypeF(bool isWritable);
+
+/**
+ * Add a capsule method (a descriptor) to a type. This will create a new capsule
+ * method descriptor and add it as an attribute to the type, using the given name.
+ *
+ * A single handle can then respond to what appear to be distinct methods on the
+ * type, but using the runtime data (from the capsule) when handling each call.
+ *
+ * @param type The type to attach the created descriptor to.
+ * @param method The method definition to associate with the descriptor.
+ * The name of the method will be used as the attribute name.
+ * @param capsule The capsule to store in the descriptor.
+ * @return True if the descriptor was added successfully, false otherwise.
+ */
+bool add_capsule_method_to_type(PyTypeObject *type, PyMethodDef *method,
+ PyObject *capsule);
+
+/**
+ * Make a new CapsuleProperty type.
+ */
+PyObject *make_capsule_property(PyMethodDef *method, PyObject *capsule,
+ bool isWritable = false);
+
+} // extern "C"
+
+#endif // PYSIDE_CAPSULEMETHOD_P_H
diff --git a/sources/pyside6/libpysideremoteobjects/pysidedynamicclass.cpp b/sources/pyside6/libpysideremoteobjects/pysidedynamicclass.cpp
new file mode 100644
index 000000000..941e38c6e
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysidedynamicclass.cpp
@@ -0,0 +1,506 @@
+// Copyright (C) 2025 Ford Motor Company
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+// Workaround to access protected functions. PySide builds with this, but
+// since this is now a separate library, we need to add it too.
+
+#include "pysidedynamiccommon_p.h"
+#include "pysidedynamicclass_p.h"
+#include "pysidecapsulemethod_p.h"
+#include "pysiderephandler_p.h"
+
+#include <basewrapper.h>
+#include <sbkconverter.h>
+#include <sbkstring.h>
+
+#include <pyside_p.h>
+#include <pysideproperty.h>
+#include <pysideqobject.h>
+#include <pysidesignal.h>
+#include <pysideutils.h>
+
+#include <QtCore/qmetaobject.h>
+#include <QtCore/qvariantlist.h>
+
+#include <QtRemoteObjects/qremoteobjectpendingcall.h>
+#include <QtRemoteObjects/qremoteobjectreplica.h>
+
+using namespace Shiboken;
+
+class FriendlyReplica : public QRemoteObjectReplica
+{
+public:
+ using QRemoteObjectReplica::send;
+ using QRemoteObjectReplica::setProperties;
+ using QRemoteObjectReplica::propAsVariant;
+ using QRemoteObjectReplica::sendWithReply;
+};
+
+extern "C"
+{
+
+PyObject *propertiesAttr()
+{
+ static PyObject *const s = Shiboken::String::createStaticString("__PROPERTIES__");
+ return s;
+}
+
+struct SourceDefs
+{
+ static PyTypeObject *getSbkType()
+ {
+ static PyTypeObject *sbkType =
+ Shiboken::Conversions::getPythonTypeObject("QObject");
+ return sbkType;
+ }
+
+ static PyObject *getBases()
+ {
+ static PyObject *bases = PyTuple_Pack(1, getSbkType());
+ return bases;
+ }
+
+ static const char *getTypePrefix()
+ {
+ return "2:PySide6.QtRemoteObjects.DynamicSource.";
+ }
+
+ static int tp_init(PyObject *self, PyObject *args, PyObject *kwds)
+ {
+ static initproc initFunc = reinterpret_cast<initproc>(PepType_GetSlot(getSbkType(), Py_tp_init));
+ int res = initFunc(self, args, kwds);
+ if (res < 0) {
+ PyErr_Print();
+ return res;
+ }
+
+ // Get the properties from the type
+ PyTypeObject *type = Py_TYPE(self);
+ auto *pyProperties = PyObject_GetAttr(reinterpret_cast<PyObject *>(type), propertiesAttr());
+ if (!pyProperties) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to get properties from type");
+ return -1;
+ }
+ // Add a copy of the properties to the object
+ auto *propPtr = reinterpret_cast<QVariantList *>(PyCapsule_GetPointer(pyProperties, nullptr));
+ auto *propertiesCopy = new QVariantList(*propPtr);
+ PyObject *capsule = PyCapsule_New(propertiesCopy, nullptr, [](PyObject *capsule) {
+ delete reinterpret_cast<QVariantList *>(PyCapsule_GetPointer(capsule, nullptr));
+ });
+ PyObject_SetAttr(self, propertiesAttr(), capsule);
+ Py_DECREF(capsule);
+ return res;
+ }
+
+ static PyObject *capsule_method_handler(PyObject *payload, PyObject *args)
+ {
+ auto *methodData = reinterpret_cast<CapsuleDescriptorData *>(PyCapsule_GetPointer(payload,
+ "Payload"));
+ if (!methodData) {
+ PyErr_SetString(PyExc_RuntimeError, "Invalid call to dynamic method. Missing payload.");
+ return nullptr;
+ }
+ PyObject *self = methodData->self;
+ if (PyCapsule_IsValid(methodData->payload, "PropertyCapsule")) {
+ // Handle property getter/setter against our hidden properties attribute
+ auto *capsule = PyCapsule_GetPointer(methodData->payload, "PropertyCapsule");
+ if (capsule) {
+ auto *ob_dict = SbkObject_GetDict_NoRef(self);
+ auto *propPtr = PyCapsule_GetPointer(PyDict_GetItem(ob_dict, propertiesAttr()),
+ nullptr);
+ auto *currentProperties = reinterpret_cast<QVariantList *>(propPtr);
+ auto *callData = reinterpret_cast<PropertyCapsule *>(capsule);
+ if (callData->indexInObject < 0
+ || callData->indexInObject >= currentProperties->size()) {
+ PyErr_Format(PyExc_RuntimeError, "Unknown property method: %s",
+ callData->name.constData());
+ return nullptr;
+ }
+ const QVariant &currentVariant = currentProperties->at(callData->indexInObject);
+
+ // Handle getter
+ if (PyTuple_Size(args) == 0)
+ return toPython(currentVariant);
+
+ // Handle setter
+ if (PyTuple_Size(args) != 1) {
+ PyErr_SetString(PyExc_TypeError, "Property setter takes exactly one argument");
+ return nullptr;
+ }
+ Conversions::SpecificConverter converter(currentVariant.metaType().name());
+ QVariant variant{currentVariant.metaType()};
+ auto metaType = currentVariant.metaType();
+ if (metaType.flags().testFlag(QMetaType::IsEnumeration)) {
+ converter.toCpp(PyTuple_GetItem(args, 0), variant.data());
+ variant.convert(metaType);
+ } else {
+ converter.toCpp(PyTuple_GetItem(args, 0), variant.data());
+ }
+ if (PyErr_Occurred()) // POD conversion can produce an error
+ return nullptr;
+ if (variant == currentVariant)
+ Py_RETURN_NONE;
+
+ currentProperties->replace(callData->indexInObject, variant);
+ // Get the QMetaObject and emit the property changed signal if there is one
+ const auto *metaObject = PySide::retrieveMetaObject(self);
+ auto metaProperty = metaObject->property(callData->propertyIndex);
+ if (metaProperty.hasNotifySignal()) {
+ // We know our custom types don't have multiple cpp objects
+ void *cptr = reinterpret_cast<SbkObject *>(self)->d->cptr[0];
+ auto *qObject = reinterpret_cast<QObject *>(cptr);
+ void *_args[] = {nullptr, variant.data()};
+ QMetaObject::activate(qObject, metaProperty.notifySignalIndex(), _args);
+ }
+ Py_RETURN_NONE;
+ }
+ }
+ if (PyCapsule_IsValid(methodData->payload, "MethodCapsule")) {
+ auto *capsule = PyCapsule_GetPointer(methodData->payload, "MethodCapsule");
+ auto *callData = reinterpret_cast<MethodCapsule *>(capsule);
+ if (callData->name.startsWith("push") && callData->name.size() > 4) {
+ const auto *metaObject = PySide::retrieveMetaObject(self);
+ // The convention for QtRO is if a property is named "something" and uses
+ // push, the name of the push method will be "pushSomething". But it is
+ // possible the name would be "Something", so we need to check upper
+ // and lower case.
+ auto name = callData->name.sliced(4);
+ auto index = metaObject->indexOfProperty(name.constData());
+ if (index < 0) {
+ name[0] = tolower(name[0]); // Try lower case
+ index = metaObject->indexOfProperty(name.constData());
+ }
+ // It is possible a .rep names a Slot "push" or "pushSomething" that
+ // isn't generated for a property. Let that fall through to regular
+ // method handling.
+ if (index >= 0) {
+ // Call the custom descriptor's set method
+ auto result = PyObject_SetAttrString(self, name.constData(),
+ PyTuple_GetItem(args, 0));
+ if (result < 0) {
+ PyErr_Print();
+ return nullptr;
+ }
+ Py_RETURN_NONE;
+ }
+ }
+ // TODO: This doesn't do much, as it is "eaten" by a PyError_Print in
+ // SignalManager::handleMetaCallError()
+ // Is there a better way to address slots that need to be implemented?
+ PyErr_Format(PyExc_NotImplementedError, "** The method %s is not implemented",
+ callData->name.constData());
+ return nullptr;
+ }
+
+ PyErr_SetString(PyExc_RuntimeError, "Unknown capsule type");
+ return nullptr;
+ }
+};
+
+struct ReplicaDefs
+{
+ static PyTypeObject *getSbkType()
+ {
+ static PyTypeObject *sbkType =
+ Shiboken::Conversions::getPythonTypeObject("QRemoteObjectReplica");
+ return sbkType;
+ }
+
+ static PyObject *getBases()
+ {
+ static PyObject *bases = PyTuple_Pack(1, getSbkType());
+ return bases;
+ }
+
+ static const char *getTypePrefix()
+ {
+ return "2:PySide6.QtRemoteObjects.DynamicReplica.";
+ }
+
+ static int tp_init(PyObject *self, PyObject *args, PyObject *kwds)
+ {
+ static initproc initFunc = reinterpret_cast<initproc>(PepType_GetSlot(getSbkType(),
+ Py_tp_init));
+ QRemoteObjectReplica *replica = nullptr;
+ if (PyTuple_Size(args) == 0) {
+ if (initFunc(self, args, kwds) < 0)
+ return -1;
+ Shiboken::Conversions::pythonToCppPointer(getSbkType(), self, &replica);
+ } else { // Process replica with arguments passed from the added node.acquire method
+ PyObject *node = nullptr;
+ PyObject *constructorType = nullptr;
+ PyObject *name = nullptr;
+ static PyTypeObject *nodeType = Shiboken::Conversions::getPythonTypeObject("QRemoteObjectNode");
+ if (!PyArg_UnpackTuple(args, "Replica.__init__", 2, 3, &node, &constructorType, &name) ||
+ !PySide::inherits(Py_TYPE(node), nodeType->tp_name)) {
+ PyErr_SetString(PyExc_TypeError,
+ "Replicas can be initialized with no arguments or by node.acquire only");
+ return -1;
+ }
+ static auto *constructorArgs = PyTuple_Pack(1, constructorType);
+ if (initFunc(self, constructorArgs, kwds) < 0)
+ return -1;
+ if (name)
+ PyObject_CallMethod(self, "initializeNode", "OO", node, name);
+ else
+ PyObject_CallMethod(self, "initializeNode", "O", node);
+ Shiboken::Conversions::pythonToCppPointer(getSbkType(), self, &replica);
+ }
+ if (!replica) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to initialize replica");
+ return -1;
+ }
+ // Get the properties from the type
+ PyTypeObject *type = Py_TYPE(self);
+ auto *pyProperties = PyObject_GetAttr(reinterpret_cast<PyObject *>(type), propertiesAttr());
+ if (!pyProperties) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to get properties from type");
+ return -1;
+ }
+ // Make a copy of the properties and set them on the replica
+ auto *propPtr = reinterpret_cast<QVariantList *>(PyCapsule_GetPointer(pyProperties, nullptr));
+ auto propertiesCopy = QVariantList(*propPtr);
+ static_cast<FriendlyReplica *>(replica)->setProperties(std::move(propertiesCopy));
+ return 0;
+ }
+
+ static PyObject *capsule_method_handler(PyObject *payload, PyObject *args)
+ {
+ auto *methodData = reinterpret_cast<CapsuleDescriptorData *>(PyCapsule_GetPointer(payload,
+ "Payload"));
+ if (!methodData) {
+ PyErr_SetString(PyExc_RuntimeError, "Invalid call to dynamic method. Missing payload.");
+ return nullptr;
+ }
+ PyObject *self = methodData->self;
+ QRemoteObjectReplica *replica = nullptr;
+ Shiboken::Conversions::pythonToCppPointer(getSbkType(), self, &replica);
+ if (PyCapsule_IsValid(methodData->payload, "PropertyCapsule")) {
+ auto *capsule = PyCapsule_GetPointer(methodData->payload, "PropertyCapsule");
+ if (capsule) {
+ auto *callData = reinterpret_cast<PropertyCapsule *>(capsule);
+ QVariant currentVariant = static_cast<FriendlyReplica *>(replica)->propAsVariant(callData->indexInObject);
+
+ // Handle getter
+ if (PyTuple_Size(args) == 0) // Getter
+ return toPython(currentVariant);
+
+ // Handle setter - currentVariant is a copy, so we can modify it
+ if (PyTuple_Size(args) != 1) {
+ PyErr_SetString(PyExc_TypeError,
+ "Property setter takes exactly one argument");
+ return nullptr;
+ }
+ Conversions::SpecificConverter converter(currentVariant.metaType().name());
+ auto metaType = currentVariant.metaType();
+ if (metaType.flags().testFlag(QMetaType::IsEnumeration)) {
+ converter.toCpp(PyTuple_GetItem(args, 0), currentVariant.data());
+ currentVariant.convert(metaType);
+ } else {
+ converter.toCpp(PyTuple_GetItem(args, 0), currentVariant.data());
+ }
+ if (PyErr_Occurred()) // POD conversion can produce an error
+ return nullptr;
+ QVariantList _args{currentVariant};
+ static_cast<FriendlyReplica *>(replica)->send(QMetaObject::WriteProperty, callData->propertyIndex, _args);
+ Py_RETURN_NONE;
+ }
+ }
+ if (PyCapsule_IsValid(methodData->payload, "MethodCapsule")) {
+ auto *capsule = PyCapsule_GetPointer(methodData->payload, "MethodCapsule");
+ if (capsule) {
+ auto *callData = reinterpret_cast<MethodCapsule *>(capsule);
+ if (PyTuple_Size(args) != callData->argumentTypes.size()) {
+ PyErr_SetString(PyExc_TypeError,
+ "Method called with incorrect number of arguments");
+ return nullptr;
+ }
+ QVariantList _args;
+ static Conversions::SpecificConverter argsConverter("QVariantList");
+ argsConverter.toCpp(args, &_args);
+ if (PyErr_Occurred()) // POD conversion can produce an error
+ return nullptr;
+ if (!callData->returnType.isValid() ||
+ (callData->returnType.isValid() && callData->returnType.id() == QMetaType::Void)) {
+ static_cast<FriendlyReplica *>(replica)->send(QMetaObject::InvokeMetaMethod, callData->methodIndex, _args);
+ Py_RETURN_NONE;
+ }
+ QRemoteObjectPendingCall *cppResult = new QRemoteObjectPendingCall;
+ *cppResult = static_cast<FriendlyReplica *>(replica)->sendWithReply(QMetaObject::InvokeMetaMethod,
+ callData->methodIndex, _args);
+ static PyTypeObject *baseType =
+ Shiboken::Conversions::getPythonTypeObject("QRemoteObjectPendingCall");
+ Q_ASSERT(baseType);
+ auto *pyResult = Shiboken::Object::newObject(baseType, cppResult, true, true);
+ return pyResult;
+ }
+ }
+
+ PyErr_SetString(PyExc_RuntimeError, "Unknown capsule type");
+ return nullptr;
+ }
+};
+
+static int DynamicType_traverse(PyObject *self, visitproc visit, void *arg)
+{
+ auto traverseProc = reinterpret_cast<traverseproc>(PepType_GetSlot(SbkObject_TypeF(),
+ Py_tp_traverse));
+ return traverseProc(self, visit, arg);
+}
+
+static int DynamicType_clear(PyObject *self)
+{
+ auto clearProc = reinterpret_cast<inquiry>(PepType_GetSlot(SbkObject_TypeF(), Py_tp_clear));
+ return clearProc(self);
+}
+
+static PyMethodDef DynamicClass_methods[] = {
+ {"get_enum", reinterpret_cast<PyCFunction>(DynamicType_get_enum), METH_O | METH_CLASS,
+ "Get enum type by name"},
+ {nullptr, nullptr, 0, nullptr}
+};
+
+static PyType_Slot DynamicClass_slots[] = {
+ {Py_tp_base, nullptr}, // inserted by introduceWrapperType
+ {Py_tp_init, nullptr}, // inserted by createDynamicType
+ {Py_tp_traverse, reinterpret_cast<void *>(DynamicType_traverse)},
+ {Py_tp_clear, reinterpret_cast<void *>(DynamicType_clear)},
+ {Py_tp_methods, reinterpret_cast<void *>(DynamicClass_methods)},
+ {0, nullptr}
+};
+
+} // extern "C"
+
+template <typename T, typename BaseType>
+PyTypeObject *createDynamicClassImpl(QMetaObject *meta)
+{
+ DynamicClass_slots[1].pfunc = reinterpret_cast<void*>(T::tp_init);
+
+ auto fullTypeName = QByteArray{T::getTypePrefix()} + meta->className();
+ PyType_Spec spec = {
+ fullTypeName.constData(),
+ 0,
+ 0,
+ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
+ DynamicClass_slots
+ };
+
+ auto type = Shiboken::ObjectType::introduceWrapperType(
+ reinterpret_cast<PyObject *>(PySideRepFile_TypeF()),
+ meta->className(),
+ meta->className(),
+ &spec,
+ &Shiboken::callCppDestructor<BaseType>,
+ T::getBases(),
+ Shiboken::ObjectType::WrapperFlags::InternalWrapper);
+
+ auto *self = reinterpret_cast<PyObject *>(type);
+ if (create_managed_py_enums(self, meta) < 0)
+ return nullptr;
+
+ PySide::Signal::registerSignals(type, meta);
+ Shiboken::ObjectType::setSubTypeInitHook(type, &PySide::initQObjectSubType);
+ PySide::initDynamicMetaObject(type, meta, 0); // Size 0?
+
+ PyMethodDef method = {
+ nullptr,
+ reinterpret_cast<PyCFunction>(T::capsule_method_handler),
+ METH_VARARGS,
+ nullptr
+ };
+ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) {
+ // Create a PropertyCapsule for each property to store the info needed for
+ // the handler. Assign the __get__ and (if needed) __set__ attributes to a
+ // PySideProperty which becomes the attribute set on the new type.
+ auto metaProperty = meta->property(i);
+ PyObject *kwds = PyDict_New();
+ auto metaType = metaProperty.metaType();
+ auto *pyPropertyType = PyUnicode_FromString(metaType.name());
+ PyDict_SetItemString(kwds, "type", pyPropertyType);
+ Py_DECREF(pyPropertyType);
+
+ method.ml_name = metaProperty.name();
+ auto *pc = new PropertyCapsule{metaProperty.name(), i, i - meta->propertyOffset()};
+ auto capsule = PyCapsule_New(pc, "PropertyCapsule", [](PyObject *capsule) {
+ delete static_cast<PropertyCapsule *>(PyCapsule_GetPointer(capsule, "PropertyCapsule"));
+ });
+ auto capsulePropObject = make_capsule_property(&method, capsule,
+ metaProperty.isWritable());
+ PyObject *fget = PyObject_GetAttrString(capsulePropObject, "__get__");
+ PyDict_SetItemString(kwds, "fget", fget);
+ if (metaProperty.isWritable()) {
+ PyObject *fset = PyObject_GetAttrString(capsulePropObject, "__set__");
+ PyDict_SetItemString(kwds, "fset", fset);
+ if (metaProperty.hasNotifySignal()) {
+ auto nameString = metaProperty.notifySignal().name();
+ auto *notify = PyObject_GetAttrString(reinterpret_cast<PyObject *>(type),
+ nameString.constData());
+ PyDict_SetItemString(kwds, "notify", notify);
+ }
+ }
+ PyObject *pyProperty = PyObject_Call(reinterpret_cast<PyObject *>(PySideProperty_TypeF()),
+ PyTuple_New(0), kwds);
+ if (PyObject_SetAttrString(reinterpret_cast<PyObject *>(type),
+ metaProperty.name(), pyProperty) < 0) {
+ return nullptr;
+ }
+ Py_DECREF(pyProperty);
+ }
+ for (int i = meta->methodOffset(); i < meta->methodCount(); ++i) {
+ // Create a CapsuleMethod for each Slot method to store the info needed
+ // for the handler.
+ auto metaMethod = meta->method(i);
+ // Note: We are creating our custom metatype ourselves, which makes our added
+ // (non-signal), methods return QMetaMethod::MethodType::Method, not
+ // MethodType::Slot. This is fine, we just need to create a CapsuleMethod
+ // for those methods.
+ if (metaMethod.methodType() == QMetaMethod::MethodType::Signal)
+ continue;
+ auto name = metaMethod.name();
+ method.ml_name = name.constData();
+ QList<QMetaType> argumentTypes;
+ for (int j = 0; j < metaMethod.parameterCount(); ++j)
+ argumentTypes << metaMethod.parameterMetaType(j);
+ MethodCapsule *capsuleData = new MethodCapsule{metaMethod.name(),
+ metaMethod.methodIndex(),
+ std::move(argumentTypes),
+ metaMethod.returnMetaType()};
+ add_capsule_method_to_type(type, &method,
+ PyCapsule_New(capsuleData, "MethodCapsule",
+ [](PyObject *capsule) {
+ delete reinterpret_cast<MethodCapsule *>(PyCapsule_GetPointer(capsule, "MethodCapsule"));
+ }));
+ }
+
+ return type;
+}
+
+PyTypeObject *createDynamicClass(QMetaObject *meta, PyObject *properties_capsule)
+{
+ bool isSource;
+ if (strncmp(meta->superClass()->className(), "QObject", 7) == 0) {
+ isSource = true;
+ } else if (strncmp(meta->superClass()->className(), "QRemoteObjectReplica", 20) == 0) {
+ isSource = false;
+ } else {
+ PyErr_SetString(PyExc_RuntimeError,
+ "Dynamic type must be a subclass of QObject or QRemoteObjectReplica");
+ return nullptr;
+ }
+
+ PyTypeObject *newType = nullptr;
+
+ if (isSource)
+ newType = createDynamicClassImpl<SourceDefs, QObject>(meta);
+ else
+ newType = createDynamicClassImpl<ReplicaDefs, QRemoteObjectReplica>(meta);
+
+ // Add the properties to the new type as an attribute
+ if (PyObject_SetAttr(reinterpret_cast<PyObject *>(newType), propertiesAttr(),
+ properties_capsule) < 0) {
+ Py_DECREF(newType);
+ return nullptr;
+ }
+
+ return newType;
+}
diff --git a/sources/pyside6/libpysideremoteobjects/pysidedynamicclass_p.h b/sources/pyside6/libpysideremoteobjects/pysidedynamicclass_p.h
new file mode 100644
index 000000000..4716f4f32
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysidedynamicclass_p.h
@@ -0,0 +1,15 @@
+// Copyright (C) 2025 Ford Motor Company
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef PYSIDE_DYNAMIC_CLASS_P_H
+#define PYSIDE_DYNAMIC_CLASS_P_H
+
+#include <sbkpython.h>
+
+#include <QtCore/qtclasshelpermacros.h>
+
+QT_FORWARD_DECLARE_STRUCT(QMetaObject)
+
+PyTypeObject *createDynamicClass(QMetaObject *meta, PyObject *properties_capsule);
+
+#endif // PYSIDE_DYNAMIC_CLASS_P_H
diff --git a/sources/pyside6/libpysideremoteobjects/pysidedynamiccommon.cpp b/sources/pyside6/libpysideremoteobjects/pysidedynamiccommon.cpp
new file mode 100644
index 000000000..b1f01fed6
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysidedynamiccommon.cpp
@@ -0,0 +1,124 @@
+// Copyright (C) 2025 Ford Motor Company
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "pysidedynamiccommon_p.h"
+#include "pysidedynamicenum_p.h"
+
+#include <sbkstring.h>
+
+#include <QtCore/qmetaobject.h>
+
+using namespace Shiboken;
+
+PyObject *toPython(const QVariant &variant)
+{
+ auto metaType = variant.metaType();
+ Conversions::SpecificConverter converter(metaType.name());
+ auto *value = converter.toPython(variant.data());
+ if (metaType.flags().testFlag(QMetaType::IsGadget)) {
+ // A single converter is used for all POD types - it converts to a Python
+ // tuple. We need an additional step to convert to our Python type for the POD.
+ // Thankfully, the converter stores the specific type we created, so we can call
+ // the constructor with the tuple.
+ auto *podType = Conversions::getPythonTypeObject(converter);
+ if (!podType) {
+ Py_DECREF(value);
+ PyErr_SetString(PyExc_RuntimeError, "Failed to get Python type for POD");
+ return nullptr;
+ }
+ PyObject *podValue = PyObject_CallObject(reinterpret_cast<PyObject *>(podType), value);
+ Py_DECREF(value);
+ if (!podValue) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to create POD instance");
+ return nullptr;
+ }
+ return podValue;
+ }
+ if (metaType.flags().testFlag(QMetaType::IsEnumeration)) {
+ // Enums are converted to Python ints
+ auto *enumType = Conversions::getPythonTypeObject(converter);
+ if (!enumType) {
+ Py_DECREF(value);
+ PyErr_SetString(PyExc_RuntimeError, "Failed to get Python type for enum");
+ return nullptr;
+ }
+ PyObject *enumValue = PyObject_CallFunctionObjArgs(reinterpret_cast<PyObject *>(enumType),
+ value, nullptr);
+ Py_DECREF(value);
+ if (!enumValue) {
+ PyErr_Print();
+ PyErr_SetString(PyExc_RuntimeError, "Failed to create enum instance");
+ return nullptr;
+ }
+ return enumValue;
+ }
+ return value;
+}
+
+
+/**
+ * @brief Creates and manages memory for Python enum types for each QEnum in the
+ * provided QMetaObject.
+ *
+ * This function iterates over the enumerators in the provided QMetaObject,
+ * creates corresponding Python enum types, and stores them in a dictionary.
+ * The dictionary is then set as an attribute ()"_enum_data") on the provided
+ * Python object, to be accessed by the _get_enum that has been added to each
+ * of our dynamic types.
+ *
+ * These are "managed" in the sense that the enums clean up their converters
+ * using our PyCapsule method, and by adding the dictionary as a Python attribute,
+ * the dictionary will be cleaned up when the containing type is garbage
+ * collected.
+ *
+ * @param self A pointer to the Python object where the enum data will be stored.
+ * @param meta A pointer to the QMetaObject containing the enumerators.
+ * @return Returns 0 on success, or -1 on failure.
+ */
+int create_managed_py_enums(PyObject *self, QMetaObject *meta)
+{
+ PyObject *enum_data = PyDict_New();
+ for (int i = meta->enumeratorOffset(); i < meta->enumeratorCount(); ++i) {
+ auto metaEnum = meta->enumerator(i);
+ auto *enumType = createEnumType(&metaEnum);
+ if (!enumType) {
+ PyErr_Print();
+ PyErr_Format(PyExc_RuntimeError, "Failed to create enum type for POD '%s'",
+ meta->className());
+ return -1;
+ }
+ PyDict_SetItemString(enum_data, metaEnum.enumName(),
+ reinterpret_cast<PyObject *>(enumType));
+ Py_DECREF(enumType);
+ }
+ if (PyObject_SetAttrString(self, "_enum_data", enum_data) < 0) {
+ PyErr_Print();
+ qWarning() << "Failed to set _enum_data attribute on type"
+ << reinterpret_cast<PyTypeObject *>(self)->tp_name;
+ return -1;
+ }
+ Py_DECREF(enum_data);
+
+ return 0;
+}
+
+PyObject *DynamicType_get_enum(PyObject *self, PyObject *name)
+{
+ // Our enum types are always stored in a dictionary attribute named "_enum_data"
+ PyObject *enum_dict = PyObject_GetAttrString(self, "_enum_data");
+ if (!enum_dict) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to get _enum_data attribute");
+ return nullptr;
+ }
+
+ PyObject *enum_type = PyDict_GetItem(enum_dict, name);
+ Py_DECREF(enum_dict);
+
+ if (!enum_type) {
+ PyErr_Format(PyExc_KeyError, "Enum '%s' not found", String::toCString(name));
+ return nullptr;
+ }
+
+ Py_INCREF(enum_type);
+ return enum_type;
+}
diff --git a/sources/pyside6/libpysideremoteobjects/pysidedynamiccommon_p.h b/sources/pyside6/libpysideremoteobjects/pysidedynamiccommon_p.h
new file mode 100644
index 000000000..1e9f8d55a
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysidedynamiccommon_p.h
@@ -0,0 +1,89 @@
+// Copyright (C) 2025 Ford Motor Company
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef PYSIDE_DYNAMIC_COMMON_P_H
+#define PYSIDE_DYNAMIC_COMMON_P_H
+
+#include <sbkconverter.h>
+
+#include <QtCore/qlist.h>
+#include <QtCore/qvariant.h>
+#include <QtCore/qmetatype.h>
+
+PyObject *toPython(const QVariant &variant);
+int create_managed_py_enums(PyObject *self, QMetaObject *meta);
+PyObject *DynamicType_get_enum(PyObject *self, PyObject *name);
+
+// Data for dynamically created property handlers
+struct PropertyCapsule
+{
+ QByteArray name;
+ int propertyIndex; // meta->indexOfProperty() - including offset
+ int indexInObject; // Index minus offset for indexing into QVariantList
+};
+
+// Data for dynamically created method handlers
+struct MethodCapsule
+{
+ QByteArray name;
+ int methodIndex;
+ QList<QMetaType> argumentTypes;
+ QMetaType returnType; // meta->indexOfMethod() - including offset
+};
+
+// These functions are used to create a PyCapsule holding a pointer to a C++
+// object, which is set as an attribute on a Python type. When the Python
+// type is garbage collected, the type's attributes are as well, resulting in
+// the capsule's cleanup running to delete the pointer. This won't be as
+// efficient as a custom tp_free on the type, but it's easier to manage.
+// And it only runs when as all references to the type (and all instances) are
+// released, so it won't be used frequently.
+
+static int capsule_count = 0;
+
+static PyObject *get_capsule_count()
+{
+ return PyLong_FromLong(capsule_count);
+}
+
+template <typename T>
+void Capsule_destructor(PyObject *capsule)
+{
+ capsule_count--;
+ T pointer = static_cast<T>(PyCapsule_GetPointer(capsule, nullptr));
+ delete pointer;
+ pointer = nullptr;
+}
+
+template <>
+inline void Capsule_destructor<SbkConverter *>(PyObject *capsule)
+{
+ capsule_count--;
+ SbkConverter *pointer = static_cast<SbkConverter *>(PyCapsule_GetPointer(capsule, nullptr));
+ Shiboken::Conversions::deleteConverter(pointer);
+ pointer = nullptr;
+}
+
+template <typename T>
+int set_cleanup_capsule_attr_for_pointer(PyTypeObject *type, const char *name, T pointer)
+{
+ static_assert(std::is_pointer<T>::value, "T must be a pointer type");
+
+ if (!pointer) {
+ PyErr_SetString(PyExc_RuntimeError, "Pointer is null");
+ return -1;
+ }
+ auto capsule = PyCapsule_New(pointer, nullptr, Capsule_destructor<T>);
+ if (!capsule)
+ return -1; // Propagate the error
+
+ if (PyObject_SetAttrString(reinterpret_cast<PyObject *>(type), name, capsule) < 0)
+ return -1; // Propagate the error
+
+ Py_DECREF(capsule);
+ capsule_count++;
+
+ return 0;
+}
+
+#endif // PYSIDE_DYNAMIC_COMMON_P_H
diff --git a/sources/pyside6/libpysideremoteobjects/pysidedynamicenum.cpp b/sources/pyside6/libpysideremoteobjects/pysidedynamicenum.cpp
new file mode 100644
index 000000000..1f92224f5
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysidedynamicenum.cpp
@@ -0,0 +1,158 @@
+// Copyright (C) 2025 Ford Motor Company
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "pysidedynamicenum_p.h"
+#include "pysidedynamiccommon_p.h"
+
+#include <autodecref.h>
+#include <sbkconverter.h>
+#include <sbkenum.h>
+
+#include <QtCore/qmetaobject.h>
+
+using namespace Shiboken;
+
+// Remote Objects transfer enums as integers of the underlying type.
+#define CREATE_ENUM_CONVERSION_FUNCTIONS(SUFFIX, INT_TYPE, PY_TYPE) \
+static void pythonToCpp_PyEnum_QEnum_##SUFFIX(PyObject *pyIn, void *cppOut) \
+{ \
+ Enum::EnumValueType value = Enum::getValue(pyIn); \
+ INT_TYPE val(value); \
+ *reinterpret_cast<INT_TYPE *>(cppOut) = val; \
+} \
+static PythonToCppFunc is_PyEnum_PythonToCpp_QEnum_##SUFFIX##_Convertible(PyObject *pyIn) \
+{ \
+ if (Enum::check(pyIn)) \
+ return pythonToCpp_PyEnum_QEnum_##SUFFIX; \
+ return {}; \
+} \
+static PyObject *cppToPython_QEnum_##SUFFIX##_PyEnum(const void *cppIn) \
+{ \
+ auto convertedCppIn = *reinterpret_cast<const INT_TYPE *>(cppIn); \
+ return PY_TYPE(convertedCppIn); \
+}
+
+CREATE_ENUM_CONVERSION_FUNCTIONS(I8, int8_t, PyLong_FromLong)
+CREATE_ENUM_CONVERSION_FUNCTIONS(I16, int16_t, PyLong_FromLong)
+CREATE_ENUM_CONVERSION_FUNCTIONS(I32, int32_t, PyLong_FromLong)
+CREATE_ENUM_CONVERSION_FUNCTIONS(U8, uint8_t, PyLong_FromUnsignedLong)
+CREATE_ENUM_CONVERSION_FUNCTIONS(U16, uint16_t, PyLong_FromUnsignedLong)
+CREATE_ENUM_CONVERSION_FUNCTIONS(U32, uint32_t, PyLong_FromUnsignedLong)
+CREATE_ENUM_CONVERSION_FUNCTIONS(I64, int64_t, PyLong_FromLongLong)
+CREATE_ENUM_CONVERSION_FUNCTIONS(U64, uint64_t, PyLong_FromUnsignedLongLong)
+
+PyTypeObject *createEnumType(QMetaEnum *metaEnum)
+{
+ static const auto namePrefix = QByteArrayLiteral("2:PySide6.QtRemoteObjects.DynamicEnum.");
+ auto fullName = namePrefix + metaEnum->scope() + "." + metaEnum->enumName();
+
+ AutoDecRef args(PyList_New(0));
+ auto *pyEnumItems = args.object();
+ auto metaType = metaEnum->metaType();
+ auto underlyingType = metaType.underlyingType();
+ bool isUnsigned = underlyingType.flags().testFlag(QMetaType::IsUnsignedEnumeration);
+ for (int idx = 0; idx < metaEnum->keyCount(); ++idx) {
+ auto *key = PyUnicode_FromString(metaEnum->key(idx));
+ auto *key_value = PyTuple_New(2);
+ PyTuple_SetItem(key_value, 0, key);
+ // Value should only return a nullopt if there is no metaObject or the index is not valid
+ auto valueOpt = metaEnum->value64(idx);
+ if (!valueOpt) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to get value64 from enum");
+ return nullptr;
+ }
+ if (isUnsigned) {
+ auto *value = PyLong_FromUnsignedLongLong(*valueOpt);
+ PyTuple_SetItem(key_value, 1, value);
+ } else {
+ auto *value = PyLong_FromLongLong(*valueOpt);
+ PyTuple_SetItem(key_value, 1, value);
+ }
+ PyList_Append(pyEnumItems, key_value);
+ }
+
+ PyTypeObject *newType{};
+ if (metaEnum->isFlag())
+ newType = Enum::createPythonEnum(fullName.constData(), pyEnumItems, "Flag");
+ else
+ newType = Enum::createPythonEnum(fullName.constData(), pyEnumItems);
+
+ SbkConverter *converter = nullptr;
+ switch (underlyingType.sizeOf()) {
+ case 1:
+ if (isUnsigned) {
+ converter = Conversions::createConverter(newType,
+ cppToPython_QEnum_U8_PyEnum);
+ Conversions::addPythonToCppValueConversion(converter,
+ pythonToCpp_PyEnum_QEnum_U8,
+ is_PyEnum_PythonToCpp_QEnum_U8_Convertible);
+ } else {
+ converter = Conversions::createConverter(newType,
+ cppToPython_QEnum_I8_PyEnum);
+ Conversions::addPythonToCppValueConversion(converter,
+ pythonToCpp_PyEnum_QEnum_I8,
+ is_PyEnum_PythonToCpp_QEnum_I8_Convertible);
+ }
+ break;
+ case 2:
+ if (isUnsigned) {
+ converter = Conversions::createConverter(newType,
+ cppToPython_QEnum_U16_PyEnum);
+ Conversions::addPythonToCppValueConversion(converter,
+ pythonToCpp_PyEnum_QEnum_U16,
+ is_PyEnum_PythonToCpp_QEnum_U16_Convertible);
+ } else {
+ converter = Conversions::createConverter(newType,
+ cppToPython_QEnum_I16_PyEnum);
+ Conversions::addPythonToCppValueConversion(converter,
+ pythonToCpp_PyEnum_QEnum_I16,
+ is_PyEnum_PythonToCpp_QEnum_I16_Convertible);
+ }
+ break;
+ case 4:
+ if (isUnsigned) {
+ converter = Conversions::createConverter(newType,
+ cppToPython_QEnum_U32_PyEnum);
+ Conversions::addPythonToCppValueConversion(converter,
+ pythonToCpp_PyEnum_QEnum_U32,
+ is_PyEnum_PythonToCpp_QEnum_U32_Convertible);
+ } else {
+ converter = Conversions::createConverter(newType,
+ cppToPython_QEnum_I32_PyEnum);
+ Conversions::addPythonToCppValueConversion(converter,
+ pythonToCpp_PyEnum_QEnum_I32,
+ is_PyEnum_PythonToCpp_QEnum_I32_Convertible);
+ }
+ break;
+ case 8:
+ if (isUnsigned) {
+ converter = Conversions::createConverter(newType,
+ cppToPython_QEnum_U64_PyEnum);
+ Conversions::addPythonToCppValueConversion(converter,
+ pythonToCpp_PyEnum_QEnum_U64,
+ is_PyEnum_PythonToCpp_QEnum_U64_Convertible);
+ } else {
+ converter = Conversions::createConverter(newType,
+ cppToPython_QEnum_I64_PyEnum);
+ Conversions::addPythonToCppValueConversion(converter,
+ pythonToCpp_PyEnum_QEnum_I64,
+ is_PyEnum_PythonToCpp_QEnum_I64_Convertible);
+ }
+ break;
+ default:
+ PyErr_SetString(PyExc_RuntimeError, "Unsupported enum underlying type");
+ return nullptr;
+ }
+ auto scopedName = QByteArray(metaEnum->scope()) + "::" + metaEnum->enumName();
+ Conversions::registerConverterName(converter, scopedName.constData());
+ Conversions::registerConverterName(converter, metaEnum->enumName());
+ // createConverter increases the ref count of type, but that will create a
+ // circular reference when we add the capsule with the converter's pointer
+ // to the type's attributes. So we need to decrease the ref count on the
+ // type after calling createConverter.
+ Py_DECREF(newType);
+ if (set_cleanup_capsule_attr_for_pointer(newType, "_converter_capsule", converter) < 0)
+ return nullptr;
+
+ return newType;
+}
diff --git a/sources/pyside6/libpysideremoteobjects/pysidedynamicenum_p.h b/sources/pyside6/libpysideremoteobjects/pysidedynamicenum_p.h
new file mode 100644
index 000000000..14181fac8
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysidedynamicenum_p.h
@@ -0,0 +1,15 @@
+// Copyright (C) 2025 Ford Motor Company
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef PYSIDE_DYNAMIC_ENUM_P_H
+#define PYSIDE_DYNAMIC_ENUM_P_H
+
+#include <sbkpython.h>
+
+#include <QtCore/qtclasshelpermacros.h>
+
+QT_FORWARD_DECLARE_CLASS(QMetaEnum)
+
+PyTypeObject *createEnumType(QMetaEnum *metaEnum);
+
+#endif // PYSIDE_DYNAMIC_ENUM_P_H
diff --git a/sources/pyside6/libpysideremoteobjects/pysidedynamicpod.cpp b/sources/pyside6/libpysideremoteobjects/pysidedynamicpod.cpp
new file mode 100644
index 000000000..abfeaa037
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysidedynamicpod.cpp
@@ -0,0 +1,260 @@
+// Copyright (C) 2025 Ford Motor Company
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "pysidedynamicpod_p.h"
+#include "pysidecapsulemethod_p.h"
+#include "pysidedynamiccommon_p.h"
+
+#include <autodecref.h>
+#include <helper.h>
+#include <pep384ext.h>
+#include <sbkconverter.h>
+#include <sbkstaticstrings.h>
+#include <sbkstring.h>
+
+#include <pysidestaticstrings.h>
+
+#include <QtCore/qmetaobject.h>
+
+using namespace Shiboken;
+
+extern "C"
+{
+
+struct PodDefs
+{
+ static PyObject *tp_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+ {
+ SBK_UNUSED(kwds);
+ AutoDecRef param_types(PyObject_GetAttrString(reinterpret_cast<PyObject *>(type),
+ "__param_types__"));
+ if (!param_types) {
+ PyErr_Format(PyExc_RuntimeError, "Failed to get POD attributes for type %s",
+ type->tp_name);
+ return nullptr;
+ }
+
+ // param_types is a tuple of PyTypeObject pointers
+ Py_ssize_t size = PyTuple_Size(param_types);
+ if (size != PyTuple_Size(args)) {
+ PyErr_Format(PyExc_TypeError,
+ "Incorrect number of arguments for type %s. Expected %zd.",
+ type->tp_name, size);
+ return nullptr;
+ }
+
+ PyObject *self = PepExt_Type_GetAllocSlot(type)(type, size);
+
+ if (!self)
+ return nullptr;
+
+ for (Py_ssize_t i = 0; i < size; ++i) {
+ PyObject *expected_type = PyTuple_GetItem(param_types, i);
+ PyObject *item = PyTuple_GetItem(args, i);
+ // Check if the item is an instance of the expected type
+ if (PyObject_IsInstance(item, expected_type)) {
+ Py_INCREF(item);
+ PyTuple_SetItem(self, i, item);
+ } else {
+ // Try to convert the item to the expected type
+ PyObject *converted_item = PyObject_CallFunctionObjArgs(expected_type, item, nullptr);
+ if (!converted_item) {
+ Py_DECREF(self);
+ PyErr_Format(PyExc_TypeError, "Argument %zd must be convertible to type %s", i,
+ reinterpret_cast<PyTypeObject *>(expected_type)->tp_name);
+ return nullptr;
+ }
+ PyTuple_SetItem(self, i, converted_item);
+ }
+ }
+
+ return self;
+ }
+
+ static PyObject *tp_repr(PyObject *self)
+ {
+ auto *type = Py_TYPE(self);
+ std::string repr(type->tp_name);
+ repr += "(";
+ for (Py_ssize_t i = 0; i < PyTuple_Size(self); ++i) {
+ if (i > 0)
+ repr += ", ";
+
+ PyObject *item_repr = PyObject_Repr(PyTuple_GetItem(self, i));
+ repr += String::toCString(item_repr);
+ }
+ repr += ")";
+ return PyUnicode_FromString(repr.c_str());
+ }
+
+ static PyObject *CapsuleMethod_handler(PyObject *payload, PyObject * /* args */)
+ {
+ auto *methodData = reinterpret_cast<CapsuleDescriptorData *>(
+ PyCapsule_GetPointer(payload, "Payload"));
+ if (!methodData) {
+ PyErr_SetString(PyExc_RuntimeError, "Invalid call to dynamic method. Missing payload.");
+ return nullptr;
+ }
+ PyObject *self = methodData->self;
+ if (PyCapsule_IsValid(methodData->payload, "PropertyCapsule")) {
+ // Handle property getter/setter against our hidden properties attribute
+ auto *capsule = PyCapsule_GetPointer(methodData->payload, "PropertyCapsule");
+ if (capsule) {
+ auto *callData = reinterpret_cast<PropertyCapsule *>(capsule);
+ if (callData->indexInObject < 0 || callData->indexInObject >= PyTuple_Size(self)) {
+ PyErr_Format(PyExc_RuntimeError, "Unknown property method: %s",
+ callData->name.constData());
+ return nullptr;
+ }
+ auto *val = PyTuple_GetItem(self, callData->indexInObject);
+ Py_INCREF(val);
+ return val;
+ }
+ }
+
+ PyErr_SetString(PyExc_RuntimeError, "Unknown capsule type");
+ return nullptr;
+ }
+};
+
+static PyMethodDef DynamicPod_tp_methods[] = {
+ {"get_enum", reinterpret_cast<PyCFunction>(DynamicType_get_enum), METH_O | METH_CLASS,
+ "Get enum type by name"},
+ {nullptr, nullptr, 0, nullptr}
+};
+
+static PyType_Slot DynamicPod_slots[] = {
+ {Py_tp_base, reinterpret_cast<void *>(&PyTuple_Type)},
+ {Py_tp_new, reinterpret_cast<void *>(PodDefs::tp_new)},
+ {Py_tp_repr, reinterpret_cast<void *>(PodDefs::tp_repr)},
+ {Py_tp_methods, reinterpret_cast<void *>(DynamicPod_tp_methods)},
+ {0, nullptr}
+};
+
+// C++ to Python conversion for POD types.
+static PyObject *cppToPython_POD_Tuple(const void *cppIn)
+{
+ const auto &cppInRef = *reinterpret_cast<const QVariantList *>(cppIn);
+ PyObject *pyOut = PyTuple_New(Py_ssize_t(cppInRef.size()));
+ Py_ssize_t idx = 0;
+ for (auto it = std::cbegin(cppInRef), end = std::cend(cppInRef); it != end; ++it, ++idx) {
+ static const Conversions::SpecificConverter argConverter("QVariant");
+ const auto &cppItem = *it;
+ PyTuple_SetItem(pyOut, idx, Shiboken::Conversions::copyToPython(argConverter, &cppItem));
+ }
+ return pyOut;
+}
+static void pythonToCpp_Tuple_POD(PyObject *pyIn, void *cppOut)
+{
+ auto &cppOutRef = *reinterpret_cast<QVariantList *>(cppOut);
+
+ Py_ssize_t tupleSize = PyTuple_Size(pyIn);
+ if (tupleSize != cppOutRef.size()) {
+ PyErr_Format(PyExc_ValueError,
+ "Size mismatch: tuple has %zd elements, but POD expects %d elements",
+ tupleSize, cppOutRef.size());
+ return;
+ }
+
+ for (Py_ssize_t i = 0; i < tupleSize; ++i) {
+ static const Conversions::SpecificConverter argConverter("QVariant");
+ PyObject *item = PyTuple_GetItem(pyIn, i);
+ QVariant &variant = cppOutRef[i];
+ Conversions::SpecificConverter converter(variant.metaType().name());
+ Shiboken::Conversions::pythonToCppCopy(converter, item, variant.data());
+ }
+}
+static PythonToCppFunc is_Tuple_PythonToCpp_POD_Convertible(PyObject *pyIn)
+{
+ if (PyTuple_Check(pyIn))
+ return pythonToCpp_Tuple_POD;
+
+ return {};
+}
+
+} // extern "C"
+
+PyTypeObject *createPodType(QMetaObject *meta)
+{
+ auto qualname = QByteArrayLiteral("DynamicPod.") + meta->className();
+ PyType_Spec spec = {
+ qualname.constData(),
+ 0,
+ 0,
+ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_TYPE_SUBCLASS,
+ DynamicPod_slots
+ };
+
+ PyObject *obType = PyType_FromSpec(&spec);
+ if (!obType)
+ return nullptr;
+
+ if (create_managed_py_enums(obType, meta) < 0)
+ return nullptr;
+
+ Py_ssize_t size = meta->propertyCount() - meta->propertyOffset();
+ AutoDecRef pyParamTypes(PyTuple_New(size));
+ for (int i = 0; i < size; ++i) {
+ auto metaProperty = meta->property(i + meta->propertyOffset());
+ auto metaType = metaProperty.metaType();
+ if (!metaType.isValid()) {
+ PyErr_Format(PyExc_RuntimeError, "Failed to get meta type for property %s",
+ metaProperty.name());
+ return nullptr;
+ }
+ auto *pyType = Conversions::getPythonTypeObject(metaType.name());
+ Py_INCREF(pyType);
+ PyTuple_SetItem(pyParamTypes, i, reinterpret_cast<PyObject *>(pyType));
+ }
+
+ auto *type = reinterpret_cast<PyTypeObject *>(obType);
+ PyMethodDef method = {
+ nullptr,
+ reinterpret_cast<PyCFunction>(PodDefs::CapsuleMethod_handler),
+ METH_VARARGS,
+ nullptr
+ };
+ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) {
+ // Create a PropertyCapsule for each property to store the info needed
+ // for the handler.
+ auto metaProperty = meta->property(i);
+
+ method.ml_name = metaProperty.name();
+ auto *capsule = PyCapsule_New(new PropertyCapsule{metaProperty.name(),
+ i,
+ i - meta->propertyOffset()},
+ "PropertyCapsule",
+ [](PyObject *capsule) {
+ delete static_cast<PropertyCapsule *>(
+ PyCapsule_GetPointer(capsule, "PropertyCapsule"));
+ });
+ auto *capsulePropObject = make_capsule_property(&method, capsule);
+ if (PyObject_SetAttrString(reinterpret_cast<PyObject *>(type), metaProperty.name(),
+ capsulePropObject) < 0) {
+ return nullptr;
+ }
+
+ Py_DECREF(capsulePropObject);
+ }
+
+ // createConverter increases the ref count of type, but that will create
+ // a circular reference. When we add the capsule with the converter's pointer
+ // to the type's attributes. So we need to decrease the ref count on the type
+ // after calling createConverter.
+ auto *converter = Shiboken::Conversions::createConverter(type, cppToPython_POD_Tuple);
+ Py_DECREF(type);
+ if (set_cleanup_capsule_attr_for_pointer(type, "_converter_capsule", converter) < 0)
+ return nullptr;
+ Shiboken::Conversions::registerConverterName(converter, meta->className());
+ Shiboken::Conversions::registerConverterName(converter, type->tp_name);
+ Shiboken::Conversions::addPythonToCppValueConversion(converter, pythonToCpp_Tuple_POD,
+ is_Tuple_PythonToCpp_POD_Convertible);
+
+ static PyObject *const module = String::createStaticString("PySide6.QtRemoteObjects");
+ AutoDecRef pyQualname(String::fromCString(qualname.constData()));
+ PyObject_SetAttr(obType, PyMagicName::qualname(), pyQualname);
+ PyObject_SetAttr(obType, PyMagicName::module(), module);
+ PyObject_SetAttrString(obType, "__param_types__", pyParamTypes);
+
+ return type;
+}
diff --git a/sources/pyside6/libpysideremoteobjects/pysidedynamicpod_p.h b/sources/pyside6/libpysideremoteobjects/pysidedynamicpod_p.h
new file mode 100644
index 000000000..6dc9db9dd
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysidedynamicpod_p.h
@@ -0,0 +1,15 @@
+// Copyright (C) 2025 Ford Motor Company
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef PYSIDE_DYNAMIC_POD_P_H
+#define PYSIDE_DYNAMIC_POD_P_H
+
+#include <sbkpython.h>
+
+#include <QtCore/qtclasshelpermacros.h>
+
+QT_FORWARD_DECLARE_STRUCT(QMetaObject)
+
+PyTypeObject *createPodType(QMetaObject *meta);
+
+#endif // PYSIDE_DYNAMIC_POD_P_H
diff --git a/sources/pyside6/libpysideremoteobjects/pysideremoteobjects.h b/sources/pyside6/libpysideremoteobjects/pysideremoteobjects.h
new file mode 100644
index 000000000..e0828c960
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysideremoteobjects.h
@@ -0,0 +1,16 @@
+// Copyright (C) 2025 Ford Motor Company
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef PYSIDEREMOTEOBJECTS_H
+#define PYSIDEREMOTEOBJECTS_H
+
+#include <sbkpython.h>
+
+namespace PySide::RemoteObjects
+{
+
+void init(PyObject *module);
+
+} // namespace PySide::RemoteObjects
+
+#endif // PYSIDEREMOTEOBJECTS_H
diff --git a/sources/pyside6/libpysideremoteobjects/pysiderephandler.cpp b/sources/pyside6/libpysideremoteobjects/pysiderephandler.cpp
new file mode 100644
index 000000000..25bdbef9b
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysiderephandler.cpp
@@ -0,0 +1,459 @@
+// Copyright (C) 2025 Ford Motor Company
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "pysiderephandler_p.h"
+#include "pysidedynamicclass_p.h"
+#include "pysidedynamicpod_p.h"
+#include "pysidedynamiccommon_p.h"
+
+#include <pep384ext.h>
+#include <sbkstring.h>
+#include <sbktypefactory.h>
+#include <signature.h>
+
+#include <pysideutils.h>
+
+#include <QtCore/qbuffer.h>
+#include <QtCore/qiodevice.h>
+#include <QtCore/qmetaobject.h>
+
+#include <QtRemoteObjects/qremoteobjectreplica.h>
+#include <QtRemoteObjects/qremoteobjectpendingcall.h>
+
+#include <private/qremoteobjectrepparser_p.h>
+
+using namespace Qt::StringLiterals;
+using namespace Shiboken;
+
+/**
+ * @file pysiderephandler.cpp
+ * @brief This file contains the implementation of the PySideRepFile type and its
+ * associated methods for handling Qt Remote Objects in PySide6.
+ *
+ * The PySideRepFile type provides functionality to parse and handle Qt Remote Objects
+ * (QtRO) files, and dynamically generate Python types for QtRO sources, replicas, and
+ * PODs (Plain Old Data structures).
+ *
+ * The RepFile_tp_methods array defines the methods available on the PySideRepFile object:
+ * - source: Generates a dynamic Python type for a QtRO source class.
+ * - replica: Generates a dynamic Python type for a QtRO replica class.
+ * - pod: Generates a dynamic Python type for a QtRO POD class.
+ *
+ * When generating a source or replica type, the generateDynamicType function is
+ * used, creating a new Python type based on the generated QMetaObject, and adds
+ * method descriptors for the required methods. A QVariantList for the types
+ * properties is also created, populated with default values if set in the input
+ * .rep file.
+*/
+
+static QVariantList generateProperties(QMetaObject *meta, const ASTClass &astClass);
+
+extern "C"
+{
+
+// Code for the PySideRepFile type
+static PyObject *RepFile_tp_string(PyObject *self);
+static PyObject *RepFile_tp_new(PyTypeObject *subtype, PyObject *args, PyObject *kwds);
+static int RepFile_tp_init(PyObject *self, PyObject *args, PyObject *kwds);
+static void RepFile_tp_free(void *self);
+static void RepFile_tp_dealloc(PySideRepFile *self);
+
+static PyObject *RepFile_get_pods(PySideRepFile *self, void * /*unused*/);
+static PyObject *RepFile_get_replicas(PySideRepFile *self, void * /*unused*/);
+static PyObject *RepFile_get_sources(PySideRepFile *self, void * /*unused*/);
+
+bool instantiateFromDefaultValue(QVariant &variant, const QString &defaultValue);
+
+static PyObject *cppToPython_POD_Tuple(const void *cppIn);
+static void pythonToCpp_Tuple_POD(PyObject *pyIn, void *cppOut);
+static PythonToCppFunc is_Tuple_PythonToCpp_POD_Convertible(PyObject *pyIn);
+
+static PyGetSetDef RepFile_tp_getters[] = {
+ {"pod", reinterpret_cast<getter>(RepFile_get_pods), nullptr, "POD dictionary", nullptr},
+ {"replica", reinterpret_cast<getter>(RepFile_get_replicas), nullptr, "Replica dictionary", nullptr},
+ {"source", reinterpret_cast<getter>(RepFile_get_sources), nullptr, "Source dictionary", nullptr},
+ {nullptr, nullptr, nullptr, nullptr, nullptr} // Sentinel
+};
+
+static PyTypeObject *createRepFileType()
+{
+ PyType_Slot PySideRepFileType_slots[] = {
+ {Py_tp_str, reinterpret_cast<void *>(RepFile_tp_string)},
+ {Py_tp_init, reinterpret_cast<void *>(RepFile_tp_init)},
+ {Py_tp_new, reinterpret_cast<void *>(RepFile_tp_new)},
+ {Py_tp_free, reinterpret_cast<void *>(RepFile_tp_free)},
+ {Py_tp_dealloc, reinterpret_cast<void *>(RepFile_tp_dealloc)},
+ {Py_tp_getset, reinterpret_cast<void *>(RepFile_tp_getters)},
+ {0, nullptr}
+ };
+
+ PyType_Spec PySideRepFileType_spec = {
+ "2:PySide6.QtRemoteObjects.RepFile",
+ sizeof(PySideRepFile),
+ 0,
+ Py_TPFLAGS_DEFAULT,
+ PySideRepFileType_slots};
+ return SbkType_FromSpec(&PySideRepFileType_spec);
+}
+
+PyTypeObject *PySideRepFile_TypeF(void)
+{
+ static auto *type = createRepFileType();
+ return type;
+}
+
+static PyObject *RepFile_tp_string(PyObject *self)
+{
+ auto *cppSelf = reinterpret_cast<PySideRepFile *>(self);
+ QString result = QStringLiteral("RepFile(Classes: [%1], PODs: [%2])")
+ .arg(cppSelf->d->classes.join(", "_L1), cppSelf->d->pods.join(", "_L1));
+ return PyUnicode_FromString(result.toUtf8().constData());
+}
+
+static PyObject *RepFile_tp_new(PyTypeObject *subtype, PyObject * /* args */, PyObject * /* kwds */)
+{
+ auto *me = PepExt_TypeCallAlloc<PySideRepFile>(subtype, 0);
+ auto *priv = new PySideRepFilePrivate;
+ priv->podDict = PyDict_New();
+ if (!priv->podDict) {
+ delete priv;
+ return nullptr;
+ }
+ priv->replicaDict = PyDict_New();
+ if (!priv->replicaDict) {
+ Py_DECREF(priv->podDict);
+ delete priv;
+ return nullptr;
+ }
+ priv->sourceDict = PyDict_New();
+ if (!priv->sourceDict) {
+ Py_DECREF(priv->podDict);
+ Py_DECREF(priv->replicaDict);
+ delete priv;
+ return nullptr;
+ }
+ me->d = priv;
+ return reinterpret_cast<PyObject *>(me);
+}
+
+static PyObject *RepFile_get_pods(PySideRepFile *self, void * /* closure */)
+{
+ Py_INCREF(self->d->podDict);
+ return self->d->podDict;
+}
+
+static PyObject *RepFile_get_replicas(PySideRepFile *self, void * /* closure */)
+{
+ Py_INCREF(self->d->replicaDict);
+ return self->d->replicaDict;
+}
+
+static PyObject *RepFile_get_sources(PySideRepFile *self, void * /* closure */)
+{
+ Py_INCREF(self->d->sourceDict);
+ return self->d->sourceDict;
+}
+
+static void RepFile_tp_dealloc(PySideRepFile *self)
+{
+ Py_XDECREF(self->d->podDict);
+ Py_XDECREF(self->d->replicaDict);
+ Py_XDECREF(self->d->sourceDict);
+ PepExt_TypeCallFree(reinterpret_cast<PyObject *>(self));
+}
+
+static int parseArgsToAST(PyObject *args, PySideRepFile *repFile)
+{
+ // Verify args is a single string argument
+ if (PyTuple_Size(args) != 1 || !PyUnicode_Check(PyTuple_GetItem(args, 0))) {
+ PyErr_SetString(PyExc_TypeError, "RepFile constructor requires a single string argument");
+ return -1;
+ }
+
+ // Wrap contents into a QBuffer
+ const auto contents = PySide::pyStringToQString(PyTuple_GetItem(args, 0));
+ auto byteArray = contents.toUtf8();
+ QBuffer buffer(&byteArray);
+ buffer.open(QIODevice::ReadOnly);
+ RepParser repparser(buffer);
+ if (!repparser.parse()) {
+ PyErr_Format(PyExc_RuntimeError, "Error parsing input, line %d: error: %s",
+ repparser.lineNumber(), qPrintable(repparser.errorString()));
+ auto lines = contents.split("\n"_L1);
+ auto lMin = std::max(1, repparser.lineNumber() - 2);
+ auto lMax = std::min(repparser.lineNumber() + 2, int(lines.size()));
+ // Print a few lines around the error
+ qWarning() << "Contents:";
+ for (int i = lMin; i <= lMax; ++i) {
+ if (i == repparser.lineNumber())
+ qWarning().nospace() << " line " << i << ": > " << lines.at(i - 1);
+ else
+ qWarning().nospace() << " line " << i << ": " << lines.at(i - 1);
+ }
+ return -1;
+ }
+
+ repFile->d->ast = repparser.ast();
+
+ return 0;
+}
+
+static const char *repName(QMetaObject *meta)
+{
+ const int ind = meta->indexOfClassInfo(QCLASSINFO_REMOTEOBJECT_TYPE);
+ return ind >= 0 ? meta->classInfo(ind).value() : "<Invalid RemoteObject>";
+}
+
+static int RepFile_tp_init(PyObject *self, PyObject *args, PyObject * /* kwds */)
+{
+ auto *cppSelf = reinterpret_cast<PySideRepFile *>(self);
+ if (parseArgsToAST(args, cppSelf) < 0)
+ return -1;
+
+ for (const auto &pod : std::as_const(cppSelf->d->ast.pods)) {
+ cppSelf->d->pods << pod.name;
+ auto *qobject = new QObject;
+ auto *meta = createAndRegisterMetaTypeFromPOD(pod, qobject);
+ if (!meta) {
+ delete qobject;
+ PyErr_Format(PyExc_RuntimeError, "Failed to create meta object for POD '%s'",
+ pod.name.toUtf8().constData());
+ return -1;
+ }
+
+ PyTypeObject *newType = createPodType(meta);
+ if (!newType) {
+ delete qobject;
+ PyErr_Print();
+ PyErr_Format(PyExc_RuntimeError, "Failed to create POD type for POD '%s'",
+ pod.name.toUtf8().constData());
+ return -1;
+ }
+ if (set_cleanup_capsule_attr_for_pointer(newType, "_qobject_capsule", qobject) < 0) {
+ delete qobject;
+ return -1;
+ }
+
+ PyDict_SetItemString(cppSelf->d->podDict, meta->className(),
+ reinterpret_cast<PyObject *>(newType));
+ Py_DECREF(newType);
+ }
+
+ if (PyErr_Occurred())
+ PyErr_Print();
+
+ for (const auto &cls : std::as_const(cppSelf->d->ast.classes)) {
+ cppSelf->d->classes << cls.name;
+
+ // Create Source type
+ {
+ auto *qobject = new QObject;
+ auto *meta = createAndRegisterSourceFromASTClass(cls, qobject);
+ if (!meta) {
+ delete qobject;
+ PyErr_Format(PyExc_RuntimeError, "Failed to create Source meta object for class '%s'",
+ cls.name.toUtf8().constData());
+ return -1;
+ }
+
+ auto properties = generateProperties(meta, cls);
+ // Check if an error occurred during generateProperties
+ if (PyErr_Occurred()) {
+ delete qobject;
+ return -1;
+ }
+ auto *propertiesPtr = new QVariantList(properties);
+ auto *pyCapsule = PyCapsule_New(propertiesPtr, nullptr, [](PyObject *capsule) {
+ delete reinterpret_cast<QVariantList *>(PyCapsule_GetPointer(capsule, nullptr));
+ });
+
+ PyTypeObject *newType = createDynamicClass(meta, pyCapsule);
+ if (!newType) {
+ delete qobject;
+ PyErr_Format(PyExc_RuntimeError,
+ "Failed to create Source Python type for class '%s'",
+ meta->className());
+ return -1;
+ }
+ if (set_cleanup_capsule_attr_for_pointer(newType, "_qobject_capsule", qobject) < 0) {
+ delete qobject;
+ return -1;
+ }
+
+ PyDict_SetItemString(cppSelf->d->sourceDict, repName(meta),
+ reinterpret_cast<PyObject *>(newType));
+ Py_DECREF(newType);
+ }
+
+ // Create Replica type
+ {
+ auto *qobject = new QObject;
+ auto *meta = createAndRegisterReplicaFromASTClass(cls, qobject);
+ if (!meta) {
+ delete qobject;
+ PyErr_Format(PyExc_RuntimeError,
+ "Failed to create Replica meta object for class '%s'",
+ qPrintable(cls.name));
+ return -1;
+ }
+
+ auto properties = generateProperties(meta, cls);
+ // Check if an error occurred during generateProperties
+ if (PyErr_Occurred()) {
+ delete qobject;
+ return -1;
+ }
+ auto *propertiesPtr = new QVariantList(properties);
+ auto *pyCapsule = PyCapsule_New(propertiesPtr, nullptr, [](PyObject *capsule) {
+ delete reinterpret_cast<QVariantList *>(PyCapsule_GetPointer(capsule, nullptr));
+ });
+
+ PyTypeObject *newType = createDynamicClass(meta, pyCapsule);
+ if (!newType) {
+ delete qobject;
+ PyErr_Format(PyExc_RuntimeError,
+ "Failed to create Replica Python type for class '%s'",
+ meta->className());
+ return -1;
+ }
+ if (set_cleanup_capsule_attr_for_pointer(newType, "_qobject_capsule", qobject) < 0) {
+ delete qobject;
+ return -1;
+ }
+
+ PyDict_SetItemString(cppSelf->d->replicaDict, repName(meta),
+ reinterpret_cast<PyObject *>(newType));
+ Py_DECREF(newType);
+ }
+ }
+
+ return 0;
+}
+
+static void RepFile_tp_free(void *self)
+{
+ PySideRepFile *obj = reinterpret_cast<PySideRepFile*>(self);
+ delete obj->d;
+}
+
+/**
+ * @brief Sets the QVariant value based on the provided default value text.
+ *
+ * This function attempts to set the provided QVariant's value based on the
+ * provided text. It evaluates the text as a Python expression, the the python
+ * type associated with the provided QMetaType. It first retrieves the Python
+ * type object corresponding to the given QMetaType, then constructs a Python
+ * expression to instantiate the type with the default value. The expression is
+ * evaluated using PyRun_String, and the result is then set on the QVariant.
+ * Note: The variant is passed by reference and modified in place.
+ *
+ * @return True if the instantiation is successful, false otherwise.
+ */
+bool instantiateFromDefaultValue(QVariant &variant, const QString &defaultValue)
+{
+ auto metaType = variant.metaType();
+ auto *pyType = Shiboken::Conversions::getPythonTypeObject(metaType.name());
+ if (!pyType) {
+ PyErr_Format(PyExc_TypeError, "Failed to find Python type for meta type: %s",
+ metaType.name());
+ return false;
+ }
+
+ // Evaluate the code
+ static PyObject *pyLocals = PyDict_New();
+
+ // Create the Python expression to evaluate
+ std::string code = std::string(pyType->tp_name) + '('
+ + defaultValue.toUtf8().constData() + ')';
+ PyObject *pyResult = PyRun_String(code.c_str(), Py_eval_input, pyLocals, pyLocals);
+
+ if (!pyResult) {
+ PyObject *ptype = nullptr;
+ PyObject *pvalue = nullptr;
+ PyObject *ptraceback = nullptr;
+ PyErr_Fetch(&ptype, &pvalue, &ptraceback);
+ PyErr_NormalizeException(&ptype, &pvalue, &ptraceback);
+ PyErr_Format(PyExc_TypeError,
+ "Failed to generate default value. Error: %s. Problematic code: %s",
+ Shiboken::String::toCString(PyObject_Str(pvalue)), code.c_str());
+ Py_XDECREF(ptype);
+ Py_XDECREF(pvalue);
+ Py_XDECREF(ptraceback);
+ Py_DECREF(pyLocals);
+ return false;
+ }
+
+ Conversions::SpecificConverter converter(metaType.name());
+ if (!converter) {
+ PyErr_Format(PyExc_TypeError, "Failed to find converter from Python type: %s to Qt type: %s",
+ pyResult->ob_type->tp_name, metaType.name());
+ Py_DECREF(pyResult);
+ return false;
+ }
+ converter.toCpp(pyResult, variant.data());
+ Py_DECREF(pyResult);
+
+ return true;
+}
+
+} // extern "C"
+
+static QVariantList generateProperties(QMetaObject *meta, const ASTClass &astClass)
+{
+ QVariantList properties;
+ auto propertyCount = astClass.properties.size();
+ properties.reserve(propertyCount);
+ for (auto i = 0; i < propertyCount; ++i) {
+ auto j = i + meta->propertyOffset(); // Corresponding property index in the meta object
+ auto metaProperty = meta->property(j);
+ auto metaType = metaProperty.metaType();
+ if (!metaType.isValid()) {
+ PyErr_Format(PyExc_RuntimeError, "Invalid meta type for property %d: %s", i,
+ astClass.properties[i].type.toUtf8().constData());
+ return {};
+ }
+ auto variant = QVariant(metaType);
+ if (auto defaultValue = astClass.properties[i].defaultValue; !defaultValue.isEmpty()) {
+ auto success = instantiateFromDefaultValue(variant, defaultValue);
+ if (!success) {
+ // Print a warning giving the property name, then propagate the error
+ qWarning() << "Failed to instantiate default value for property: "
+ << metaProperty.name();
+ return {};
+ }
+ }
+ properties << variant;
+ }
+ return properties;
+}
+
+namespace PySide::RemoteObjects
+{
+
+static const char *RepFile_SignatureStrings[] = {
+ "PySide6.RemoteObjects.RepFile(self,content:str)",
+ nullptr}; // Sentinel
+
+void init(PyObject *module)
+{
+ if (InitSignatureStrings(PySideRepFile_TypeF(), RepFile_SignatureStrings) < 0)
+ return;
+
+ qRegisterMetaType<QRemoteObjectPendingCall>();
+ qRegisterMetaType<QRemoteObjectPendingCallWatcher>();
+
+ Py_INCREF(PySideRepFile_TypeF());
+ PyModule_AddObject(module, "RepFile", reinterpret_cast<PyObject *>(PySideRepFile_TypeF()));
+
+ // Add a test helper to verify type reference counting
+ static PyMethodDef get_capsule_count_def = {
+ "getCapsuleCount", // name of the function in Python
+ reinterpret_cast<PyCFunction>(get_capsule_count), // C function pointer
+ METH_NOARGS, // flags indicating parameters
+ "Returns the current count of PyCapsule objects" // docstring
+ };
+
+ PyModule_AddObject(module, "getCapsuleCount", PyCFunction_New(&get_capsule_count_def, nullptr));
+}
+
+} // namespace PySide::RemoteObjects
diff --git a/sources/pyside6/libpysideremoteobjects/pysiderephandler_p.h b/sources/pyside6/libpysideremoteobjects/pysiderephandler_p.h
new file mode 100644
index 000000000..5956f0b49
--- /dev/null
+++ b/sources/pyside6/libpysideremoteobjects/pysiderephandler_p.h
@@ -0,0 +1,35 @@
+// Copyright (C) 2025 Ford Motor Company
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef PYSIDE_REPHANDLER_P_H
+#define PYSIDE_REPHANDLER_P_H
+
+#include <sbkpython.h>
+
+#include <QtRemoteObjects/repparser.h>
+
+#include <QtCore/qstringlist.h>
+
+struct PySideRepFilePrivate
+{
+ AST ast;
+ PyObject *podDict{};
+ PyObject *replicaDict{};
+ PyObject *sourceDict{};
+ QStringList classes;
+ QStringList pods;
+};
+
+extern "C"
+{
+ extern PyTypeObject *PySideRepFile_TypeF(void);
+
+ // Internal object
+ struct PySideRepFile
+ {
+ PyObject_HEAD
+ PySideRepFilePrivate *d;
+ };
+}; // extern "C"
+
+#endif // PYSIDE_REPHANDLER_P_H