使用 CsWin32 和 ComWrappers 实现 COM 接口

[复制链接]
发表于 2025-9-26 22:51:08 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
根本概念

CsWin32 是微软开辟的一个 C# 的源天生器,可以按需天生 C# PInvoke 代码,也支持天生体系的 COM 接口界说。
ComWrappers 是 dotnet 5 引入的新的和 COM api 互操纵的组件。
天生支持 AOT 的 COM 接口

使用 CsWin32 天生 COM 接口界说时,默认会天生使用 Builtin COM Interop 技能的代码,这种接口使用 ComImportAttribute 修饰,不支持 Native AOT。
CsWin32 提供了一个开关,在 NativeMethods.json 中设置 { "allowMarshaling": false },可以使 CsWin32 天生更为原始的 COM 接口。
CsWin32 天生的 IClassFactory 接口
  1. using winmdroot = global::Windows.Win32;
  2. namespace Windows.Win32
  3. {
  4.         namespace System.Com
  5.         {
  6.                 [Guid("00000001-0000-0000-C000-000000000046")]
  7.                 [SupportedOSPlatform("windows5.0")]
  8.                 [global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Windows.CsWin32", "0.3.183+73e6125f79.RR")]
  9.                 internal unsafe partial struct IClassFactory
  10.                         :IVTable<IClassFactory,IClassFactory.Vtbl>,IComIID                {
  11.             // 对 IUnknown 和 IClassFactory 接口指针的函数调用的包装
  12.                         [OverloadResolutionPriority(1)]
  13.                         internal unsafe winmdroot.Foundation.HRESULT QueryInterface(in global::System.Guid riid, out void* ppvObject)
  14.                         {
  15.                                 fixed (void** ppvObjectLocal = &ppvObject)
  16.                                 {
  17.                                         fixed (global::System.Guid* riidLocal = &riid)
  18.                                         {
  19.                                                 winmdroot.Foundation.HRESULT __result = this.QueryInterface(riidLocal, ppvObjectLocal);
  20.                                                 return __result;
  21.                                         }
  22.                                 }
  23.                         }
  24.                         public unsafe winmdroot.Foundation.HRESULT QueryInterface(global::System.Guid* riid, void** ppvObject)
  25.                         {
  26.                                 return ((delegate *unmanaged [Stdcall]<IClassFactory*,global::System.Guid* ,void** ,winmdroot.Foundation.HRESULT>)lpVtbl[0])((IClassFactory*)Unsafe.AsPointer(ref this), riid, ppvObject);
  27.                         }
  28.                         public uint AddRef()
  29.                         {
  30.                                 return ((delegate *unmanaged [Stdcall]<IClassFactory*,uint>)lpVtbl[1])((IClassFactory*)Unsafe.AsPointer(ref this));
  31.                         }
  32.                         public uint Release()
  33.                         {
  34.                                 return ((delegate *unmanaged [Stdcall]<IClassFactory*,uint>)lpVtbl[2])((IClassFactory*)Unsafe.AsPointer(ref this));
  35.                         }
  36.                         /// <inheritdoc cref="CreateInstance(winmdroot.System.Com.IUnknown*, global::System.Guid*, void**)"/>
  37.                         [OverloadResolutionPriority(1)]
  38.                         internal unsafe winmdroot.Foundation.HRESULT CreateInstance(winmdroot.System.Com.IUnknown* pUnkOuter, in global::System.Guid riid, out void* ppvObject)
  39.                         {
  40.                                 fixed (void** ppvObjectLocal = &ppvObject)
  41.                                 {
  42.                                         fixed (global::System.Guid* riidLocal = &riid)
  43.                                         {
  44.                                                 winmdroot.Foundation.HRESULT __result = this.CreateInstance(pUnkOuter, riidLocal, ppvObjectLocal);
  45.                                                 return __result;
  46.                                         }
  47.                                 }
  48.                         }
  49.             // 将 COM 接口方法调用转发到托管对象方法调用的包装
  50.                         [UnmanagedCallersOnly(CallConvs = new []{
  51. typeof(CallConvStdcall)}
  52. )]
  53.                         private static winmdroot.Foundation.HRESULT CreateInstance(IClassFactory* pThis, [Optional] winmdroot.System.Com.IUnknown* pUnkOuter, global::System.Guid* riid, void** ppvObject)
  54.                         {
  55.                                 try
  56.                                 {
  57.                                         winmdroot.Foundation.HRESULT __hr = ComHelpers.UnwrapCCW(pThis, out Interface __object);
  58.                                         if (__hr.Failed)
  59.                                         {
  60.                                                 return __hr;
  61.                                         }
  62.                                         return __object.CreateInstance(pUnkOuter, riid, ppvObject);
  63.                                 }
  64.                                 catch (Exception ex)
  65.                                 {
  66.                                         return (winmdroot.Foundation.HRESULT)ex.HResult;
  67.                                 }
  68.                         }
  69.                         public unsafe winmdroot.Foundation.HRESULT CreateInstance([Optional] winmdroot.System.Com.IUnknown* pUnkOuter, global::System.Guid* riid, void** ppvObject)
  70.                         {
  71.                                 return ((delegate *unmanaged [Stdcall]<IClassFactory*,winmdroot.System.Com.IUnknown* ,global::System.Guid* ,void** ,winmdroot.Foundation.HRESULT>)lpVtbl[3])((IClassFactory*)Unsafe.AsPointer(ref this), pUnkOuter, riid, ppvObject);
  72.                         }
  73.                         [UnmanagedCallersOnly(CallConvs = new []{
  74. typeof(CallConvStdcall)}
  75. )]
  76.                         private static winmdroot.Foundation.HRESULT LockServer(IClassFactory* pThis, winmdroot.Foundation.BOOL fLock)
  77.                         {
  78.                                 try
  79.                                 {
  80.                                         winmdroot.Foundation.HRESULT __hr = ComHelpers.UnwrapCCW(pThis, out Interface __object);
  81.                                         if (__hr.Failed)
  82.                                         {
  83.                                                 return __hr;
  84.                                         }
  85.                                         return __object.LockServer(fLock);
  86.                                 }
  87.                                 catch (Exception ex)
  88.                                 {
  89.                                         return (winmdroot.Foundation.HRESULT)ex.HResult;
  90.                                 }
  91.                         }
  92.                         public winmdroot.Foundation.HRESULT LockServer(winmdroot.Foundation.BOOL fLock)
  93.                         {
  94.                                 return ((delegate *unmanaged [Stdcall]<IClassFactory*,winmdroot.Foundation.BOOL ,winmdroot.Foundation.HRESULT>)lpVtbl[4])((IClassFactory*)Unsafe.AsPointer(ref this), fLock);
  95.                         }
  96.                         internal unsafe global::Windows.Win32.Foundation.HRESULT QueryInterface<T>(out T* ppv)
  97. where T : unmanaged
  98.                         {
  99.                                 Guid guid = typeof(T).GUID;
  100.                                 void* pv;
  101.                                 var hr = this.QueryInterface(&guid, &pv);
  102.                                 if (hr.Succeeded)
  103.                                 {
  104.                                         ppv = (T*)pv;
  105.                                 }
  106.                                 else
  107.                                 {
  108.                                         ppv = null;
  109.                                 }
  110.                                 return hr;
  111.                         }
  112.             // IClassFactory 接口的函数表定义
  113.                         internal struct Vtbl
  114.                         {
  115.                                 internal delegate *unmanaged [Stdcall]<IClassFactory*,global::System.Guid* ,void** ,winmdroot.Foundation.HRESULT> QueryInterface_1;
  116.                                 internal delegate *unmanaged [Stdcall]<IClassFactory*,uint> AddRef_2;
  117.                                 internal delegate *unmanaged [Stdcall]<IClassFactory*,uint> Release_3;
  118.                                 internal delegate *unmanaged [Stdcall]<IClassFactory*,winmdroot.System.Com.IUnknown* ,global::System.Guid* ,void** ,winmdroot.Foundation.HRESULT> CreateInstance_4;
  119.                                 internal delegate *unmanaged [Stdcall]<IClassFactory*,winmdroot.Foundation.BOOL ,winmdroot.Foundation.HRESULT> LockServer_5;
  120.                         }
  121.             // 使用生成的函数逻辑填充函数表
  122.                         public static void PopulateVTable(Vtbl* vtable)
  123.                         {
  124.                                 vtable->CreateInstance_4 = &CreateInstance;
  125.                                 vtable->LockServer_5 = &LockServer;
  126.                         }
  127.             // COM 接口指针中的函数表指针
  128.                         private void** lpVtbl;
  129.             // 接口的 GUID
  130.                         internal static readonly Guid IID_Guid = new Guid(0x00000001, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46);
  131.                         static ref readonly Guid IComIID.Guid                        {
  132.                                                                 [MethodImpl(MethodImplOptions.AggressiveInlining)]
  133. get
  134.                                 {
  135.                                         ReadOnlySpan<byte> data = new byte[]                                        {
  136. 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x46                                        };
  137.                                         return ref Unsafe.As<byte,Guid>(ref MemoryMarshal.GetReference(data));
  138.                                 }
  139.                         }
  140.             // COM 接口对等的 C# 接口定义
  141.                         [Guid("00000001-0000-0000-C000-000000000046"),InterfaceType(ComInterfaceType.InterfaceIsIUnknown),ComImport()]
  142.                         [SupportedOSPlatform("windows5.0")]
  143.                         internal interface Interface
  144.                         {
  145.                                 [PreserveSig()]
  146.                                 unsafe winmdroot.Foundation.HRESULT CreateInstance([Optional] winmdroot.System.Com.IUnknown* pUnkOuter, global::System.Guid* riid, void** ppvObject);
  147.                                                                 [PreserveSig()]
  148. winmdroot.Foundation.HRESULT LockServer(winmdroot.Foundation.BOOL fLock);
  149.                         }
  150.                 }
  151.         }
  152. }
复制代码
天生的 COM 接口是一个结构体,它表达了 COM 接口规范中界说的内存结构,即 COM 接口指针是指向虚函数指针表的指针。
结构体有以下几部门:

  • IUnknown 和 IClassFactory 接口方法的函数指针调用的包装。
  • 使用 C# 实现 COM 接口时,将 COM 接口方法调用转发到托管对象方法调用的包装。
  • IClassFactory 接口的函数表对应的结构体界说。
  • 使用添补函数表结构的封装。
  • COM 接口指针中的函数表指针。
  • 更容易访问的接口 GUID 属性。
  • COM 接口对等的 C# 接口界说。
使用天生的 COM 接口界说操纵传入的 COM 接口指针

使用这个结构体的方式很简朴,将 COM 接口指针直接转换成此结构体指针,就能调用此中的实例方法了。
  1. unsafe void Test(nint punk)
  2. {
  3.     if (Marshal.QueryInterface(punk, in IClassFactory.IID_Guid, out var ppv) == 0)
  4.     {
  5.         try
  6.         {
  7.             var pClassFactory = (IClassFactory*)ppv;
  8.             pClassFactory->LockServer(true);
  9.             // ...
  10.             pClassFactory->Release();
  11.         }
  12.         finally
  13.         {
  14.             Marshal.Release(ppv);
  15.         }
  16.     }
  17. }
复制代码
使用 C# 编写支持 AOT 的 COM 对象

编写 COM 对象时情况轻微有些复杂。dotnet 8 时引入了 StrategyBasedComWrappers 和ComWrappers 源天生器,可以使用 GeneratedComClassAttribute 编写 COM 对象,但干系的接口界说都须要在源码中提供,没办法使用 CsWin32 已经整理好的接口界说。
幸亏 CsWin32 提供了和 ComWrappers 互操纵的接口,颠末亿点点简朴的设置就能复用上述天生的 COM 接口界说了。
参考 ComWrappers 文档,创建托管对象包装器最告急的一部门就是通过 ComWrappers.ComputeVtables 方法向运行时提供目标 COM 接口的 GUID 和函数表界说。
上文分析了 CsWin32 天生的 COM 接口界说的内容,此中有接口 GUID,接口函数表结构,将调用转发到托管对象调用的接口函数实现,也就是说我们只须要将这些东西组合在一起,就能将 CsWin32 和 ComWrappers 团结使用了。
我们以 IStream 为例,托管实现参考 WPF 中的 ManagedIStream。
起首我们编写一些辅助代码用以天生 ComWrappers 所需的 ComInterfaceEntry。
参考 ComInterfaceTable,此中 IComIID 接口界说了接口 GUID 静态属性,IVTable 提供了静态函数表指针属性,它从 IVTable 接口中获取静态的指向 COM 接口函数表的指针。
  1. internal readonly unsafe struct ComInterfaceTable
  2. {
  3.     public ComWrappers.ComInterfaceEntry* Entries { get; init; }
  4.     public int Count { get; init; }
  5.     /// <summary>
  6.     ///  Create an interface table for the given interface.
  7.     /// </summary>
  8.     public static ComInterfaceTable Create<TComInterface>()
  9.         where TComInterface : unmanaged, IComIID, IVTable
  10.     {
  11.         Span<ComWrappers.ComInterfaceEntry> entries = AllocateEntries<TComInterface>(1);
  12.         entries[0] = GetEntry<TComInterface>();
  13.         return new()
  14.         {
  15.             Entries = (ComWrappers.ComInterfaceEntry*)Unsafe.AsPointer(ref entries[0]),
  16.             Count = entries.Length
  17.         };
  18.     }
  19.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
  20.     private static Span<ComWrappers.ComInterfaceEntry> AllocateEntries<T>(int count)
  21.     {
  22.         Span<ComWrappers.ComInterfaceEntry> entries = new(
  23.             (ComWrappers.ComInterfaceEntry*)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(T), sizeof(ComWrappers.ComInterfaceEntry) * (count + 1)),
  24.             count);
  25.         return entries;
  26.     }
  27.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
  28.     private static ComWrappers.ComInterfaceEntry GetEntry<TComInterface>() where TComInterface : unmanaged, IComIID, IVTable
  29.         => new()
  30.         {
  31.             Vtable = (nint)TComInterface.VTable,
  32.             IID = *GetIID<TComInterface>()
  33.         };
  34.     // https://github.com/dotnet/winforms/blob/f020fe71f615cb51aa61970f5aaa757bb981499e/src/System.Private.Windows.Core/src/Windows/Win32/System/Com/IID.cs#L32
  35.     private static Guid* GetIID<T>() where T : unmanaged, IComIID
  36.         => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in T.Guid));
  37. }
复制代码
接下来须要将 COM 接口的函数表与托管对象关联的接口。参考 IManagedWrapper。
  1. internal unsafe interface IManagedWrapper
  2. {
  3.     ComInterfaceTable GetComInterfaceTable();
  4. }
  5. internal unsafe interface IManagedWrapper<TComInterface> : IManagedWrapper
  6.     where TComInterface : unmanaged, IVTable, IComIID
  7. {
  8.     // Allocates a ComInterfaceTable include VTable for the given interface type.
  9.     private static ComInterfaceTable InterfaceTable { get; set; } = ComInterfaceTable.Create<TComInterface>();
  10.     ComInterfaceTable IManagedWrapper.GetComInterfaceTable() => InterfaceTable;
  11. }
复制代码
第一次读取 IVTable 接口中的函数表指针时,它会分配一段内存,而且将函数表中的函数指针指向 CsWin32 天生的静态方法,参考 IVTable`2.cs 和 ComHelpers.cs。
COM 体系中通过 IUnknown 接口界说的 QueryInterface、AddRef、Release 三个方法提供范例转换和引用计数管理功能,这部门我们须要对接到 ComWrappers 上,以将引用计数和 dotnet gc 关联起来。
访问 IVTable 接口天生函数表时会主动调用 ComHelpers.PopulateIUnknown, ComHelpers.PopulateIUnknown 内部会调用 ComHelpers.PopulateIUnknownImpl,我们可以通过此方法和 ComWrappers 的运行时部门关联。参考 WinFormsComWrappers。
  1. namespace Windows.Win32
  2. {
  3.     unsafe partial class ComHelpers
  4.     {
  5.         // Populate vtable using IUnknown method implemented by ComWrappers
  6.         static partial void PopulateIUnknownImpl<TComInterface>(IUnknown.Vtbl* vtable) where TComInterface : unmanaged
  7.         {
  8.             ComWrappers.GetIUnknownImpl(out nint fpQueryInterface, out nint fpAddRef, out nint fpRelease);
  9.             vtable->QueryInterface_1 = (delegate* unmanaged[Stdcall]<IUnknown*, Guid*, void*, HRESULT>)fpQueryInterface;
  10.             vtable->AddRef_2 = (delegate* unmanaged[Stdcall]<IUnknown*, uint>)fpAddRef;
  11.             vtable->Release_3 = (delegate* unmanaged[Stdcall]<IUnknown*, uint>)fpRelease;
  12.         }
  13.     }
  14. }
复制代码
随后实现我们自界说的 ComWrappers,让运行时可以从我们的 C# 范例中读取 COM 接口函数表,并将天生的 RCW 和托管对象关联起来。参考 WinFormsComWrappers。
  1. public class CustomComWrappers : ComWrappers
  2. {
  3.     protected override unsafe ComWrappers.ComInterfaceEntry* ComputeVtables(object obj, CreateComInterfaceFlags flags, out int count)
  4.     {
  5.         if (obj is not IManagedWrapper vtables)
  6.         {
  7.             Debug.Fail("object does not implement IManagedWrapper");
  8.             count = 0;
  9.             return null;
  10.         }
  11.         // Bind the vtables for the interfaces implemented by the object.
  12.         ComInterfaceTable table = vtables.GetComInterfaceTable();
  13.         count = table.Count;
  14.         return table.Entries;
  15.     }
  16.     protected override object? CreateObject(nint externalComObject, CreateObjectFlags flags)
  17.     {
  18.         throw new NotImplementedException();
  19.     }
  20.     protected override void ReleaseObjects(IEnumerable objects)
  21.     {
  22.         throw new NotImplementedException();
  23.     }
  24. }
复制代码
如今我们有了全部的根本办法,可以开始编写 ManagedIStream 了。这里只展示范例界说,具体方法实现不再赘述。
  1. public class ManagedIStream : IManagedWrapper<IStream>, IStream.Interface
  2. {
  3.     //...
  4. }
  5. // 创建 ComWrappers
  6. var comWrappers = new CustomComWrappers();
  7. // 创建内存流
  8. var stream = new MemoryStream(100);
  9. stream.Write([1, 2, 3, 4, 5, 6, 7, 8, 9]);
  10. // 创建托管包装器
  11. var wrapper = new ManagedIStream(stream);
  12. // 使用 ComWrappers 创建托管包装器的 COM 指针,注意此时返回的是 IUnknown 而非 IStream
  13. var punk = (IUnknown*)comWrappers.GetOrCreateComInterfaceForObject(
  14.     wrapper,
  15.     CreateComInterfaceFlags.None);
  16. punk->QueryInterface<IStream>(out var pStream).ThrowOnFailure();
  17. // 此时 pStream 可以传递给其他 COM 接口使用。
复制代码
查察全部代码 https://gist.github.com/cnbluefire/7438e4c062f34b89e6855ad57605219a。
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表