I soon hope to be releasing “Smoketest”, a testing framework that I have developed over the past few years. It has actually been in production use for most of that time (albeit by my own good self) but also continues to develop and evolve.  On the occasions when I have mentioned it, people have asked me to publish it, but I have been reluctant to do so up to now for a number of reasons, not least that it needs a bit of polishing to make it suitable for public scrutiny.

There were also a number of things about the framework that I was not happy with and I knew that fixing them would demand “breaking changes”. I don’t mind inconveniencing myself but I did not want to release something only to then have to break a lot of other people’s code written on the back of it (or end up tied in to less than desirable ways of working for the sake of NOT breaking anything).

The biggest problem holding me back has been that I wanted the framework to be extensible.

Not only did I want people using it to be able to test things that the framework knew how to test, but to also for people to be able to extend the framework itself with further fundamental or higher level tests that could essentially become part of the framework without having to modify the framework code to achieve this.

The good news is, I finally found a solution to this problem, and once I realised how to do it I can’t quite understand how I failed to see it before!

The trick was to subvert QueryInterface() – that part of IUnknown that allows us to obtain an interface reference for any interface supported by some object to which we already have some other reference:

  type
    TSomeObject = class(TInterfacedObject, ISomeInterface,
                                           IAnotherInterface)
    end;

  var
    some: ISomeInterface;
    other: IAnotherInterface;
  begin
    some  := TSomeObject.Create as ISomeInterface;
    other := some as IAnotherInterface;
  end;

It occurred to me that this entire mechanism hinges on the implementation of QueryInterface() inherited by TSomeObject. It is this inherited implementation which determines whether or not the object supports a requested interface as declared in the class itself, and returns the appropriate reference by what-ever voodoo is involved in obtaining such things at runtime.

But I could replace that implementation with something else. Something that didn’t use compiler generated information to lookup the required reference, but did so in a far more ordinary fashion.

I should note at this point that what I was contemplating was only possible in this very limited instance due to two things:

  1. The lifetime of the objects involved was being explicitly managed by my framework, so confused reference counting was not an issue
  2. The interface references that would result would themselves be very short lived and be long gone when the underlying objects were disposed of so there would be no danger arising from mixing object and interface references

These two conditions are not at all common, so what I am about to describe is absolutely not suitable for wider use, but it demonstrates just what is possible with a little thinking “outside the box” when called for, without having to resort (or rush) to generics or anonymous methods, or other crimes against Pascal syntax. 😉

Fundamentally, the job of QueryInterface() is to take some specified interface ID and return a valid reference of the appropriate interface type if supported. Nobody said that the reference had to be to the same object that was being asked for the interface in the first place! (well, actually, this is more-or-less implicit in the reference counting model that underpins COM, but this isn’t COM and as I said before, reference counting specifically does not play any part in this exercise)

So, what I have in Smoketest is a TTest class that can satisfy requests for interfaces not by giving up a reference to an interface that it implements itself, but by consulting a list maintained by the framework itself, associating specific interface ID’s with specific implementation classes:

interface

  // Not complete... intended to illustrate QueryInterface() 
  //  declaration and implementation only ...

  type
    TTest = class(TIncident, IUnknown)
    protected
      function QueryInterface(const aIID: TGUID; out aObj): HRESULT; stdcall;
    end;

implementation

  { - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - }
  function TTest.QueryInterface(const aIID: TGUID; out aObj): HRESULT;
  var
    extClass: TExpectationClass;
    expectation: TExpectation;
  begin
    if Smoketest.FindExtension(aIID, extClass) then
    begin
      result      := 0;
      expectation := extClass.Create(self);

      if NOT expectation.GetInterface(aIID, aObj) then
        result := E_NOINTERFACE;
    end
    else
      result := inherited QueryInterface(aIID, aObj);
  end;

The IUnknown interface has to be redeclared in the class declaration otherwise the QueryInterface() implementation provided by the class will not be used by that interface. Effectively this achieves the “override” of the inherited QueryInterface() implementation (which is not virtual and so cannot be more directly overridden in the normal fashion).

If TTest.QueryInterface() fails to locate an extension for the specified interface ID it simply calls inherited, in case the interface is one that the TTest object itself implements (currently this is the least likely outcome, hence extensions are looked for first).

Note: The extension classes are derived from a TExpectation class – “expectation” in the above code is not a typo for “extension”. 🙂

If an extension class is identified then an instance of that class is instantiated and it is a reference to that newly created object that is then returned.

i.e. the following code actually creates a new object:

   var
     test: TTest;
     some: ISomeInterface;
   begin
       :
     some := test as ISomeInterface;
       :
   end;

I am normally strongly averse to side-effects, but this is one of the exceptions that proves the rule. The side-effect is entirely contained within the Smoketest framework, which also deals with all the consequences of the resulting side-effects.

All will become clear when I publish more details of Smoketest itself.

But for now, I thought I would share what I thought was a creative [sic] technique involving QueryInterface().

10 thoughts on “Thinking Creatively with QueryInterface()

  1. I guess you are aware that such a QueryInterface does not meet the requirements of IUnknown::QueryInterface:

    There are four requirements for implementations of QueryInterface (In these cases, “must succeed” means “must succeed barring catastrophic failure.”):

    – The set of interfaces accessible on an object through QueryInterface must be static, not dynamic. This means that if a call to QueryInterface for a pointer to a specified interface succeeds the first time, it must succeed again, and if it fails the first time, it must fail on all subsequent queries.
    – It must be reflexive — if a client holds a pointer to an interface on an object, and queries for that interface, the call must succeed.
    – It must be symmetric — if a client holding a pointer to one interface queries successfully for another, a query through the obtained pointer for the first interface must succeed.
    – It must be transitive — if a client holding a pointer to one interface queries successfully for a second, and through that pointer queries successfully for a third interface, a query for the first interface through the pointer for the third interface must succeed.

    1. Rule #1 is complied with, although I would say that the rule itself is a bit vague. On the one hand it says the list of supported interfaces must be dynamic not static. In my case the interfaces that are supported are not strictly “static” in that they depend on which interfaces have been registered as extensions which is determined at runtime. But during any one execution of an application using the framework, the rest of rule #1 is complied with: A request for an interface that succeeds on one occasion will succeed on every occasion in that session. Similarly any request that fails will always fail, in that session. But you could make a change to the code, recompile and now a new interface may be supported that wasn’t previously. If that constitutes a violation of this rule then the rule is meaningless as the same applies to ANY mechanism by which the supported interfaces are changed from one compilation of the code to the next. The only difference is that instead of adding a declaration to a class, the addition of this interface is achieved via a separate registration mechanism. Nowhere does the IUnknown::QueryInterface “spec” dictate HOW the support of an interface should be declared or otherwise achieved.

      Rule #2 is complied with. Having obtained a reference to an extension interface, if you ask that interface for itself then you will get it will NOT create a new object but simply return it’s own reference back to you, because at that point you are talking to the IUnknown::QueryInterface of the extension class, not the original TTest class. It is only TTest.QueryInterface which has this “creative” behaviour. Note also that if you ask TTest for it’s IUnknown explicitly you will get TTest‘s IUnknown, not that you will ever need to do that directly when using the framework, but you could if you were merely wishing to test for compliance with the IUnknown spec. 🙂

      It is rules #3 and #4 that are NOT complied with, but that is by design and does not apply to the use cases for the code that will consume this technique.

      You forgot also to quote the rules for AddRef() and Release() which are also disregarded in this framework. These functions are replaced with versions that do not maintain a reference count and which do not Free the object when the reference count hits zero – a common, but often frowned upon – technique for maintaining the use of interfaces without having reference counted lifetime management imposed as a result.

      All those rules apply to the COM specification for IUnknown. The use to which I am putting QI is (almost) entirely internal to the framework – where it is apparent to the consumer of the framework it is still exposed in such a way that the normal rules of COM simply don’t apply (unless the consumer is deliberately mis-using the framework itself). I tried to call this out where relevant and as often as possible and point out that this is absolutely NOT a general purpose technique, but a highly specialised one.

      I think when you see how it is used in the Smoketest framework (which will be covered in a future post) you will appreciate just how self-contained it is. 🙂

      1. Yeah, I was just pointing it out for other readers. No doubt what you have done works well for your needs.

  2. I see nothing wrong in how you use QI, but objects’ lifetimes are normally managed via refcounting if refcounting is used, even if you use nonstandard IUnknown implementation. Sure I only express my own experience with nonstandard refcounted interfaces, don’t know anything about your framework.

    1. Yes I am well aware of the rules for IUnknown in COM. And even in this case, if you ask a TTest for it’s IUnknown then you WILL get the IUnknown of the TTest, so that part is complied with. But note the very first sentence of the article you linked to:

      There are three main rules that govern implementing the IUnknown::QueryInterface method on a COM object:

      and you forgot to include that part of my article following the statement you quoted which clarified that:

      this isn’t COM

      🙂

  3. Interesting; both the framework and the technique.

    I played a bit with QueryInterface in the early days (and with a non-refcounted implementation of classes that expose interfaces), but never found a right framework to apply it too.

    Your framework looks like a great use for such a thing.

  4. What you’ve just thought of is how the Delphi IDE implements plugins.

    What do you think happens when you use BorlandIDEServices as IOTAxxxx or BorlandIDEServices as INTAxxxx? The very same thing that you’ve thought of!

    1. It doesn’t surprise me in the least. As I say myself, once the solution I needed occurred to me I was surprised I hadn’t thought of it before, it being so obvious. But having never needed or wanted to implement an IDE plugin or been interested as to how an undocumented function to which I don’t have the source works, the solution I came up with had to come from inspiration, not plagiarism.

      Two people can have the same “original” idea, independently. 🙂

Comments are closed.