i have an object which delegates implementation of a particularly complex interface to a child object. This is exactly i think is the job of TAggregatedObject
. The "child" object maintains a weak reference to its "controller", and all QueryInterface
requests are passed back to the parent. This maintains the rule that IUnknown
is always the same object.
So, my parent (i.e. "Controller") object declares that it implements the IStream
interface:
type
TRobot = class(TInterfacedObject, IStream)
private
function GetStream: IStream;
public
property Stream: IStream read GetStrem implements IStream;
end;
Note: This is a hypothetical example. i chose the word
Robot
because it sounds complicated, and and word is only 5 letters long - it's short. i also choseIStream
because its short. i was going to useIPersistFile
orIPersistFileInit
, but they're longer, and make the example code harder to real. In other words: It's a hypothetical example.
Now i have my child object that will implement IStream
:
type
TRobotStream = class(TAggregatedObject, IStream)
public
...
end;
All that's left, and this is where my problem starts: creating the RobotStream
when it's asked for:
function TRobot.GetStream: IStream;
begin
Result := TRobotStream.Create(Self) as IStream;
end;
This code fails to compile, with the error Operator not applicable to this operand type.
.
This is because delphi is trying to perform the as IStream
on an object that doesn't implement IUnknown
:
TAggregatedObject = class
...
{ IUnknown }
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
...
The IUnknown methods may be there, but the object doesn't advertise that it supports IUnknown
. Without an IUnknown
interface, Delphi can't call QueryInterface
to perform the cast.
So i change my TRobotStream
class to advertise that it implements the missing interface (which it does; it inherits it from its ancestor):
type
TRobotStream = class(TAggregatedObject, IUnknown, IStream)
...
And now it compiles, but crashes at runtime on the line:
Result := TRobotStream.Create(Self) as IStream;
Now i can see what's happening, but i can't explain why. Delphi is calling IntfClear
, on my parent Robot
object, on the way out of the child object's constructor.
i don't know the proper way to prevent this. i could try forcing the cast:
Result := TRobotStream.Create(Self as IUnknown) as IStream;
and hope that keeps a reference. Turns out that it does keep the reference - no crash on the way out of the constructor.
Note: This is confusing to me. Since i am passing an object where an interface is expected. i would assume that the compiler is implicitly preforming a typecast, i.e.:
Result := TRobotStream.Create(Self
as IUnknown);
in order to satisfy the call. The fact that the syntax checker didn't complain let me to assume all was correct.
But the crashes aren't over. i've changed the line to:
Result := TRobotStream.Create(Self as IUnknown) as IStream;
And the code does indeed return from the constructor of TRobotStream
without destroying my parent object, but now i get a stack overflow.
The reason is that TAggregatedObject
defers all QueryInterface
(i.e. type casts) back to the parent object. In my case i am casting a TRobotStream
to an IStream
.
When i ask the TRobotStream
for its IStream
at the end of:
Result := TRobotStream.Create(Self as IUnknown) as IStream;
It turns around and asks its controller for the IStream
interface, which triggers a call to:
Result := TRobotStream.Create(Self as IUnknown) as IStream;
Result := TRobotStream.Create(Self as IUnknown) as IStream;
which turns around and calls:
Result := TRobotStream.Create(Self as IUnknown) as IStream;
Result := TRobotStream.Create(Self as IUnknown) as IStream;
Result := TRobotStream.Create(Self as IUnknown) as IStream;
Boom! Stack overflow.
Blindly, i try removing the final cast to IStream
, let Delphi try to implicitely cast the object to an interface (which i just saw above doesn't work right):
Result := TRobotStream.Create(Self as IUnknown);
And now there is no crash; which i don't understand this very much. i've constructed an object, an object which supports multiple interfaces. How is it now that Delphi knows to cast the interface? Is it performing the proper reference counting? i saw above that it doesn't. Is there a subtle bug waiting to crash for the customer?
So i'm left with four possible ways to call my one line. Which one of them is valid?
Result := TRobotStream.Create(Self);
Result := TRobotStream.Create(Self as IUnknown);
Result := TRobotStream.Create(Self) as IStream;
Result := TRobotStream.Create(Self as IUnknown) as IStream;
The Real Question
i hit quite a few subtle bugs, and difficult to understand intricacies of the compiler. This leads me to believe that i have done everything completely wrong. If needed, ignore everything i said, and help me answer the question:
What is the proper way to delegate interface implementation to a child object?
Maybe i should be using TContainedObject
instead of TAggregatedObject
. Maybe the two work in tandem, where the parent should be TAggregatedObject
and the child is TContainedObject
. Maybe it's the other way around. Maybe neither apply in this case.
Note: Everything in the main part of my post can be ignored. It was just to show that i have thought about it. There are those who would argue that by including what i have tried, i have poisoned the possible answers; rather than answering my question, people might focus on my failed question.
The real goal is to delegate interface implementation to a child object. This question contains my detailed attempts at solving the problem with
TAggregatedObject
. You don't even see my other two solution patterns. One of which suffers from circular refernce counts, and the breaks theIUnknown
equivalence rule.Rob Kennedy might remember; and asked me to make a question that asks for a solution to the problem, rather than a solution to a problem in one of my solutions.
Edit: grammerified
Edit 2: No such thing as a robot controller. Well, there is - i worked with Funuc RJ2 controllers all the time. But not in this example!
Edit 3*
TRobotStream = class(TAggregatedObject, IStream)
public
{ IStream }
function Seek(dlibMove: Largeint; dwOrigin: Longint;
out libNewPosition: Largeint): HResult; stdcall;
function SetSize(libNewSize: Largeint): HResult; stdcall;
function CopyTo(stm: IStream; cb: Largeint; out cbRead: Largeint; out cbWritten: Largeint): HResult; stdcall;
function Commit(grfCommitFlags: Longint): HResult; stdcall;
function Revert: HResult; stdcall;
function LockRegion(libOffset: Largeint; cb: Largeint; dwLockType: Longint): HResult; stdcall;
function UnlockRegion(libOffset: Largeint; cb: Largeint; dwLockType: Longint): HResult; stdcall;
function Stat(out statstg: TStatStg; grfStatFlag: Longint): HResult; stdcall;
function Clone(out stm: IStream): HResult; stdcall;
function Read(pv: Pointer; cb: Longint; pcbRead: PLongint): HResult; stdcall;
function Write(pv: Pointer; cb: Longint; pcbWritten: PLongint): HResult; stdcall;
end;
TRobot = class(TInterfacedObject, IStream)
private
FStream: TRobotStream;
function GetStream: IStream;
public
destructor Destroy; override;
property Stream: IStream read GetStream implements IStream;
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.Button1Click(Sender: TObject);
var
rs: IStream;
begin
rs := TRobot.Create;
LoadRobotFromDatabase(rs); //dummy method, just to demonstrate we use the stream
rs := nil;
end;
procedure TForm1.LoadRobotFromDatabase(rs: IStream);
begin
rs.Revert; //dummy method call, just to prove we can call it
end;
destructor TRobot.Destroy;
begin
FStream.Free;
inherited;
end;
function TRobot.GetStream: IStream;
begin
if FStream = nil then
FStream := TRobotStream.Create(Self);
result := FStream;
end;
Problem here is that the "parent" TRobot
object is destroyed during the call to:
FStream := TRobotStream.Create(Self);