Handling Com Events in Python (was Python and Windows Scripting Host)

Alex Martelli alex at magenta.com
Fri Aug 11 07:59:06 EDT 2000


"Mark Hammond" <MarkH at ActiveState.com> wrote in message
news:7OHk5.1082$gP5.11390 at news-server.bigpond.net.au...
>
> [Syver]
> > > > Hmm, yes, but that mixes up the object you're automating with
> the
> > > > one that's receiving/handling its events.  I find that somewhat
> > > > unpleasant/untrivial.
>
> I agree, but had a hard time thinking of something better.

As I went through your Python<->COM design, as I said in a later post
(which you also quote), I was very fresh from conducting an extremely
similar effort regarding our proprietary scripting language GPL, so,
believe me!, I can perfectly well understand most of the design choices
you had to take and the route you took on each.


> Presumably, whenever you respond to events, you also need to control
> the application in some way.  There is almost always some state that
> needs to be maintained between the app and its event handlers.

Yes, but I find this expression of the issue slightly ambiguous.  Rather,
I would identify the following actors as being necessarily present in
any event-handling scenario:
    -- a control (or other COM server) X that:
        + responds to method calls and property get/set/let
        + generates events
    -- a controller (COM client) Y that:
        + instantiates X OR receives an X reference from somewhere
        + want to makes calls on X's methods/properties
        + wants to respond to some events X generates

Y will presumably instantiate some object Z, with methods that
correspond to a subset of X's generated events.  Z will probably
need to communicate with Y (controller-state interaction) AND
with X (control-state interaction).

I suspect we agree on each substantial issue here; just trying to
fix a precise terminology.


> I saw 3 discrete choices:
> 1) VB model, with "Object_Event" name methods, global to the module.
> No classes for the events at all.

Handy for some quick-and-dirty hacks, I guess.  But I think we
agree it sucks most of the time, yes.

> 2) Discrete objects for the event, and for the object itself.

The natural architecture, subject to sanity checks of course.

> 3) Same object for the event and the object.
>
> 1) just sucks, IMO.  2) sounds appealing, but makes it very hard to
> communicate state between the 2 objects.  I feared that the reality
> was that in 100% of cases, each object would have a reference to the
> other, meaning a nasty circular reference.
>
> So I went with 3.

The circular reference IS there between the COM objects, of
course.  As long as some Z is still interested in responding to
X's events, neither X nor Z will go away.  But that's exacly why
IConnectionPoint has an Unadvise method: the fact "interest in
responding to X's events with a certain Z has ceased" must be
*explicitly* communicated.  As the Platform SDK puts it,
"""
When an advisory connection is terminated, the connection point
calls the Release method on the pointer that was saved for the
connection during the IConnectionPoint::Advise method. This
Release reverses the AddRef that was performed during the
IConnectionPoint::Advise when the connection point calls the
advisory sink's QueryInterface
"""

As Python, luckily, uses a reference-count approach that strongly
parallels COM's, the Python objects that 'shadow' (implement, or
proxy/facade) the COM objects could also emulate this kind of
behavior, i.e. require explicit communication that interest in
responding to events for a given (X,Z) pair has ceased.  I had
other problems in com-enabling GPL, facing a mix of stack-
discipline and mark-and-sweep GC rather than RC, and ended
up with the following kludge...:
    -- a reference to the COM object generating the event is
        always passed as an extra first parameter to any method
        that has been registered as an event-handler
I register bound-methods for specific events rather than
whole-objects for whole source-interfaces (much like in
Delphi and other Hejlsberg's designs such as .NET), and I
suspect that might be nice in Python too, but it's definitely
a detail.  The key kludge is that, if an event has (in the IDL)
a signature such as, say:
    VARIANT_BOOL onclick([in] IHTMLEventObj* pEvtObj);
then the bound-method to be registered to handle it
would be, in Python terms:

class HandlerClass:
    def OnClick(self,source,pEvtObj):
        return 1

aHandler=HandlerClass()

aHandler.cookie=mgrObj.Advise(source,'onclick',aHandler.OnClick)

and the removal would be:

mgrObj.UnAdvise(aHandler.cookie)


The manager-object mgrObj internally synthesizes the COM
object[s] that actually handle the COM-events from the COM
server that is wrapped by 'source'.  When that server fires an
onclick event, the bound-method[s] that have Advise'd on it
get called with TWO arguments -- the first one being the
source object itself.  [Actually that would not be needed for
this case of MSHTML events since the pEvtObj carries a nice
srcElement property that gives me 'source' again, but, of
course, I can't rely on that in general!-)].

Anyway, the point is that the user-level object (the Python
class HandlerClass here) *need NOT hold* a reference to
source.  Key advantage from my POV is that this lets
HandlerClass be stack-discipline and/or mark-and-sweep
garbage collected, without worry on 'impedance matching'
that to the reference-count discipline of COM gc.  The
reference-count loop is still there of course -- it's the
job of UnAdvise to break it *explicitly* -- but it's between
"inner" objects (not user-level accessible, not directly; so
I can subject them to RC-discipline whatever the language
specifies for user-level objects).

Hmmm, on reflection it matters that, in my case, bound
methods (aka delegates) are *weak* references on the
bound-object.  But anyway, you don't have this problem
since Python user-objects *are* under RC anyway (I do
wonder whether the delegates-are-weak-references
paradigm holds for .NET -- I think it doesn't, in which
case I predict very similar issues on *their* bridging
between COM-level events and .NET-level delegates).

At a user-level, the key advantage is that this lets one
object/boundmethod be used to respond to events
from many distinct sources.  What source fired this
event being an argument to the event-handling method
allows that, of course.  Users who normally script HTML
pages are used to this convenience through the use
of event.srcElement, of course.  I posted recently a
simple example where I script HTML "from the outside"
with a Python script which wants to highlight the whole
table-row on mouseover/mouseout, etc.  It's even
better where elements sourcing events are 'expando'
(in Javascript/DHTML terms), as of course they are in
Python (as the Python-wrapper will be the one passed
as the first argument to the event-handling method),
since the event-handler can depose state on the very
event-sources and avoid any need to do such mapping
internally!

And of course, if the handler 'already knows' or doesn't
care who sourced the event, it's easy for it to ignore
the first parameter to the event-handling method.


If I were to do it again -- I think I'd drop the use of
bound-methods/delegates and go back to whole-class
connection.  But maybe that's because hooking a whole
source-interface is what I'm doing anyway every time I
serve events in COM in other contexts (or in Java, &c), so
that's what I'm used to; if I was, instead, regularly using
Delphi, VJ++6/WFC, or .NET, and so used to delegates, I
might feel differently.  I could be happy either way on
this as a user, I think.  For the implementer, impedance
matching between the two approaches is a bother, IMHO.

But the idea of passing in, as artificial first parameter, the
event-source, is a definite plus, IMHO.  A win for both the
user AND the implementer.  In fact, if I were to do it over,
I might be even more drastic: synthesize an event-object,
along IHTMLEventObj lines, summarizing all I do know
about the event (including a srcElement property, but
also including the parameters passed to the event).  This
would allow a solution to the problem of event parameters
that must be read-write: as *properties of the synthetic
event object*, they can EASILY be read-write, of course
(while trying to do [in,out] parameters in languages which
are not naturally set up that way is a real bother).  So all
event-handling methods would have the same signature,
accepting a single argument that is the synthetic event
object (not a big problem in Python, where it's easy to
define a method which takes arbitrary parameters, but I
didn't have that facility in GPL).


> Note that events are still marked experimental, mainly as I am not
> happy with them.  Also note that "DispatchWithEvents" is really just
> an event helper, and isnt that many lines of code - power users are
> free to handle events in any way they like.

*blink* how does one do that short of getting the CVS tree
and adding C-level stuff?  E.g., how could I prototype the
above-mentioned idea of synthetic event objects without
mucking with the C sources?


> [Alex]
> > complex, aka non-trivial, I think; here, in particular, we're mixing
> > two things which, while related, have conceptually distinct
> > identities.  Nothing too terrible, I guess (and I don't have a
> better
> > architecture to propose, really; I ended up implementing something
> > rather similar when I COM-enabled a proprietary scripting language
> > that we develop and distribute with our applications -- although
> > there are, I think, a few worthwhile enhancements which could be
> > taken from that architecture and applied to Python's win32com,
> > it's really minor issues).
>
> Agreed completely.  Your ideas would be most welcome.

One example, besides the above essay, is that for many COM objects
I don't need to know their provenance to be able to interpretively
handle their events.  As long as they support IProvideClassInfo2, I
can simply get their default source-interface and hook that (if it's a
dispinterface, of course -- but it usually is).  Or, more laboriously,
if they have a fully implemented IDispatch::GetTypeInfo, I can use
that to GetContainingTypeLib and work from there (together with
the IConnectionPointContainer::EnumConnectionPoints that will
tell me the source interfaces they support).  I don't believe Python
goes as far as this in its current event-support, does it?  (Maybe
wisely -- Word97 Application object hangs if you ECP on its ICPC
interface after the first entry, as I learned to my chagrin -- I had
to specialcase a test for it to avoid ECP'ing in that case!-).

Much of the concerns I worked on do not really apply given
Python's awesome "underlying" power, but enough of them
are general concerns about COM and Automation, that I think
I could perhaps be helpful.


Alex






More information about the Python-list mailing list