问题由来
需要根据不同的插件传递不同的内容。
接口定义
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的线索
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,这时就是正确调用对象的成员函数了。由于每个类实现接口的顺序不一定相同,因此对于相同的接口在不同的类中实现,就有不同的接口跳转表(当然,可能编辑器能够聪明地检查到一些类的“接口跳转表”偏移量相同,也可以共享使用)。
通过这里得到了解决问题的关键,如果能得到接口的偏移地址,那么就可以得到对象实例
呵呵~~看到曙光了,加油!
寻找偏移地址
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
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对象和接口的实质有了更深层次的了解。
如何通过COM接口得到实现该接口的对象实例
我的程序为一个基于COM的插件结构,框架需要向插件传递一个IResource接口。IResource
如果从解决问题出发,通过定义配置接口,如:
http://www.delphibbs.com/delphibbs/dispq.asp?lid=2433841
众所周知,所有的DELPHI对象都是从TObject继承下来的,而创建对象也是通过
Var
费劲周折得来的结果,可能对整个问题并没有太多的意义