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:
- The lifetime of the objects involved was being explicitly managed by my framework, so confused reference counting was not an issue
- 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().
Tags: Delphi, interfaces, Smoketest, test driven development, Testing
-
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. -
Nobody said that the reference had to be to the same object that was being asked for the interface in the first place!
It does when QI’ing for IUnknown:
-
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!
Comments are now closed.


DelphiFeeds
10 comments