使用 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<IClassFactory,IClassFactory.Vtbl>,IComIID {
// 对 IUnknown 和 IClassFactory 接口指针的函数调用的包装
internal unsafe winmdroot.Foundation.HRESULT QueryInterface(in global::System.Guid riid, out void* ppvObject)
{
fixed (void** ppvObjectLocal = &ppvObject)
{
fixed (global::System.Guid* riidLocal = &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 <IClassFactory*,global::System.Guid* ,void** ,winmdroot.Foundation.HRESULT>)lpVtbl)((IClassFactory*)Unsafe.AsPointer(ref this), riid, ppvObject);
}
public uint AddRef()
{
return ((delegate *unmanaged <IClassFactory*,uint>)lpVtbl)((IClassFactory*)Unsafe.AsPointer(ref this));
}
public uint Release()
{
return ((delegate *unmanaged <IClassFactory*,uint>)lpVtbl)((IClassFactory*)Unsafe.AsPointer(ref this));
}
/// <inheritdoc cref="CreateInstance(winmdroot.System.Com.IUnknown*, global::System.Guid*, void**)"/>
internal unsafe winmdroot.Foundation.HRESULT CreateInstance(winmdroot.System.Com.IUnknown* pUnkOuter, in global::System.Guid riid, out void* ppvObject)
{
fixed (void** ppvObjectLocal = &ppvObject)
{
fixed (global::System.Guid* riidLocal = &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 <IClassFactory*,winmdroot.System.Com.IUnknown* ,global::System.Guid* ,void** ,winmdroot.Foundation.HRESULT>)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 <IClassFactory*,winmdroot.Foundation.BOOL ,winmdroot.Foundation.HRESULT>)lpVtbl)((IClassFactory*)Unsafe.AsPointer(ref this), fLock);
}
internal unsafe global::Windows.Win32.Foundation.HRESULT QueryInterface<T>(out T* ppv)
where T : unmanaged
{
Guid guid = typeof(T).GUID;
void* pv;
var hr = this.QueryInterface(&guid, &pv);
if (hr.Succeeded)
{
ppv = (T*)pv;
}
else
{
ppv = null;
}
return hr;
}
// IClassFactory 接口的函数表定义
internal struct Vtbl
{
internal delegate *unmanaged <IClassFactory*,global::System.Guid* ,void** ,winmdroot.Foundation.HRESULT> QueryInterface_1;
internal delegate *unmanaged <IClassFactory*,uint> AddRef_2;
internal delegate *unmanaged <IClassFactory*,uint> Release_3;
internal delegate *unmanaged <IClassFactory*,winmdroot.System.Com.IUnknown* ,global::System.Guid* ,void** ,winmdroot.Foundation.HRESULT> CreateInstance_4;
internal delegate *unmanaged <IClassFactory*,winmdroot.Foundation.BOOL ,winmdroot.Foundation.HRESULT> LockServer_5;
}
// 使用生成的函数逻辑填充函数表
public static void PopulateVTable(Vtbl* vtable)
{
vtable->CreateInstance_4 = &CreateInstance;
vtable->LockServer_5 = &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<byte> data = new byte[] {
0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x46 };
return ref Unsafe.As<byte,Guid>(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->LockServer(true);
// ...
pClassFactory->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; }
/// <summary>
///Create an interface table for the given interface.
/// </summary>
public static ComInterfaceTable Create<TComInterface>()
where TComInterface : unmanaged, IComIID, IVTable
{
Span<ComWrappers.ComInterfaceEntry> entries = AllocateEntries<TComInterface>(1);
entries = GetEntry<TComInterface>();
return new()
{
Entries = (ComWrappers.ComInterfaceEntry*)Unsafe.AsPointer(ref entries),
Count = entries.Length
};
}
private static Span<ComWrappers.ComInterfaceEntry> AllocateEntries<T>(int count)
{
Span<ComWrappers.ComInterfaceEntry> entries = new(
(ComWrappers.ComInterfaceEntry*)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(T), sizeof(ComWrappers.ComInterfaceEntry) * (count + 1)),
count);
return entries;
}
private static ComWrappers.ComInterfaceEntry GetEntry<TComInterface>() where TComInterface : unmanaged, IComIID, IVTable
=> new()
{
Vtable = (nint)TComInterface.VTable,
IID = *GetIID<TComInterface>()
};
// https://github.com/dotnet/winforms/blob/f020fe71f615cb51aa61970f5aaa757bb981499e/src/System.Private.Windows.Core/src/Windows/Win32/System/Com/IID.cs#L32
private static Guid* GetIID<T>() where T : unmanaged, IComIID
=> (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<TComInterface> : IManagedWrapper
where TComInterface : unmanaged, IVTable, IComIID
{
// Allocates a ComInterfaceTable include VTable for the given interface type.
private static ComInterfaceTable InterfaceTable { get; set; } = ComInterfaceTable.Create<TComInterface>();
ComInterfaceTable IManagedWrapper.GetComInterfaceTable() => 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<TComInterface>(IUnknown.Vtbl* vtable) where TComInterface : unmanaged
{
ComWrappers.GetIUnknownImpl(out nint fpQueryInterface, out nint fpAddRef, out nint fpRelease);
vtable->QueryInterface_1 = (delegate* unmanaged<IUnknown*, Guid*, void*, HRESULT>)fpQueryInterface;
vtable->AddRef_2 = (delegate* unmanaged<IUnknown*, uint>)fpAddRef;
vtable->Release_3 = (delegate* unmanaged<IUnknown*, uint>)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<IStream>, 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->QueryInterface<IStream>(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]