From 11c5cd45c0db2568b5a273a02090488245978a40 Mon Sep 17 00:00:00 2001 From: junkmd Date: Sat, 10 Jan 2026 20:33:32 +0900 Subject: [PATCH 1/3] feat: Introduce `malloc.py` for COM memory management. Introduces `malloc.py` which provides `IMalloc` interface definition. Renamed `test_outparam.py` to `test_from_outparam.py`. Update `pyproject.toml` for renamed test file. --- comtypes/malloc.py | 108 ++++++++++++++++++ ...test_outparam.py => test_from_outparam.py} | 0 pyproject.toml | 2 +- 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 comtypes/malloc.py rename comtypes/test/{test_outparam.py => test_from_outparam.py} (100%) diff --git a/comtypes/malloc.py b/comtypes/malloc.py new file mode 100644 index 00000000..88172ca5 --- /dev/null +++ b/comtypes/malloc.py @@ -0,0 +1,108 @@ +import logging +import unittest +from ctypes import ( + HRESULT, + POINTER, + OleDLL, + WinDLL, + byref, + c_int, + c_size_t, + c_ulong, + c_void_p, + c_wchar, + c_wchar_p, + cast, + memmove, + sizeof, + wstring_at, +) +from ctypes.wintypes import DWORD, LPVOID +from unittest.mock import patch + +from comtypes import COMMETHOD, GUID, IUnknown +from comtypes.GUID import _CoTaskMemFree + +logger = logging.getLogger(__name__) + + +class IMalloc(IUnknown): + _iid_ = GUID("{00000002-0000-0000-C000-000000000046}") + _methods_ = [ + COMMETHOD([], c_void_p, "Alloc", ([], c_ulong, "cb")), + COMMETHOD([], c_void_p, "Realloc", ([], c_void_p, "pv"), ([], c_ulong, "cb")), + COMMETHOD([], None, "Free", ([], c_void_p, "py")), + COMMETHOD([], c_ulong, "GetSize", ([], c_void_p, "pv")), + COMMETHOD([], c_int, "DidAlloc", ([], c_void_p, "pv")), + COMMETHOD([], None, "HeapMinimize"), # 25 + ] + + +_ole32 = OleDLL("ole32") + +_CoGetMalloc = _ole32.CoGetMalloc +_CoGetMalloc.argtypes = [DWORD, POINTER(POINTER(IMalloc))] +_CoGetMalloc.restype = HRESULT + +_ole32_nohresult = WinDLL("ole32") + +SIZE_T = c_size_t +_CoTaskMemAlloc = _ole32_nohresult.CoTaskMemAlloc +_CoTaskMemAlloc.argtypes = [SIZE_T] +_CoTaskMemAlloc.restype = LPVOID + +malloc = POINTER(IMalloc)() +_CoGetMalloc(1, byref(malloc)) +assert bool(malloc) + + +def from_outparam(self): + if not self: + return None + result = wstring_at(self) + # `DidAlloc` method returns; + # * 1 (allocated) + # * 0 (not allocated) + # * -1 (cannot determine or NULL) + # https://learn.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-imalloc-didalloc + assert malloc.DidAlloc(self), "memory was NOT allocated by CoTaskMemAlloc" + _CoTaskMemFree(self) + return result + + +def comstring(text, typ=c_wchar_p): + size = (len(text) + 1) * sizeof(c_wchar) + mem = _CoTaskMemAlloc(size) + logger.debug("malloc'd 0x%x, %d bytes" % (mem, size)) + ptr = cast(mem, typ) + memmove(mem, text, size) + return ptr + + +class Test(unittest.TestCase): + @patch.object(c_wchar_p, "__ctypes_from_outparam__", from_outparam) + def test_c_char(self): + ptr = c_wchar_p("abc") + # The normal constructor does not allocate memory using `CoTaskMemAlloc`. + # Therefore, calling the patched `ptr.__ctypes_from_outparam__()` would + # attempt to free invalid memory, potentially leading to a crash. + self.assertEqual(malloc.DidAlloc(ptr), 0) + + x = comstring("Hello, World") + y = comstring("foo bar") + z = comstring("spam, spam, and spam") + + # The `__ctypes_from_outparam__` method is called to convert an output + # parameter into a Python object. In this test, the custom + # `from_outparam` function not only converts the `c_wchar_p` to a + # Python string but also frees the associated memory. Therefore, it can + # only be called once for each allocated memory block. + for wchar_ptr, expected in [ + (x, "Hello, World"), + (y, "foo bar"), + (z, "spam, spam, and spam"), + ]: + with self.subTest(wchar_ptr=wchar_ptr, expected=expected): + self.assertEqual(malloc.DidAlloc(wchar_ptr), 1) + self.assertEqual(wchar_ptr.__ctypes_from_outparam__(), expected) + self.assertEqual(malloc.DidAlloc(wchar_ptr), 0) diff --git a/comtypes/test/test_outparam.py b/comtypes/test/test_from_outparam.py similarity index 100% rename from comtypes/test/test_outparam.py rename to comtypes/test/test_from_outparam.py diff --git a/pyproject.toml b/pyproject.toml index f9cea138..b44e4dd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ ignore = ["E402"] "comtypes/test/test_client.py" = ["F401"] "comtypes/test/test_dict.py" = ["F841"] "comtypes/test/test_eventinterface.py" = ["F841"] -"comtypes/test/test_outparam.py" = ["F841"] +"comtypes/test/test_from_outparam.py" = ["F841"] "comtypes/test/test_sapi.py" = ["E401"] "comtypes/test/test_server.py" = ["F401", "F841"] "comtypes/test/test_subinterface.py" = ["E401", "F401", "F403", "F405"] From a3cea9631c73a1888218f4569e30c8bf8bd0c6cd Mon Sep 17 00:00:00 2001 From: junkmd Date: Sat, 10 Jan 2026 20:50:27 +0900 Subject: [PATCH 2/3] test: Add blank `test_malloc.py` for `IMalloc` interface. --- comtypes/test/test_malloc.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 comtypes/test/test_malloc.py diff --git a/comtypes/test/test_malloc.py b/comtypes/test/test_malloc.py new file mode 100644 index 00000000..597e58b7 --- /dev/null +++ b/comtypes/test/test_malloc.py @@ -0,0 +1,6 @@ +import unittest as ut + +from comtypes.malloc import IMalloc # noqa + + +class Test(ut.TestCase): ... From d9c4b4895bf01f2ecf53a45006b6283f0760d2d0 Mon Sep 17 00:00:00 2001 From: junkmd Date: Sat, 10 Jan 2026 20:57:23 +0900 Subject: [PATCH 3/3] fix: Eliminate the testcase. --- comtypes/malloc.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/comtypes/malloc.py b/comtypes/malloc.py index 88172ca5..d4543d41 100644 --- a/comtypes/malloc.py +++ b/comtypes/malloc.py @@ -1,5 +1,4 @@ import logging -import unittest from ctypes import ( HRESULT, POINTER, @@ -18,7 +17,6 @@ wstring_at, ) from ctypes.wintypes import DWORD, LPVOID -from unittest.mock import patch from comtypes import COMMETHOD, GUID, IUnknown from comtypes.GUID import _CoTaskMemFree @@ -77,32 +75,3 @@ def comstring(text, typ=c_wchar_p): ptr = cast(mem, typ) memmove(mem, text, size) return ptr - - -class Test(unittest.TestCase): - @patch.object(c_wchar_p, "__ctypes_from_outparam__", from_outparam) - def test_c_char(self): - ptr = c_wchar_p("abc") - # The normal constructor does not allocate memory using `CoTaskMemAlloc`. - # Therefore, calling the patched `ptr.__ctypes_from_outparam__()` would - # attempt to free invalid memory, potentially leading to a crash. - self.assertEqual(malloc.DidAlloc(ptr), 0) - - x = comstring("Hello, World") - y = comstring("foo bar") - z = comstring("spam, spam, and spam") - - # The `__ctypes_from_outparam__` method is called to convert an output - # parameter into a Python object. In this test, the custom - # `from_outparam` function not only converts the `c_wchar_p` to a - # Python string but also frees the associated memory. Therefore, it can - # only be called once for each allocated memory block. - for wchar_ptr, expected in [ - (x, "Hello, World"), - (y, "foo bar"), - (z, "spam, spam, and spam"), - ]: - with self.subTest(wchar_ptr=wchar_ptr, expected=expected): - self.assertEqual(malloc.DidAlloc(wchar_ptr), 1) - self.assertEqual(wchar_ptr.__ctypes_from_outparam__(), expected) - self.assertEqual(malloc.DidAlloc(wchar_ptr), 0)