马学刚 發表於 2025-9-26 15:58:00

使用 CsWin32 和 ComWrappers 实现 COM 接口

<h4 id="基础概念">基础概念</h4>
<p>CsWin32 是微软开发的一个 C# 的源生成器,可以按需生成 C# PInvoke 代码,也支持生成系统的 COM 接口定义。<br>
ComWrappers 是 dotnet 5 引入的新的和 COM api 互操作的组件。</p>
<h4 id="生成支持-aot-的-com-接口">生成支持 AOT 的 COM 接口</h4>
<p>使用 CsWin32 生成 COM 接口定义时,默认会生成使用 Builtin COM Interop 技术的代码,这种接口使用 ComImportAttribute 修饰,不支持 Native AOT。</p>
<p>CsWin32 提供了一个开关,在 NativeMethods.json 中设置 <code>{ "allowMarshaling": false }</code>,可以使 CsWin32 生成更为原始的 COM 接口。</p>
<details>
<summary>CsWin32 生成的 IClassFactory 接口</summary>
<pre><code class="language-cs">using winmdroot = global::Windows.Win32;
namespace Windows.Win32
{
        namespace System.Com
        {
               
               
               
                internal unsafe partial struct IClassFactory
                        :IVTable&lt;IClassFactory,IClassFactory.Vtbl&gt;,IComIID                {

            // 对 IUnknown 和 IClassFactory 接口指针的函数调用的包装

                       
                        internal unsafe winmdroot.Foundation.HRESULT QueryInterface(in global::System.Guid riid, out void* ppvObject)
                        {
                                fixed (void** ppvObjectLocal = &amp;ppvObject)
                                {
                                        fixed (global::System.Guid* riidLocal = &amp;riid)
                                        {
                                                winmdroot.Foundation.HRESULT __result = this.QueryInterface(riidLocal, ppvObjectLocal);
                                                return __result;
                                        }
                                }
                        }

                        public unsafe winmdroot.Foundation.HRESULT QueryInterface(global::System.Guid* riid, void** ppvObject)
                        {
                                return ((delegate *unmanaged &lt;IClassFactory*,global::System.Guid* ,void** ,winmdroot.Foundation.HRESULT&gt;)lpVtbl)((IClassFactory*)Unsafe.AsPointer(ref this), riid, ppvObject);
                        }

                        public uint AddRef()
                        {
                                return ((delegate *unmanaged &lt;IClassFactory*,uint&gt;)lpVtbl)((IClassFactory*)Unsafe.AsPointer(ref this));
                        }

                        public uint Release()
                        {
                                return ((delegate *unmanaged &lt;IClassFactory*,uint&gt;)lpVtbl)((IClassFactory*)Unsafe.AsPointer(ref this));
                        }

                        /// &lt;inheritdoc cref="CreateInstance(winmdroot.System.Com.IUnknown*, global::System.Guid*, void**)"/&gt;
                       
                        internal unsafe winmdroot.Foundation.HRESULT CreateInstance(winmdroot.System.Com.IUnknown* pUnkOuter, in global::System.Guid riid, out void* ppvObject)
                        {
                                fixed (void** ppvObjectLocal = &amp;ppvObject)
                                {
                                        fixed (global::System.Guid* riidLocal = &amp;riid)
                                        {
                                                winmdroot.Foundation.HRESULT __result = this.CreateInstance(pUnkOuter, riidLocal, ppvObjectLocal);
                                                return __result;
                                        }
                                }
                        }

            // 将 COM 接口方法调用转发到托管对象方法调用的包装

                        {
typeof(CallConvStdcall)}
)]
                        private static winmdroot.Foundation.HRESULT CreateInstance(IClassFactory* pThis, winmdroot.System.Com.IUnknown* pUnkOuter, global::System.Guid* riid, void** ppvObject)
                        {
                                try
                                {
                                        winmdroot.Foundation.HRESULT __hr = ComHelpers.UnwrapCCW(pThis, out Interface __object);
                                        if (__hr.Failed)
                                        {
                                                return __hr;
                                        }
                                        return __object.CreateInstance(pUnkOuter, riid, ppvObject);
                                }
                                catch (Exception ex)
                                {
                                        return (winmdroot.Foundation.HRESULT)ex.HResult;
                                }
                        }

                        public unsafe winmdroot.Foundation.HRESULT CreateInstance( winmdroot.System.Com.IUnknown* pUnkOuter, global::System.Guid* riid, void** ppvObject)
                        {
                                return ((delegate *unmanaged &lt;IClassFactory*,winmdroot.System.Com.IUnknown* ,global::System.Guid* ,void** ,winmdroot.Foundation.HRESULT&gt;)lpVtbl)((IClassFactory*)Unsafe.AsPointer(ref this), pUnkOuter, riid, ppvObject);
                        }

                        {
typeof(CallConvStdcall)}
)]
                        private static winmdroot.Foundation.HRESULT LockServer(IClassFactory* pThis, winmdroot.Foundation.BOOL fLock)
                        {
                                try
                                {
                                        winmdroot.Foundation.HRESULT __hr = ComHelpers.UnwrapCCW(pThis, out Interface __object);
                                        if (__hr.Failed)
                                        {
                                                return __hr;
                                        }
                                        return __object.LockServer(fLock);
                                }
                                catch (Exception ex)
                                {
                                        return (winmdroot.Foundation.HRESULT)ex.HResult;
                                }
                        }

                        public winmdroot.Foundation.HRESULT LockServer(winmdroot.Foundation.BOOL fLock)
                        {
                                return ((delegate *unmanaged &lt;IClassFactory*,winmdroot.Foundation.BOOL ,winmdroot.Foundation.HRESULT&gt;)lpVtbl)((IClassFactory*)Unsafe.AsPointer(ref this), fLock);
                        }

                        internal unsafe global::Windows.Win32.Foundation.HRESULT QueryInterface&lt;T&gt;(out T* ppv)
where T : unmanaged

                        {
                                Guid guid = typeof(T).GUID;
                                void* pv;
                                var hr = this.QueryInterface(&amp;guid, &amp;pv);
                                if (hr.Succeeded)

                                {
                                        ppv = (T*)pv;
                                }
                                else

                                {
                                        ppv = null;
                                }

                                return hr;
                        }

            // IClassFactory 接口的函数表定义

                        internal struct Vtbl
                        {
                                internal delegate *unmanaged &lt;IClassFactory*,global::System.Guid* ,void** ,winmdroot.Foundation.HRESULT&gt; QueryInterface_1;

                                internal delegate *unmanaged &lt;IClassFactory*,uint&gt; AddRef_2;

                                internal delegate *unmanaged &lt;IClassFactory*,uint&gt; Release_3;

                                internal delegate *unmanaged &lt;IClassFactory*,winmdroot.System.Com.IUnknown* ,global::System.Guid* ,void** ,winmdroot.Foundation.HRESULT&gt; CreateInstance_4;

                                internal delegate *unmanaged &lt;IClassFactory*,winmdroot.Foundation.BOOL ,winmdroot.Foundation.HRESULT&gt; LockServer_5;
                        }

            // 使用生成的函数逻辑填充函数表

                        public static void PopulateVTable(Vtbl* vtable)
                        {
                                vtable-&gt;CreateInstance_4 = &amp;CreateInstance;
                                vtable-&gt;LockServer_5 = &amp;LockServer;
                        }

            // COM 接口指针中的函数表指针

                        private void** lpVtbl;


            // 接口的 GUID

                        internal static readonly Guid IID_Guid = new Guid(0x00000001, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46);

                        static ref readonly Guid IComIID.Guid                        {
                                                               
get
                                {
                                        ReadOnlySpan&lt;byte&gt; data = new byte[]                                        {
0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x46                                        };
                                        return ref Unsafe.As&lt;byte,Guid&gt;(ref MemoryMarshal.GetReference(data));
                                }
                        }

            // COM 接口对等的 C# 接口定义

                       
                       
                        internal interface Interface
                        {
                               
                                unsafe winmdroot.Foundation.HRESULT CreateInstance( winmdroot.System.Com.IUnknown* pUnkOuter, global::System.Guid* riid, void** ppvObject);

                                                               
winmdroot.Foundation.HRESULT LockServer(winmdroot.Foundation.BOOL fLock);
                        }
                }
        }
}


</code></pre>
</details><br>
<p>生成的 COM 接口是一个结构体,它表达了 COM 接口规范中定义的内存结构,即 COM 接口指针是指向虚函数指针表的指针。</p>
<p>结构体有以下几部分:</p>
<ul>
<li>IUnknown 和 IClassFactory 接口方法的函数指针调用的包装。</li>
<li>使用 C# 实现 COM 接口时,将 COM 接口方法调用转发到托管对象方法调用的包装。</li>
<li>IClassFactory 接口的函数表对应的结构体定义。</li>
<li>使用填充函数表结构的封装。</li>
<li>COM 接口指针中的函数表指针。</li>
<li>更容易访问的接口 GUID 属性。</li>
<li>COM 接口对等的 C# 接口定义。</li>
</ul>
<h4 id="使用生成的-com-接口定义操作传入的-com-接口指针">使用生成的 COM 接口定义操作传入的 COM 接口指针</h4>
<p>使用这个结构体的方式很简单,将 COM 接口指针直接转换成此结构体指针,就能调用其中的实例方法了。</p>
<pre><code class="language-cs">unsafe void Test(nint punk)
{
    if (Marshal.QueryInterface(punk, in IClassFactory.IID_Guid, out var ppv) == 0)
    {
      try
      {
            var pClassFactory = (IClassFactory*)ppv;
            pClassFactory-&gt;LockServer(true);
            // ...
            pClassFactory-&gt;Release();
      }
      finally
      {
            Marshal.Release(ppv);
      }
    }
}
</code></pre>
<h4 id="使用-c-编写支持-aot-的-com-对象">使用 C# 编写支持 AOT 的 COM 对象</h4>
<p>编写 COM 对象时情况稍微有些复杂。dotnet 8 时引入了 <code>StrategyBasedComWrappers</code> 和ComWrappers 源生成器,可以使用 GeneratedComClassAttribute 编写 COM 对象,但相关的接口定义都需要在源码中提供,没办法利用 CsWin32 已经整理好的接口定义。</p>
<p>好在 CsWin32 提供了和 ComWrappers 互操作的接口,经过<strong>亿</strong>点点<strong>简单</strong>的配置就能复用上述生成的 COM 接口定义了。</p>
<p>参考 ComWrappers 文档,创建托管对象包装器最重要的一部分就是通过 <code>ComWrappers.ComputeVtables</code> 方法向运行时提供目标 COM 接口的 GUID 和函数表定义。</p>
<p>上文分析了 CsWin32 生成的 COM 接口定义的内容,其中有接口 GUID,接口函数表结构,将调用转发到托管对象调用的接口函数实现,也就是说我们只需要将这些东西组合在一起,就能将 CsWin32 和 ComWrappers 联合使用了。</p>
<p>我们以 <code>IStream</code> 为例,托管实现参考 WPF 中的 <code>ManagedIStream</code>。</p>
<p>首先我们编写一些辅助代码用以生成 ComWrappers 所需的 ComInterfaceEntry。<br>
参考 ComInterfaceTable,其中 IComIID 接口定义了接口 GUID 静态属性,IVTable 提供了静态函数表指针属性,它从 IVTable 接口中获取静态的指向 COM 接口函数表的指针。</p>
<pre><code class="language-cs">internal readonly unsafe struct ComInterfaceTable
{
    public ComWrappers.ComInterfaceEntry* Entries { get; init; }
    public int Count { get; init; }

    /// &lt;summary&gt;
    ///Create an interface table for the given interface.
    /// &lt;/summary&gt;
    public static ComInterfaceTable Create&lt;TComInterface&gt;()
      where TComInterface : unmanaged, IComIID, IVTable
    {
      Span&lt;ComWrappers.ComInterfaceEntry&gt; entries = AllocateEntries&lt;TComInterface&gt;(1);
      entries = GetEntry&lt;TComInterface&gt;();

      return new()
      {
            Entries = (ComWrappers.ComInterfaceEntry*)Unsafe.AsPointer(ref entries),
            Count = entries.Length
      };
    }

   
    private static Span&lt;ComWrappers.ComInterfaceEntry&gt; AllocateEntries&lt;T&gt;(int count)
    {
      Span&lt;ComWrappers.ComInterfaceEntry&gt; entries = new(
            (ComWrappers.ComInterfaceEntry*)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(T), sizeof(ComWrappers.ComInterfaceEntry) * (count + 1)),
            count);
      return entries;
    }

   
    private static ComWrappers.ComInterfaceEntry GetEntry&lt;TComInterface&gt;() where TComInterface : unmanaged, IComIID, IVTable
      =&gt; new()
      {
            Vtable = (nint)TComInterface.VTable,
            IID = *GetIID&lt;TComInterface&gt;()
      };

    // https://github.com/dotnet/winforms/blob/f020fe71f615cb51aa61970f5aaa757bb981499e/src/System.Private.Windows.Core/src/Windows/Win32/System/Com/IID.cs#L32
    private static Guid* GetIID&lt;T&gt;() where T : unmanaged, IComIID
      =&gt; (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in T.Guid));
}
</code></pre>
<p>接下来需要将 COM 接口的函数表与托管对象关联的接口。参考 IManagedWrapper。</p>
<pre><code class="language-cs">internal unsafe interface IManagedWrapper
{
    ComInterfaceTable GetComInterfaceTable();
}

internal unsafe interface IManagedWrapper&lt;TComInterface&gt; : IManagedWrapper
    where TComInterface : unmanaged, IVTable, IComIID
{
    // Allocates a ComInterfaceTable include VTable for the given interface type.
    private static ComInterfaceTable InterfaceTable { get; set; } = ComInterfaceTable.Create&lt;TComInterface&gt;();

    ComInterfaceTable IManagedWrapper.GetComInterfaceTable() =&gt; InterfaceTable;
}
</code></pre>
<p>第一次读取 IVTable 接口中的函数表指针时,它会分配一段内存,并且将函数表中的函数指针指向 CsWin32 生成的静态方法,参考 IVTable`2.cs 和 ComHelpers.cs。</p>
<p>COM 体系中通过 IUnknown 接口定义的 QueryInterface、AddRef、Release 三个方法提供类型转换和引用计数管理功能,这部分我们需要对接到 ComWrappers 上,以将引用计数和 dotnet gc 关联起来。</p>
<p>访问 <code>IVTable</code> 接口生成函数表时会自动调用 <code>ComHelpers.PopulateIUnknown</code>, <code>ComHelpers.PopulateIUnknown</code> 内部会调用 <code>ComHelpers.PopulateIUnknownImpl</code>,我们可以通过此方法和 ComWrappers 的运行时部分关联。参考 WinFormsComWrappers。</p>
<pre><code class="language-cs">namespace Windows.Win32
{
    unsafe partial class ComHelpers
    {
      // Populate vtable using IUnknown method implemented by ComWrappers
      static partial void PopulateIUnknownImpl&lt;TComInterface&gt;(IUnknown.Vtbl* vtable) where TComInterface : unmanaged
      {
            ComWrappers.GetIUnknownImpl(out nint fpQueryInterface, out nint fpAddRef, out nint fpRelease);

            vtable-&gt;QueryInterface_1 = (delegate* unmanaged&lt;IUnknown*, Guid*, void*, HRESULT&gt;)fpQueryInterface;
            vtable-&gt;AddRef_2 = (delegate* unmanaged&lt;IUnknown*, uint&gt;)fpAddRef;
            vtable-&gt;Release_3 = (delegate* unmanaged&lt;IUnknown*, uint&gt;)fpRelease;
      }
    }
}
</code></pre>
<p>随后实现我们自定义的 ComWrappers,让运行时可以从我们的 C# 类型中读取 COM 接口函数表,并将生成的 RCW 和托管对象关联起来。参考 WinFormsComWrappers。</p>
<pre><code class="language-cs">public class CustomComWrappers : ComWrappers
{
    protected override unsafe ComWrappers.ComInterfaceEntry* ComputeVtables(object obj, CreateComInterfaceFlags flags, out int count)
    {
      if (obj is not IManagedWrapper vtables)
      {
            Debug.Fail("object does not implement IManagedWrapper");
            count = 0;
            return null;
      }

      // Bind the vtables for the interfaces implemented by the object.
      ComInterfaceTable table = vtables.GetComInterfaceTable();
      count = table.Count;

      return table.Entries;
    }

    protected override object? CreateObject(nint externalComObject, CreateObjectFlags flags)
    {
      throw new NotImplementedException();
    }

    protected override void ReleaseObjects(IEnumerable objects)
    {
      throw new NotImplementedException();
    }
}
</code></pre>
<p>现在我们有了全部的基础设施,可以开始编写 ManagedIStream 了。这里只展示类型定义,具体方法实现不再赘述。</p>
<pre><code class="language-cs">public class ManagedIStream : IManagedWrapper&lt;IStream&gt;, IStream.Interface
{
    //...
}

// 创建 ComWrappers
var comWrappers = new CustomComWrappers();

// 创建内存流
var stream = new MemoryStream(100);
stream.Write();

// 创建托管包装器
var wrapper = new ManagedIStream(stream);

// 使用 ComWrappers 创建托管包装器的 COM 指针,注意此时返回的是 IUnknown 而非 IStream
var punk = (IUnknown*)comWrappers.GetOrCreateComInterfaceForObject(
    wrapper,
    CreateComInterfaceFlags.None);

punk-&gt;QueryInterface&lt;IStream&gt;(out var pStream).ThrowOnFailure();

// 此时 pStream 可以传递给其他 COM 接口使用。
</code></pre>
<p>查看全部代码 https://gist.github.com/cnbluefire/7438e4c062f34b89e6855ad57605219a。</p><br><br>
来源:https://www.cnblogs.com/blue-fire/p/19113676
頁: [1]
查看完整版本: 使用 CsWin32 和 ComWrappers 实现 COM 接口