程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Delphi >> 如何通過COM接口得到實現該接口的對象實例

如何通過COM接口得到實現該接口的對象實例

編輯:Delphi

問題由來

我的程序為一個基於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對象和接口的實質有了更深層次的了解。
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved