問題由來
我的程序為一個基於COM的插件結構,框架需要向插件傳遞一個IResource接口。IResource
需要根據不同的插件傳遞不同的內容。
接口定義
IResource = Interface(IDispatch)
Function GetPath: String; safecall;
End;
實現類
TResource = TClass(TAutoObject, IResource)
protected
Function GetPath: String; SafeCall;
Public
Path: String;
End;
Function GetPath: String;
Begin
Result:= Path;
End;
調用部分:
Var
Resource: IResource;
ResourceObj: TResource;
Begin
Resource:= CreateComObject(CLASS_Resource) As IResource;
//想通過強制轉換得到TResource;結果失敗了:(
ResourceObj:= TResource(Resource);
ResourceObj.Path:= '這裡設置不同的值';
End;
請問:
如何通過IResource得到TResource,從而達到設置PATH值的目的?
目前我采用的方案是再定義一個ISetValue的接口修改裡面的PATH屬性,感覺用起來比較
麻煩。
問題的延伸
如果從解決問題出發,通過定義配置接口,如:
IObjRef = Interface
function GetObjRef: TObject; safecall;
end;
這樣得到對象,再對PATH賦值,這樣做在沒有破壞COM的封裝,實現起來也比較清晰。問題至此基本解決。
但本著從分析Delphi對象與接口之間的關系的出發點,我們還是繼續標題中提出的問題:
如何通過COM接口得到實現該接口的對象實例 ?
SAVETIME的線索
http://www.delphibbs.com/Delphibbs/dispq.ASP?lid=2433841
SAVETIME的這篇文章中提到了關於Delphi中對象與接口之間在編譯器實現的內存空間情況:
----------------|-----------------|----------|--------------|-----------------
對象/接口指針 | 對象內存空間 | | 虛方法表 |
----------------|-----------------|----------|--------------|-----------------
MyObject -> | VMTptr 00|--------->| VirtA 00|
| FRefCount 04| | VirtB 04|
MyIntf -> | IInterface 08|----|
| FFIEldA 0C| | | IInterface 跳轉表 |
| FFIEldB 10| |---------> | addr of QueryInterface |
MyIntfB -> | IIntfB 14|---------| | addr of _AddRef |
MyIntfA -> | IIntfA 18|--| | | addr of _Release |
| |
| | | IIntfB 跳轉表 |
| |----> | addr of ProcB |
| | addr of VirtB |
|
| | IIntfA 跳轉表 |
|-----------> | addr of ProcA |
| addr of VirtA |
------------------------------------------------------------------------------
一個對象在調用類的成員函數的時候,比如執行 MyObject.ProcA,會隱含傳遞一個 Self 指針給這個成員函數:MyObject.ProcA(Self)。Self 就是對象數據空間的地址。那麼編譯器如何知道 Self 指針?原來對象指針 MyObject 指向的地址就是 Self,編譯器直接取出 MyObject^ 就可以作為 Self。
在以接口的方式調用成員函數的時候,比如 MyIntfA.ProcA,這時編譯器不知道 MyIntfA 到底指向哪種類型(class)的對象,無法知道 MyIntfA 與 Self 之間的距離(實際上,在上面的例子中 Delphi 編譯器知道 MyIntfA 與 Self 之間的距離,只是為了與 COM 的二進制格式兼容,使其它語言也能夠使用接口指針調用接口成員函數,必須使用後期的 Self 指針修正),編譯器直接把 MyIntfA 指向的地址設置為 Self。從上圖可以看到,MyIntfA 指向 MyObject 對象空間中 $18 偏移地址。這時的 Self 指針當然是錯誤的,編譯器不能直接調用 TMyObject.ProcA,而是調用 IIntfA 的“接口跳轉表”中的 ProcA。“接口跳轉表”中的 ProcA 的內容就是對 Self 指針進行修正(Self - $18),然後再調用 TMyObject.ProcA,這時就是正確調用對象的成員函數了。由於每個類實現接口的順序不一定相同,因此對於相同的接口在不同的類中實現,就有不同的接口跳轉表(當然,可能編輯器能夠聰明地檢查到一些類的“接口跳轉表”偏移量相同,也可以共享使用)。
通過這裡得到了解決問題的關鍵,如果能得到接口的偏移地址,那麼就可以得到對象實例
呵呵~~看到曙光了,加油!
尋找偏移地址
眾所周知,所有的Delphi對象都是從TObject繼承下來的,而創建對象也是通過
class function TObject.InitInstance(Instance: Pointer): TObject;
來分配內存空間的,仔細分析這段代碼。
class function TObject.InitInstance(Instance: Pointer): TObject;
{$IFDEF PUREPASCAL}
var
IntfTable: PInterfaceTable;
ClassPtr: TClass;
I: Integer;
begin
FillChar(Instance^, InstanceSize, 0);
PInteger(Instance)^ := Integer(Self);
ClassPtr := Self;
while ClassPtr <> nil do
begin
IntfTable := ClassPtr.GetInterfaceTable;
if IntfTable <> nil then
for I := 0 to IntfTable.EntryCount-1 do
with IntfTable.EntrIEs[I] do
begin
if VTable <> nil then
//就是它了IOffset,它就是接口的偏移地址
PInteger(@PChar(Instance)[IOffset])^ := Integer(VTable);
end;
ClassPtr := ClassPtr.ClassParent;
end;
Result := Instance;
end;
找到了IOffset,在跟蹤發現它屬於 接口標識的接口項(PInterfaceEntry)
PInterfaceEntry = ^TInterfaceEntry;
TInterfaceEntry = packed record
IID: TGUID;
VTable: Pointer;
IOffset: Integer;
ImplGetter: Integer;
end;
問題出來了,得到PInterfaceEntry 就得到了一切
輕松得到PInterfaceEntry
Var
eResourceObj: TResource;
eEntry: PInterfaceEntry;
eAutoObjFactory: TAutoObjectFactory;
Begin
eResource:= CreateComObject(CLASS_Resource) as IResource;
//得到類工廠
eAutoObjFactory:= TAutoObjectFactory(ComClassManager.GetFactoryFromClassID(CLASS_Resource));
//得到接口標識的接口項
eEntry:= eAutoObjFactory.DispIntfEntry;
//IOffset為接口的偏移地址,eResource減去IOffset所得到的地址就是對象實例
eResourceObj:= TResource(Integer(eResource)-eEntry.IOffset);
eResourceObj.Path:= '這裡設置不同的值'';
End;
結論
費勁周折得來的結果,可能對整個問題並沒有太多的意義
但是,過程確實非常有意義,通過這個過程讓我對Delphi對象和接口的實質有了更深層次的了解。