在C# 中枚举COM对象的方法和属性名称

以前满世界问过这个问题,没有人理偶的说,还是自己动手搞定比较好。

一般来说,一个COM对象在提供的时候,通常还会提供一个类型库,在其中定义了COM对象的所有方法名称、参数名称、属性名称等等信息。我们要做的就是从类型库中取出这些信息。
当然,某些只供C++程序员使用的COM对象没有类型库,而代之以C++的头文件和/或idl文件,对这种情况,一般没有办法在程序中枚举出对象的方法属性:毕竟去找C++头文件不太现实,何况在非开发环境下,根本就没有头文件的说。
因此,我们将讨论当COM对象存在TypeLib的情况下,枚举方法/属性名称的问题。
从COM对象定位到TypeLib
在一般情况下,COM对象的TypeLib信息存储在注册表中:在HK_CLASSROOT\CLSID\{ClassID}\的注册表项下,有一个名为TypeLib的子项,其中定义了这个COM对象类型库的ID;而在HK_CLASSROOT\TypeLib 注册表项下,列举了系统中所有TypeLib。
看看我们首先要做什么:从ProgID 取得 ClassID,这个工作可以通过调用COM 基础库的 CLSIDFromProgID 函数来完成,在Platform SDK中,该函数的定义如下:

HRESULT CLSIDFromProgID(
  LPCOLESTR lpszProgID,
  LPCLSID pclsid

);

为了在.Net中使用这个函数,我们用DllImport Attribute 把这个函数引入.Net 中:


class UnsafeNativeMethods{
[DllImport("ole32.dll",CharSet=CharSet.Unicode,PreserveSig=false)]
public static extern void CLSIDFromProgID([In,MarshalAs(UnmanagedType.BStr)] string lpszProgID,[Out]out Guid pclsid);
………

然后,我们可以在.Net 中调用这个函数取得ClassID了:


Guid clsid;
UnsafeNativeMethods.CLSIDFromProgID(progID,out clsid);

OK, 升级宝物Class ID 入手,Level Up!Strength + 3, Life + 5,必杀技 dll import 习得。 :) 下一个任务:取得TypeLib。
l         取得TypeLib。
为访问TypeLib,COM 提供了二个接口:ITypeLib 和 ITypeInfo,其中ITypeLib 提供对 TypeLib 的访问,而ITypeInfo 则表示TypeLib中定义的某一项ITypeInfo。
要获得ITypeInfo,COM有二个函数可以做这件事情:LoadTypeLib 和 LoadRegTypeLib。其中 LoadTypeLib 需要 TypeLib 文件的路径作为参数,而LoadRegTypeLib 则根据TypeLib的TypeLib ID和TypeLib的版本号取得 ITypeLib。在这里,我们用LoadRegTypeLib来取得ITypeLib 接口。
先来准备需要的参数:TypeLibID和TypeLib的版本号,这些信息需要从注册表里得到:


RegistryKey regKey = Registry.ClassesRoot;
regKey = regKey.OpenSubKey("CLSID\\{" + clsid.ToString() + "}\\TypeLib");
Guid typeLibID = new Guid(regKey.GetValue("").ToString());
//Get TypeLib Versions
short iMajorVer,iMinusVer;
regKey = Microsoft.Win32.Registry.ClassesRoot;
regKey = regKey.OpenSubKey("TypeLib\\{" + typeLibID.ToString() + "}");
string[] aryTemp = regKey.GetSubKeyNames();
string sVersion = aryTemp[0];
aryTemp = sVersion.Split('.');
iMajorVer = short.Parse(aryTemp[0],System.Globalization.NumberStyles.AllowHexSpecifier);
iMinusVer = short.Parse(aryTemp[1] ,System.Globalization.NumberStyles.AllowHexSpecifier);

这里要注意一点:在注册表里记录的TypeLib版本号是以十六进制格式表示的,运气好的话,你会发现类似”1.a”之类的版本号,所以我们最好把它们看成16进制来转换。
现在可以调用LoadRegTypeLib 了,和CLSIDFromProgID一样,先import进来:
这是LoadRegTypeLib 的函数原型:

HRESULT LoadRegTypeLib( 
  REFGUID  rguid,             
  unsigned short  wVerMajor,  
  unsigned short  wVerMinor,  
  LCID  lcid,                 
  ITypeLib FAR* FAR*  pptlib  

);

恩,在.Net里,这个函数是这个样子的:


[DllImport("oleaut32.dll",CharSet=CharSet.Unicode,PreserveSig=false)]
[LCIDConversion(3)]
public static extern UCOMITypeLib LoadRegTypeLib(ref Guid rguid, [In,MarshalAs(UnmanagedType.U2)]short wVerMajor, [In,MarshalAs(UnmanagedType.U2)]short wVerMinor);

哈,一个小小的技巧:在.Net 的定义里,偶没有定义原型里 lcid 这个参数,这是因为偶应用了LCIDConversionAttribute,这个Attribute 意思就是说这个方法需要一个LCID做参数,参数的位置嘛:[LCIDConversion(3)]——第三个参数。这样在调用这个方法的时候,.Net 的封送拆收器将自动提供 LCID 参数。不错把:)
另外,在.Net Framwork的System.Runtime.InteropServices 命名空间里,定义了一些常用COM Interface的.Net托管定义,虽然不多,但幸运的是我们要用到的ITypeLib和 ITypeInfo这二个接口都有,也就是System.Runtime.InteropService.UCOMITypeLibSystem.Runtime.InteropService.UCOMITypeInfo。这就省下了我们自己定义接口的工作。
好了,接下来的事情狠简单了:


UCOMITypeLib typeLib;
typeLib = UnsafeNativeMethods.LoadRegTypeLib(ref typeLibID,iMajorVer,iMinusVer);

Bingo!Mission Complete!只剩下最后一个任务:定位到对应我们的COM对象的ITypeInfo,并从中取出我们需要的信息。
ITypeInfo接口的GetITypeInfo方法和GetITypeInfoCount方法一起提供了遍历TypeLib中所有ITypeInfo的能力,不过既然我们手上有COM对象的ClassID,利用GetITypeInfoOfGuid 方法就可以获得COM对象的ITypeInfo了。


UCOMITypeInfo ITypeInfo;
typeLib.GetITypeInfoOfGuid(ref clsid,out ITypeInfo);

拿到ITypeInfo之后,首先我们需要看看这个ITypeInfo里有多少方法/属性,这需要我们调用它的GetTypeAttr 方法获得TYPEATTR结构。


TYPEATTR typeattr;
IntPtr p_typeattr = IntPtr.Zero;
ITypeInfo.GetTypeAttr(out p_typeattr);
typeattr = (TYPEATTR)Marshal.PtrToStructure(p_typeattr,typeof(TYPEATTR));

获得TYPEATTR结构有那么一点点麻烦,因为 .Net的不支持非托管签名的 TYPEATTR** 参数,所以只有使用引用 IntPtr 参数定义 GetTypeAttr。然后我们需要用Marshal.PtrToStructure将数据从非托管内存块封送到托管对象。在TYPEATTR结构中,cFuns字段表示当前TrpeInfo描述的函数数目,而每个函数的描述则是通过ITypeInfo的GetFuncDesc方法取得的FUNCDESC结构描述的。


if(typeattr.cFuncs > 0)
{
for(int i=0;i<typeattr.cFuncs;++i)
{
//Get FUNCDESC struct
FUNCDESC funcdesc;
IntPtr p_funcDesc;
ITypeInfo.GetFuncDesc(i,out p_funcDesc);
funcdesc = (FUNCDESC)Marshal.PtrToStructure(p_funcDesc,typeof(FUNCDESC));
……

和TYPEATTR一样,FUNCDESC结构也需要Marshal.PtrToStructure处理一下,偶就不多说了。
讨厌的是:FUNCDESC结构里并没有函数的名称,我们只能通过它的memid字段和invkind字段知道这个函数的成员ID和函数的类型。函数的类型是我们需要的:它告诉我们这个函数是一个方法或者是一个属性的Get/Set方法,而名称这个东西,我们还得求助于ITypeInfo:GetNames 方法获取具有指定成员ID的成员名称,它的返回一个string数组,对方法而言,这个数组第一个元素是方法名称,后面的元素则是方法的参数名,而对属性而言,属性名称也出现在数组的第一个元素。
好了,现在除了一件事情,该说的偶已经都说了,我们已经知道了如何从ITypeInfo获得方法/属性的名称,至于如何如何先建立二个空的Collection分别用于存放方法和属性名称;如何如何遍历ITypeInfo的所有FuncDesc,根据每个不同的函数类型向对应的Collection中插入元素,这些简单操作偶就不想多说了。我们剩下的唯一的问题是:对所有VB生成的COM对象,按照上面的步骤走下来,我们什么方法属性也看不到。
原因嘛,用OleView看一下这些dll的TypeLib就明白了:VB会生成一个名为”_”+类名的类接口,这个接口继承自IDispatch,所有的方法属性都在这个接口上定义,而实现类只是简单的实现这个接口,在它的TypeLib里,真正对应ClassID的实现类里没有任何成员。
既然知道了原因,办法也就有了:我们在枚举一个COM对象的所有方法/属性时,不应该只枚举仅对应它自己ClassID的TypeInfo,这个TypeInfo继承的所有其他接口中定义的方法/属性也要照样拿出来,而要定位到它继承的其他接口,我们要做的事情其实和遍历这个ITypeInfo的所有FUNCDESC差不多:


if(typeattr.cImplTypes > 0)
{
for(int i=0;i<typeattr.cImplTypes;++i)
{
       int href;
       UCOMITypeInfo imptypeinfo;
       typeinfo.GetRefTypeOfImplType(i,out href);
       typeinfo.GetRefTypeInfo(href,out imptypeinfo);
       //Now we can do the same thing to the imptypeinfo like typeinfo
……
}
}


TYPEATTR的cImplTypes字段表示这个ItypeInfo实现的接口数目,ITypeInfo的GetRefTypeOfImplType 方法获取对某个已实现接口的句柄的引用,而GetRefTypeInfo 方法从这个句柄的引用获取该接口的ITypeInfo。很明显,我们可以写一个递归函数来走遍所有COM对象实现的接口,而且我们可以确信这个递归是有出口的:因为COM里所有的接口归根到底都派生自“我不知道”接口 。^-^
最后,我想在大多数情况下,你不会希望在COM对象的方法列表里看到QueryInterface或者AddRef这类IUnknown接口的方法,而IDispatch接口那些类似Invoke之类的方法想来有兴趣的人也不多,不过反正这种底层方法就那么几个,在你遍历的时候尽可以判断一下过滤掉这些方法名称。

免责声明:
在本文中,为了清晰起见,所有给出的代码中都没有错误处理。如果你在你的代码中使用本文中的部分代码,由此造成的诸如程序出错、系统宕机、走路撞树、手机爆炸、洪水毁堤、地球毁灭等等一切后果,本人概不负责。

posted @ 2010-02-15 12:40  猪悟能  阅读(1305)  评论(0编辑  收藏  举报