Skip to content

IDynamicInterfaceCastable interface #36654

@AaronRobinsonMSFT

Description

@AaronRobinsonMSFT

Background and Motivation

In the .NET Native runtime support existed for a .NET class to participate in a C-style cast when that class didn't support the cast-to type. The COM QueryInterface() is an example where this kind of scenario exists. In COM, the instance is represented by an opaque IUnknown pointer and the only way to discover what is supported by the instance is to call QueryInterface() with a GUID representing the question of "Does this instance support this type?". The answer is typically a simple "yes" (S_OK) or "no" (E_NOINTERFACE). If the answer is "yes", a casted to instance of the type is returned, otherwise null. There are scenarios where the current instance may not have implemented this type but can provide an instance that does - this is called a tear-off.

In .NET, the metadata for a type is static and therefore if a type can't be cast to another type because it isn't in metadata that is correct. This means that a type has no reason to participate in the casting question. However, when implementing support for a COM scenario this rigidity isn't as beneficial since it may not be possible to know all the supported types on an instance. This proposal provides a way to let types provide an answer and an object instance that does satisfy the requested type eventhough the original instance does not and still adhere to the static metadata constraints of .NET.

In .NET Native there are two mechanisms to address this problem. The first, ICastable interface, proved to have usability issues. Usage of the ICastable API was error prone and had a potentially catastrophic consequence if used incorrectly - silent success or an unstable runtime. The ICastable API exists in the CoreCLR runtime but is not publicly exposed and exists only to support MCG scenarios.

The second approach was CastableObject. This approach didn't return a type but instead returned an actual object instance to dispatch on. The CastableObject type is an abstract type that contained some minor state for caching purposes. This approach did require inserting a new type into the user's type hierarchy. Updating the type hierarchy and the stored state of the abstract type made this solution more reliable, but less performant than ICastable.

For CoreCLR, the following proposal is based on lessons learned from .NET Native along with a recent C# language feature, default interfaces, that make a modified version of the ICastable approach easier to implement with confidence. The proposed interface followed by a usage example are described below.

Goals

  • Support the ComWrappers API in creating C# friendly wrappers for external IUnknown based objects.
  • Avoid having to call QueryInterface() for all possible supported types when an external IUnknown based object enters the runtime.
  • Support IL Linker scenarios.

Non-Goals

  • Remove or alter current ICastable scenarios.

Proposed API

namespace System.Runtime.CompilerServices
{
    /// <summary>
    /// Interface used to participate in a type cast failure.
    /// </summary>
    public interface ICastableObject
    {
        /// <summary>
        /// Called when an implementing class instance is cast to an interface type that
        /// is not contained in the class's metadata.
        /// </summary>
        /// <param name="interfaceType">The interface type.</param>
        /// <param name="throwIfNotFound">Indicates if the function should throw an exception rather than default(RuntimeTypeHandle).</param>
        /// <returns>The type that should be used to dispatch for <paramref name="interfaceType"/> on the current object.</returns>
        /// <remarks>
        /// This is called if casting this object to the given interface type would
        /// otherwise fail. Casting here means the IL isinst and castclass instructions
        /// in the case where they are given an interface type as the target type. This
        /// function may also be called during interface dispatch.
        ///
        /// The returned type must be an interface type marked with the <see cref="CastableObjectImplementationAttribute"/>, otherwise <see cref="System. InvalidOperationException" />
        /// will be thrown. When the <paramref name="throwIfNotFound" /> is set to false,
        /// a return value of default(RuntimeTypeHandle) is permitted. If <paramref name="throwIfNotFound" />
        /// is true and default(RuntimeTypeHandle) is returned then <see cref="System.InvalidCastException" />
        /// will be thrown unless an exception is thrown by the implementation.
        /// </remarks>
        RuntimeTypeHandle GetInterfaceImplementation(RuntimeTypeHandle interfaceType, bool throwIfNotFound);
    }

    /// <summary>
    /// Attribute required by any type that is returned by <see cref="ICastableObject.GetInterfaceImplementation(RuntimeTypeHandle, bool)"/>.
    /// </summary>
    /// <remarks>
    /// This attribute is used to enforce policy in the runtime and make
    /// <see cref="ICastableObject" /> scenarios linker friendly.
    /// </remarks>
    [AttributeUsage(AttributeTargets.Interface, AllowMultiple = false, Inherited = false)]
    public sealed class CastableObjectImplementationAttribute : Attribute
    {
    }
}

Design notes

  • The default interface method itself will be called with a this pointer that by definition should implement the enclosing type (e.g. IFooImpl) as well as any implementing types (e.g. IFoo). We will need to define the exact semantics and requirements here.
  • Virtual Stub Dispatch (VSD) caching.
    • ICastableObject could control casting on a per object basis, as we can easily call GetInterfaceImplementation() at each cast opportunity.
    • ICastableObject would control the result of a dispatching on an interface at a per TYPE level. So, a given type could not use different default interface type (e.g. IFooImpl) for different instances. This would be sufficient for any plausible use of this feature for interop, but it might impact useablility for aspect oriented programming, etc.
    • The end result is that a given object may or may not implement the interface, but if it does, all implementations must be the same.
  • By throwing an exception from the interface impl the debugger will report the exception as coming from that type. We should suggest to users of this API to utilize some debugger attributes to make everything look normal.

Usage Examples

Consider the following interface and class.

interface IFoo
{
    int CallMe(int i);
}

class Baz
{
    ...
}

class Bar : ICastableObject
{
    ...

    // Call when cast is performed on an instance of Bar but the type isn't in Bar's metadata.
    RuntimeTypeHandle ICastableObject.GetInterfaceImplementation(RuntimeTypeHandle interfaceType, bool throwIfNotFound)
    {
        Debug.Assert(interfaceType.Value != IntPtr.Zero);

        if (interfaceType == typeof(IFoo).TypeHandle)
            return typeof(IFooImpl).TypeHandle;
        
        if (throwIfNotFound)
        {
            var typeName = Type.GetTypeFromHandle(interfaceType).FullName;
            throw new InvalidCastException($"Don't support {typeName}");
        }

        return default;
    }

    // An "implemented" interface instance that will handle "this" of type "Bar".
    // Note that when this default interface implementation is called, the "this" will
    // be typed as a "IFooImpl".
    [CastableObjectImplementation]
    public interface IFooImpl : IFoo
    {
        int IFoo.CallMe(int i)
        {
            // Perform desired action on the "this" pointer which will be of type Bar.
            //  - Cast to some other type
            //  - Unsafe.As<T>()
            //  - Table look up
            //  - etc.
            ...
        }
    }
}

The following is an example of usage.

Baz z = ...;

// Will result in InvalidCastException being thrown.
IFoo zf = (IFoo)z;

Bar b = ...;

// However, since Bar implements ICastableObject, GetInterfaceImplementation will be called.
IFoo bf = (IFoo)b;
// bf is a Bar.IFooImpl with a Bar 'this'

// Will call Bar.IFooImpl.CallMe()
bf.CallMe(27);

Community impact

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions