diff options
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 |
