It's been an insanely busy month for me, between having multiple out-of-town guests, throwing a party for the people who couldn't make it to the wedding, and oh yeah, getting up to speed on the C# compiler and trying to understand the implications that LINQ features are going to have on the current implementation. Not much time for blogging. Hopefully October will be a little bit more under control.
Anyway, I was talking about early-bound event binding. Basically the idea is that the source and the sink agree upon an interface that the source can call on the sink whenever the source wishes to fire an event. Before we get into that more, I want to talk briefly about early- and late-bound code.
At an implementation level, what is the difference between early- and late- bound code? In the early-bound world, a particular function on an interface gets called when the caller dereferences the callee's virtual function table and transfers control directly to the function by changing the instruction pointer on the chip. In the late-bound world, the caller calls IDispatch::Invoke and passes in a magic number that tells the callee what function it should be calling, and the callee is then responsible for dispatching the function appropriately. (Hence the name "IDispatch".)
The script engines use IDispatch all the time to call out to functions on objects. When calling an object late-bound, you don't know until you actually try the call whether or not it is going to succeed, because you do not know the interface the object implements. So you call GetIdsOfNames to get the magic number associated with a particular function, Invoke on that magic number, and let the callee sort it out. In this situation, the caller (the script engine) knows the name of the function it wants to call, and the callee (the object) can map the name to the appropriate identifier.
Now consider how this works if IDispatch is the interface over which the source (the object) is calling back the sink (the script host). This appears to be the same situation, but in fact it is completely different. It appears the same because from the perspective of how COM actually manages all the calls, its exactly the same. IDispatch is an interface like any other, and the source can call the sink's IDispatch to its heart's content if that's the interface that they agree to talk over.
But look at it from the point of view of the sink: the caller knows what the magic number means and the callee does not, but it's the callee who is being asked to do the dispatching! You're sitting there sinking events of who knows what object, and every now and then you get a call on IDispatch::Invoke with some unknown dispid. What the heck are you supposed to do with that? In the late-bound world, instead of getting a nice direct call on "Tick()... Tick()... Tick()..." you're getting "12... 12... 12..." and you have no idea what "12" means.
Let's tie this in to scripting. Suppose you're in Windows Script Host and you do something like:
Set Timer = CreateObject("mytimer")
WScript.ConnectObject Timer, "Timer_"
There are many IDispatch objects here and we'll look at three of them. First, there's the source, that is, the timer object. Then there's the sink, owned by the host and created when the source is connected to it. Finally there is the script engine itself, upon which the host can dispatch calls to global functions such as the event handler.
ConnectObject is given a source object. It has no idea what early-bound outgoing interface is on that object, and it certainly doesn't have an implementation of such an interface even if there is one, so it is going to have to do a late bound sink. It creates an object to act as the sink, gets an IDispatch-enabled connection point from the source, and advises it.
Now the script engine calls Start (via IDispatch on the source) and goes to sleep.
Pretty soon the timer invokes the sink, passing in the dispatch identifier for Tick.
The sink needs to know which sub to call in the script engine.
Fortunately, it has an IDispatch pointer to the source, and a dispatch identifier.
Unfortunately, though IDispatch provides a function which maps from name to id, it provides no function that maps the other way.
Fortunately, IDispatch does provide a function that enables the host to obtain a type information structure which contains a list of all the methods and what their dispatch identifiers are. Therefore we can search all the methods in the type info and check to see which has the desired dispatch identifier. That then gives us the method name, so we know which event handler function to dispatch on the script engine.
Unfortunately there is a major design flaw in IDispatch -- it gives you the type info for the incoming interface -- the ITimer. But there is no way to take the ITypeInfo for ITimer and say "give me the type info for the class as a whole so that I can obtain the type info for the outgoing interface".
Fortunately, IProvideClassInfo was invented. IPCI gives you back the "root" type info for an object, from which you can obtain a type info for the default outgoing interface, from which you can map the dispatch identifier to the name.
Unfortunately, many objects do not implement IProvideClassInfo, and therefore cannot have their events hooked up by ConnectObject.
(I feel a little bit like the ending of Dr. Strangelove here. Fortunately, they stop General Ripper in time. Unfortunately, the secret code died with him. Fortunately they figure out the code. Unfortunately, the radio is broken. Fortunately, the bomb bay doors are stuck. Unfortunately, they fix them, and the Doomsday Device destroys the world. Bummer.)
The moral of this story is simple: for late bound events to work, at some point the class of the source must be known. And therefore:
- "automagic event binding" in IE only works on "named items" present in the script engine. (The script engines require that the host provide a coclass type info for named items, and build the sinks very early on, before other code runs.)
- WScript.CreateObject can always hook up events. The class type info is known because CreateObject had to create an instance of the class.
- WScript.ConnectObject can hook up events only if the object implements IProvideClassInfo
I hope that clears up any confusion about what IProvideClassInfo is for!