-
-
Notifications
You must be signed in to change notification settings - Fork 33.4k
Description
Feature or enhancement
Proposal:
I am one of the maintainers of comtypes. comtypes is based on ctypes to implement IUnknown and other COM stuffs.
In the past, I reported in gh-124520 that projects dependent on ctypes was broken due to changes in Python 3.13.
I am currently researching whether there are any effective ways to proactively prevent such regressions beyond what I attempted in gh-125783.
I noticed that the cpython repository might not contain tests for basic COM methods, such as QueryInterface, AddRef, and Release.
There are many projects besides comtypes that define COM interfaces and call COM methods, so I think it’s important to test them.
I think that simple tests like the one below, which implements a very basic COM interface and calls its methods, might help prevent regressions.
(I removed the complex parts in comtypes, such as defining methods, registering pointer types, and using __del__ to call Release through metaclasse magic.)
(I have confirmed that this test passes in a virtual environment with Python 3.11.2, which I could quickly set up.)
import sys
import unittest
def setUpModule():
if sys.platform != "win32":
raise unittest.SkipTest("Win32 only")
import ctypes
import gc
from ctypes import HRESULT, POINTER, byref
from ctypes.wintypes import BYTE, DWORD, HGLOBAL, WORD
ole32 = ctypes.oledll.ole32
oleaut32 = ctypes.oledll.oleaut32
def CLSIDFromString(name):
guid = GUID()
ole32.CLSIDFromString(name, byref(guid))
return guid
class GUID(ctypes.Structure):
_fields_ = [
("Data1", DWORD),
("Data2", WORD),
("Data3", WORD),
("Data4", BYTE * 8),
]
PyInstanceMethod_New = ctypes.pythonapi.PyInstanceMethod_New
PyInstanceMethod_New.argtypes = [ctypes.py_object]
PyInstanceMethod_New.restype = ctypes.py_object
PyInstanceMethod_Type = type(PyInstanceMethod_New(id))
class COM_METHOD:
def __init__(self, index, restype, *argtypes):
self.index = index
self.proto = ctypes.WINFUNCTYPE(restype, *argtypes)
def __set_name__(self, owner, name):
self.mth = PyInstanceMethod_Type(self.proto(self.index, name))
def __call__(self, *args, **kwargs):
return self.mth(*args, **kwargs)
def __get__(self, instance, owner):
if instance is None:
return self
return self.mth.__get__(instance)
class IUnknown(ctypes.c_void_p):
IID = CLSIDFromString("{00000000-0000-0000-C000-000000000046}")
QueryInterface = COM_METHOD(0, HRESULT, POINTER(GUID), POINTER(ctypes.c_void_p))
AddRef = COM_METHOD(1, ctypes.c_long)
Release = COM_METHOD(2, ctypes.c_long)
class ICreateTypeLib(IUnknown):
IID = CLSIDFromString("{00020406-0000-0000-C000-000000000046}")
# `CreateTypeInfo` and more methods should be implemented
class ICreateTypeLib2(ICreateTypeLib):
IID = CLSIDFromString("{0002040F-0000-0000-C000-000000000046}")
# `DeleteTypeInfo` and more methods should be implemented
class ISequentialStream(IUnknown):
IID = CLSIDFromString("{0C733A30-2A1C-11CE-ADE5-00AA0044773D}")
# `Read` and `Write` methods should be implemented
class IStream(ISequentialStream):
IID = CLSIDFromString("{0000000C-0000-0000-C000-000000000046}")
# `Seek` and more methods should be implemented
CreateTypeLib2 = oleaut32.CreateTypeLib2
CreateTypeLib2.argtypes = (ctypes.c_int, ctypes.c_wchar_p, POINTER(ICreateTypeLib2))
CreateStreamOnHGlobal = ole32.CreateStreamOnHGlobal
CreateStreamOnHGlobal.argtypes = (HGLOBAL, ctypes.c_bool, POINTER(IStream))
COINIT_APARTMENTTHREADED = 0x2
S_OK = 0
E_NOINTERFACE = -2147467262
class Test(unittest.TestCase):
def setUp(self):
ole32.CoInitializeEx(None, COINIT_APARTMENTTHREADED)
def tearDown(self):
ole32.CoUninitialize()
gc.collect()
def test_create_typelib_2(self):
pctlib = ICreateTypeLib2()
hr = CreateTypeLib2(0, "sample.tlb", pctlib)
self.assertEqual(S_OK, hr)
self.assertEqual(2, pctlib.AddRef())
self.assertEqual(3, pctlib.AddRef())
self.assertEqual(2, pctlib.Release())
self.assertEqual(1, pctlib.Release())
self.assertEqual(0, pctlib.Release())
def test_stream(self):
pstm = IStream()
hr = CreateStreamOnHGlobal(None, True, pstm)
self.assertEqual(S_OK, hr)
self.assertEqual(2, pstm.AddRef())
self.assertEqual(3, pstm.AddRef())
self.assertEqual(2, pstm.Release())
self.assertEqual(1, pstm.Release())
self.assertEqual(0, pstm.Release())
def test_query_interface(self):
pctlib2 = ICreateTypeLib2()
CreateTypeLib2(0, "sample.tlb", pctlib2)
pctlib = ICreateTypeLib()
hr1 = pctlib2.QueryInterface(byref(ICreateTypeLib.IID), byref(pctlib))
self.assertEqual(S_OK, hr1)
self.assertEqual(1, pctlib.Release())
punk = IUnknown()
hr2 = pctlib.QueryInterface(byref(IUnknown.IID), byref(punk))
self.assertEqual(S_OK, hr2)
self.assertEqual(1, punk.Release())
pstm = IStream()
with self.assertRaises(WindowsError) as e: # Why not `COMError`?
punk.QueryInterface(byref(IStream.IID), byref(pstm))
self.assertEqual(E_NOINTERFACE, e.exception.winerror)
self.assertEqual(0, punk.Release())- I am not sure why a
WindowsErroris raised instead of aCOMErrorwhenQueryInterfacefails. - Perhaps an interface with even fewer methods should be used in the test.
- At this stage, I think creating custom COM type libraries and interfaces might be excessive.
I welcome any feedback.
Has this already been discussed elsewhere?
No response given
Links to previous discussion of this feature:
No response