Udostępnij za pośrednictwem


Wherefore IDispatchEx?

Quite a while back I said that I'd write a post describing why we invented IDispatchEx. I already covered the part about using it to probe the stack for security reasons, but what other reasons did we implement that thing? The documentation is pretty much accurate, but sparse in places. (According to the doc, GetNameSpaceParent, uh, gets the namespace parent.) I won't go into the flag-for-flag level of detail that the doc does well, but I thought I might describe what features motivated the various methods on IDispatchEx. The features are:

Expando objects

In most objects, the methods you've got are the ones you get. But JScript objects and some IE DOM objects can have arbitrary, user-defined properties and methods added to them at will. We call such objects "expando" objects.

Strictly speaking,

IDispatchEx is not required to implement an expando object. You could have an implementation of IDispatch which added a new named field every time GetIdsOfNames was called, and everything would work just fine. What IDispatchEx gives you is the ability to pass in a flag (fdexNameEnsure) that specifically requests expando semantics. An object model could choose to, say, only create new fields when the flag was passed in and error out if an unknown name was passed in without the flag. This would then allow the caller to probe for the existence of a property without creating it as a side effect. (Those semantics are up to the callee though -- callers cannot assume that not passing the flag means no expando semantics!)

Use the

GetDispID method to access this feature.

Delete members

JScript has a

delete operator which does not work at all like C++'s delete operator. Rather, JScript's delete operator removes an expando field from an object. It's pretty useless actually, because doing so does not free up any memory beyond simply clearing the field. Why not? Because implementers must ensure that if the property is re-added that it gets the same dispatch identifier the second time; someone might be caching the dispid. That means that the property bucket and its name has to be kept around, and worse, that every property bucket needs a flag that marks whether it's deleted or not. (The implementer must also ensure that property enumeration continues to work even if the enumerator -- see below -- is presently sitting on a property that was just deleted.) Use DeleteMemberByName or DeleteMemberByDispID in the unlikely event that you want to use this feature.

Case sensitivity

Visual Basic is case-insensitive, and

IDispatch was built by the VBA team back in the day, so they never implemented support for case-sensitive languages. JScript is case sensitive, so IDispatchEx lets you pass in flags (fdexNameCaseSensitive, fdexNameCaseInsensitive) on GetDispID that control the case-sensitivity of the lookup.

Property enumeration

In the non-expando world, the property set of an object is stable, so there's little need to enumerate it. If you did need to enumerate it, you could always just look at the static typeinfo. But constructing dynamic typeinfos in a world with expando objects is difficult and expensive, so instead we added the ability to enumerate the valid dispatch identifiers (

GetNextDispID) and turn the identifier back into a name (GetMemberName). This is how JScript's for-in loop is implemented.

Constructors

JScript functions may be called as constructors, which is a little weird and has

different semantics from calling a regular function. IDispatchEx adds a DISPATCH_CONSTRUCT flag that can be passed to InvokeEx to let the callee know that it is being invoked as a constructor.

Namespace chaining

JScript supports some pretty intense scope resolution semantics, well beyond the local/class/global

scoping rules in VBScript. Think closures, or with blocks, which allow construction of scope chains of arbitrary length. But that's not all -- it gets even more fun.

To make the

this

object semantics that you guys were arguing about the other day work, if you call a function that has a this argument, and it was invoked with the fdexImplicitParents flag, then we run up the GetNameSpaceParent chain to get all the parent scope objects and stuff them into the scope chain for the function invocation. Don't try this at home, kids; I barely remember how any of this stuff works.

Debugger support

Finally IDispatchEx has a GetMemberProperties method which is never used by JScript. Rather, it's used by the script debugger so that the debugger can tell you whether a given field is a method, property, etc.

So there you go -- a whole lot of new features that were difficult to shoehorn into IDispatch, so we implemented a new interface.

Comments

  • Anonymous
    October 07, 2004
    I'm saddened that you correctly implemented enumeration of js properties deleted during the enumeration. I have nothing to complain about for this entry now.
  • Anonymous
    October 07, 2004
    Don't worry, I'm sure there's still plenty of stuff we did wrong.
  • Anonymous
    October 07, 2004
    Two things:

    First, why did you implement property enumeration using GetNextDispID and not using IEnumVARIANT? For example by introducing a new stock-property that returns such an enumeration (if the object supports it). This would have made this feature accessible using VBScript's FOR EACH, and also consistent with other script friendly enumerations.

    Second, IDispatchEx highlights the COM designed error that is IDispatch. Support for dynamic invocation should have been implemented through an external service, in manner similar to marshaling, not as an interface you must derive from. The existing design makes IDispatchEx close to useless because one you've derived your main interface from IDispatch you can't use IDispatchEx with that class.

    Oh well, no point it taking jibes at COM - it was, and is, a good thing overall, and you can't expect something so big to be perfect. Also, MS did get it right in .NET with reflection.
  • Anonymous
    October 07, 2004
  1. Because there's a difference between enumerating the elements of a collection and the properties of an object. If they both use the same mechanism then it becomes impossible to build an IDispatchEx object which is both a collection AND enumerates its properties. Though there are few such objects, we anticipated that there might be one someday.

    2) I encourage you to go back in time and convince Doug that the dispatch architecture is sub-optimal. And while you're back in 1993, send me an email that predicts the peak of the MSFT stock price.
  • Anonymous
    October 08, 2004
    The comment has been removed

  • Anonymous
    October 08, 2004
    But if we added a new stock property to fetch the enumerator then you couldn't use it via For-Each-Next. My point is simply that an object can be both a collection of items and a collection of properties, and therefore there must be two different ways to get at them; the same construct can't do both consistently.

    Now, given that we have to have a distinction somewhere, why not simply define a new magic dispid that hands out IEnumVARIANT pointers? Sure, we could have done it that way, but it's unnecessarily heavyweight. IEnumVARIANT supports multiple enumerators on a collection, rolling back enumerators, fetching multiple values at once -- all kinds of stuff that we didn't need.

    The property enumeration code is complex enough -- we don't want to be in the business of introducing more complexity to solve problems we don't actually need to solve.

  • Anonymous
    October 11, 2004
    As far as I can tell, there is nothing wrong with building an object that implements both IDispatch and IDispatchEx, with IDispatchEx delegating common functionality to IDispatch. As an added bonus, so long as the IDispatch-derived interface is not marked as "nonextensible", dynamically added properties are accessible from VB6.

    As for enumerating properties, some DAO objects have a "properties" property that can be used to enumerate properties at run-time. Sure, it's a pain to have to implement it every time, but theoretically it would be possible for one to define an interface called say IEnumerableProperties, that would define a "Properties" property and to then create an object that could wrap any IDispatch or IDispatchEx interface and provide IEnumerableProperties.

    Guess what I'll be doing for the rest of the week.

    And Eric, fantastic blog! I don't know why you keep writing about a "dead" technology, but I hope you keep it up!

  • Anonymous
    October 11, 2004
    I've blogged about this before, but I have a hard time thinking about a technology still used by millions of people every day as "dead". Just because we're not doing a whole lot of new feature work on any particular technology doesn't mean that it's not still important or vital to customers.

  • Anonymous
    October 12, 2004
    Hey, you're preaching to the choir (I'm one of the millions).

    I found your explanation for why the MS scripting engines don't cache dispids interesting, but I also find it interesting that VB6 doesn't cache dispids either.

    So, who does cache dispids?

  • Anonymous
    October 13, 2004
    It comes down to what exactly you mean by "cached".

    The thing is, what's the difference between getting back a dispatch id from and "immediately" -- ie, a few nanoseconds later -- invoking, versus getting back a dispatch id and invoking a few milliseconds later? A few seconds later? A few hours later?

    Logically, there's no difference. The object gives you back an id, and it must guarantee now and forever that as long as the object is alive, that id is meaningful.

    So, everyone caches dispids for at least a few nanoseconds. And if we let you cache things for nanoseconds, we have to let you cache them for longer.

  • Anonymous
    October 18, 2004
    Good point!

    I recently implemented IPersistStreamInit on an object that also implements IDispatchEx. My implementation of IPersistStreamInit does not persist properties that have been deleted, as I cannot conceive of a situation where an object could be read from a stream by a caller who will attempt to use a property that was deleted before the object was persisted.

    Do you see any flaws in my logic?

  • Anonymous
    October 18, 2004
    I might add, that my persistence implementation does not guarantee that ANY properties will have the same dispids after the object is read back as they had when the object was persisted.

    I can't see a problem with that.

  • Anonymous
    October 18, 2004
    It depends on your desired persistence semantics. Is an object that is freeze-dried and then reconstituted the SAME object, or is it a DIFFERENT object with the same state?

    One could make arguments both ways. In general, I think you're safe -- there's no requirement that dispids persist beyond the destruction of the object. (However, I also think that it would be a better design to persist them if possible.)

  • Anonymous
    August 04, 2005
    The comment has been removed

  • Anonymous
    August 25, 2005
    Well, it's not really a leak. The object takes up more and more memory, to be sure, but eventually that memory is released when the object goes away. It's not like this memory is lost until the process shuts down.

    The real takeaway here is that "delete" does not do what you think it does. It's really quite useless.

  • Anonymous
    November 27, 2007
    Thanks for your post, it was very helpful. I implemented IDispatchEx in my C++ objects, and now I'm able to see dynamic properties in MSVC during script debugging. The only problem is: I have my own C++ objects that acts like a JScript arrays. I want to see them in debugger in the same form I see JScript array, but I don't know how to implement this. As I get, MSVC debugger shows JScript arrays in special way, don't using usual GenNextDISPID scheme. How can add the same behavior to my C++ classes? I implemented _NewEnum and returned IEnumVARIANT from it but this didn't helped.

  • Anonymous
    November 27, 2007
    It seems I got it :) I should simply return numbers as a members names! So, it seems MSVC script debugger didn't make a difference between: a = [1, 2]; and a = {"0":1, "1":2};