File: System\Runtime\InteropServices\Marshalling\ComImportInteropInterfaceDetailsStrategy.cs
Web Access
Project: src\src\libraries\System.Runtime.InteropServices\src\System.Runtime.InteropServices.csproj (System.Runtime.InteropServices)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
 
namespace System.Runtime.InteropServices.Marshalling
{
    /// <summary>
    /// An interface details strategy that enables discovering both interfaces defined with source-generated COM (i.e. <see cref="GeneratedComInterfaceAttribute"/> and <see cref="IUnknownDerivedAttribute{T, TImpl}"/>) and built-in COM (i.e. <see cref="ComImportAttribute"/>).
    /// </summary>
    /// <remarks>
    /// This strategy is meant for intermediary adoption scenarios and is not compatible with trimming or NativeAOT by design. Since built-in COM is not trim friendly or AOT-compatible, these restrictions are okay.
    /// This strategy only supports "COM Object Wrapper" scenarios, so casting a COM object wrapper to a <see cref="ComImportAttribute"/>-attributed type. It does not support exposing a <see cref="ComImportAttribute"/>-attributed type as an additional interface on a managed object wrapper.
    /// The strategy provides <see cref="DynamicInterfaceCastableImplementationAttribute"/>-based implementations of <see cref="ComImportAttribute"/>-attributed interfaces by dynamically generating an interface using <see cref="System.Reflection.Emit"/> that has the following shape:
    /// <code>
    /// [assembly:IgnoresAccessChecksTo("AssemblyContainingIComInterface")]
    /// [assembly:IgnoresAccessChecksTo("AssemblyContainingRetType")]
    /// [assembly:IgnoresAccessChecksTo("AssemblyContainingArgType1")]
    /// [assembly:IgnoresAccessChecksTo("AssemblyContainingArgType2")]
    /// // One attribute per containing assembly of each type used in each method signature of the interface.
    ///
    /// namespace System.Runtime.CompilerServices
    /// {
    ///     [AssemblyUsage(AttributeTargets.Assembly, AllowMultiple = true)]
    ///     internal class IgnoresAccessChecksToAttribute : Attribute
    ///     {
    ///         public IgnoresAccessChecksToAttribute(string assemblyName) { }
    ///     }
    /// }
    ///
    /// [DynamicInterfaceCastableImplementation]
    /// interface InterfaceForwarder : IComInterface
    /// {
    ///     RetType IComInterface.Method1(ArgType1 arg1, ArgType2 arg2, ...)
    ///     {
    ///         return ((IComInterface)((IComImportAdapter)this).GetRuntimeCallableWrapper())(arg1, arg2, ...);
    ///     }
    /// }
    /// </code>
    ///
    /// This mechanism allows source-generated COM interop to allow using built-in COM interfaces with runtime-defined marshalling behavior with minimal work on the source-generated COM interop side.
    /// Additionally, by scoping the majority of the logic to this class, we make this logic more easily trimmable.
    ///
    /// We emit the <c>IgnoresAccessChecksToAttribute</c> to enable casting to internal <see cref="ComImportAttribute"/> types, which is a very common scenario (most <see cref="ComImportAttribute"/> types are internal).
    /// </remarks>
    [RequiresDynamicCode("Enabling interop between source-generated and built-in COM is not supported when trimming is enabled.")]
    [RequiresUnreferencedCode("Enabling interop between source-generated and built-in COM requires dynamic code generation.")]
    internal sealed class ComImportInteropInterfaceDetailsStrategy : IIUnknownInterfaceDetailsStrategy
    {
        public static readonly IIUnknownInterfaceDetailsStrategy Instance = new ComImportInteropInterfaceDetailsStrategy();
 
        private readonly ConditionalWeakTable<Type, Type> _forwarderInterfaceCache = new();
 
        // TODO: Support exposing ComImport interfaces through StrategyBasedComWrappers?
        public IComExposedDetails? GetComExposedTypeDetails(RuntimeTypeHandle type) => DefaultIUnknownInterfaceDetailsStrategy.Instance.GetComExposedTypeDetails(type);
 
        public IIUnknownDerivedDetails? GetIUnknownDerivedDetails(RuntimeTypeHandle type)
        {
            Type runtimeType = Type.GetTypeFromHandle(type)!;
            if (!runtimeType.IsImport)
            {
                return DefaultIUnknownInterfaceDetailsStrategy.Instance.GetIUnknownDerivedDetails(type);
            }
 
            Type implementationType = _forwarderInterfaceCache.GetValue(runtimeType, runtimeType =>
            {
                AssemblyBuilder assembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("ComImportForwarder"), runtimeType.IsCollectible ? AssemblyBuilderAccess.RunAndCollect : AssemblyBuilderAccess.Run);
                ModuleBuilder module = assembly.DefineDynamicModule("ComImportForwarder");
 
                ConstructorInfo ignoresAccessChecksToAttributeConstructor = GetIgnoresAccessChecksToAttributeConstructor(module);
 
                assembly.SetCustomAttribute(new CustomAttributeBuilder(ignoresAccessChecksToAttributeConstructor, new object[] { typeof(IComImportAdapter).Assembly.GetName().Name! }));
 
                TypeBuilder implementation = module.DefineType("InterfaceForwarder", TypeAttributes.Interface | TypeAttributes.Abstract, parent: null, interfaces: runtimeType.GetInterfaces());
                implementation.AddInterfaceImplementation(runtimeType);
                implementation.SetCustomAttribute(new CustomAttributeBuilder(typeof(DynamicInterfaceCastableImplementationAttribute).GetConstructor(Array.Empty<Type>())!, Array.Empty<object>()));
 
                foreach (Type iface in implementation.GetInterfaces())
                {
                    assembly.SetCustomAttribute(new CustomAttributeBuilder(ignoresAccessChecksToAttributeConstructor, new object[] { iface.Assembly.GetName().Name! }));
                    foreach (MethodInfo method in iface.GetMethods())
                    {
                        Type[] returnTypeOptionalModifiers = method.ReturnParameter.GetOptionalCustomModifiers();
                        Type[] returnTypeRequiredModifiers = method.ReturnParameter.GetRequiredCustomModifiers();
                        ParameterInfo[] parameters = method.GetParameters();
                        var parameterTypes = new Type[parameters.Length];
                        var parameterOptionalModifiers = new Type[parameters.Length][];
                        var parameterRequiredModifiers = new Type[parameters.Length][];
                        for (int i = 0; i < parameters.Length; i++)
                        {
                            parameterTypes[i] = parameters[i].ParameterType;
                            parameterOptionalModifiers[i] = parameters[i].GetOptionalCustomModifiers();
                            parameterRequiredModifiers[i] = parameters[i].GetRequiredCustomModifiers();
                        }
                        MethodBuilder builder = implementation.DefineMethod(method.Name, MethodAttributes.Private | MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.Virtual, CallingConventions.HasThis, method.ReturnType, returnTypeRequiredModifiers, returnTypeOptionalModifiers, parameterTypes, parameterRequiredModifiers, parameterOptionalModifiers);
                        ILGenerator il = builder.GetILGenerator();
                        il.Emit(OpCodes.Ldarg_0);
                        il.Emit(OpCodes.Castclass, typeof(IComImportAdapter));
                        il.Emit(OpCodes.Callvirt, IComImportAdapter.GetRuntimeCallableWrapperMethod);
                        il.Emit(OpCodes.Castclass, iface);
                        for (int i = 0; i < parameters.Length; i++)
                        {
                            il.Emit(OpCodes.Ldarg, i + 1);
                        }
                        il.Emit(OpCodes.Callvirt, method);
                        il.Emit(OpCodes.Ret);
                        implementation.DefineMethodOverride(builder, method);
                    }
                }
 
                return implementation.CreateType();
            });
 
            return new ComImportDetails(runtimeType.GUID, implementationType);
        }
 
        private static ConstructorInfo GetIgnoresAccessChecksToAttributeConstructor(ModuleBuilder moduleBuilder)
        {
            Type attributeType = EmitIgnoresAccessChecksToAttribute(moduleBuilder);
            return attributeType.GetConstructor(new Type[] { typeof(string) })!;
        }
 
        [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
        private static Type EmitIgnoresAccessChecksToAttribute(ModuleBuilder moduleBuilder)
        {
            var tb = moduleBuilder.DefineType(
                "System.Runtime.CompilerServices.IgnoresAccessChecksToAttribute",
                TypeAttributes.NotPublic,
                typeof(Attribute));
 
            var attributeUsage = new CustomAttributeBuilder(
                s_attributeUsageCtor,
                new object[] { AttributeTargets.Assembly },
                new PropertyInfo[] { s_attributeUsageAllowMultipleProperty },
                new object[] { true });
            tb.SetCustomAttribute(attributeUsage);
 
            var cb = tb.DefineConstructor(
                MethodAttributes.Public |
                MethodAttributes.HideBySig |
                MethodAttributes.SpecialName |
                MethodAttributes.RTSpecialName,
                CallingConventions.Standard,
                new Type[] { typeof(string) });
            cb.DefineParameter(1, ParameterAttributes.None, "assemblyName");
 
            var il = cb.GetILGenerator();
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Call, s_attributeBaseClassCtor);
            il.Emit(OpCodes.Ret);
 
            return tb.CreateType()!;
        }
 
        /// <summary>
        /// The <see cref="Attribute()"/> constructor.
        /// </summary>
        private static readonly ConstructorInfo s_attributeBaseClassCtor = typeof(Attribute).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)[0];
 
        /// <summary>
        /// The <see cref="AttributeUsageAttribute(AttributeTargets)"/> constructor.
        /// </summary>
        private static readonly ConstructorInfo s_attributeUsageCtor = typeof(AttributeUsageAttribute).GetConstructor(new Type[] { typeof(AttributeTargets) })!;
 
        /// <summary>
        /// The <see cref="AttributeUsageAttribute.AllowMultiple"/> property.
        /// </summary>
        private static readonly PropertyInfo s_attributeUsageAllowMultipleProperty = typeof(AttributeUsageAttribute).GetProperty(nameof(AttributeUsageAttribute.AllowMultiple))!;
 
        private sealed class ComImportDetails(Guid iid, Type implementation) : IIUnknownDerivedDetails
        {
            public Guid Iid { get; } = iid;
 
            public Type Implementation { get; } = implementation;
 
            public unsafe void** ManagedVirtualMethodTable => null;
        }
 
        /// <summary>
        /// This interface enables a COM Object Wrapper (such as <see cref="ComObject"/>) to provide a built-in COM object to enable integration between built-in COM objects and
        /// other COM interop systems like source-generated COM.
        /// </summary>
        internal interface IComImportAdapter
        {
            internal static readonly MethodInfo GetRuntimeCallableWrapperMethod = typeof(IComImportAdapter).GetMethod(nameof(GetRuntimeCallableWrapper))!;
 
            /// <summary>
            /// Gets the built-in COM object that corresponds to the same underlying COM object as this wrapper.
            /// </summary>
            /// <returns>The built-in RCW</returns>
            /// <remarks>The returned object must be an object such that a call to <see cref="Marshal.IsComObject(object)"/> would return true.</remarks>
            object GetRuntimeCallableWrapper();
        }
    }
}