Skip to content

Commit 4ab801f

Browse files
authored
Merge pull request #89 from hakril/comimplem_multiinterface
COMImplementation can now implement multiple interface at once
2 parents f8ca256 + d014ff0 commit 4ab801f

2 files changed

Lines changed: 218 additions & 29 deletions

File tree

‎tests/test_com.py‎

Lines changed: 131 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ class ICallFrameEventsImplemSimple(windows.com.COMImplementation):
3434

3535
def __init__(self):
3636
super(ICallFrameEventsImplemSimple, self).__init__()
37-
self.called = 0
37+
self.oncall = 0
3838

3939
def OnCall(self, This, pFrame):
40-
self.called += 1
41-
return self.called
40+
self.oncall += 1
41+
return self.oncall
4242

4343
def test_com_implementation_simple():
4444
obj = ICallFrameEventsImplemSimple()
45-
assert obj.com_refcount == 1
45+
assert obj._com_refcount == 1
4646
assert obj.AddRef() == 2
4747
assert obj.Release() == 1
4848
assert id(obj) in obj._get_keepalive_registry()
@@ -55,7 +55,7 @@ def test_com_implementation_query_interface():
5555
# Emulate a call from C code
5656
obj.QueryInterface(obj._as_parameter_, ctypes.pointer(gdef.IUnknown.IID), ctypes.pointer(iunk))
5757
assert obj._as_parameter_ == iunk.value
58-
assert obj.com_refcount == 2 # +1 on QueryInterface
58+
assert obj._com_refcount == 2 # +1 on QueryInterface
5959
assert iunk.Release() == 1 # We are now working via ctypes/vtable call
6060

6161
def test_com_implementation_keep_alive():
@@ -75,3 +75,129 @@ def test_com_implementation_keep_alive():
7575
assert objcom.Release() == 0 # trigger Revoke / free !
7676
gc.collect()
7777
assert wref() is None # Check object is dead in python world
78+
79+
80+
# Test multiple interface code
81+
82+
class SimpleMultipleInterfaceImplem(windows.com.COMImplementation):
83+
IMPLEMENT = [
84+
gdef.ICallFrameEvents, # OnCall
85+
gdef.IPersist # GetClassID
86+
]
87+
88+
def __init__(self):
89+
super(SimpleMultipleInterfaceImplem, self).__init__()
90+
self.oncall = 0
91+
self.getclassid = 0
92+
self.global_call = 0 # Proof this is the same object
93+
94+
def OnCall(self, This, pFrame):
95+
self.oncall += 1
96+
self.global_call += 1
97+
return self.oncall
98+
99+
def GetClassID(self, This, pClassID):
100+
self.getclassid += 1
101+
self.global_call += 1
102+
return self.getclassid
103+
104+
def test_com_multiple_implementation_no_collision():
105+
obj = SimpleMultipleInterfaceImplem()
106+
with pytest.raises(ValueError):
107+
x = obj._as_parameter_ # _as_parameter_ cannot be ask directly on object implementing multiple interfaces
108+
109+
# Create a C-like pointer from the interface
110+
objcom = gdef.ICallFrameEvents(obj.as_interface(gdef.ICallFrameEvents))
111+
assert objcom.OnCall(None) == 1 # Count the number of call : proof of correct python-side execution
112+
assert objcom.OnCall(None) == 2
113+
assert obj.global_call == 2
114+
115+
objcom2 = gdef.IPersist(obj.as_interface(gdef.IPersist))
116+
assert objcom2.GetClassID(None) == 1 # Count the number of call : proof of correct python-side execution
117+
assert objcom2.GetClassID(None) == 2
118+
assert obj.global_call == 4
119+
120+
121+
122+
def test_com_multiple_implementation_query_interface():
123+
obj = SimpleMultipleInterfaceImplem()
124+
iunk = gdef.IUnknown()
125+
# Emulate a call from C code
126+
obj.QueryInterface(obj.as_interface(gdef.ICallFrameEvents), ctypes.pointer(gdef.IUnknown.IID), ctypes.pointer(iunk))
127+
assert obj._com_refcount == 2
128+
assert iunk.value
129+
assert iunk.value in obj._vtables_ptr.values()
130+
# Use the standard C/ctypes Iunknown pointer to query other types and verify pointer/refcount
131+
assert iunk.query(gdef.ICallFrameEvents).value == obj._vtables_ptr[str(gdef.ICallFrameEvents.IID)]
132+
assert obj._com_refcount == 3
133+
assert iunk.query(gdef.IPersist).value == obj._vtables_ptr[str(gdef.IPersist.IID)]
134+
assert obj._com_refcount == 4
135+
136+
137+
class CollisionMultipleInterfaceImplem(windows.com.COMImplementation):
138+
# Both share the same root & have "Invoke"
139+
IMPLEMENT = [
140+
gdef.IIdleSettings, # Invoke
141+
gdef.ITaskFolderCollection, # Invoke
142+
]
143+
144+
def __init__(self):
145+
super(CollisionMultipleInterfaceImplem, self).__init__()
146+
self.iidlesettings_invoke = 0
147+
self.itaskfoldercollection_invoke = 0
148+
149+
def IIdleSettings_Invoke(self, This, dispIdMember, riid, lcid, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr):
150+
print('IIdleSettings.Invoke')
151+
self.iidlesettings_invoke += 1
152+
return self.iidlesettings_invoke
153+
154+
def ITaskFolderCollection_Invoke(self, This, dispIdMember, riid, lcid, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr):
155+
print('ITaskFolderCollection.Invoke')
156+
self.itaskfoldercollection_invoke += 1
157+
return self.itaskfoldercollection_invoke
158+
159+
def not_implemented(self, *args):
160+
return gdef.E_NOTIMPL
161+
162+
# IIdleSettings
163+
164+
IIdleSettings_GetTypeInfoCount = not_implemented
165+
IIdleSettings_GetTypeInfo = not_implemented
166+
IIdleSettings_GetIDsOfNames = not_implemented
167+
get_IdleDuration = not_implemented
168+
put_IdleDuration = not_implemented
169+
get_WaitTimeout = not_implemented
170+
put_WaitTimeout = not_implemented
171+
get_StopOnIdleEnd = not_implemented
172+
put_StopOnIdleEnd = not_implemented
173+
get_RestartOnIdle = not_implemented
174+
put_RestartOnIdle = not_implemented
175+
176+
# ITaskFolderCollection
177+
178+
ITaskFolderCollection_GetTypeInfoCount = not_implemented
179+
ITaskFolderCollection_GetTypeInfo = not_implemented
180+
ITaskFolderCollection_GetIDsOfNames = not_implemented
181+
get_Count = not_implemented
182+
get_Item = not_implemented
183+
get__NewEnum = not_implemented
184+
185+
186+
def test_com_multiple_implementation_with_collision():
187+
obj = CollisionMultipleInterfaceImplem()
188+
assert obj
189+
190+
# Emulate a call from C code
191+
iunk = gdef.IUnknown(obj.as_interface(gdef.IUnknown))
192+
193+
idlesetting = iunk.query(gdef.IIdleSettings)
194+
taskfoldercollection = iunk.query(gdef.ITaskFolderCollection)
195+
taskfoldercollection.Invoke(0, None, 0, 0, None, None, None, None)
196+
assert obj.itaskfoldercollection_invoke == 1
197+
assert obj.iidlesettings_invoke == 0
198+
taskfoldercollection.Invoke(0, None, 0, 0, None, None, None, None)
199+
assert obj.itaskfoldercollection_invoke == 2
200+
assert obj.iidlesettings_invoke == 0
201+
idlesetting.Invoke(0, None, 0, 0, None, None, None, None)
202+
assert obj.itaskfoldercollection_invoke == 2
203+
assert obj.iidlesettings_invoke == 1

‎windows/com.py‎

Lines changed: 87 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import ctypes
44
import functools
55
from ctypes import HRESULT, byref, cast
6+
from collections import defaultdict
67

78
import windows
89
from windows import winproxy
@@ -364,36 +365,98 @@ def extract_methods_order(self, interface):
364365
index_and_method = sorted((self.get_index_of_method(m),name, m) for name, m in interface._functions_.items())
365366
return index_and_method
366367

367-
def verify_implem(self, interface):
368-
for func_name in interface._functions_:
369-
implem = getattr(self, func_name, None)
370-
if implem is None:
371-
raise ValueError("<{0}> implementing <{1}> has no method <{2}>".format(type(self).__name__, self.IMPLEMENT.__name__, func_name))
372-
if not callable(implem):
373-
raise ValueError("{0} implementing <{1}>: <{2}> is not callable".format(type(self).__name__, self.IMPLEMENT.__name__, func_name))
368+
369+
def _get_implemented_interfaces(self):
370+
implist = self.IMPLEMENT
371+
if isinstance(implist, type) and issubclass(implist, windows.generated_def.interfaces.COMInterface):
372+
# Only one class :
373+
implist = (implist,)
374+
return tuple(implist)
375+
376+
def _verify_interfaces_collision(self):
377+
function_names = defaultdict(int)
378+
for interface in self._get_implemented_interfaces():
379+
for function in interface._functions_:
380+
function_names[function] += 1
381+
return set(function for function, count in function_names.items() if count > 1) - {"QueryInterface", "AddRef", "Release"}
382+
383+
def _verify_interfaces_implem(self):
384+
for interface in self._get_implemented_interfaces():
385+
for func_name in interface._functions_:
386+
explicit_func_name = "{}_{}".format(interface.__name__, func_name)
387+
explicit_implem = getattr(self, explicit_func_name, None)
388+
if explicit_implem is not None:
389+
if not callable(explicit_implem):
390+
raise ValueError("{0} implementing <{1}>: <{2}> is not callable".format(type(self).__name__, interface.__name__, explicit_func_name))
391+
continue # Ok for explicit implem
392+
393+
# No explicit implem : go for implicit
394+
implem = getattr(self, func_name, None)
395+
if implem is None:
396+
raise ValueError("<{0}> implementing <{1}> has no method <{2}>".format(type(self).__name__, interface.__name__, func_name))
397+
if not callable(implem):
398+
raise ValueError("{0} implementing <{1}>: <{2}> is not callable".format(type(self).__name__, interface.__name__, func_name))
399+
400+
# Prevent multiple usage of the same name if multiple implemented interface have a function with the same name: force explicit name
401+
if func_name in self._colliding_functions:
402+
raise ValueError("{0} method <{1}> is used by more than 1 implemented interface : use explicit method name 'Interface_Method' (Ex: IPersist_GetClassID) ".format(type(self).__name__, func_name))
403+
374404
return True
375405

376406
def _create_vtable(self, interface):
377407
implems = []
378408
names = []
379409
for index, name, method in self.extract_methods_order(interface):
380-
func_implem = getattr(self, name)
410+
explicit_func_name = "{}_{}".format(interface.__name__, name)
411+
# Explicit name (IPersist_GetClassID)
412+
func_implem = getattr(self, explicit_func_name, None)
413+
if func_implem is None:
414+
# Implicit name (GetClassID) # Collision have been checked in verify_interfaces_implem()
415+
func_implem = getattr(self, name, None)
416+
381417
#'this' is a COM-interface of the type we are implementing
382418
types = [method.restype, interface] + list(method.argtypes)
383419
implems.append(ctypes.WINFUNCTYPE(*types)(func_implem))
384420
names.append(name)
385421
class Vtable(ctypes.Structure):
386422
_fields_ = [(name, ctypes.c_void_p) for name in names]
387-
return Vtable(*[ctypes.cast(x, ctypes.c_void_p) for x in implems]), implems
423+
vtable = Vtable(*[ctypes.cast(x, ctypes.c_void_p) for x in implems])
424+
vtable._implems = implems # Keep a reference to prevent early garbage-collection
425+
return ctypes.pointer(vtable)
426+
427+
def _create_vtables(self):
428+
vtables = {}
429+
for interface in self._get_implemented_interfaces():
430+
vtables[str(interface.IID).upper()] = self._create_vtable(interface)
431+
return vtables
432+
433+
@property
434+
def _as_parameter_(self):
435+
implist = self._get_implemented_interfaces()
436+
if len(implist) > 1:
437+
raise ValueError("Cannot use _as_parameter_ on {} implementing multiple COM interface : use .as_interface() for ctypes parameter passing".format(self))
438+
return self._vtables_ptr[str(implist[0].IID)]
439+
440+
def as_interface(self, interface):
441+
try:
442+
iid = interface.IID
443+
except AttributeError:
444+
iid = interface
445+
446+
if iid == gdef.IUnknown.IID: # Any interface pointer will respect IUnknown
447+
return list(self._vtables_ptr.values())[0]
448+
449+
try:
450+
return self._vtables_ptr[str(iid)]
451+
except KeyError as e:
452+
raise ValueError("{} does not implement IID {} (interface={})".format(self, iid, interface))
388453

389454
def __init__(self):
390-
self.verify_implem(self.IMPLEMENT)
391-
vtable, implems = self._create_vtable(self.IMPLEMENT)
392-
self.vtable = vtable
393-
self.implems = implems
394-
self.vtable_pointer = ctypes.pointer(self.vtable)
395-
self._as_parameter_ = ctypes.addressof(self.vtable_pointer)
396-
self.com_refcount = 1
455+
self._colliding_functions = self._verify_interfaces_collision()
456+
self._verify_interfaces_implem()
457+
self._vtables = self._create_vtables()
458+
self._vtables_ptr = {interface: ctypes.addressof(vtable) for interface, vtable in self._vtables.items()}
459+
self._com_refcount = 1
397460
self.register()
398461

399462
# PFW COMImplementation API
@@ -417,23 +480,23 @@ def revoke(self):
417480

418481
def QueryInterface(self, this, piid, result):
419482
"""Default ``QueryInterface`` implementation that returns ``self`` if piid is the implemented interface"""
420-
if piid[0] in (gdef.IUnknown.IID, self.IMPLEMENT.IID):
421-
result[0] = this
483+
if piid[0] == (gdef.IUnknown.IID) or piid[0] in (i.IID for i in self._get_implemented_interfaces()):
484+
result[0] = self.as_interface(piid[0])
422485
self.AddRef()
423486
return 1
424487
return E_NOINTERFACE
425488

426489
def AddRef(self, *args):
427490
"""Default ``AddRef`` implementation that returns ``1``"""
428-
self.com_refcount += 1
429-
return self.com_refcount
491+
self._com_refcount += 1
492+
return self._com_refcount
430493

431494
def Release(self, *args):
432495
"""Default ``Release`` implementation that returns ``1``"""
433-
self.com_refcount -= 1
434-
if self.com_refcount == 0:
496+
self._com_refcount -= 1
497+
if self._com_refcount == 0:
435498
self.revoke()
436-
return self.com_refcount
499+
return self._com_refcount
437500

438501
def __repr__(self):
439-
return "<{0} refcount={1} at {2:#08x}>".format(type(self).__name__, self.com_refcount, id(self))
502+
return "<{0} refcount={1} at {2:#08x}>".format(type(self).__name__, self._com_refcount, id(self))

0 commit comments

Comments
 (0)