基于 IDispatch 的 COM 对象的反射






4.86/5 (28投票s)
使用 .NET 的 TypeToTypeInfoMarshaler 从基于 IDispatch 的 COM 对象获取完整的 .NET 类型以及成员信息。
引言
.NET 的反射 API 提供了关于托管类型的属性、方法和事件的丰富信息。然而,它对于非托管 COM 类型并不那么好用。COM 中最接近反射的功能是 IDispatch 返回 ITypeInfo 的能力,而 .NET 的反射 API 不会自动为基于 IDispatch
的 COM 对象使用 ITypeInfo
。通常可以获取丰富的类型信息,但这需要通过 .NET 内置的 TypeToTypeInfoMarshaler
使用自定义的 IDispatch
声明来做一些额外的工作。
背景
如果您正在使用一个强类型 COM 对象,并且已经引用了一个互操作程序集(例如,一个 PIA 或由 TlbImp.exe 生成的程序集),那么通过运行时可调用包装器 (runtime callable wrapper) 可以通过反射自动获得丰富的类型信息。然而,如果您只收到一个未知类型的对象(例如,由非托管代码或 Activator.CreateInstance
创建的对象),那么对其使用反射可能会令人失望。如果该对象是一个非托管 COM 对象,则默认的反射结果将是针对 System.__ComObject
类型,这是一个在 mscorlib.dll 中定义的内部类型。例如:
Type fsoType = Type.GetTypeFromProgID("Scripting.FileSystemObject");
object fso = Activator.CreateInstance(fsoType);
Console.WriteLine("TypeName: {0}", fso.GetType().FullName);
foreach (MemberInfo member in fso.GetType().GetMembers())
{
Console.WriteLine("{0} -- {1}", member.MemberType, member);
}
将产生输出:
TypeName: System.__ComObject
Method -- System.Object GetLifetimeService()
Method -- System.Object InitializeLifetimeService()
Method -- System.Runtime.Remoting.ObjRef CreateObjRef(System.Type)
Method -- System.String ToString()
Method -- Boolean Equals(System.Object)
Method -- Int32 GetHashCode()
Method -- System.Type GetType()
获取 .NET 的 System.__ComObject
的类型信息很少有用。最好从底层 COM 对象的 IDispatch
实现中获取类型信息,但这需要更多的工作。DispatchUtility
类(在附带的示例代码中)使用 IDispatch
的接口 ID (IID) 私有声明了 IDispatchInfo
接口,但它只声明了 IDispatch
的前三个方法,因为我们只需要它们来获取类型信息。
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("00020400-0000-0000-C000-000000000046")]
private interface IDispatchInfo
{
// Gets the number of Types that the object provides (0 or 1).
[PreserveSig]
int GetTypeInfoCount(out int typeInfoCount);
// Gets the Type information for an object if GetTypeInfoCount returned 1.
void GetTypeInfo(int typeInfoIndex, int lcid,
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef =
typeof(System.Runtime.InteropServices.CustomMarshalers.TypeToTypeInfoMarshaler))]
out Type typeInfo);
// Gets the DISPID of the specified member name.
[PreserveSig]
int GetDispId(ref Guid riid, ref string name, int nameCount, int lcid,
out int dispId);
// NOTE: The real IDispatch also has an Invoke method next, but we don't need it.
}
收集和转换 .NET 类型信息的实际工作由 IDispatchInfo.GetTypeInfo
的最后一个参数上的 TypeToTypeInfoMarshaler
处理。原始的 IDispatch
接口(在 Windows SDK 的 OAIdl.idl 文件中声明)将 GetTypeInfo
声明为输出 ITypeInfo
,但 .NET 的 TypeToTypeInfoMarshaler
会将其转换为一个丰富的 .NET Type 实例。
IDispatchInfo
还提供了一个简化的 IDispatch.GetIDsOfNames
,它一次只处理一个名称。它将该方法声明为 GetDispId
,并调整参数声明,使其能够正确地处理单个 ID 和名称。
IDispatchInfo
忽略了第四个 IDispatch
方法(即 Invoke
),因为 .NET 中已经有几种动态调用方法(例如,通过使用“[DISPID=n]
”语法的 Type.InvokeMember
或通过 C# 的 dynamic 关键字)。由于前三个方法提供了类型信息的元数据发现和 DISPID,所以它们是我们真正需要的。
注意:当向现有的非托管 COM 对象请求信息时,使用此 IDispatch
的部分实现是安全的,因为 C# 编译器将像原始 IDispatch
接口一样生成此接口的 vtable,直到其前三个方法。但是,在托管对象上实现 IDispatchInfo
并将其传递给任何期望真实 IDispatch
的非托管代码是不安全的。由于只有一个部分的 IDispatch
vtable
,如果有人尝试使用后续的 vtable
成员(如 Invoke),可能会发生各种问题(例如,访问冲突和内存损坏)。这是 IDispatchInfo
在 DispatchUtility
中被声明为 private
嵌套接口的原因之一。
Using the Code
DispatchUtility
类提供了 static
方法来检查对象是否实现了 IDispatch
,获取 .NET 类型信息,获取 DISPID
,以及按名称或 DISPID
动态调用成员。
public static class DispatchUtility
{
// Gets whether the specified object implements IDispatch.
public static bool ImplementsIDispatch(object obj) { ... }
// Gets a Type that can be used with reflection.
public static Type GetType(object obj, bool throwIfNotFound) { ... }
// Tries to get the DISPID for the requested member name.
public static bool TryGetDispId(object obj, string name, out int dispId) { ... }
// Invokes a member by DISPID.
public static object Invoke(object obj, int dispId, object[] args) { ... }
// Invokes a member by name.
public static object Invoke(object obj, string memberName, object[] args) { ... }
}
我们可以修改前面的示例代码,使用 DispatchUtility.GetType(fso, true)
代替 fso.GetType()
。
Type fsoType = Type.GetTypeFromProgID("Scripting.FileSystemObject");
object fso = Activator.CreateInstance(fsoType);
Type dispatchType = DispatchUtility.GetType(fso, true);
Console.WriteLine("TypeName: {0}", dispatchType.FullName);
foreach (MemberInfo member in dispatchType.GetMembers())
{
Console.WriteLine("{0} -- {1}", member.MemberType, member);
}
这将产生以下输出:
TypeName: Scripting.IFileSystem3
Method -- Scripting.Drives get_Drives()
Method -- System.String BuildPath(System.String, System.String)
Method -- System.String GetDriveName(System.String)
Method -- System.String GetParentFolderName(System.String)
Method -- System.String GetFileName(System.String)
Method -- System.String GetBaseName(System.String)
Method -- System.String GetExtensionName(System.String)
Method -- System.String GetAbsolutePathName(System.String)
Method -- System.String GetTempName()
Method -- Boolean DriveExists(System.String)
Method -- Boolean FileExists(System.String)
Method -- Boolean FolderExists(System.String)
Method -- Scripting.Drive GetDrive(System.String)
Method -- Scripting.File GetFile(System.String)
Method -- Scripting.Folder GetFolder(System.String)
Method -- Scripting.Folder GetSpecialFolder(Scripting.SpecialFolderConst)
Method -- Void DeleteFile(System.String, Boolean)
Method -- Void DeleteFolder(System.String, Boolean)
Method -- Void MoveFile(System.String, System.String)
Method -- Void MoveFolder(System.String, System.String)
Method -- Void CopyFile(System.String, System.String, Boolean)
Method -- Void CopyFolder(System.String, System.String, Boolean)
Method -- Scripting.Folder CreateFolder(System.String)
Method -- Scripting.TextStream CreateTextFile(System.String, Boolean, Boolean)
Method -- Scripting.TextStream OpenTextFile
(System.String, Scripting.IOMode, Boolean, Scripting.Tristate)
Method -- Scripting.TextStream GetStandardStream(Scripting.StandardStreamTypes, Boolean)
Method -- System.String GetFileVersion(System.String)
Property -- Scripting.Drives Drives
使用 DispatchUtility.GetType
,我们可以获得丰富的类型信息,例如属性和方法详细信息以及一个良好的接口类型名称。这比获取 System.__ComObject
的成员要好得多。之所以有效,是因为我们正在使用的 COM 类型已注册了类型库,因此当 DispatchUtility
内部调用 IDispatch.GetTypeInfo
时,它能够返回一个 ITypeInfo
。然后 .NET 的 TypeToTypeInfoMarshaler
将 ITypeInfo
转换为一个 .NET Type。
如果 IDispatch.GetTypeInfo
方法无法返回 ITypeInfo
(例如,如果该对象没有注册类型库),那么我们就无法获得 .NET Type 实例。此限制会影响大多数实现了 IDispatchEx
的“expando”对象,这些对象可以在运行时动态添加和删除成员(例如 JScript 对象)。通常,ITypeInfo
只返回静态类型信息,因此对于基于 IDispatchEx
的对象,动态添加的成员将不会被报告。
DispatchUtility
类实现包含在一个文件中,因此易于集成到现有项目中。该类仅需要对 System.dll 和 CustomMarshalers.dll 的程序集引用,它们是核心 .NET Framework 的一部分。它应该在 .NET 2.0 或更高版本上支持“Any CPU”。在 .NET 4.0 或更高版本上,对 UnmanagedCode 权限的 LinkDemands 不需要,可以忽略或删除。
兴趣点
网上许多文章错误地声称必须引用 COM 互操作程序集才能在 .NET 中获得丰富的类型信息,例如 Microsoft 支持文章 320523 和 StackOverflow 帖子 "How to enumerate members of COM object in C#?" 和 "Get property names via reflection of an COM Object"。一些更高级的文章建议直接处理 ITypeInfo
,例如 "Inspecting COM Objects With Reflection" 和 "Obtain Type Information of IDispatch-Based COM Objects from Managed Code"。不幸的是,直接使用 ITypeInfo
需要大量的手动互操作工作,而且它不会给你一个 System.Type
实例。但是,如本文所述,使用 TypeToTypeInfoMarshaler
要容易得多,并且它以标准的 .NET Type 格式提供了丰富的类型信息。
历史
- 2013 年 1 月 7 日:初始版本