Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[API Proposal]: COM source generator APIs #79121

Closed
AaronRobinsonMSFT opened this issue Dec 1, 2022 · 20 comments · Fixed by #84329
Closed

[API Proposal]: COM source generator APIs #79121

AaronRobinsonMSFT opened this issue Dec 1, 2022 · 20 comments · Fixed by #84329
Labels
api-approved API was approved in API review, it can be implemented area-System.Runtime.InteropServices source-generator Indicates an issue with a source generator feature
Milestone

Comments

@AaronRobinsonMSFT
Copy link
Member

AaronRobinsonMSFT commented Dec 1, 2022

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

@AaronRobinsonMSFT AaronRobinsonMSFT added api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Runtime.InteropServices labels Dec 1, 2022
@AaronRobinsonMSFT AaronRobinsonMSFT added this to the 8.0.0 milestone Dec 1, 2022
@ghost
Copy link

ghost commented Dec 1, 2022

Tagging subscribers to this area: @dotnet/interop-contrib
See info in area-owners.md if you want to be subscribed.

Issue Details

Background and motivation

Source generation of interop code have 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.Runtime.InteropServices;

/// <summary>
/// Type used to uniquely identify a COM interface.
/// </summary>
/// <param name="Iid">Interface ID</param>
public readonly record struct InterfaceId(Guid Iid);

/// <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<InterfaceId>
    where TImpl : T
{
    public IUnknownDerivedAttribute();

    /// <inheritdoc />
    public Guid Iid => T.TypeKey.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<InterfaceId>
{
    /// <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; }

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

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

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

API Usage

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

Alternative Designs

No response

Risks

No response

Author: AaronRobinsonMSFT
Assignees: -
Labels:

api-suggestion, area-System.Runtime.InteropServices

Milestone: 8.0.0

@jkoritzinsky
Copy link
Member

A few questions:

  1. Do we want to put these APIs in System.Runtime.InteropServices.Marshalling?
  2. The working example has a few "default" implementations of the various interfaces. Do we want to include some of those sealed classes in this API proposal (in particular DefaultIUnknownInterfaceDetailsStrategy)?

@jkotas
Copy link
Member

jkotas commented Dec 2, 2022

This seems to be introducing number of abstractions in attempt to cover potential future non-COM scenarios. What would be the bare minimal API that is just for COM and nothing else?

@jkoritzinsky
Copy link
Member

jkoritzinsky commented Dec 2, 2022

All of the types here are focused exclusively on COM. The different strategy interfaces allow developers to pay-for-play for each of the COM features they want to use. For example:

  • "All of my COM objects are free-threaded, I don't need thread-affinity support"
  • "I know for certain that my COM interface will only be queried for 2 interfaces, so I'll only cache the QI results for these two interfaces and I'll use fields instead of a dictionary"
  • "I know that I only support Windows platforms where the minimum DXGI version is 1.6. I'll save on QI costs and use how inheritance works in COM to redirect all QI calls for IDXGIFactory through IDXGIFactory6 to just use the pointer for IDXGIFactory6"

All of these statements are statements that customers could easily decide they want to do for performance that each make assumptions that we can't guarantee for all platforms. In the built-in COM support, nothing is pay-for-play. Everyone pays for the costs of every feature of COM. We'd like to reduce some of these costs and complexity in the new system by allowing people to pay for what they use (and for us to not need to layer on all of the support at once).

Additionally, some of the support (in particular IIUnknownDetails and IUnknownDerivedAttribute<T, TImpl> is to allow us to provide a linker-friendly design so that we can be linker/AOT compatible.

The only types mentioned in this proposal that is for "non-COM" scenarios are IUnmanagedVirtualMethodTableProvider<T> and IUnmanagedInterfaceType<TKey>, whose definitions aren't actually included in this proposal (which are also changing shape a bit in #77130, which should hopefully be in soon).

@AaronRobinsonMSFT
Copy link
Member Author

"All of my COM objects are free-threaded, I don't need thread-affinity support"

Given recent discussions this might not ever be a priority. Going forward do we think this level of support is necessary ever? I can think of WinRT, but are there others?

@AaronRobinsonMSFT
Copy link
Member Author

"All of my COM objects are free-threaded, I don't need thread-affinity support"

Given recent discussions this might not ever be a priority. Going forward do we think this level of support is necessary ever? I can think of WinRT, but are there others?

Another way of saying this is, if .NET 8+ is continuing to be a cross-platform first platform should supporting/enabling a COM thread affinity scenario ever be a priority?

@jkoritzinsky
Copy link
Member

"All of my COM objects are free-threaded, I don't need thread-affinity support"

Given recent discussions this might not ever be a priority. Going forward do we think this level of support is necessary ever? I can think of WinRT, but are there others?

Another way of saying this is, if .NET 8+ is continuing to be a cross-platform first platform should supporting/enabling a COM thread affinity scenario ever be a priority?

I think we should provide a mechanism for developers to be able to use thread-affinized COM objects safely with source-generated COM, but I definitely agree that it shouldn't be a feature that everyone gets by default. It should be something that is either not in-box or is marked with [SupportedOSPlatform("windows")] and opt-in by the user. Especially since the failure mode can be pretty rough for thread-affinity issues that might not show up in testing (crash on the finalizer thread when that's the first use on the wrong thread, but finalizer doesn't run often enough to catch it in unit tests).

There are enough commonly-used STA-thread-affinized COM objects that I think allowing people to opt-in to using them and still use source-generated COM for the marshalling stubs is a good idea.

@jkoritzinsky
Copy link
Member

"All of my COM objects are free-threaded, I don't need thread-affinity support"

Given recent discussions this might not ever be a priority. Going forward do we think this level of support is necessary ever? I can think of WinRT, but are there others?

Another way of saying this is, if .NET 8+ is continuing to be a cross-platform first platform should supporting/enabling a COM thread affinity scenario ever be a priority?

I think we should provide a mechanism for developers to be able to use thread-affinized COM objects safely with source-generated COM, but I definitely agree that it shouldn't be a feature that everyone gets by default. It should be something that is either not in-box or is marked with [SupportedOSPlatform("windows")] and opt-in by the user. Especially since the failure mode can be pretty rough for thread-affinity issues that might not show up in testing (crash on the finalizer thread when that's the first use on the wrong thread, but finalizer doesn't run often enough to catch it in unit tests).

There are enough commonly-used STA-thread-affinized COM objects that I think allowing people to opt-in to using them and still use source-generated COM for the marshalling stubs is a good idea.

I think this really boils down to the question of what we're building. Are we building something that is meant to replace MCG and nothing else or are we building something that we want to be the future of COM interop in the way that LibraryImport is the future of DllImport. For LibraryImport, we removed a few features of DllImport, but for each feature removed, there's either a new mechanism (ICustomMarshaler -> the new custom marshaller model), or an easy workaround (ExactSpelling -> add the suffix, PreserveSig -> add a call to Marshal.ThrowExceptionForHR). If we build a model such that users cannot add in thread-affinity support and we will never provide thread-affinity support, then we effectively block many users from moving to source-generated COM.

I know we won't get 100% feature parity any time soon if ever, but explicitly removing elements of the design in a way that blocks off users from adding the features we don't want to build in, that feels wrong to me and that we're selling ourselves short of what we could build for our users.

@AaronRobinsonMSFT
Copy link
Member Author

What would be the bare minimal API that is just for COM and nothing else?

This would also include the debate over public readonly record struct InterfaceId(Guid Iid);. Do we have justification for this sort of hook? Are there reasons for creating yet another abstraction instead of just using a Guid?

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

This could also be removed. There is no obvious reason for it other than the use of the ReadOnlySpan<T> being used for the RCW's vtable.

@jkotas
Copy link
Member

jkotas commented Dec 2, 2022

free-threaded vs. thread-affinity

What is the set of behaviors that this covers and how can these behaviors be achieved using the proposed interfaces?

@jkoritzinsky
Copy link
Member

free-threaded vs. thread-affinity

What is the set of behaviors that this covers and how can these behaviors be achieved using the proposed interfaces?

The free-threaded vs thread-affinity issue basically falls to how you get a pointer to use for a given COM interface. Do you just call QueryInterface and cache the pointer to be used for all threads, or do you capture the context or add the object to the GIT (or get an AgileReference) for the object so calls to it from different threads can be safely marshalled back to the correct thread apartment and have the results marshalled back? It's much cheaper to not handle any of this complexity, but sometimes this complexity is required by the unmanged COM object (most if not all Windows Shell APIs come to mind).

The support could be achieved by providing a customized IIUnknownStrategy that captures an IAgileReference on first use and then calls IAgileReference::Resolve instead of QueryInterface on the underlying object when the IIUnknownStrategy.QueryInterface method is called and providing a customized IIUnkownCacheStrategy that caches the results of IIUnknownStrategy.QueryInterface in a Thread-local dictionary or a dictionary keyed on apartment context and the interface id.

@jkotas
Copy link
Member

jkotas commented Dec 2, 2022

Does it mean that there will be multiple calls of these strategy interfaces for each actual .NET -> COM call? It looks expensive.

@AaronRobinsonMSFT
Copy link
Member Author

Does it mean that there will be multiple calls of these strategy interfaces for each actual .NET -> COM call? It looks expensive.

It can get expensive for sure. If we assume the interface has registered proxy/stubs, then we could rely on COM apartment marshalling. This would reduce our need for IAgileReference/GIT to only the initial access and cleanup and we could perform tricks with creating/forcing proxies for all interfaces that have thread affinity.

@jkoritzinsky
Copy link
Member

Does it mean that there will be multiple calls of these strategy interfaces for each actual .NET -> COM call? It looks expensive.

Once there's an entry in the cache for whatever the user has defined as the requirements (once per Type or once per Type+apartment), there will only be one call to a strategy interface to get the table info from the RuntimeTypeHandle. The first call to populate the cache could be expensive though. I could see dropping the "Type to interface info" interface and making the class an internal implementation detail if the perf-hit of providing the flexibility is too high.

@jkoritzinsky
Copy link
Member

I've updated this proposal based on our experiments in the ComObjectRedux repo (referenced in the API usage section).

@jkoritzinsky jkoritzinsky added source-generator Indicates an issue with a source generator feature blocking Marks issues that we want to fast track in order to unblock other important work api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels Jan 24, 2023
@jkoritzinsky
Copy link
Member

Moved the "vtable"-level APIs that are absolutely required for the COM generator into this issue and out of #80204 to ensure we get to all of the COM APIs first before we get to the generalized vtable APIs.

@terrajobst terrajobst added api-needs-work API needs work before it is approved, it is NOT ready for implementation and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Feb 7, 2023
@terrajobst
Copy link
Member

terrajobst commented Feb 7, 2023

Video

  • VirtualMethodTableInfo
    • Do we need the Deconstruct? Yes, as it simplifies the generated code.
  • IUnmanagedVirtualMethodTableProvider
    • Should we have a generic method GetVirtualMethodTableInfoForKey()? We concluded that we don't want to pay for the additional complexity for a usability gain.
  • ComObject.FinalRelease
    • Should be a void returning method
    • Make it a no-op if the COM object can't be released
    • Make the second call a no-op
    • Throw InvalidOperationException rather than ObjectDisposedException when the COM object is used after calling FinalRelease
using System.Collections;

namespace System.Runtime.InteropServices.Marshalling;

public readonly unsafe struct VirtualMethodTableInfo
{
    public VirtualMethodTableInfo(void* thisPointer, void** virtualMethodTable);
    public void* ThisPointer { get; }
    public void** VirtualMethodTable { get; }
    public void Deconstruct(out void* thisPointer, out void** virtualMethodTable);
}

public interface IUnmanagedVirtualMethodTableProvider
{
    VirtualMethodTableInfo GetVirtualMethodTableInfoForKey(Type type);
}

public interface IUnmanagedInterfaceType
{
    public abstract static unsafe void* VirtualMethodTableManagedImplementation { get; }
}

public interface IIUnknownInterfaceType : IUnmanagedInterfaceType
{
    public abstract static Guid Iid { get; }
}

public interface IUnknownDerivedDetails
{
    Guid Iid { get; }
    Type Implementation { get; }
    unsafe void* VirtualMethodTableManagedImplementation { get; }
}

[AttributeUsage(AttributeTargets.Interface)]
public class IUnknownDerivedAttribute<T, TImpl> : Attribute, IUnknownDerivedDetails
    where T : IIUnknownInterfaceType
    where TImpl : T
{
    public Guid Iid { get; }
    public Type Implementation { get; }
    public unsafe void* VirtualMethodTableManagedImplementation { get; }
}

public unsafe interface IIUnknownStrategy
{
    void* CreateInstancePointer(void* unknown);
    int QueryInterface(void* instancePtr, in Guid iid, out void* ppObj);
    int Release(void* instancePtr);
}

public interface IIUnknownInterfaceDetailsStrategy
{
    IUnknownDerivedDetails? GetIUnknownDerivedDetails(RuntimeTypeHandle type);
}

public unsafe interface IIUnknownCacheStrategy
{
    public readonly struct TableInfo
    {
        public void* ThisPtr { get; init; }
        public void** Table { get; init; }
        public RuntimeTypeHandle ManagedType { get; init; }
    }

    TableInfo ConstructTableInfo(RuntimeTypeHandle handle, IUnknownDerivedDetails interfaceDetails, void* ptr);
    bool TryGetTableInfo(RuntimeTypeHandle handle, out TableInfo info);
    bool TrySetTableInfo(RuntimeTypeHandle handle, TableInfo info);
    void Clear(IIUnknownStrategy unknownStrategy);
}

public sealed class ComObject : IDynamicInterfaceCastable, IUnmanagedVirtualMethodTableProvider
{
    ~ComObject();
    public IDisposable? FinalRelease { get; }
    RuntimeTypeHandle IDynamicInterfaceCastable.GetInterfaceImplementation(RuntimeTypeHandle interfaceType);
    bool IDynamicInterfaceCastable.IsInterfaceImplemented(RuntimeTypeHandle interfaceType, bool throwIfNotImplemented);
    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);
}

@terrajobst terrajobst added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-needs-work API needs work before it is approved, it is NOT ready for implementation labels Feb 7, 2023
@bartonjs
Copy link
Member

bartonjs commented Feb 9, 2023

  • The generic on GeneratedComInterfaceAttribute is being removed (per the feature team)
  • Consider moving GeneratedComInterfaceAttribute to System.Runtime.InteropServices for visibility and parity/discovery with ComImportAttribute
  • GeneratedComWrappersBase needs a new name because a) all feature users need to derive from it (so it shouldn't use the suffix "Base") and b) the type isn't tied to sourcegen, it could be successfully used independent of the generators.
    • Suggestions were ComObjectComWrappers and StrategyBasedComWrappers (StrategyBasedComWrappers was preferred by the feature team in the meeting)
  • GeneratedComWrappersBase.GetOrCreateUniqueObjectForComInstance should be removed until there's a demonstrated need for it to be public.
  • The DefaultIUnknownInterfaceDetailsStrategy, FreeThreadedStrategy, and DefaultCaching types don't need to be public.
    • DefaultIUnknownInterfaceDetailsStrategy.Instance => (public static) GeneratedComWrappersBase.DefaultIUnknownInterfaceDetailsStrategy { get; }
    • FreeThreadedStrategy.Instance => (public static) GeneratedComWrappersBase.DefaultIUnknownStrategy { get; }
    • DefaultCaching..ctor() => (protected static) GeneratedComWrappersBase.CreateDefaultCacheStrategy()
  • CreateInterfaceDetailsStrategy => GetOrCreateInterfaceDetailsStrategy (to indicate reuse is permitted)
  • CreateIUnknownStrategy => GetOrCreateIUnknownStrategy (to indicate reuse is permitted)
  • (CreateCacheStrategy does not get "GetOr-" to indicate that reuse is NOT permitted)
  • IIUnknownStrategy.Release's return type should be uint, not int, and parameter should be instance instead of instancePtr
  • VirtualMethodTableInfo.ThisPointer => VirtualMethodTableInfo.Instance (and updated in parameters accordingly)
  • IUnmanagedVirtualMethodTableProvider.GetVirtualMethodTableInfoForKey => IUnmanagedVirtualMethodTableProvider.GetVirtualMethodTableInfoForType
  • VirtualMethodTableManagedImplementation's property type should be void** instead of void*, for consistency with other vtable exposure
  • VirtualMethodTableManagedImplementation => ManagedVirtualMethodTable
  • IIUnknownStrategy.QueryInterface should take iid by value, not by in (for all the reasons why in is hard)
  • IUnknownDerivedAttribute should be sealed.
  • IUnknownDerivedDetails => IDerivedIUnknownDetails or IIUnknownDerivedDetails ("II" seems to be preferred)
  • IIUnknownStrategy.QueryInterface's first parameter instancePtr => instance
  • IIUnknownCacheStrategy.TableInfo property renames: ThisPtr => Instance, Table => VirtualMethodTable, ManagedType => Implementation

(Some updates from the previous session may not have been applied to the surface here, but it should be pretty close to correct.)

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="instance">The pointer to the instance.</param>
    /// <param name="virtualMethodTable">The block of memory that represents the virtual method table.</param>
    public VirtualMethodTableInfo(void* instance, void** virtualMethodTable);

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

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

    /// <summary>
    /// Deconstruct this structure into its two fields.
    /// </summary>
    /// <param name="instance">The <see cref="Instance"/> result</param>
    /// <param name="virtualMethodTable">The <see cref="VirtualMethodTable"/> result</param>
    public void Deconstruct(out void* instance, 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>
    VirtualMethodTableInfo GetVirtualMethodTableInfoForKey(Type type);
}

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

    /// <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** ManagedVirtualMethodTable { get; }
}

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

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

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

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

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

    /// <inheritdoc />
    public unsafe void** ManagedVirtualMethodTable { 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="instance">A pointer representing the unmanaged instance.</param>
    /// <param name="interfaceId">The Interface ID (IID) to query for.</param>
    /// <param name="obj">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* instance, Guid interfaceId, out void* obj);

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

/// <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* Instance { get; init; }
        public void** VirtualMethodTable { get; init; }
        public RuntimeTypeHandle Implementation { 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 void FinalRelease();

    /// <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 : Attribute
{
}

public abstract class StrategyBasedComWrappers : ComWrappers
{
    public static IIUnknownInterfaceDetailsStrategy DefaultIUnknownInterfaceDetailsStrategy { get; }

    public static IIUnknownStrategy DefaultIIUnknownStrategy { get; }

    protected static IIUnknownCacheStrategy CreateDefaultCacheStrategy();

    protected virtual IIUnknownInterfaceDetailsStrategy GetOrCreateInterfaceDetailsStrategy() => DefaultIUnknownInterfaceDetailsStrategy;

    protected virtual IIUnknownStrategy GetOrCreateIUnknownStrategy() => FreeThreadedStrategy;

    protected virtual IIUnknownCacheStrategy CreateCacheStrategy() => CreateDefaultCacheStrategy();

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

    protected override sealed void ReleaseObjects(IEnumerable objects);
}

@jkoritzinsky jkoritzinsky removed the blocking Marks issues that we want to fast track in order to unblock other important work label Feb 9, 2023
@bartonjs bartonjs added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Feb 9, 2023
@miloush
Copy link
Contributor

miloush commented Feb 28, 2023

Is there a possibility to support .NET inheritance for COM inheritance? i.e.

[ComImport, Guid(...), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IFoo
{
    void Method1();
}

[ComImport, Guid(...), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IFoo2 : IFoo
{
    void Method2();
}

with the vtable for IFoo2 being

QueryInterface
AddRef
Release
Method1
Method2

@jkoritzinsky
Copy link
Member

Yes, we plan to support using inheritance to represent COM inheritance.

@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Apr 4, 2023
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Apr 13, 2023
@ghost ghost locked as resolved and limited conversation to collaborators May 14, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Runtime.InteropServices source-generator Indicates an issue with a source generator feature
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants