Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of 'com_record' as a subclassable Python type. #2437

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions com/win32com/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,46 @@ def Record(name, object):
)


# Registration function for com_record subclasses.
def register_record_class(cls):
"""
Register a subclass of com_record to enable creation of the represented record objects.

A subclass of com_record requires the following class attributes to be instantiable:

TLBID : The GUID of the containing TypeLibrary as a string.
MJVER : The major version number of the TypeLibrary as an integer.
MNVER : The minor version number of the TypeLibrary as an integer.
LCID : The LCID of the TypeLibrary as an integer.
GUID : The GUID of the COM Record as a string.

with GUID strings in {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} notation.

To instantiate such a subclasses it has to be registered via this function.
"""
if cls not in pythoncom.com_record.__subclasses__():
geppi marked this conversation as resolved.
Show resolved Hide resolved
raise TypeError("Only subclasses of 'com_record' can be registered.")
try:
obj = cls()
except:
raise TypeError(f"Class {cls.__name__} cannot be instantiated.")
geppi marked this conversation as resolved.
Show resolved Hide resolved
# Since the class can be instantiated we know that it represents a valid COM Record
# in a properly registered TypeLibrary and that it has a 'GUID' class attribute.
if cls.GUID in pythoncom.RecordClasses:
raise ValueError(
f"Record class with same GUID {cls.GUID} "
f"is already registered with name '{pythoncom.RecordClasses[cls.GUID].__name__}'."
)
# Finally check that the name of the subclass we're going to register matches the
geppi marked this conversation as resolved.
Show resolved Hide resolved
# name of the COM Record in the TypeLibrary.
if cls.__name__ != obj.__record_type_name__:
raise ValueError(
f"Name of class does not match the record type name '{obj.__record_type_name__}' "
f"for GUID {cls.GUID} in the TypeLibrary."
)
pythoncom.RecordClasses[cls.GUID] = cls


############################################
# The base of all makepy generated classes
############################################
Expand Down
177 changes: 133 additions & 44 deletions com/win32com/src/PyRecord.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#include <new>
#include "stdafx.h"
#include "PythonCOM.h"
#include "PyRecord.h"

extern PyObject *g_obPyCom_MapRecordGUIDToRecordClass;

// @doc

// The owner of the record buffer - many records may point here!
Expand Down Expand Up @@ -31,7 +34,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 +94,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));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably add a null check? It should have already been there for new, but there are many more error conditions now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. The question is where to put this null check?
There are other places where PyRecord::new_record is used without a null check.

Should we instead raise a Python exception in PyRecord::new_record itself in every place where it currently returns a null without raising an exception?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, it will only currently return null when OOM. In this patch we have a more complicated situation - sometimes when it returns null there will be an exception set, whereas OOM will not. Having those OOM cases call PyErr_NoMem() seems easy, and it looks to me like all of the callers of PyRecord::new_record will do the right thing if null/false is returned - but what the docs say will not be Ok is PyTuple_SET_ITEM.

Copy link
Contributor Author

@geppi geppi Jan 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added commit c9aeec2 and commit 61662ab to resolve this.

this_dest_data += cb_elem;
source_data += cb_elem;
}
Expand Down Expand Up @@ -141,7 +144,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 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 +203,50 @@ 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 registered subclass
// of the 'com_record' base type, it instantiates this subclass.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment needs an update for the param. Or maybe a new function PyRecord::new_record_with_type taking the extra param, which helps move the type-finding out?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tp_new method is the only code path that would use such a PyRecord::new_record_with_type function and it would duplicate code that's already in the PyRecord::new_record function. Therefore I would prefer to keep this extra parameter which shortcuts the type identification procedure and just update the comment.

PyRecord *PyRecord::new_record(IRecordInfo *ri, PVOID data, PyRecordBuffer *owner)
{
GUID structguid;
OLECHAR *guidString;
PyObject *guidUnicode, *recordType;
// 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;
if (!(guidUnicode = PyUnicode_FromWideChar(guidString, -1)))
return NULL;
::CoTaskMemFree(guidString);
recordType = PyDict_GetItem(g_obPyCom_MapRecordGUIDToRecordClass, guidUnicode);
Py_DECREF(guidUnicode);
// If the Record GUID is registered as a subclass of com_record
// we return an object of the subclass type.
if (recordType && PyObject_IsSubclass(recordType, (PyObject *)&PyRecord::Type)) {
type = (PyTypeObject *)recordType;
}
// 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,44 +256,81 @@ 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 */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_compare */
&PyRecord::tp_repr, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
PyRecord::getattro, /* tp_getattro */
PyRecord::setattro, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT, /* tp_flags */
0, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
PyRecord::tp_richcompare, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
PyRecord::methods, /* tp_methods */
0, /* tp_members */
0, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
0, /* tp_new */
(destructor)PyRecord::tp_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_compare */
&PyRecord::tp_repr, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
PyRecord::getattro, /* tp_getattro */
PyRecord::setattro, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
0, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
PyRecord::tp_richcompare, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
PyRecord::methods, /* tp_methods */
0, /* tp_members */
0, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
(newfunc)PyRecord::tp_new, /* tp_new */
};

static PyObject *PyRecord_reduce(PyObject *self, PyObject *args)
Expand Down Expand Up @@ -435,6 +511,15 @@ PyObject *PyRecord::getattro(PyObject *self, PyObject *obname)
char *name = PYWIN_ATTR_CONVERT(obname);
if (name == NULL)
return NULL;
if (strcmp(name, "__record_type_name__") == 0) {
BSTR rec_name;
HRESULT hr = pyrec->pri->GetName(&rec_name);
if (FAILED(hr))
return PyCom_BuildPyException(hr, pyrec->pri, IID_IRecordInfo);
PyObject *res = PyWinCoreString_FromString(rec_name);
SysFreeString(rec_name);
return res;
}
if (strcmp(name, "__members__") == 0) {
ULONG cnames = 0;
HRESULT hr = pyrec->pri->GetFieldNames(&cnames, NULL);
Expand Down Expand Up @@ -496,7 +581,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 +616,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 +730,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);
}
12 changes: 12 additions & 0 deletions com/win32com/src/PythonCOM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ extern PyObject *pythoncom_IsGatewayRegistered(PyObject *self, PyObject *args);
extern PyObject *g_obPyCom_MapIIDToType;
extern PyObject *g_obPyCom_MapGatewayIIDToName;
extern PyObject *g_obPyCom_MapInterfaceNameToIID;
extern PyObject *g_obPyCom_MapRecordGUIDToRecordClass;

PyObject *g_obEmpty = NULL;
PyObject *g_obMissing = NULL;
Expand Down Expand Up @@ -2195,6 +2196,13 @@ PYWIN_MODULE_INIT_FUNC(pythoncom)
PYWIN_MODULE_INIT_RETURN_ERROR;
}

// Initialize the dictionary for registering com_record subclasses.
g_obPyCom_MapRecordGUIDToRecordClass = PyDict_New();
if (g_obPyCom_MapRecordGUIDToRecordClass == NULL) {
PYWIN_MODULE_INIT_RETURN_ERROR;
}
PyDict_SetItemString(dict, "RecordClasses", g_obPyCom_MapRecordGUIDToRecordClass);

// XXX - more error checking?
PyDict_SetItemString(dict, "TypeIIDs", g_obPyCom_MapIIDToType);
PyDict_SetItemString(dict, "ServerInterfaces", g_obPyCom_MapGatewayIIDToName);
Expand Down Expand Up @@ -2248,6 +2256,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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it have "type" in the name? (I've no idea about idiomatic python tbh!)

Copy link
Contributor Author

@geppi geppi Jan 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, in the example code on python.org it has the same name as in the tp_name slot. So the module attribute is

pythoncom.com_record
<class 'com_record'>

and the type of an instance is

type(instance)
<class 'com_record'>

which, I would say, makes sense.

Although more precisely, in the example code, the tp_name slot also specifies the module name of the new type.
However, I didn't want to change this to PYWIN_OBJECT_HEAD "pythoncom.com_record" because I didn't want to break any existing code.

Of course the name of the module attribute could be changed to com_record_type which would only make a difference in one place as far as I understand:

pythoncom.com_record_type
<class 'com_record'>

Up to you.

PYWIN_MODULE_INIT_RETURN_ERROR;

// Setup our sub-modules
if (!initunivgw(dict))
PYWIN_MODULE_INIT_RETURN_ERROR;
Expand Down
9 changes: 5 additions & 4 deletions com/win32com/src/Register.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,11 @@ generates Windows .hlp files.
#include "PyICancelMethodCalls.h"

// PyObject *CLSIDMapping; // Maps CLSIDs onto PyClassObjects
PyObject *g_obPyCom_MapIIDToType = NULL; // map of IID's to client types.
PyObject *g_obPyCom_MapGatewayIIDToName = NULL; // map of IID's to names
PyObject *g_obPyCom_MapInterfaceNameToIID = NULL; // map of names to IID
PyObject *g_obPyCom_MapServerIIDToGateway = NULL; // map of IID's to gateways.
PyObject *g_obPyCom_MapIIDToType = NULL; // map of IID's to client types.
PyObject *g_obPyCom_MapGatewayIIDToName = NULL; // map of IID's to names
PyObject *g_obPyCom_MapInterfaceNameToIID = NULL; // map of names to IID
PyObject *g_obPyCom_MapServerIIDToGateway = NULL; // map of IID's to gateways.
PyObject *g_obPyCom_MapRecordGUIDToRecordClass = NULL; // map of COM Record GUIDs to subclasses of com_record.

// Register a Python on both the UID and Name maps.
int PyCom_RegisterClientType(PyTypeObject *typeOb, const GUID *guid)
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