65.9K
CodeProject 正在变化。 阅读更多。
Home

基于 IDispatch 的 COM 对象的反射

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (28投票s)

2013年1月7日

CPOL

5分钟阅读

viewsIcon

71995

downloadIcon

3467

使用 .NET 的 TypeToTypeInfoMarshaler 从基于 IDispatch 的 COM 对象获取完整的 .NET 类型以及成员信息。

Console

引言

.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),可能会发生各种问题(例如,访问冲突和内存损坏)。这是 IDispatchInfoDispatchUtility 中被声明为 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 的 TypeToTypeInfoMarshalerITypeInfo 转换为一个 .NET Type。

如果 IDispatch.GetTypeInfo 方法无法返回 ITypeInfo(例如,如果该对象没有注册类型库),那么我们就无法获得 .NET Type 实例。此限制会影响大多数实现了 IDispatchEx 的“expando”对象,这些对象可以在运行时动态添加和删除成员(例如 JScript 对象)。通常,ITypeInfo 只返回静态类型信息,因此对于基于 IDispatchEx 的对象,动态添加的成员将不会被报告。

DispatchUtility 类实现包含在一个文件中,因此易于集成到现有项目中。该类仅需要对 System.dllCustomMarshalers.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 日:初始版本
© . All rights reserved.