基础概念
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 接口
cs
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 接口指针直接转换成此结构体指针,就能调用其中的实例方法了。
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);
}
}
}
使用 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 接口函数表的指针。
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[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。
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;
}
第一次读取 IVTable 接口中的函数表指针时,它会分配一段内存,并且将函数表中的函数指针指向 CsWin32 生成的静态方法,参考 IVTable`2.cs 和 ComHelpers.cs。
COM 体系中通过 IUnknown 接口定义的 QueryInterface、AddRef、Release 三个方法提供类型转换和引用计数管理功能,这部分我们需要对接到 ComWrappers 上,以将引用计数和 dotnet gc 关联起来。
访问 IVTable
接口生成函数表时会自动调用 ComHelpers.PopulateIUnknown
, ComHelpers.PopulateIUnknown
内部会调用 ComHelpers.PopulateIUnknownImpl
,我们可以通过此方法和 ComWrappers 的运行时部分关联。参考 WinFormsComWrappers。
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[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。
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();
}
}
现在我们有了全部的基础设施,可以开始编写 ManagedIStream 了。这里只展示类型定义,具体方法实现不再赘述。
cs
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。