i am using the follow test code to add an object to the GlobalInterfaceTable:
function TForm1.AddSomethingToGit(): DWORD;
var
unk: IUnknown;
cookie: DWORD;
git: IGlobalInterfaceTable;
begin
unk := TCounter.Create;
if FGit = nil then
begin
git := CoGlobalInterfaceTable.Create;
Fgit := git; //yes, i didn't use InterlockedCompareExchange. Can you imagine trying to explain that syntax to people?
end;
OleCheck(Fgit.RegisterInterfaceInGlobal(unk, IUnknown, {out}cookie));
Result := cookie;
end;
And i call the test code from a button handler:
procedure TForm1.Button1Click(Sender: TObject);
begin
AddSomethingToGit();
end;
And everything is good. The object it sitting in the global interface table, waiting to be extracted. i know it is still in there because the the destructor in TInterfacedObject has not been run e.g. breakpoint never hit:
Note: if i close the test app right now, then i will see the GlobalInterfaceTable call
Release
on my object, freeing it. But that's during shutdown, for now i'm still in memory.
But if i call the same test function from an ADO callback:
conn := CreateTrustedSqlServerConnection(serverName, defaultDatabaseName);
dataSet := TADODataSet.Create(nil);
dataSet.Connection := conn;
dataSet.OnFetchComplete := FetchComplete;
dataSet.CursorLocation := clUseClient;
dataSet.CommandText := 'WAITFOR DELAY ''00:00:03''; SELECT GETDATE() AS foo';
dataSet.CommandType := cmdText;
dataSet.ExecuteOptions := [eoAsyncFetch];
dataSet.Open();
with the callback:
procedure TForm1.FetchComplete(DataSet: TCustomADODataSet;
const Error: Error; var EventStatus: TEventStatus);
begin
AddSomethingToGit();
end;
the object i placed into the Global Interface Table is destroyed as soon as the callback returns, hitting the breakpoint in TInterfacedObject
.
In reality i wouldn't be adding a dummy test object to the GIT during the ADO async callback, i would be adding an actual ADO interface. But when that didn't work we trim the failing code down to the bare-bones.
tl;dr: i try to add an object to the Global Interface Table, but it gets destroyed as soon as i put it in there.
i thought maybe i had to manually call AddRef
before placing the object into the GIT, but the GIT register method calls AddRef
itself.
How to construct an IGlobalInterfaceTable
:
class function CoGlobalInterfaceTable.Create: IGlobalInterfaceTable;
begin
// There is a single instance of the global interface table per process, so all calls to this function in a process return the same instance.
OleCheck(CoCreateInstance(CLSID_StdGlobalInterfaceTable, nil, CLSCTX_INPROC_SERVER, IGlobalInterfaceTable, Result));
end;
with the (not my) Delphi translation of the interface:
IGlobalInterfaceTable = interface(IUnknown)
['{00000146-0000-0000-C000-000000000046}']
function RegisterInterfaceInGlobal(pUnk: IUnknown; const riid: TIID; out dwCookie: DWORD): HRESULT; stdcall;
function RevokeInterfaceFromGlobal(dwCookie: DWORD): HRESULT; stdcall;
function GetInterfaceFromGlobal(dwCookie: DWORD; const riid: TIID; out ppv): HRESULT; stdcall;
end;
And for completeness:
const
CLSID_StdGlobalInterfaceTable : TGUID = '{00000323-0000-0000-C000-000000000046}';
i desperately wanted to avoid adding my own object, for fear someone would think my object was screwed up. That's why originally i demonstrated with Delphi's in-built TInterfacedObject
. In order to confirm that it really is "my" object that's being destroyed, i changed references in the question from TInterfacedObject
to TCounter
:
TCounter = class(TInterfacedObject, IUnknown)
private
FFingerprint: string;
public
constructor Create;
destructor Destroy; override;
end;
{ TCounter }
constructor TCounter.Create;
begin
inherited Create;
FFingerprint := 'Rob Kennedy';
end;
destructor TCounter.Destroy;
begin
if FFingerprint = 'Rob Kennedy' then
Beep;
inherited;
end;
And my TCounter.Destroy
is hit.
i figured out the problem; it's likely an (undocumented) fundamental limitation of IGlobalInterfaceTable
that makes it essentially unusable:
Any object added to the GlobalInterfaceTable must be retrieved before the adding apartment is torn down.
Running the following from a separate thread:
procedure TAddToGitThread.Execute;
var
unk: IUnknown;
cookie: DWORD;
git: IGlobalInterfaceTable;
begin
CoInitialize(nil);
try
unk := TCounter.Create;
git := CoGlobalInterfaceTable.Create;
OleCheck(git.RegisterInterfaceInGlobal(unk, IUnknown, {out}cookie));
unk := nil;
finally
CoUninitialize; <--objects added from this apartment Released
end;
end;
As soon as the apartment associated with the separate thread is uninitialized: any objects still in the GlobalInterfaceTable are flushed.
This makes it impossible the post messages containing GIT cookie values between threads.
Even artificially inflating the reference count, to prevent object destruction won't help you:
procedure TAddToGitThread.Execute;
var
unk: IUnknown;
cookie: DWORD;
git: IGlobalInterfaceTable;
begin
CoInitialize(nil);
try
unk := TCounter.Create;
unk._AddRef; //inflate reference count to prevent destruction on apartment teardown
git := CoGlobalInterfaceTable.Create;
OleCheck(git.RegisterInterfaceInGlobal(unk, IUnknown, {out}cookie));
unk := nil;
finally
CoUninitialize; <--object added from this apartment is Released, but not freed
end;
end;
Even though the object was not destroyed (because we inflated the reference count), it will no longer be in the global interface table. Asking for it will just fail:
hr := _git.GetInterfaceFromGlobal(_cookie, IUnknown, {out}unk);
returns
0x80070057 The parameter is incorrect
You wanted to use that object added to the GIT? You had should have grabbed it while you didn't have the chance!
i hate bugs where i do nothing wrong, but it still fails.