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.dll")]
static nint GetPointerToComInterface();

[DllImport("MyComObjectProvider.dll")]
static void GivePointerToComInterface(nint comObject);

// 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.dll")]
static nint GetPointerToComInterface();

[LibraryImport("MyComObjectProvider.dll")]
static void GivePointerToComInterface(nint comObject);

// 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.GetOrCreateObjectForComInterface(ptr);
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);
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 属性的基接口。

封送 API

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

另请参阅