For Windows App SDK apps, to provide Windows widgets, it's required to write some COM interop code according to the official documentation:
First, Implement a class factory that will instantiate WidgetProvider on request:
// FactoryHelper.cs
using Microsoft.Windows.Widgets.Providers;
using System.Runtime.InteropServices;
using WinRT;
namespace COM
{
static class Guids
{
public const string IClassFactory = "00000001-0000-0000-C000-000000000046";
public const string IUnknown = "00000000-0000-0000-C000-000000000046";
}
///
/// IClassFactory declaration
///
[ComImport, ComVisible(false), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid(COM.Guids.IClassFactory)]
internal interface IClassFactory
{
[PreserveSig]
int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject);
[PreserveSig]
int LockServer(bool fLock);
}
[ComVisible(true)]
class WidgetProviderFactory<T> : IClassFactory
where T : IWidgetProvider, new()
{
public int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject)
{
ppvObject = IntPtr.Zero;
if (pUnkOuter != IntPtr.Zero)
{
Marshal.ThrowExceptionForHR(CLASS_E_NOAGGREGATION);
}
if (riid == typeof(T).GUID || riid == Guid.Parse(COM.Guids.IUnknown))
{
// Create the instance of the .NET object
ppvObject = MarshalInspectable<IWidgetProvider>.FromManaged(new T());
}
else
{
// The object that ppvObject points to does not support the
// interface identified by riid.
Marshal.ThrowExceptionForHR(E_NOINTERFACE);
}
return 0;
}
int IClassFactory.LockServer(bool fLock)
{
return 0;
}
private const int CLASS_E_NOAGGREGATION = -2147221232;
private const int E_NOINTERFACE = -2147467262;
}
}
Then, Register the widget provider class object with OLE:
// Program.cs
using System.Runtime.InteropServices;
using ComTypes = System.Runtime.InteropServices.ComTypes;
using Microsoft.Windows.Widgets;
using ExampleWidgetProvider;
using COM;
using System;
[DllImport("kernel32.dll")]
static extern IntPtr GetConsoleWindow();
[DllImport("ole32.dll")]
static extern int CoRegisterClassObject(
[MarshalAs(UnmanagedType.LPStruct)] Guid rclsid,
[MarshalAs(UnmanagedType.IUnknown)] object pUnk,
uint dwClsContext,
uint flags,
out uint lpdwRegister);
[DllImport("ole32.dll")] static extern int CoRevokeClassObject(uint dwRegister);
Console.WriteLine("Registering Widget Provider");
uint cookie;
Guid CLSID_Factory = Guid.Parse("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX");
CoRegisterClassObject(CLSID_Factory, new WidgetProviderFactory<WidgetProvider>(), 0x4, 0x1, out cookie);
Console.WriteLine("Registered successfully. Press ENTER to exit.");
Console.ReadLine();
if (GetConsoleWindow() != IntPtr.Zero)
{
Console.WriteLine("Registered successfully. Press ENTER to exit.");
Console.ReadLine();
}
else
{
// Wait until the manager has disposed of the last widget provider.
using (var emptyWidgetListEvent = WidgetProvider.GetEmptyWidgetListEvent())
{
emptyWidgetListEvent.WaitOne();
}
CoRevokeClassObject(cookie);
}
I want to use AOT in my app. But once I enabled AOT in my project (<PublishAot>true</PublishAot>
), the CoRegisterClassObject
call in the second block of code will show a warning:
P/invoke method 'CoRegisterClassObject(Guid, Object, Ulnt32, Ulnt32, out UInt32)' declares a parameter with COM marshalling. Correctness of COM interop cannot be guaranteed after trimming. Interfaces and interface members might be removed.
and throw an exception when excuting:
System.NotSupportedException: “Built-in COM has been disabled via a feature switch. See https://aka.ms/dotnet-illink/com for more information.”
Accoding to this link (Known trimming incompatibilities), this kind of COM interop code is incompatible with .NET trimming and AOT, and the alternative approach is to use COM Wrappers or its source generation: Source generation for ComWrappers.
The source generation approach seems to be simpler. But for a .NET developer who knows nothing about COM, it's still hard to read the documentation and find out how to rewrite the code.
Thanks for anyone who can try to solve this!
Here's a code that replaces the program's main code and COM class factory and is compatible with .NET 8 AOT (so with runtime marshalling disabled) and the newer ComWrappers source generation:
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
using Microsoft.Windows.Widgets.Providers;
using WinRT;
// AOT: declare we disable runtime marshalling to enable early compilation errors
[assembly: DisableRuntimeMarshalling]
namespace ConsoleApp;
// AOT, COM Source wrapper generator: don't use implicit root class or face unexpected ERROR_BAD_FORMAT exceptions
internal partial class Program
{
// AOT: use LibraryImport
[LibraryImport("kernel32")]
public static partial IntPtr GetConsoleWindow();
// AOT: don't use 'object' for IUnknown parameters
[LibraryImport("ole32")]
public static partial int CoRegisterClassObject(in Guid rclsid, IntPtr pUnk, uint dwClsContext, uint flags, out uint lpdwRegister);
[LibraryImport("ole32")]
public static partial int CoRevokeClassObject(uint dwRegister);
static void Main()
{
Console.WriteLine("Registering Widget Provider");
// ask for the widget provider's IUnknown pointer
var provider = new WidgetProviderFactory<WidgetProvider>();
var comWrappers = new StrategyBasedComWrappers();
var unk = comWrappers.GetOrCreateComInterfaceForObject(provider, CreateComInterfaceFlags.None);
Guid CLSID_Factory = Guid.Parse("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX");
CoRegisterClassObject(CLSID_Factory, unk, 0x4, 0x1, out var cookie);
Console.WriteLine("Registered successfully. Press ENTER to exit.");
Console.ReadLine();
if (GetConsoleWindow() != IntPtr.Zero)
{
Console.WriteLine("Registered successfully. Press ENTER to exit.");
Console.ReadLine();
}
else
{
// Wait until the manager has disposed of the last widget provider.
using (var emptyWidgetListEvent = WidgetProvider.GetEmptyWidgetListEvent())
{
emptyWidgetListEvent.WaitOne();
}
_ = CoRevokeClassObject(cookie);
}
}
// AOT: use GeneratedComInterface
[GeneratedComInterface, Guid(Guids.IClassFactory)]
public partial interface IClassFactory
{
[PreserveSig]
int CreateInstance(IntPtr pUnkOuter, in Guid riid, out IntPtr ppvObject);
// AOT: mark .NET's bool as UnmanagedType.Bool (Win32 BOOL)
[PreserveSig]
int LockServer([MarshalAs(UnmanagedType.Bool)] bool fLock);
}
// AOT: use GeneratedComClass
[GeneratedComClass]
public partial class WidgetProviderFactory<T> : IClassFactory where T : IWidgetProvider, new()
{
public int CreateInstance(IntPtr pUnkOuter, in Guid riid, out IntPtr ppvObject)
{
ppvObject = IntPtr.Zero;
if (pUnkOuter != IntPtr.Zero)
{
Marshal.ThrowExceptionForHR(CLASS_E_NOAGGREGATION);
}
if (riid == typeof(T).GUID || riid == Guid.Parse(Guids.IUnknown))
{
// Create the instance of the .NET object
ppvObject = MarshalInspectable<IWidgetProvider>.FromManaged(new T());
}
else
{
// The object that ppvObject points to does not support the interface identified by riid.
Marshal.ThrowExceptionForHR(E_NOINTERFACE);
}
return 0;
}
int IClassFactory.LockServer(bool fLock) => 0;
private const int CLASS_E_NOAGGREGATION = -2147221232;
private const int E_NOINTERFACE = -2147467262;
}
static class Guids
{
public const string IClassFactory = "00000001-0000-0000-C000-000000000046";
public const string IUnknown = "00000000-0000-0000-C000-000000000046";
}
}
And my .csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows10.0.26100</TargetFramework>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<PublishAot>true</PublishAot>
<IsAotCompatible>true</IsAotCompatible>
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.6.240923002" />
</ItemGroup>
</Project>
Few remarks (also highlighted in code as comments):
ERROR_BAD_FORMAT
(0x8007000B
) errors at runtime on first COM call like CoRegisterClassObject (although it seems to compile ok...), declare a real class like the usual "Program" for example in a real namespace.DllImport
(and methods declaration must be partial
instead of extern
)[MarshalAs(UnmanagedType.IUnknown)] object pUnk
parameters but use raw IntPtr
instead (for IUnknown
).