diff options
| author | Brett Stottlemyer <bstottle@ford.com> | 2024-12-18 10:33:56 -0500 |
|---|---|---|
| committer | Friedemann Kleint <Friedemann.Kleint@qt.io> | 2025-03-13 16:28:42 +0100 |
| commit | 19abd816e73bebdd489408d0a3b7676822bff39c (patch) | |
| tree | 8459ae9401f5e190995b3e24b6ae6968cf457baf /sources/pyside6/libpysideremoteobjects | |
| parent | 3c66c456aeab597b7cb046f81c7f015433bb57a4 (diff) | |
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 <Friedemann.Kleint@qt.io>
Reviewed-by: Brett Stottlemyer <brett.stottlemyer@gmail.com>
Diffstat (limited to 'sources/pyside6/libpysideremoteobjects')
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 ¤tVariant = 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 |
