Skip to content

Commit

Permalink
Implementation of 'com_record' as a subclassable Python type.
Browse files Browse the repository at this point in the history
A 'tp_new' method was added to the 'PyTypeObject' slots of 'com_record'.
Replacement new is now used to create an instance of a 'com_record' type,
i.e. in 'tp_new' as well as in the factory functions. The 'tp_dealloc'
method explicitely calls the destructor before finally calling 'tp_free'.

Records returned from a call to a COM method do get the subclass type
according to the GUID in the COM RecordInfo. If no subclass with that
GUID is found, the generic 'com_record' base class is used.

The algorithm that retrieves the list of subclasses of 'com_record'
only uses methods and data structures of the public Python API.
This is important because in Python 3.12 the type of the 'tp_subclasses'
slot of a 'PyTypeObject' was changed to 'void*' to indicate that for some
types it does not hold a valid 'PyObject*'.

The 'PyRecord_Check' function was modified to test if the object is an
instance of 'com_record' or a derived type.

The implementation does not break existing code.
It is not required to define 'com_record' subclasses, i.e. it is possible
to work with the generic 'com_record' type as before using the factory
function.
  • Loading branch information
geppi committed Dec 13, 2024
1 parent 58ea154 commit 0c591f8
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 14 deletions.
128 changes: 115 additions & 13 deletions com/win32com/src/PyRecord.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include <new>
#include "stdafx.h"
#include "PythonCOM.h"
#include "PyRecord.h"
Expand Down Expand Up @@ -31,7 +32,7 @@ class PyRecordBuffer {
long ref;
};

BOOL PyRecord_Check(PyObject *ob) { return ((ob)->ob_type == &PyRecord::Type); }
BOOL PyRecord_Check(PyObject *ob) { return PyObject_IsInstance(ob, (PyObject *) &PyRecord::Type); }

BOOL PyObject_AsVARIANTRecordInfo(PyObject *ob, VARIANT *pv)
{
Expand Down Expand Up @@ -91,7 +92,7 @@ PyObject *PyObject_FromSAFEARRAYRecordInfo(SAFEARRAY *psa)
hr = info->RecordCopy(source_data, this_dest_data);
if (FAILED(hr))
goto exit;
PyTuple_SET_ITEM(ret_tuple, i, new PyRecord(info, this_dest_data, owner));
PyTuple_SET_ITEM(ret_tuple, i, PyRecord::new_record(info, this_dest_data, owner));
this_dest_data += cb_elem;
source_data += cb_elem;
}
Expand Down Expand Up @@ -141,7 +142,7 @@ PyObject *PyObject_FromRecordInfo(IRecordInfo *ri, void *data, ULONG cbData)
delete owner;
return PyCom_BuildPyException(hr, ri, IID_IRecordInfo);
}
return new PyRecord(ri, owner->data, owner);
return (PyObject *) PyRecord::new_record(ri, owner->data, owner);
}

// @pymethod <o PyRecord>|pythoncom|GetRecordFromGuids|Creates a new record object from the given GUIDs
Expand Down Expand Up @@ -200,14 +201,69 @@ PyObject *pythoncom_GetRecordFromTypeInfo(PyObject *self, PyObject *args)
return ret;
}

PyRecord::PyRecord(IRecordInfo *ri, PVOID data, PyRecordBuffer *owner)
// This function creates a new 'com_record' instance with placement new.
// If the particular Record GUID belongs to a directly derived subclass
// of the 'com_record' base type, it instantiates this subclass.
PyRecord *PyRecord::new_record(IRecordInfo *ri, PVOID data, PyRecordBuffer *owner)
{
PyObject *list, *raw, *ref;
Py_ssize_t i;
GUID structguid;
OLECHAR* guidString;
// By default we create an instance of the base 'com_record' type.
PyTypeObject *type = &PyRecord::Type;
// Retrieve the GUID of the Record to be created.
HRESULT hr = ri->GetGuid(&structguid);
if (FAILED(hr))
{
PyCom_BuildPyException(hr);
return NULL;
}
if (S_OK != StringFromCLSID(structguid, &guidString))
return NULL;
// Obtain a copy of the subclasses list to iterate over.
list = PyObject_CallMethod((PyObject*)type, "__subclasses__", NULL);
// We now have a list of the directly derived subclasses of 'com_record'.
// If no subclasses have been defined the list is empty.
// Iterate over the list and try to find a subclass with matching GUID.
PyObject *recordIter = PyObject_GetIter(list);
PyTypeObject *recordType;
wchar_t *item_guid;
while (recordType = (PyTypeObject *) PyIter_Next(recordIter))
{
if (PyObject *item = PyDict_GetItemString(recordType->tp_dict, "GUID"))
{
if (!(item_guid = PyUnicode_AsWideCharString(item, NULL)))
continue;
if (wcscmp(guidString, item_guid) == 0)
{
type = recordType;
PyMem_Free(item_guid);
break;
}
PyMem_Free(item_guid);
}
Py_DECREF(recordType);
}
Py_DECREF(recordIter);
Py_DECREF(list);
::CoTaskMemFree(guidString);
// Finally allocate the memory for the the appropriate
// Record type and construct the instance with placement new.
char *buf = (char *) PyRecord::Type.tp_alloc(type, 0);
if (buf == NULL) {
delete owner;
return NULL;
}
return new(buf) PyRecord(ri, owner->data, owner);
}

PyRecord::PyRecord(IRecordInfo *ri, PVOID data, PyRecordBuffer *buf_owner)
{
ob_type = &PyRecord::Type;
_Py_NewReference(this);
ri->AddRef();
pri = ri;
pdata = data;
this->owner = owner;
owner = buf_owner;
owner->AddRef();
};

Expand All @@ -217,11 +273,53 @@ PyRecord::~PyRecord()
pri->Release();
}

PyObject *PyRecord::tp_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
PyObject *item, *obGuid, *obInfoGuid;
int major, minor, lcid;
GUID guid, infoGuid;
if (type == &PyRecord::Type)
// If the base 'com_record' type was called try to get the
// information required for instance creation from the call parameters.
{
if (!PyArg_ParseTuple(args, "OiiiO:__new__",
&obGuid, // @pyparm <o PyIID>|iid||The GUID of the type library
&major, // @pyparm int|verMajor||The major version number of the type lib.
&minor, // @pyparm int|verMinor||The minor version number of the type lib.
&lcid, // @pyparm int|lcid||The LCID of the type lib.
&obInfoGuid)) // @pyparm <o PyIID>|infoIID||The GUID of the record info in the library
return NULL;
if (!PyWinObject_AsIID(obGuid, &guid))
return NULL;
if (!PyWinObject_AsIID(obInfoGuid, &infoGuid))
return NULL;
}
// Otherwise try to get the information from the class variables of the derived type.
else if (!(item = PyDict_GetItemString(type->tp_dict, "GUID")) ||
!PyWinObject_AsIID(item, &infoGuid)||
!(item = PyDict_GetItemString(type->tp_dict, "TLBID")) ||
!PyWinObject_AsIID(item, &guid) ||
!(item = PyDict_GetItemString(type->tp_dict, "MJVER")) ||
((major = PyLong_AsLong(item)) == -1) ||
!(item = PyDict_GetItemString(type->tp_dict, "MNVER")) ||
((minor = PyLong_AsLong(item)) == -1) ||
!(item = PyDict_GetItemString(type->tp_dict, "LCID")) ||
((lcid = PyLong_AsLong(item)) == -1))
return NULL;
IRecordInfo *ri = NULL;
HRESULT hr = GetRecordInfoFromGuids(guid, major, minor, lcid, infoGuid, &ri);
if (FAILED(hr))
return PyCom_BuildPyException(hr);
PyObject *ret = PyObject_FromRecordInfo(ri, NULL, 0);
ri->Release();
return ret;
}

PyTypeObject PyRecord::Type = {
PYWIN_OBJECT_HEAD "com_record",
sizeof(PyRecord),
0,
PyRecord::tp_dealloc, /* tp_dealloc */
(destructor) PyRecord::tp_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
Expand All @@ -236,7 +334,7 @@ PyTypeObject PyRecord::Type = {
PyRecord::getattro, /* tp_getattro */
PyRecord::setattro, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT, /* tp_flags */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
0, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
Expand All @@ -254,7 +352,7 @@ PyTypeObject PyRecord::Type = {
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
0, /* tp_new */
(newfunc)PyRecord::tp_new,/* tp_new */
};

static PyObject *PyRecord_reduce(PyObject *self, PyObject *args)
Expand Down Expand Up @@ -496,7 +594,7 @@ PyObject *PyRecord::getattro(PyObject *self, PyObject *obname)
// Short-circuit sub-structs and arrays here, so we don't allocate a new chunk
// of memory and copy it - we need sub-structs to persist.
if (V_VT(&vret) == (VT_BYREF | VT_RECORD))
return new PyRecord(V_RECORDINFO(&vret), V_RECORD(&vret), pyrec->owner);
return PyRecord::new_record(V_RECORDINFO(&vret), V_RECORD(&vret), pyrec->owner);
else if (V_VT(&vret) == (VT_BYREF | VT_ARRAY | VT_RECORD)) {
SAFEARRAY *psa = *V_ARRAYREF(&vret);
if (SafeArrayGetDim(psa) != 1)
Expand Down Expand Up @@ -531,7 +629,7 @@ PyObject *PyRecord::getattro(PyObject *self, PyObject *obname)
// in the last parameter, i.e. 'sub_data == NULL'.
this_data = (BYTE *)psa->pvData;
for (i = 0; i < nelems; i++) {
PyTuple_SET_ITEM(ret_tuple, i, new PyRecord(sub, this_data, pyrec->owner));
PyTuple_SET_ITEM(ret_tuple, i, PyRecord::new_record(sub, this_data, pyrec->owner));
this_data += element_size;
}
array_end:
Expand Down Expand Up @@ -645,4 +743,8 @@ PyObject *PyRecord::tp_richcompare(PyObject *self, PyObject *other, int op)
return ret;
}

void PyRecord::tp_dealloc(PyObject *ob) { delete (PyRecord *)ob; }
void PyRecord::tp_dealloc(PyRecord *self)
{
self->~PyRecord();
Py_TYPE(self)->tp_free((PyObject *) self);
}
4 changes: 4 additions & 0 deletions com/win32com/src/PythonCOM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2248,6 +2248,10 @@ PYWIN_MODULE_INIT_FUNC(pythoncom)
PyType_Ready(&PyRecord::Type) == -1)
PYWIN_MODULE_INIT_RETURN_ERROR;

// Add the PyRecord type as a module attribute
if (PyModule_AddObject(module, "com_record", (PyObject *)&PyRecord::Type) != 0)
PYWIN_MODULE_INIT_RETURN_ERROR;

// Setup our sub-modules
if (!initunivgw(dict))
PYWIN_MODULE_INIT_RETURN_ERROR;
Expand Down
4 changes: 3 additions & 1 deletion com/win32com/src/include/PyRecord.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ class PyRecord : public PyObject {
PyRecord(IRecordInfo *ri, PVOID data, PyRecordBuffer *owner);
~PyRecord();

static void tp_dealloc(PyObject *ob);
static PyRecord *new_record(IRecordInfo *ri, PVOID data, PyRecordBuffer *owner);
static PyObject *tp_new(PyTypeObject *type, PyObject *args, PyObject *kwds);
static void tp_dealloc(PyRecord *ob);
static PyObject *getattro(PyObject *self, PyObject *obname);
static int setattro(PyObject *self, PyObject *obname, PyObject *v);
static PyObject *tp_repr(PyObject *self);
Expand Down

0 comments on commit 0c591f8

Please sign in to comment.