Thursday, October 15, 2009

Simplifying the Model with the Universal Delegator


3 4



Simplifying the Model with the Universal Delegator



The biggest drawback of the fully reusable split identity solution you saw in the previous section is that it is not transparent to the object in the ring that previously held a direct reference to what becomes the creator of the back pointer object. Figure 9-5 shows that object B can no longer hold an interface pointer of a type supported by object A but must instead grapple with IBackPtr. When object B wants to talk to object A, it first needs to call IBackPtr::GetTarget to retrieve the interface pointer for object A that it is interested in before making any calls. This involves a separate step to which error handling must be applied. In many cases, this solution will be just fine. But if object B already has been developed and its source code perhaps is not under your control, things become a little more difficult. The same holds true if object B is an event source expecting to connect to a specific event sink interface. You could develop a version of CBackPtr specific to object A that
implements the interface of object A that object B intends to hold onto, simply by forwarding all calls after calling GetTarget on itself. In the case of connectable objects, this specific back pointer then could issue the appropriate Advise call to object B. This will work, but it proliferates the back pointer implementation and introduces a dual point of maintenance for each back pointer creator and consumer pair with this need.



Wouldn't it be nice if the split identity solution could work just like Visual Basic's solution for connectable objects? In our scenario, there are two identities, but the one handed to the event source or ring transparently supports all the methods that the holder of the ring object's reference supports—without having to write more than one single implementation of the back pointer class.



What we need to achieve this is a second-level interceptor for access to the back pointer's creator. The back pointer itself should function as this interceptor. The interception service it provides is calling GetTarget prior to forwarding the intercepted call and releasing the interface pointer thus obtained after forwarding the call. But think back to our discussion of interception services in Chapter�4. The call to GetTarget definitely could fail if the back pointer's creator had self-destructed. Therefore, we will need to deal with all three of the classic problems of providing generic interception services: the language problem, the failure problem, and the post-processing problem. You're probably thinking it seems hopeless and just too big of a job.



Well, I'd agree with you if it weren't for a piece of software Keith Brown has written and makes freely available:8 the Universal Delegator. The Universal Delegator solves all the problems of interception for you; it contains some assembly code for the Intel platform. It allows you to specify what it calls hooks, which you can use to perform your own preprocessing and postprocessing for each delegated call. That's where you would do the calling of GetTarget and the releasing of the interface pointer to the creator, respectively. By extending the split identity solution with this piece of software, you can shim a back pointer instance between any two objects in a reference cycle without having to modify the interface types the two can use and store in order to make calls to one another. And you will need only a single implementation of CBackPtr. No dual points of maintenance exist. This extended solution now will work transparently to break reference cycles among connectable objects where the event sink is a C++ COM+ object.



The Universal Delegator won't work right out of the box to accomplish this purpose. Keith anticipated a reference cycle associated with common usage of just the Universal Delegator itself. Many developers like using the Universal Delegator to wrap services (such as security services) around their objects before they expose them to other objects. This would create a reference cycle between the Universal Delegator and the object to which it delegates. Keith has created his own split identity scheme to disarm this reference cycle. But unfortunately it works the wrong way for our purposes: The Universal Delegator's creator can hold onto a secondary identity exposed by it. But the Universal Delegator itself always maintains a reference-counted interface pointer to its delegation target.



If you want to merge the CBackPtr implementation with that of the Universal Delegator, the strong reference it maintains to its delegation target is one of the hurdles you will have to overcome by altering Keith's source code a bit. I don't expect any major gotchas, but it might take a few hours' worth of work. However, I'm sure that the result will be worth the time spent. And who knows, perhaps someone will post his solution to the Web!



Split identity of course is not a symmetric solution. In that regard, it differs greatly from how a garbage collector reclaims abandoned rings. With split identity, you must pick one of the ring's nodes for breaking the reference cycle. Because that particular object is then no longer kept alive by other objects in the ring, it better be the one that clients hold onto as long as they need the entire ring structure to carry out operations successfully. Normally, picking the right object is straightforward based on your ring or connectable object design. I will call that object the beginning of the ring and the one that holds a reference to its back pointer object the end, reinforcing the asymmetric nature of the solution. Because the end does not keep the beginning alive (whereas the beginning keeps the end and all intermediates alive), the end needs to be prepared for failure when trying to access the beginning. Such failure can ripple from near the beginning toward the end of the ring across several objects. You might not be able to shield the implementation of your ring objects from such failures.



Not being able to control the scope during which the end keeps the beginning alive is the first of two weak points of using the Universal Delegator with split identity. Without the Universal Delegator, the end calls GetTarget on the beginning and holds onto the resulting interface pointer for as long as it needs to, possibly across multiple method invocations on the beginning. After GetTarget returns successfully but before the resulting interface pointer is released, the beginning's lifetime is guaranteed to the end. This could be quite important to the implementation of the object at the end of the ring, as well as to its clients inside and outside the ring. It also might simplify that implementation. But with the Universal Delegator, GetTarget is not called (by the end) and every call the end makes on the beginning could be its last, since the beginning could vanish at any moment. If this poses a problem but you still want to take advantage of the convenience of the Universal
Delegator solution where applicable, you might opt for a hybrid solution. You could deploy the Universal Delegator to provide interception services at the level of the back pointer object but also call GetTarget when doing so simplifies things.



The second weakness of the Universal Delegator is that its use is not portable across platforms. You get stuck with a piece of assembly language that you later might have to maintain and port. Of course, whether this is a problem for you greatly depends on the nature of your project. Nothing in this world is free, I suppose.



1 comment:

  1. Hi

    I wonder if you have found any update version of Universal Delegator that works for both x86 and x64?
    If so, please let me know (ehaerim@gmail.com).
    thx

    ReplyDelete