ComWrappers 的源生成

.NET 8 引入了一个源生成器,用于为你创建 ComWrappers API 的实现。 生成器可识别 GeneratedComInterfaceAttribute

.NET 运行时的内置(非源代码生成)、仅限 Windows 的 COM 互操作系统会生成 IL 存根(JIT-ed 的 IL 指令流),以方便从托管代码转换到 COM,反之亦然。 由于此 IL 存根是在运行时生成的,因此它与 NativeAOTIL 剪裁不兼容。 运行时存根生成还可以使诊断封送问题变得困难。

内置互操作使用 ComImportDllImport 等属性,这些属性依赖于运行时的代码生成。 下面的代码显示了此用法的示例:

[ComImport]
interface IFoo
{
    void Method(int i);
}

[DllImport("MyComObjectProvider")]
static nint GetPointerToComInterface(); // C definition - IUnknown* GetPointerToComInterface();

[DllImport("MyComObjectProvider")]
static void GivePointerToComInterface(nint comObject); // C definition - void GivePointerToComInterface(IUnknown* pUnk);

// Use the system to create a Runtime Callable Wrapper to use in managed code
nint ptr = GetPointerToComInterface();
IFoo foo = (IFoo)Marshal.GetObjectForIUnknown(ptr);
foo.Method(0);
...
// Use the system to create a COM Callable Wrapper to pass to unmanaged code
IFoo foo = GetManagedIFoo();
nint ptr = Marshal.GetIUnknownForObject(foo);
GivePointerToComInterface(ptr);

ComWrappers API 允许在 C# 中与 COM 交互,而无需使用内置 COM 系统,但 需要大量的样本和手动编写的不安全代码。 COM 接口生成器自动执行此过程,使 ComWrappers 与内置 COM 一样简单,但以可剪裁和 AOT 友好的方式交付它。

基本用法

若要使用 COM 接口生成器,请在要从 COM 导入或向其公开的接口定义中添加 GeneratedComInterfaceAttributeGuidAttribute 属性。 类型必须标记为 partial,并具有 internalpublic 可见性,才能访问生成的代码。

[GeneratedComInterface]
[Guid("3faca0d2-e7f1-4e9c-82a6-404fd6e0aab8")]
internal partial interface IFoo
{
    void Method(int i);
}

然后,若要向 COM 公开实现接口的类,请将该 GeneratedComClassAttribute 类添加到实现类。 此类也必须为 partialinternalpublic

[GeneratedComClass]
internal partial class Foo : IFoo
{
    public void Method(int i)
    {
        // Do things
    }
}

在编译时,生成器将创建 ComWrappers API 的实现,你可以使用 StrategyBasedComWrappers 类型或自定义派生类型来使用或公开 COM 接口。

[LibraryImport("MyComObjectProvider")]
private static partial nint GetPointerToComInterface(); // C definition - IUnknown* GetPointerToComInterface();

[LibraryImport("MyComObjectProvider")]
private static partial void GivePointerToComInterface(nint comObject); // C definition - void GivePointerToComInterface(IUnknown* pUnk);

// Use the ComWrappers API to create a Runtime Callable Wrapper to use in managed code
ComWrappers cw = new StrategyBasedComWrappers();
nint ptr = GetPointerToComInterface();
IFoo foo = (IFoo)cw.GetOrCreateObjectForComInstance(ptr, CreateObjectFlags.None);
foo.Method(0);
...
// Use the system to create a COM Callable Wrapper to pass to unmanaged code
ComWrappers cw = new StrategyBasedComWrappers();
Foo foo = new();
nint ptr = cw.GetOrCreateComInterfaceForObject(foo, CreateComInterfaceFlags.None);
GivePointerToComInterface(ptr);

自定义封送

COM 接口生成器遵循 MarshalUsingAttribute 属性和 MarshalAsAttribute 属性的某些用法来自定义参数封送。 有关详细信息,请参阅如何 使用 MarshalUsing 属性自定义源生成的封送,和 使用 MarshalAs 属性自定义参数封送GeneratedComInterfaceAttribute.StringMarshallingGeneratedComInterfaceAttribute.StringMarshallingCustomType 属性适用于接口中所有类型 string 的参数和返回类型(如果没有其他封送属性)。

隐式 HRESULT 和 PreserveSig

C# 中的 COM 方法具有不同于本机方法的签名。 标准 COM 的返回类型为 HRESULT,即表示错误和成功状态的 4 字节整数类型。 默认情况下,此 HRESULT 返回值在 C# 签名中隐藏,并在返回错误值时转换为异常。 本机 COM 签名的最后一个“out”参数可以选择转换为 C# 签名中的返回。

例如,以下代码片段显示了 C# 方法签名和生成器推断的相应本机签名。

void Method1(int i);

int Method2(float i);
HRESULT Method1(int i);

HRESULT Method2(float i, _Out_ int* returnValue);

如果要自行处理 HRESULT,则可以使用该方法上的 PreserveSigAttribute 来指示生成器不应执行此转换。 以下代码片段演示了应用 [PreserveSig] 时生成器所需的本机签名。 COM 方法必须返回 HRESULT,因此任何具有 PreserveSig 的方法的返回值都应为 int

[PreserveSig]
int Method1(int i, out int j);

[PreserveSig]
int Method2(float i);
HRESULT Method1(int i, int* j);

HRESULT Method2(float i);

有关详细信息,请参阅 .NET 互操作 中的隐式方法签名转换

与内置 COM 的不兼容和差异

IUnknown

唯一支持的接口基础是 IUnknown。 源生成的 COM 不支持具有非 InterfaceIsIUnknown 值的 InterfaceTypeAttribute 的接口。 不带 InterfaceTypeAttribute 的任何接口都会假定为派生自 IUnknown。 这不同于内置 COM,其中默认接口为 InterfaceIsDual

封送默认值和支持

源生成的 COM 具有与内置 COM 不同的默认封送行为。

  • 在内置 COM 系统中,除具有隐式 [In, Out] 属性的 blittable 元素数组外,所有类型都具有隐式 [In] 属性。 在源生成的 COM 中,所有类型(包括 blittable 元素的数组)都具有 [In] 语义。

  • 仅在数组上允许 [In][Out] 属性。 如果在其他类型上 [Out][In, Out] 行为是必需的,请使用 inout 参数修饰符。

派生接口

在内置 COM 系统中,如果你有派生自其他 COM 接口的接口,则必须使用 new 关键字在基接口上为每个基方法声明影子方法。 有关详细信息,请参阅 COM 接口继承和 .NET

[ComImport]
[Guid("3faca0d2-e7f1-4e9c-82a6-404fd6e0aab8")]
interface IBase
{
    void Method1(int i);
    void Method2(float i);
}

[ComImport]
[Guid("3faca0d2-e7f1-4e9c-82a6-404fd6e0aab8")]
interface IDerived : IBase
{
    new void Method1(int i);
    new void Method2(float f);
    void Method3(long l);
    void Method4(double d);
}

COM 接口生成器不需要任何基方法的影子方法。 若要创建从另一个继承的方法,只需将基接口指示为 C# 基接口并添加派生接口的方法。 有关详细信息,请参阅 设计文档

[GeneratedComInterface]
[Guid("3faca0d2-e7f1-4e9c-82a6-404fd6e0aab8")]
interface IBase
{
    void Method1(int i);
    void Method2(float i);
}

[GeneratedComInterface]
[Guid("3faca0d2-e7f1-4e9c-82a6-404fd6e0aab8")]
interface IDerived : IBase
{
    void Method3(long l);
    void Method4(double d);
}

请注意,具有 GeneratedComInterface 属性的接口只能继承自具有 GeneratedComInterface 属性的基接口。

跨程序集边界的派生接口

在 .NET 8 中,不支持定义具有 GeneratedComInterfaceAttribute 属性的接口,该属性派生自另一个程序集中定义的 GeneratedComInterface 属性接口。

在 .NET 9 及更高版本中,此场景须遵循以下限制:

  • 必须将基接口类型编译为与派生类型相同的目标框架。
  • 基接口类型不得隐藏其基接口的任何成员(如有)。

此外,在重新生成项目之前,对于在另一个程序集中定义的基接口链中生成的任何虚拟方法偏移的任何更改,均不会在派生接口中予以考虑。

注意

在 .NET 9 及更高版本中,跨程序集边界继承生成的 COM 接口时,系统会发出警告,以告知你使用此功能的限制和缺陷。 可以禁用此警告来确认限制,并跨程序集边界继承。

封送 API

Marshal 中的某些 API 与源生成的 COM 不兼容。 将这些方法替换为 ComWrappers 实现上的相应方法。

另请参阅