Skip to content

[API Proposal]: COM source generator APIs #79121

Closed
@AaronRobinsonMSFT

Description

@AaronRobinsonMSFT

Background and motivation

Source generation of interop code has proven successful in various projects, especially the build-in LibraryImport. Supporting this for COM interop is the next logical step but requires new APIs for defining how the source is generated, UX for requesting generation, and hooks for customization.

VTable based source generator design.
Work item for COM source generator: #76767

API Proposal

using System.Collections;

namespace System.Runtime.InteropServices.Marshalling;

/// <summary>
/// Information about a virtual method table and the unmanaged instance pointer.
/// </summary>
public readonly unsafe struct VirtualMethodTableInfo
{
    /// <summary>
    /// Construct a <see cref="VirtualMethodTableInfo"/> from a given instance pointer and table memory.
    /// </summary>
    /// <param name="thisPointer">The pointer to the instance.</param>
    /// <param name="virtualMethodTable">The block of memory that represents the virtual method table.</param>
    public VirtualMethodTableInfo(void* thisPointer, void** virtualMethodTable);

    /// <summary>
    /// The unmanaged instance pointer
    /// </summary>
    public void* ThisPointer { get; }

    /// <summary>
    /// The virtual method table.
    /// </summary>
    public void** VirtualMethodTable { get; }

    /// <summary>
    /// Deconstruct this structure into its two fields.
    /// </summary>
    /// <param name="thisPointer">The <see cref="ThisPointer"/> result</param>
    /// <param name="virtualMethodTable">The <see cref="VirtualMethodTable"/> result</param>
    public void Deconstruct(out void* thisPointer, out void** virtualMethodTable);
}

/// <summary>
/// This interface allows an object to provide information about a virtual method table for a managed interface to enable invoking methods in the virtual method table.
/// </summary>
public interface IUnmanagedVirtualMethodTableProvider
{
    /// <summary>
    /// Get the information about the virtual method table for a given unmanaged interface type represented by <paramref name="type"/>.
    /// </summary>
    /// <param name="type">The managed type for the unmanaged interface.</param>
    /// <returns>The virtual method table information for the unmanaged interface.</returns>
    public VirtualMethodTableInfo GetVirtualMethodTableInfoForKey(Type type);
}

/// <summary>
/// This interface allows another interface to define that it represents a managed projection of an unmanaged interface from some unmanaged type system and supports passing managed implementations of unmanaged interfaces to unmanaged code.
/// </summary>
public interface IUnmanagedInterfaceType
{
    /// <summary>
    /// Get a pointer to the virtual method table of managed implementations of the unmanaged interface type.
    /// </summary>
    /// <returns>A pointer to the virtual method table of managed implementations of the unmanaged interface type</returns>
    /// <remarks>
    /// Implementation will be provided by a source generator if not explicitly implemented.
    /// This property can return <c>null</c>. If it does, then the interface is not supported for passing managed implementations to unmanaged code.
    /// </remarks>
    public abstract static unsafe void* VirtualMethodTableManagedImplementation { get; }
}

/// <summary>
///  Identify the Interface ID (IID) for an IUnknown based interface.
/// </summary>
public interface IIUnknownInterfaceType : IUnmanagedInterfaceType
{
    public abstract static Guid Iid { get; }
}

/// <summary>
/// Details for the IUnknown derived interface.
/// </summary>
public interface IUnknownDerivedDetails
{
    /// <summary>
    /// Interface ID.
    /// </summary>
    public Guid Iid { get; }

    /// <summary>
    /// Managed type used to project the IUnknown derived interface.
    /// </summary>
    public Type Implementation { get; }

    /// <summary>
    /// A pointer to the virtual method table to enable unmanaged callers to call a managed implementation of the interface.
    /// </summary>
    public unsafe void* VirtualMethodTableManagedImplementation { get; }
}

[AttributeUsage(AttributeTargets.Interface)]
public class IUnknownDerivedAttribute<T, TImpl> : Attribute, IUnknownDerivedDetails
    where T : IIUnknownInterfaceType
    where TImpl : T
{
    /// <inheritdoc />
    public Guid Iid { get; }

    /// <inheritdoc />
    public Type Implementation { get; }

    /// <inheritdoc />
    public unsafe void* VirtualMethodTableManagedImplementation { get; }
}

/// <summary>
/// IUnknown interaction strategy.
/// </summary>
public unsafe interface IIUnknownStrategy
{
    /// <summary>
    /// Create an instance pointer that represents the provided IUnknown instance.
    /// </summary>
    /// <param name="unknown">The IUnknown instance.</param>
    /// <returns>A pointer representing the unmanaged instance.</returns>
    /// <remarks>
    /// This method is used to create an instance pointer that can be used to interact with the other members of this interface.
    /// For example, this method can return an IAgileReference instance for the provided IUnknown instance
    /// that can be used in the QueryInterface and Release methods to enable creating thread-local instance pointers to us
    /// through the IAgileReference APIs instead of directly calling QueryInterface on the IUnknown.
    /// </remarks>
    public void* CreateInstancePointer(void* unknown);

    /// <summary>
    /// Perform a QueryInterface() for an IID on the unmanaged instance.
    /// </summary>
    /// <param name="instancePtr">A pointer representing the unmanaged instance.</param>
    /// <param name="iid">The IID (Interface ID) to query for.</param>
    /// <param name="ppObj">The resulting interface</param>
    /// <returns>Returns an HRESULT represents the success of the operation</returns>
    /// <seealso cref="Marshal.QueryInterface(nint, ref Guid, out nint)"/>
    public int QueryInterface(void* instancePtr, in Guid iid, out void* ppObj);

    /// <summary>
    /// Perform a Release() call on the supplied unmanaged instance.
    /// </summary>
    /// <param name="instancePtr">A pointer representing the unmanaged instance.</param>
    /// <returns>The current reference count.</returns>
    /// <seealso cref="Marshal.Release(nint)"/>
    public int Release(void* instancePtr);
}

/// <summary>
/// Strategy for acquiring interface details.
/// </summary>
public interface IIUnknownInterfaceDetailsStrategy
{
    /// <summary>
    /// Given a <see cref="RuntimeTypeHandle"/> get the IUnknown details.
    /// </summary>
    /// <param name="type">RuntimeTypeHandle instance</param>
    /// <returns>Details if type is known.</returns>
    IUnknownDerivedDetails? GetIUnknownDerivedDetails(RuntimeTypeHandle type);
}

/// <summary>
/// Unmanaged virtual method table look up strategy.
/// </summary>
public unsafe interface IIUnknownCacheStrategy
{
    public readonly struct TableInfo
    {
        public void* ThisPtr { get; init; }
        public void** Table { get; init; }
        public RuntimeTypeHandle ManagedType { get; init; }
    }

    /// <summary>
    /// Construct a <see cref="TableInfo"/> instance.
    /// </summary>
    /// <param name="handle">RuntimeTypeHandle instance</param>
    /// <param name="ptr">Pointer to the instance to query</param>
    /// <param name="info">A <see cref="TableInfo"/> instance</param>
    /// <returns>True if success, otherwise false.</returns>
    TableInfo ConstructTableInfo(RuntimeTypeHandle handle, IUnknownDerivedDetails interfaceDetails, void* ptr);

    /// <summary>
    /// Get associated <see cref="TableInfo"/>.
    /// </summary>
    /// <param name="handle">RuntimeTypeHandle instance</param>
    /// <param name="info">A <see cref="TableInfo"/> instance</param>
    /// <returns>True if found, otherwise false.</returns>
    bool TryGetTableInfo(RuntimeTypeHandle handle, out TableInfo info);

    /// <summary>
    /// Set associated <see cref="TableInfo"/>.
    /// </summary>
    /// <param name="handle">RuntimeTypeHandle instance</param>
    /// <param name="info">A <see cref="TableInfo"/> instance</param>
    /// <returns>True if set, otherwise false.</returns>
    bool TrySetTableInfo(RuntimeTypeHandle handle, TableInfo info);

    /// <summary>
    /// Clear the cache
    /// </summary>
    /// <param name="unknownStrategy">The <see cref="IIUnknownStrategy"/> to use for clearing</param>
    void Clear(IIUnknownStrategy unknownStrategy);
}

/// <summary>
/// Base class for all COM source generated Runtime Callable Wrapper (RCWs).
/// </summary>
public sealed class ComObject : IDynamicInterfaceCastable, IUnmanagedVirtualMethodTableProvider
{
    ~ComObject();

    /// <summary>
    /// Returns an IDisposable that can be used to perform a final release
    /// on this COM object wrapper.
    /// </summary>
    /// <remarks>
    /// This property will only be non-null if the ComObject was created using
    /// CreateObjectFlags.UniqueInstance.
    /// </remarks>
    public IDisposable? FinalRelease { get; }

    /// <inheritdoc />
    RuntimeTypeHandle IDynamicInterfaceCastable.GetInterfaceImplementation(RuntimeTypeHandle interfaceType);

    /// <inheritdoc />
    bool IDynamicInterfaceCastable.IsInterfaceImplemented(RuntimeTypeHandle interfaceType, bool throwIfNotImplemented);

    /// <inheritdoc />
    VirtualMethodTableInfo IUnmanagedVirtualMethodTableProvider.GetVirtualMethodTableInfoForKey(Type type);
}

[AttributeUsage(AttributeTargets.Interface)]
public sealed class GeneratedComInterfaceAttribute<TComWrappers> : Attribute
    where TComWrappers : GeneratedComWrappersBase
{
}

public abstract class GeneratedComWrappersBase : ComWrappers
{
    protected virtual IIUnknownInterfaceDetailsStrategy CreateInterfaceDetailsStrategy() => DefaultIUnknownInterfaceDetailsStrategy.Instance;

    protected virtual IIUnknownStrategy CreateIUnknownStrategy() => FreeThreadedStrategy.Instance;

    protected virtual IIUnknownCacheStrategy CreateCacheStrategy() => new DefaultCaching();

    protected override sealed unsafe object CreateObject(nint externalComObject, CreateObjectFlags flags);

    protected override sealed void ReleaseObjects(IEnumerable objects);

    public ComObject GetOrCreateUniqueObjectForComInstance(nint comInstance, CreateObjectFlags flags);
}

public sealed class DefaultIUnknownInterfaceDetailsStrategy : IIUnknownInterfaceDetailsStrategy
{
    public static readonly IIUnknownInterfaceDetailsStrategy Instance;

    public IUnknownDerivedDetails? GetIUnknownDerivedDetails(RuntimeTypeHandle type);
}

public sealed unsafe class FreeThreadedStrategy : IIUnknownStrategy
{
    public static readonly IIUnknownStrategy Instance;

    void* IIUnknownStrategy.CreateInstancePointer(void* unknown);

    unsafe int IIUnknownStrategy.QueryInterface(void* thisPtr, in Guid handle, out void* ppObj);

    unsafe int IIUnknownStrategy.Release(void* thisPtr);
}

public sealed unsafe class DefaultCaching : IIUnknownCacheStrategy
{
    private readonly Dictionary<RuntimeTypeHandle, IIUnknownCacheStrategy.TableInfo> _cache = new();

    IIUnknownCacheStrategy.TableInfo IIUnknownCacheStrategy.ConstructTableInfo(RuntimeTypeHandle handle, IUnknownDerivedDetails details, void* ptr);

    bool IIUnknownCacheStrategy.TryGetTableInfo(RuntimeTypeHandle handle, out IIUnknownCacheStrategy.TableInfo info);

    bool IIUnknownCacheStrategy.TrySetTableInfo(RuntimeTypeHandle handle, IIUnknownCacheStrategy.TableInfo info);

    void IIUnknownCacheStrategy.Clear(IIUnknownStrategy unknownStrategy);
}
Original API proposal for reference from comments

API Proposal

using System.Runtime.InteropServices;

/// <summary>
/// Details for the IUnknown derived interface.
/// </summary>
public interface IUnknownDerivedDetails
{
    /// <summary>
    /// Interface ID.
    /// </summary>
    public Guid Iid { get; }

    /// <summary>
    /// Managed typed used to project the IUnknown derived interface.
    /// </summary>
    public Type Implementation { get; }

    /// <summary>
    /// Total length of the vtable.
    /// </summary>
    public int VTableTotalLength { get; }
}

/// <summary>
/// Attribute used to indicate an interface derives from IUnknown.
/// </summary>
/// <typeparam name="T">The managed definition of the derived interface.</typeparam>
/// <typeparam name="TImpl">The managed implementation of the derived interface.</typeparam>
[AttributeUsage(AttributeTargets.Interface)]
public class IUnknownDerivedAttribute<T, TImpl> : Attribute, IUnknownDerivedDetails
    where T : IUnmanagedInterfaceType, IIUnknownInterfaceType
    where TImpl : T
{
    public IUnknownDerivedAttribute();

    /// <inheritdoc />
    public Guid Iid => T.Iid;

    /// <inheritdoc />
    public Type Implementation => typeof(TImpl);

    /// <inheritdoc />
    public int VTableTotalLength => T.VTableLength;
}

/// <summary>
/// IUnknown interaction strategy.
/// </summary>
public unsafe interface IIUnknownStrategy
{
    /// <summary>
    /// Perform a QueryInterface() for an IID on the unmanaged IUnknown.
    /// </summary>
    /// <param name="thisPtr">The IUnknown instance.</param>
    /// <param name="iid">The IID (Interface ID) to query for.</param>
    /// <param name="ppObj">The resulting interface</param>
    /// <returns>Returns an HRESULT represents the success of the operation</returns>
    /// <seealso cref="Marshal.QueryInterface(nint, ref Guid, out nint)"/>
    public int QueryInterface(void* thisPtr, in Guid iid, out void* ppObj);

    /// <summary>
    /// Perform a Release() call on the supplied IUnknown instance.
    /// </summary>
    /// <param name="thisPtr">The IUnknown instance.</param>
    /// <returns>The current reference count.</returns>
    /// <seealso cref="Marshal.Release(nint)"/>
    public int Release(void* thisPtr);
}

/// <summary>
/// Strategy for acquiring interface details.
/// </summary>
public interface IIUnknownInterfaceDetailsStrategy
{
    /// <summary>
    /// Given a <see cref="RuntimeTypeHandle"/> get the IUnknown details.
    /// </summary>
    /// <param name="type">RuntimeTypeHandle instance</param>
    /// <returns>Details if type is known.</returns>
    IUnknownDerivedDetails? GetIUnknownDerivedDetails(RuntimeTypeHandle type);
}

/// <summary>
/// Unmanaged virtual method table look up strategy.
/// </summary>
public unsafe interface IIUnknownCacheStrategy
{
    public readonly struct TableInfo
    {
        public void* ThisPtr { get; init; }
        public void** Table { get; init; }
        public int TableLength { get; init; }
        public RuntimeTypeHandle ManagedType { get; init; }
    }

    /// <summary>
    /// Construct a <see cref="TableInfo"/> instance.
    /// </summary>
    /// <param name="handle">RuntimeTypeHandle instance</param>
    /// <param name="ptr">Pointer to the instance to query</param>
    /// <param name="info">A <see cref="TableInfo"/> instance</param>
    /// <returns>True if success, otherwise false.</returns>
    TableInfo ConstructTableInfo(RuntimeTypeHandle handle, IUnknownDerivedDetails interfaceDetails, void* ptr);

    /// <summary>
    /// Get associated <see cref="TableInfo"/>.
    /// </summary>
    /// <param name="handle">RuntimeTypeHandle instance</param>
    /// <param name="info">A <see cref="TableInfo"/> instance</param>
    /// <returns>True if found, otherwise false.</returns>
    bool TryGetTableInfo(RuntimeTypeHandle handle, out TableInfo info);

    /// <summary>
    /// Set associated <see cref="TableInfo"/>.
    /// </summary>
    /// <param name="handle">RuntimeTypeHandle instance</param>
    /// <param name="info">A <see cref="TableInfo"/> instance</param>
    /// <returns>True if set, otherwise false.</returns>
    bool TrySetTableInfo(RuntimeTypeHandle handle, TableInfo info);

    /// <summary>
    /// Clear the cache
    /// </summary>
    /// <param name="unknownStrategy">The <see cref="IIUnknownStrategy"/> to use for clearing</param>
    void Clear(IIUnknownStrategy unknownStrategy);
}

/// <summary>
/// Base class for all COM source generated Runtime Callable Wrapper (RCWs).
/// </summary>
public abstract class ComObject : IDynamicInterfaceCastable, IUnmanagedVirtualMethodTableProvider
{
    /// <summary>
    /// Initialize ComObject instance.
    /// </summary>
    /// <param name="interfaceDetailsStrategy">Strategy for getting details</param>
    /// <param name="iunknownStrategy">Interaction strategy for IUnknown</param>
    /// <param name="cacheStrategy">Caching strategy</param>
    protected ComObject(IIUnknownInterfaceDetailsStrategy interfaceDetailsStrategy, IIUnknownStrategy iunknownStrategy, IIUnknownCacheStrategy cacheStrategy);

    ~ComObject();

    /// <summary>
    /// Pointer to the unmanaged instance.
    /// </summary>
    protected void* ThisPtr { get; init; }

    /// <summary>
    /// Interface details strategy.
    /// </summary>
    protected IIUnknownInterfaceDetailsStrategy InterfaceDetailsStrategy { get; init; }

    /// <summary>
    /// IUnknown interaction strategy.
    /// </summary>
    protected IIUnknownStrategy IUnknownStrategy { get; init; }

    /// <summary>
    /// Caching strategy.
    /// </summary>
    protected IIUnknownCacheStrategy CacheStrategy { get; init; }

    /// <summary>
    /// Returns an IDisposable that can be used to perform a final release
    /// on this COM object wrapper.
    /// </summary>
    /// <remarks>
    /// This property will only be non-null if the ComObject was created using
    /// CreateObjectFlags.UniqueInstance.
    /// </remarks>
    public IDisposable? FinalRelease { get; }

    /// <inheritdoc />
    RuntimeTypeHandle IDynamicInterfaceCastable.GetInterfaceImplementation(RuntimeTypeHandle interfaceType);

    /// <inheritdoc />
    bool IDynamicInterfaceCastable.IsInterfaceImplemented(RuntimeTypeHandle interfaceType, bool throwIfNotImplemented);

    /// <inheritdoc />
    VirtualMethodTableInfo IUnmanagedVirtualMethodTableProvider.GetVirtualMethodTableInfoForKey(Type type);
}

API Usage

Working example: https://github.com/AaronRobinsonMSFT/ComObjectRedux

Alternative Designs

No response

Risks

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions