Skip to content

Commit f8ca256

Browse files
authored
Merge pull request #88 from hakril/COMImplementation_keepalive
COMImplementation class are now kept alive based on com refcount & gl…
2 parents c4d3cd2 + 603d3e2 commit f8ca256

2 files changed

Lines changed: 109 additions & 2 deletions

File tree

tests/test_com.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import ctypes
2+
import gc
3+
import weakref
4+
5+
import pytest
6+
7+
import windows
8+
import windows.com
9+
10+
import windows.generated_def as gdef
11+
12+
# ICallFrameEvents is a good candidate for testing :
13+
# - only 1 fonction (not counting IUnkown())
14+
# - Only 1 param for the function
15+
16+
# class ICallFrameEventsImplem(windows.com.COMImplementation):
17+
# IMPLEMENT = ICallFrameEvents
18+
#
19+
# def OnCall(self, This, pFrame):
20+
# print('ICallFrameEvents.OnCall')
21+
# return E_NOTIMPL
22+
23+
class ICallFrameEventsImplemIncomplete(windows.com.COMImplementation):
24+
IMPLEMENT = gdef.ICallFrameEvents
25+
26+
27+
def test_com_implementation_incomplete():
28+
with pytest.raises(ValueError):
29+
obj = ICallFrameEventsImplemIncomplete()
30+
31+
32+
class ICallFrameEventsImplemSimple(windows.com.COMImplementation):
33+
IMPLEMENT = gdef.ICallFrameEvents
34+
35+
def __init__(self):
36+
super(ICallFrameEventsImplemSimple, self).__init__()
37+
self.called = 0
38+
39+
def OnCall(self, This, pFrame):
40+
self.called += 1
41+
return self.called
42+
43+
def test_com_implementation_simple():
44+
obj = ICallFrameEventsImplemSimple()
45+
assert obj.com_refcount == 1
46+
assert obj.AddRef() == 2
47+
assert obj.Release() == 1
48+
assert id(obj) in obj._get_keepalive_registry()
49+
assert obj.Release() == 0
50+
assert id(obj) not in obj._get_keepalive_registry()
51+
52+
def test_com_implementation_query_interface():
53+
obj = ICallFrameEventsImplemSimple()
54+
iunk = gdef.IUnknown()
55+
# Emulate a call from C code
56+
obj.QueryInterface(obj._as_parameter_, ctypes.pointer(gdef.IUnknown.IID), ctypes.pointer(iunk))
57+
assert obj._as_parameter_ == iunk.value
58+
assert obj.com_refcount == 2 # +1 on QueryInterface
59+
assert iunk.Release() == 1 # We are now working via ctypes/vtable call
60+
61+
def test_com_implementation_keep_alive():
62+
obj = ICallFrameEventsImplemSimple()
63+
# Create a C-like pointer from the interface
64+
objcom = gdef.ICallFrameEvents(obj._as_parameter_)
65+
assert objcom.AddRef() == 2
66+
wref = weakref.ref(obj)
67+
assert wref() is obj # Check object is still alive in python world
68+
del obj # No more python-side direct reference
69+
gc.collect() # Force gc : code below would crash without keep-alive logic
70+
assert objcom.Release() == 1
71+
assert objcom.OnCall(None) == 1 # Count the number of call : proof of correct python-side execution
72+
assert objcom.OnCall(None) == 2
73+
gc.collect()
74+
assert objcom.OnCall(None) == 3
75+
assert objcom.Release() == 0 # trigger Revoke / free !
76+
gc.collect()
77+
assert wref() is None # Check object is dead in python world

windows/com.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ def __repr__(self):
349349
class COMImplementation(object):
350350
"""The base class to implements COM object respecting a given interface"""
351351
IMPLEMENT = None
352+
COM_KEEPALIVE_REGISTRY = {} # Will keep on COM-alive object to prevent early garbage collection
352353

353354
def get_index_of_method(self, method):
354355
# This code is horrible but not totally my fault
@@ -392,18 +393,47 @@ def __init__(self):
392393
self.implems = implems
393394
self.vtable_pointer = ctypes.pointer(self.vtable)
394395
self._as_parameter_ = ctypes.addressof(self.vtable_pointer)
396+
self.com_refcount = 1
397+
self.register()
398+
399+
# PFW COMImplementation API
400+
def _get_keepalive_registry(self):
401+
"""Allow subclass to chose where are store alive COMImplementation object"""
402+
return self.COM_KEEPALIVE_REGISTRY
403+
404+
def register(self):
405+
"""Called once at instanciation : should be used to register it in any pointer-keeper logic to prevent early garbage-collection
406+
The method : revoke() will be called when RefCount go to 0.
407+
"""
408+
self._get_keepalive_registry()[id(self)] = self # Add object to global table to prevent early garbage collection
409+
return True
410+
411+
def revoke(self):
412+
"""Called once when Refcount is lowered to 0 : use it to remove from any global-state object for refcounting.
413+
This indicate the object can be safely garbage collected from the COM point of view
414+
"""
415+
del self._get_keepalive_registry()[id(self)]
416+
return True
395417

396418
def QueryInterface(self, this, piid, result):
397419
"""Default ``QueryInterface`` implementation that returns ``self`` if piid is the implemented interface"""
398420
if piid[0] in (gdef.IUnknown.IID, self.IMPLEMENT.IID):
399421
result[0] = this
422+
self.AddRef()
400423
return 1
401424
return E_NOINTERFACE
402425

403426
def AddRef(self, *args):
404427
"""Default ``AddRef`` implementation that returns ``1``"""
405-
return 1
428+
self.com_refcount += 1
429+
return self.com_refcount
406430

407431
def Release(self, *args):
408432
"""Default ``Release`` implementation that returns ``1``"""
409-
return 0
433+
self.com_refcount -= 1
434+
if self.com_refcount == 0:
435+
self.revoke()
436+
return self.com_refcount
437+
438+
def __repr__(self):
439+
return "<{0} refcount={1} at {2:#08x}>".format(type(self).__name__, self.com_refcount, id(self))

0 commit comments

Comments
 (0)