Skip to content

Commit 11c5cd4

Browse files
committed
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.
1 parent 925c5cb commit 11c5cd4

File tree

3 files changed

+109
-1
lines changed

3 files changed

+109
-1
lines changed

comtypes/malloc.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import logging
2+
import unittest
3+
from ctypes import (
4+
HRESULT,
5+
POINTER,
6+
OleDLL,
7+
WinDLL,
8+
byref,
9+
c_int,
10+
c_size_t,
11+
c_ulong,
12+
c_void_p,
13+
c_wchar,
14+
c_wchar_p,
15+
cast,
16+
memmove,
17+
sizeof,
18+
wstring_at,
19+
)
20+
from ctypes.wintypes import DWORD, LPVOID
21+
from unittest.mock import patch
22+
23+
from comtypes import COMMETHOD, GUID, IUnknown
24+
from comtypes.GUID import _CoTaskMemFree
25+
26+
logger = logging.getLogger(__name__)
27+
28+
29+
class IMalloc(IUnknown):
30+
_iid_ = GUID("{00000002-0000-0000-C000-000000000046}")
31+
_methods_ = [
32+
COMMETHOD([], c_void_p, "Alloc", ([], c_ulong, "cb")),
33+
COMMETHOD([], c_void_p, "Realloc", ([], c_void_p, "pv"), ([], c_ulong, "cb")),
34+
COMMETHOD([], None, "Free", ([], c_void_p, "py")),
35+
COMMETHOD([], c_ulong, "GetSize", ([], c_void_p, "pv")),
36+
COMMETHOD([], c_int, "DidAlloc", ([], c_void_p, "pv")),
37+
COMMETHOD([], None, "HeapMinimize"), # 25
38+
]
39+
40+
41+
_ole32 = OleDLL("ole32")
42+
43+
_CoGetMalloc = _ole32.CoGetMalloc
44+
_CoGetMalloc.argtypes = [DWORD, POINTER(POINTER(IMalloc))]
45+
_CoGetMalloc.restype = HRESULT
46+
47+
_ole32_nohresult = WinDLL("ole32")
48+
49+
SIZE_T = c_size_t
50+
_CoTaskMemAlloc = _ole32_nohresult.CoTaskMemAlloc
51+
_CoTaskMemAlloc.argtypes = [SIZE_T]
52+
_CoTaskMemAlloc.restype = LPVOID
53+
54+
malloc = POINTER(IMalloc)()
55+
_CoGetMalloc(1, byref(malloc))
56+
assert bool(malloc)
57+
58+
59+
def from_outparam(self):
60+
if not self:
61+
return None
62+
result = wstring_at(self)
63+
# `DidAlloc` method returns;
64+
# * 1 (allocated)
65+
# * 0 (not allocated)
66+
# * -1 (cannot determine or NULL)
67+
# https://learn.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-imalloc-didalloc
68+
assert malloc.DidAlloc(self), "memory was NOT allocated by CoTaskMemAlloc"
69+
_CoTaskMemFree(self)
70+
return result
71+
72+
73+
def comstring(text, typ=c_wchar_p):
74+
size = (len(text) + 1) * sizeof(c_wchar)
75+
mem = _CoTaskMemAlloc(size)
76+
logger.debug("malloc'd 0x%x, %d bytes" % (mem, size))
77+
ptr = cast(mem, typ)
78+
memmove(mem, text, size)
79+
return ptr
80+
81+
82+
class Test(unittest.TestCase):
83+
@patch.object(c_wchar_p, "__ctypes_from_outparam__", from_outparam)
84+
def test_c_char(self):
85+
ptr = c_wchar_p("abc")
86+
# The normal constructor does not allocate memory using `CoTaskMemAlloc`.
87+
# Therefore, calling the patched `ptr.__ctypes_from_outparam__()` would
88+
# attempt to free invalid memory, potentially leading to a crash.
89+
self.assertEqual(malloc.DidAlloc(ptr), 0)
90+
91+
x = comstring("Hello, World")
92+
y = comstring("foo bar")
93+
z = comstring("spam, spam, and spam")
94+
95+
# The `__ctypes_from_outparam__` method is called to convert an output
96+
# parameter into a Python object. In this test, the custom
97+
# `from_outparam` function not only converts the `c_wchar_p` to a
98+
# Python string but also frees the associated memory. Therefore, it can
99+
# only be called once for each allocated memory block.
100+
for wchar_ptr, expected in [
101+
(x, "Hello, World"),
102+
(y, "foo bar"),
103+
(z, "spam, spam, and spam"),
104+
]:
105+
with self.subTest(wchar_ptr=wchar_ptr, expected=expected):
106+
self.assertEqual(malloc.DidAlloc(wchar_ptr), 1)
107+
self.assertEqual(wchar_ptr.__ctypes_from_outparam__(), expected)
108+
self.assertEqual(malloc.DidAlloc(wchar_ptr), 0)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ ignore = ["E402"]
8989
"comtypes/test/test_client.py" = ["F401"]
9090
"comtypes/test/test_dict.py" = ["F841"]
9191
"comtypes/test/test_eventinterface.py" = ["F841"]
92-
"comtypes/test/test_outparam.py" = ["F841"]
92+
"comtypes/test/test_from_outparam.py" = ["F841"]
9393
"comtypes/test/test_sapi.py" = ["E401"]
9494
"comtypes/test/test_server.py" = ["F401", "F841"]
9595
"comtypes/test/test_subinterface.py" = ["E401", "F401", "F403", "F405"]

0 commit comments

Comments
 (0)