使用 CsWin32 和 ComWrappers 實現 COM 接口
基礎概念
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 接口
using winmdroot = global::Windows.Win32;
namespace Windows.Win32
{
namespace System.Com
{
[Guid("00000001-0000-0000-C000-000000000046")]
[SupportedOSPlatform("windows5.0")]
[global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Windows.CsWin32", "0.3.183+73e6125f79.RR")]
internal unsafe partial struct IClassFactory
:IVTable<IClassFactory,IClassFactory.Vtbl>,IComIID {
// 對 IUnknown 和 IClassFactory 接口指針的函數調用的包裝
[OverloadResolutionPriority(1)]
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 [Stdcall]<IClassFactory*,global::System.Guid* ,void** ,winmdroot.Foundation.HRESULT>)lpVtbl[0])((IClassFactory*)Unsafe.AsPointer(ref this), riid, ppvObject);
}
public uint AddRef()
{
return ((delegate *unmanaged [Stdcall]<IClassFactory*,uint>)lpVtbl[1])((IClassFactory*)Unsafe.AsPointer(ref this));
}
public uint Release()
{
return ((delegate *unmanaged [Stdcall]<IClassFactory*,uint>)lpVtbl[2])((IClassFactory*)Unsafe.AsPointer(ref this));
}
/// <inheritdoc cref="CreateInstance(winmdroot.System.Com.IUnknown*, global::System.Guid*, void**)"/>
[OverloadResolutionPriority(1)]
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 接口方法調用轉發到托管對象方法調用的包裝
[UnmanagedCallersOnly(CallConvs = new []{
typeof(CallConvStdcall)}
)]
private static winmdroot.Foundation.HRESULT CreateInstance(IClassFactory* pThis, [Optional] 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([Optional] winmdroot.System.Com.IUnknown* pUnkOuter, global::System.Guid* riid, void** ppvObject)
{
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);
}
[UnmanagedCallersOnly(CallConvs = new []{
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 [Stdcall]<IClassFactory*,winmdroot.Foundation.BOOL ,winmdroot.Foundation.HRESULT>)lpVtbl[4])((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 [Stdcall]<IClassFactory*,global::System.Guid* ,void** ,winmdroot.Foundation.HRESULT> QueryInterface_1;
internal delegate *unmanaged [Stdcall]<IClassFactory*,uint> AddRef_2;
internal delegate *unmanaged [Stdcall]<IClassFactory*,uint> Release_3;
internal delegate *unmanaged [Stdcall]<IClassFactory*,winmdroot.System.Com.IUnknown* ,global::System.Guid* ,void** ,winmdroot.Foundation.HRESULT> CreateInstance_4;
internal delegate *unmanaged [Stdcall]<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 {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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# 接口定義
[Guid("00000001-0000-0000-C000-000000000046"),InterfaceType(ComInterfaceType.InterfaceIsIUnknown),ComImport()]
[SupportedOSPlatform("windows5.0")]
internal interface Interface
{
[PreserveSig()]
unsafe winmdroot.Foundation.HRESULT CreateInstance([Optional] winmdroot.System.Com.IUnknown* pUnkOuter, global::System.Guid* riid, void** ppvObject);
[PreserveSig()]
winmdroot.Foundation.HRESULT LockServer(winmdroot.Foundation.BOOL fLock);
}
}
}
}
生成的 COM 接口是一個結構體,它表達了 COM 接口規范中定義的內存結構,即 COM 接口指針是指向虛函數指針表的指針。
結構體有以下幾部分:
- IUnknown 和 IClassFactory 接口方法的函數指針調用的包裝。
- 使用 C# 實現 COM 接口時,將 COM 接口方法調用轉發到托管對象方法調用的包裝。
- IClassFactory 接口的函數表對應的結構體定義。
- 使用填充函數表結構的封裝。
- COM 接口指針中的函數表指針。
- 更容易訪問的接口 GUID 屬性。
- COM 接口對等的 C# 接口定義。
使用生成的 COM 接口定義操作傳入的 COM 接口指針
使用這個結構體的方式很簡單,將 COM 接口指針直接轉換成此結構體指針,就能調用其中的實例方法了。
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);
}
}
}
使用 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 接口函數表的指針。
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[0] = GetEntry<TComInterface>();
return new()
{
Entries = (ComWrappers.ComInterfaceEntry*)Unsafe.AsPointer(ref entries[0]),
Count = entries.Length
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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));
}
接下來需要將 COM 接口的函數表與托管對象關聯的接口。參考 IManagedWrapper。
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;
}
第一次讀取 IVTable 接口中的函數表指針時,它會分配一段內存,并且將函數表中的函數指針指向 CsWin32 生成的靜態方法,參考 IVTable`2.cs 和 ComHelpers.cs。
COM 體系中通過 IUnknown 接口定義的 QueryInterface、AddRef、Release 三個方法提供類型轉換和引用計數管理功能,這部分我們需要對接到 ComWrappers 上,以將引用計數和 dotnet gc 關聯起來。
訪問 IVTable 接口生成函數表時會自動調用 ComHelpers.PopulateIUnknown, ComHelpers.PopulateIUnknown 內部會調用 ComHelpers.PopulateIUnknownImpl,我們可以通過此方法和 ComWrappers 的運行時部分關聯。參考 WinFormsComWrappers。
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[Stdcall]<IUnknown*, Guid*, void*, HRESULT>)fpQueryInterface;
vtable->AddRef_2 = (delegate* unmanaged[Stdcall]<IUnknown*, uint>)fpAddRef;
vtable->Release_3 = (delegate* unmanaged[Stdcall]<IUnknown*, uint>)fpRelease;
}
}
}
隨后實現我們自定義的 ComWrappers,讓運行時可以從我們的 C# 類型中讀取 COM 接口函數表,并將生成的 RCW 和托管對象關聯起來。參考 WinFormsComWrappers。
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();
}
}
現在我們有了全部的基礎設施,可以開始編寫 ManagedIStream 了。這里只展示類型定義,具體方法實現不再贅述。
public class ManagedIStream : IManagedWrapper<IStream>, IStream.Interface
{
//...
}
// 創建 ComWrappers
var comWrappers = new CustomComWrappers();
// 創建內存流
var stream = new MemoryStream(100);
stream.Write([1, 2, 3, 4, 5, 6, 7, 8, 9]);
// 創建托管包裝器
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 接口使用。
查看全部代碼 https://gist.github.com/cnbluefire/7438e4c062f34b89e6855ad57605219a。
浙公網安備 33010602011771號