Description
Work has begun to provide support for WinUI 3.0. This support is expected to manifest in a way similar to the CppWinRT tool by way of a new source generation tool (e.g. CsWinRT). In order to support this new tool, APIs for integrating and coordinating with the runtime object lifetime are necessary.
Rationale and Usage
The below API surface provides a way for a third party tool to generate what are colloquially known as Runtime Callable Wrappers (RCW) and COM Callable Wrappers (CCW) in a way that allows safe interaction with managed object lifetime and identity.
A specific example of the need for lifetime coordination is in WinRT scenarios involving UI (e.g. WinUI 3.0) via the IReferenceTrackerManager
interface.
Goals:
- Enable source generation of interop code in WinRT/WinUI scenarios.
- An API that generally aligns with how existing 3rd party source generators work (e.g. SharpGenTools).
- Limit exposing APIs that manage lifetime in a micro way (e.g. avoid GC hooks at dangerous times).
- Semantics in .NET Framework/.NET Core for WinRT scenarios should be able to be replicated.
- Provide a mechanism for 3rd parties to be able to support Reference Tracker scenarios.
- Ensure AOT scenarios are considered.
Non-Goals:
- Replace the existing built-in RCW/CCW infrastructure.
- Change anything related to P/Invoke Interop.
- Hide WinRT and/or COM concepts.
Outstanding questions:
- Implement new COM interop API for RCW/CCW creation/management #32091 (comment)
- Implement new COM interop API for RCW/CCW creation/management #32091 (comment)
Proposed API
namespace System.Runtime
{
public static partial class RuntimeHelpers
{
/// <summary>
/// Allocate memory that is associated with the <paramref name="type"/> and
/// will be freed if and when the <see cref="System.Type"/> is unloaded.
/// </summary>
/// <param name="type">Type associated with the allocated memory.</param>
/// <param name="size">Amount of memory in bytes to allocate.</param>
/// <returns>The allocated memory</returns>
public static IntPtr AllocateTypeAssociatedMemory(Type type, int size);
}
}
namespace System.Runtime.InteropServices
{
/// <summary>
/// Enumeration of flags for <see cref="ComWrappers.GetOrCreateComInterfaceForObject(object, CreateComInterfaceFlags)"/>.
/// </summary>
[Flags]
public enum CreateComInterfaceFlags
{
None = 0,
/// <summary>
/// The caller will provide an IUnknown Vtable.
/// </summary>
/// <remarks>
/// This is useful in scenarios when the caller has no need to rely on an IUnknown instance
/// that is used when running managed code is not possible (i.e. during a GC). In traditional
/// COM scenarios this is common, but scenarios involving <see href="https://docs.microsoft.com/windows/win32/api/windows.ui.xaml.hosting.referencetracker/nn-windows-ui-xaml-hosting-referencetracker-ireferencetrackertarget">Reference Tracker hosting</see>
/// calling of the IUnknown API during a GC is possible.
/// </remarks>
CallerDefinedIUnknown = 1,
/// <summary>
/// Flag used to indicate the COM interface should implement <see href="https://docs.microsoft.com/windows/win32/api/windows.ui.xaml.hosting.referencetracker/nn-windows-ui-xaml-hosting-referencetracker-ireferencetrackertarget">IReferenceTrackerTarget</see>.
/// When this flag is passed, the resulting COM interface will have an internal implementation of IUnknown
/// and as such none should be supplied by the caller.
/// </summary>
TrackerSupport = 2,
}
/// <summary>
/// Enumeration of flags for <see cref="ComWrappers.GetOrCreateObjectForComInstance(IntPtr, CreateObjectFlags, object?)"/>.
/// </summary>
[Flags]
public enum CreateObjectFlags
{
None = 0,
/// <summary>
/// Indicate if the supplied external COM object implements the <see href="https://docs.microsoft.com/windows/win32/api/windows.ui.xaml.hosting.referencetracker/nn-windows-ui-xaml-hosting-referencetracker-ireferencetracker">IReferenceTracker</see>.
/// </summary>
TrackerObject = 1,
/// <summary>
/// Ignore any internal caching and always create a unique instance.
/// </summary>
UniqueInstance = 2,
}
/// <summary>
/// Class for managing wrappers of COM IUnknown types.
/// </summary>
[CLSCompliant(false)]
public abstract partial class ComWrappers
{
/// <summary>
/// Interface type and pointer to targeted VTable.
/// </summary>
public struct ComInterfaceEntry
{
/// <summary>
/// Interface IID.
/// </summary>
public Guid IID;
/// <summary>
/// Memory must have the same lifetime as the memory returned from the call to <see cref="ComputeVtables(object, CreateComInterfaceFlags, out int)"/>.
/// </summary>
public IntPtr Vtable;
}
/// <summary>
/// ABI for function dispatch of a COM interface.
/// </summary>
public struct ComInterfaceDispatch
{
public IntPtr vftbl;
/// <summary>
/// Given a <see cref="System.IntPtr"/> from a generated VTable, convert to the target type.
/// </summary>
/// <typeparam name="T">Desired type.</typeparam>
/// <param name="dispatchPtr">Pointer supplied to VTable function entry.</param>
/// <returns>Instance of type associated with dispatched function call.</returns>
public static unsafe T GetInstance<T>(ComInterfaceDispatch* dispatchPtr) where T : class;
}
/// <summary>
/// Create an COM representation of the supplied object that can be passed to an non-managed environment.
/// </summary>
/// <param name="instance">A GC Handle to the managed object to expose outside the .NET runtime.</param>
/// <param name="flags">Flags used to configure the generated interface.</param>
/// <returns>The generated COM interface that can be passed outside the .NET runtime.</returns>
public IntPtr GetOrCreateComInterfaceForObject(object instance, CreateComInterfaceFlags flags);
/// <summary>
/// Compute the desired VTables for <paramref name="obj"/> respecting the values of <paramref name="flags"/>.
/// </summary>
/// <param name="obj">Target of the returned VTables.</param>
/// <param name="flags">Flags used to compute VTables.</param>
/// <param name="count">The number of elements contained in the returned memory.</param>
/// <returns><see cref="ComInterfaceEntry" /> pointer containing memory for all COM interface entries.</returns>
/// <remarks>
/// All memory returned from this function must either be unmanaged memory, pinned managed memory, or have been
/// allocated with the <see cref="System.Runtime.CompilerServices.RuntimeHelpers.AllocateTypeAssociatedMemory(Type, int)"/> API.
///
/// If the interface entries cannot be created and <code>null</code> is returned, the call to <see cref="ComWrappers.GetOrCreateComInterfaceForObject(object, CreateComInterfaceFlags)"/> will throw a <see cref="System.ArgumentNullException"/>.
/// </remarks>
protected unsafe abstract ComInterfaceEntry* ComputeVtables(object obj, CreateComInterfaceFlags flags, out int count);
/// <summary>
/// Get the currently registered managed object or creates a new managed object and registers it.
/// </summary>
/// <param name="externalComObject">Object to import for usage into the .NET runtime.</param>
/// <param name="flags">Flags used to describe the external object.</param>
/// <param name="wrapper">An optional <see cref="object"/> to be used as the wrapper for the external object</param>
/// <returns>Returns a managed object associated with the supplied external COM object.</returns>
/// <remarks>
/// Providing a <paramref name="wrapper"/> instance means <see cref="ComWrappers.GetOrCreateObjectForComInstance(IntPtr, CreateObjectFlags, object?)"/>
/// will not be called.
///
/// If the <paramref name="wrapper"/> instance already has an associated external object a <see cref="System.NotSupportedException"/> will be thrown.
/// </remarks>
public object GetOrCreateObjectForComInstance(IntPtr externalComObject, CreateObjectFlags flags, object? wrapper = null);
/// <summary>
/// Create a managed object for the object pointed at by <paramref name="externalComObject"/> respecting the values of <paramref name="flags"/>.
/// </summary>
/// <param name="externalComObject">Object to import for usage into the .NET runtime.</param>
/// <param name="flags">Flags used to describe the external object.</param>
/// <returns>Returns a managed object associated with the supplied external COM object.</returns>
/// <remarks>
/// If the object cannot be created and <code>null</code> is returned, the call to <see cref="ComWrappers.GetOrCreateObjectForComInstance(IntPtr, CreateObjectFlags, object?)"/> will throw a <see cref="System.ArgumentNullException"/>.
/// </remarks>
protected abstract object? CreateObject(IntPtr externalComObject, CreateObjectFlags flags);
/// <summary>
/// Called when a request is made for a collection of objects to be released.
/// </summary>
/// <param name="objects">Collection of objects to release.</param>
/// <remarks>
/// The default implementation of this function throws <see cref="System.NotImplementedException"/>.
/// </remarks>
protected virtual void ReleaseObjects(IEnumerable objects);
/// <summary>
/// Register this class's implementation to be used as the single global instance.
/// </summary>
/// <remarks>
/// This function can only be called a single time. Subsequent calls to this function will result
/// in a <see cref="System.InvalidOperationException"/> being thrown.
///
/// Scenarios where the global instance may be used are:
/// * Object tracking via the <see cref="CreateComInterfaceFlags.TrackerSupport" /> and <see cref="CreateObjectFlags.TrackerObject" /> flags.
/// * Usage of COM related Marshal APIs.
/// </remarks>
public void RegisterAsGlobalInstance();
/// <summary>
/// Get the runtime provided IUnknown implementation.
/// </summary>
/// <param name="fpQueryInterface">Function pointer to QueryInterface.</param>
/// <param name="fpAddRef">Function pointer to AddRef.</param>
/// <param name="fpRelease">Function pointer to Release.</param>
protected static void GetIUnknownImpl(out IntPtr fpQueryInterface, out IntPtr fpAddRef, out IntPtr fpRelease);
}
}
Example usage
The below example is merely for illustrative purposes. In a production ready consumption of the API many of the Marshal
APIs would not be used and the VTable layouts should be done in a static manner for optimal efficiency.
[Guid("197BC142-7A71-4637-B504-894DE79C4A22")]
interface IPrint
{
public void PrintInt(int i);
}
class Print : IPrint
{
public void PrintInt(int i)
{
Console.WriteLine($"{nameof(IPrint.PrintInt)} - 0x{i:x}");
}
}
struct IUnknownVftbl
{
public IntPtr QueryInterface;
public IntPtr AddRef;
public IntPtr Release;
}
struct IPrintVftbl
{
public IUnknownVftbl IUnknownImpl;
public IntPtr PrintInt;
public delegate int _PrintInt(IntPtr thisPtr, int i);
public static _PrintInt pPrintInt = new _PrintInt(PrintIntInternal);
public static int PrintIntInternal(IntPtr dispatchPtr, int i)
{
unsafe
{
try
{
ComWrappers.ComInterfaceDispatch.GetInstance<IPrint>((ComWrappers.ComInterfaceDispatch*)dispatchPtr).PrintInt(i);
}
catch (Exception e)
{
return e.HResult;
}
}
return 0; // S_OK;
}
}
struct VtblPtr
{
public IntPtr Vtbl;
}
class IExternalObject
{
private struct IExternalObjectVftbl
{
public IntPtr QueryInterface;
public _AddRef AddRef;
public _Release Release;
public _AddObjectRef AddObjectRef;
public _RemoveObjectRef DropObjectRef;
}
private delegate int _AddRef(IntPtr This);
private delegate int _Release(IntPtr This);
private delegate int _AddObjectRef(IntPtr This, IntPtr o);
private delegate int _RemoveObjectRef(IntPtr This, IntPtr o);
private readonly IntPtr instance;
private readonly IExternalObjectVftbl vtable;
public IExternalObject(IntPtr instance)
{
var inst = Marshal.PtrToStructure<VtblPtr>(instance);
this.vtable = Marshal.PtrToStructure<IExternalObjectVftbl>(inst.Vtbl);
this.instance = instance;
}
~IExternalObject()
{
if (this.instance != IntPtr.Zero)
{
this.vtable.Release(this.instance);
}
}
public void AddObjectRef(object inst)
{
...
}
public void DropObjectRef(object inst)
{
...
}
}
class MyComWrappers : ComWrappers
{
protected unsafe override ComInterfaceEntry* ComputeVtables(object obj, CreateComInterfaceFlags flags, out int count)
{
IntPtr fpQueryInteface = default;
IntPtr fpAddRef = default;
IntPtr fpRelease = default;
ComWrappers.GetIUnknownImpl(out fpQueryInteface, out fpAddRef, out fpRelease);
var tables = new List<ComInterfaceEntry>();
var vtbl1 = new IPrintVftbl()
{
IUnknownImpl = new IUnknownVftbl()
{
QueryInterface = fpQueryInteface,
AddRef = fpAddRef,
Release = fpRelease
},
PrintInt = Marshal.GetFunctionPointerForDelegate(IPrintVftbl.pPrintInt)
};
var vtblRaw1 = RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(IPrintVftbl), sizeof(IPrintVftbl));
Marshal.StructureToPtr(vtbl1, vtblRaw1, false);
tables.Add(new ComInterfaceEntry { IID = IID_IPrint, Vtable = vtblRaw1 });
if (flags.HasFlag(CreateComInterfaceFlags.CallerDefinedIUnknown))
{
var vtbl2 = new IUnknownVftbl()
{
QueryInterface = fpQueryInteface,
AddRef = fpAddRef,
Release = fpRelease
};
var vtblRaw2 = RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(IUnknownVftbl), sizeof(IUnknownVftbl));
Marshal.StructureToPtr(vtbl2, vtblRaw2, false);
tables.Add(new ComInterfaceEntry { IID = IID_IUnknown, Vtable = vtblRaw2 });
}
// Return pointer to memory containing ComInterfaceEntry collection
}
protected override object CreateObject(IntPtr externalComObject, CreateObjectFlags flags)
{
return new IExternalObject(externalComObject);
}
}
/cc @jkotas @Scottj1s @dunhor @jkoritzinsky @davidwrighton @terrajobst @tannergooding @jeffschwMSFT