diff options
Diffstat (limited to 'sources/pyside6/libpysideremoteobjects/pysidedynamicclass.cpp')
| -rw-r--r-- | sources/pyside6/libpysideremoteobjects/pysidedynamicclass.cpp | 506 |
1 files changed, 506 insertions, 0 deletions
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; +} |
