diff --git a/src/libraries/System.Reflection.DispatchProxy/src/System.Reflection.DispatchProxy.csproj b/src/libraries/System.Reflection.DispatchProxy/src/System.Reflection.DispatchProxy.csproj index 80891e3e0192b..babd7f14e4ed6 100644 --- a/src/libraries/System.Reflection.DispatchProxy/src/System.Reflection.DispatchProxy.csproj +++ b/src/libraries/System.Reflection.DispatchProxy/src/System.Reflection.DispatchProxy.csproj @@ -21,6 +21,7 @@ + diff --git a/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs b/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs index edf5ff293ab5e..b9e3fdac3051a 100644 --- a/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs +++ b/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs @@ -6,7 +6,8 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection.Emit; -using System.Runtime.ExceptionServices; +using System.Runtime.CompilerServices; +using System.Runtime.Loader; using System.Threading; namespace System.Reflection @@ -44,32 +45,18 @@ internal static class DispatchProxyGenerator // It is the first field in the class and the first ctor parameter. private const int MethodInfosFieldAndCtorParameterIndex = 0; - // Proxies are requested for a pair of types: base type and interface type. - // The generated proxy will subclass the given base type and implement the interface type. - // We maintain a cache keyed by 'base type' containing a dictionary keyed by interface type, - // containing the generated proxy type for that pair. There are likely to be few (maybe only 1) - // base type in use for many interface types. - // Note: this differs from Silverlight's RealProxy implementation which keys strictly off the - // interface type. But this does not allow the same interface type to be used with more than a - // single base type. The implementation here permits multiple interface types to be used with - // multiple base types, and the generated proxy types will be unique. - // This cache of generated types grows unbounded, one element per unique T/ProxyT pair. - // This approach is used to prevent regenerating identical proxy types for identical T/Proxy pairs, - // which would ultimately be a more expensive leak. - // Proxy instances are not cached. Their lifetime is entirely owned by the caller of DispatchProxy.Create. - private static readonly Dictionary> s_baseTypeAndInterfaceToGeneratedProxyType = new Dictionary>(); - private static readonly ProxyAssembly s_proxyAssembly = new ProxyAssembly(); + // We group AssemblyBuilders by the ALC of the base type's assembly. + // This allows us to granularly unload generated proxy types. + private static readonly ConditionalWeakTable s_alcProxyAssemblyMap = new(); private static readonly MethodInfo s_dispatchProxyInvokeMethod = typeof(DispatchProxy).GetMethod("Invoke", BindingFlags.NonPublic | BindingFlags.Instance)!; - private static readonly MethodInfo s_getTypeFromHandleMethod = typeof(Type).GetRuntimeMethod("GetTypeFromHandle", new Type[] { typeof(RuntimeTypeHandle) })!; + private static readonly MethodInfo s_getTypeFromHandleMethod = typeof(Type).GetMethod("GetTypeFromHandle", new Type[] { typeof(RuntimeTypeHandle) })!; private static readonly MethodInfo s_makeGenericMethodMethod = GetGenericMethodMethodInfo(); [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "MakeGenericMethod is safe here because the user code invoking the generic method will reference " + "the GenericTypes being used, which will guarantee the requirements of the generic method.")] - private static MethodInfo GetGenericMethodMethodInfo() - { - return typeof(MethodInfo).GetMethod("MakeGenericMethod", new Type[] { typeof(Type[]) })!; - } + private static MethodInfo GetGenericMethodMethodInfo() => + typeof(MethodInfo).GetMethod("MakeGenericMethod", new Type[] { typeof(Type[]) })!; // Returns a new instance of a proxy the derives from 'baseType' and implements 'interfaceType' internal static object CreateProxyInstance( @@ -79,78 +66,12 @@ internal static object CreateProxyInstance( Debug.Assert(baseType != null); Debug.Assert(interfaceType != null); - GeneratedTypeInfo proxiedType = GetProxyType(baseType, interfaceType); - return Activator.CreateInstance(proxiedType.GeneratedType, new object[] { proxiedType.MethodInfos })!; - } - - private static GeneratedTypeInfo GetProxyType( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type baseType, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type interfaceType) - { - lock (s_baseTypeAndInterfaceToGeneratedProxyType) - { - if (!s_baseTypeAndInterfaceToGeneratedProxyType.TryGetValue(baseType, out Dictionary? interfaceToProxy)) - { - interfaceToProxy = new Dictionary(); - s_baseTypeAndInterfaceToGeneratedProxyType[baseType] = interfaceToProxy; - } - - if (!interfaceToProxy.TryGetValue(interfaceType, out GeneratedTypeInfo? generatedProxy)) - { - generatedProxy = GenerateProxyType(baseType, interfaceType); - interfaceToProxy[interfaceType] = generatedProxy; - } - - return generatedProxy; - } - } - - // Unconditionally generates a new proxy type derived from 'baseType' and implements 'interfaceType' - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2062:UnrecognizedReflectionPattern", - Justification = "interfaceType is annotated as preserve All members, so any Types returned from GetInterfaces should be preserved as well once https://github.com/mono/linker/issues/1731 is fixed.")] - private static GeneratedTypeInfo GenerateProxyType( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type baseType, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type interfaceType) - { - // Parameter validation is deferred until the point we need to create the proxy. - // This prevents unnecessary overhead revalidating cached proxy types. - - // The interface type must be an interface, not a class - if (!interfaceType.IsInterface) - { - // "T" is the generic parameter seen via the public contract - throw new ArgumentException(SR.Format(SR.InterfaceType_Must_Be_Interface, interfaceType.FullName), "T"); - } - - // The base type cannot be sealed because the proxy needs to subclass it. - if (baseType.IsSealed) - { - // "TProxy" is the generic parameter seen via the public contract - throw new ArgumentException(SR.Format(SR.BaseType_Cannot_Be_Sealed, baseType.FullName), "TProxy"); - } - - // The base type cannot be abstract - if (baseType.IsAbstract) - { - throw new ArgumentException(SR.Format(SR.BaseType_Cannot_Be_Abstract, baseType.FullName), "TProxy"); - } - - // The base type must have a public default ctor - if (baseType.GetConstructor(Type.EmptyTypes) == null) - { - throw new ArgumentException(SR.Format(SR.BaseType_Must_Have_Default_Ctor, baseType.FullName), "TProxy"); - } - - // Create a type that derives from 'baseType' provided by caller - ProxyBuilder pb = s_proxyAssembly.CreateProxy("generatedProxy", baseType); - - foreach (Type t in interfaceType.GetInterfaces()) - pb.AddInterfaceImpl(t); + AssemblyLoadContext? alc = AssemblyLoadContext.GetLoadContext(baseType.Assembly); + Debug.Assert(alc != null); - pb.AddInterfaceImpl(interfaceType); - - GeneratedTypeInfo generatedProxyType = pb.CreateType(); - return generatedProxyType; + ProxyAssembly proxyAssembly = s_alcProxyAssemblyMap.GetValue(alc, static x => new ProxyAssembly(x)); + GeneratedTypeInfo proxiedType = proxyAssembly.GetProxyType(baseType, interfaceType); + return Activator.CreateInstance(proxiedType.GeneratedType, new object[] { proxiedType.MethodInfos })!; } private sealed class GeneratedTypeInfo @@ -170,16 +91,43 @@ public GeneratedTypeInfo( private sealed class ProxyAssembly { + // Proxies are requested for a pair of types: base type and interface type. + // The generated proxy will subclass the given base type and implement the interface type. + // We maintain a cache keyed by 'base type' containing a dictionary keyed by interface type, + // containing the generated proxy type for that pair. There are likely to be few (maybe only 1) + // base type in use for many interface types. + // Note: this differs from Silverlight's RealProxy implementation which keys strictly off the + // interface type. But this does not allow the same interface type to be used with more than a + // single base type. The implementation here permits multiple interface types to be used with + // multiple base types, and the generated proxy types will be unique. + // This cache of generated types grows unbounded, one element per unique T/ProxyT pair. + // This approach is used to prevent regenerating identical proxy types for identical T/Proxy pairs, + // which would ultimately be a more expensive leak. + // Proxy instances are not cached. Their lifetime is entirely owned by the caller of DispatchProxy.Create. + private readonly Dictionary> _baseTypeAndInterfaceToGeneratedProxyType = new Dictionary>(); + private readonly AssemblyBuilder _ab; private readonly ModuleBuilder _mb; private int _typeId; - private readonly HashSet _ignoresAccessAssemblyNames = new HashSet(); + private readonly HashSet _ignoresAccessAssemblyNames = new HashSet(); private ConstructorInfo? _ignoresAccessChecksToAttributeConstructor; - public ProxyAssembly() + public ProxyAssembly(AssemblyLoadContext alc) { - _ab = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("ProxyBuilder"), AssemblyBuilderAccess.Run); + string name; + if (alc == AssemblyLoadContext.Default) + { + name = "ProxyBuilder"; + } + else + { + string? alcName = alc.Name; + name = string.IsNullOrEmpty(alcName) ? $"DispatchProxyTypes.{alc.GetHashCode()}" : $"DispatchProxyTypes.{alcName}"; + } + AssemblyBuilderAccess builderAccess = + alc.IsCollectible ? AssemblyBuilderAccess.RunAndCollect : AssemblyBuilderAccess.Run; + _ab = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(name), builderAccess); _mb = _ab.DefineDynamicModule("testmod"); } @@ -199,9 +147,79 @@ internal ConstructorInfo IgnoresAccessChecksAttributeConstructor } } + public GeneratedTypeInfo GetProxyType( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type baseType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type interfaceType) + { + lock (_baseTypeAndInterfaceToGeneratedProxyType) + { + if (!_baseTypeAndInterfaceToGeneratedProxyType.TryGetValue(baseType, out Dictionary? interfaceToProxy)) + { + interfaceToProxy = new Dictionary(); + _baseTypeAndInterfaceToGeneratedProxyType[baseType] = interfaceToProxy; + } + + if (!interfaceToProxy.TryGetValue(interfaceType, out GeneratedTypeInfo? generatedProxy)) + { + generatedProxy = GenerateProxyType(baseType, interfaceType); + interfaceToProxy[interfaceType] = generatedProxy; + } + + return generatedProxy; + } + } + + // Unconditionally generates a new proxy type derived from 'baseType' and implements 'interfaceType' + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2062:UnrecognizedReflectionPattern", + Justification = "interfaceType is annotated as preserve All members, so any Types returned from GetInterfaces should be preserved as well once https://github.com/mono/linker/issues/1731 is fixed.")] + private GeneratedTypeInfo GenerateProxyType( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type baseType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type interfaceType) + { + // Parameter validation is deferred until the point we need to create the proxy. + // This prevents unnecessary overhead revalidating cached proxy types. + + // The interface type must be an interface, not a class + if (!interfaceType.IsInterface) + { + // "T" is the generic parameter seen via the public contract + throw new ArgumentException(SR.Format(SR.InterfaceType_Must_Be_Interface, interfaceType.FullName), "T"); + } + + // The base type cannot be sealed because the proxy needs to subclass it. + if (baseType.IsSealed) + { + // "TProxy" is the generic parameter seen via the public contract + throw new ArgumentException(SR.Format(SR.BaseType_Cannot_Be_Sealed, baseType.FullName), "TProxy"); + } + + // The base type cannot be abstract + if (baseType.IsAbstract) + { + throw new ArgumentException(SR.Format(SR.BaseType_Cannot_Be_Abstract, baseType.FullName), "TProxy"); + } + + // The base type must have a public default ctor + if (baseType.GetConstructor(Type.EmptyTypes) == null) + { + throw new ArgumentException(SR.Format(SR.BaseType_Must_Have_Default_Ctor, baseType.FullName), "TProxy"); + } + + // Create a type that derives from 'baseType' provided by caller + ProxyBuilder pb = CreateProxy("generatedProxy", baseType); + + foreach (Type t in interfaceType.GetInterfaces()) + pb.AddInterfaceImpl(t); + + pb.AddInterfaceImpl(interfaceType); + + GeneratedTypeInfo generatedProxyType = pb.CreateType(); + return generatedProxyType; + } + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2067:UnrecognizedReflectionPattern", Justification = "Only the parameterless ctor is referenced on proxyBaseType. Other members can be trimmed if unused.")] - public ProxyBuilder CreateProxy( + private ProxyBuilder CreateProxy( string name, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type proxyBaseType) { @@ -231,10 +249,9 @@ internal void EnsureTypeIsVisible(Type type) if (!type.IsVisible) { string assemblyName = type.Assembly.GetName().Name!; - if (!_ignoresAccessAssemblyNames.Contains(assemblyName)) + if (_ignoresAccessAssemblyNames.Add(assemblyName)) { GenerateInstanceOfIgnoresAccessChecksToAttribute(assemblyName); - _ignoresAccessAssemblyNames.Add(assemblyName); } } } @@ -278,11 +295,11 @@ private void Complete() ILGenerator il = cb.GetILGenerator(); // chained ctor call - ConstructorInfo? baseCtor = _proxyBaseType.GetConstructor(Type.EmptyTypes); + ConstructorInfo baseCtor = _proxyBaseType.GetConstructor(Type.EmptyTypes)!; Debug.Assert(baseCtor != null); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, baseCtor!); + il.Emit(OpCodes.Call, baseCtor); // store all the fields for (int i = 0; i < args.Length; i++) @@ -520,64 +537,7 @@ private MethodBuilder AddMethodImpl(MethodInfo mi, int methodInfoIndex) return mdb; } - // TypeCode does not exist in ProjectK or ProjectN. - // This lookup method was copied from PortableLibraryThunks\Internal\PortableLibraryThunks\System\TypeThunks.cs - // but returns the integer value equivalent to its TypeCode enum. - private static int GetTypeCode(Type? type) - { - if (type == null) - return 0; // TypeCode.Empty; - - if (type == typeof(bool)) - return 3; // TypeCode.Boolean; - - if (type == typeof(char)) - return 4; // TypeCode.Char; - - if (type == typeof(sbyte)) - return 5; // TypeCode.SByte; - - if (type == typeof(byte)) - return 6; // TypeCode.Byte; - - if (type == typeof(short)) - return 7; // TypeCode.Int16; - - if (type == typeof(ushort)) - return 8; // TypeCode.UInt16; - - if (type == typeof(int)) - return 9; // TypeCode.Int32; - - if (type == typeof(uint)) - return 10; // TypeCode.UInt32; - - if (type == typeof(long)) - return 11; // TypeCode.Int64; - - if (type == typeof(ulong)) - return 12; // TypeCode.UInt64; - - if (type == typeof(float)) - return 13; // TypeCode.Single; - - if (type == typeof(double)) - return 14; // TypeCode.Double; - - if (type == typeof(decimal)) - return 15; // TypeCode.Decimal; - - if (type == typeof(DateTime)) - return 16; // TypeCode.DateTime; - - if (type == typeof(string)) - return 18; // TypeCode.String; - - if (type.IsEnum) - return GetTypeCode(Enum.GetUnderlyingType(type)); - - return 1; // TypeCode.Object; - } + private static int GetTypeCode(Type type) => (int)Type.GetTypeCode(type); private static readonly OpCode[] s_convOpCodes = new OpCode[] { OpCodes.Nop, //Empty = 0, diff --git a/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs b/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs index cb5edc26d5d5f..9118e69a2a586 100644 --- a/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs +++ b/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs @@ -6,6 +6,8 @@ using System.Linq; using System.Reflection; using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using System.Runtime.Loader; using System.Text; using Xunit; @@ -622,5 +624,61 @@ private static void testRefOutInInvocation(Action invocation, Assert.Equal(expected, result); } + + private static TestType_IHelloService CreateTestHelloProxy() => + DispatchProxy.Create(); + + [Fact] + public static void Test_Unloadability() + { + if (typeof(DispatchProxyTests).Assembly.Location == "") + return; + + WeakReference wr = CreateProxyInUnloadableAlc(); + + for (int i = 0; i < 10 && wr.IsAlive; i++) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + + Assert.False(wr.IsAlive, "The ALC could not be unloaded."); + + [MethodImpl(MethodImplOptions.NoInlining)] + static WeakReference CreateProxyInUnloadableAlc() + { + var alc = new AssemblyLoadContext(nameof(Test_Unloadability), true); + alc.LoadFromAssemblyPath(typeof(DispatchProxyTests).Assembly.Location) + .GetType(typeof(DispatchProxyTests).FullName, true) + .GetMethod(nameof(CreateTestHelloProxy), BindingFlags.Static | BindingFlags.NonPublic) + .Invoke(null, null); + return new WeakReference(alc); + } + } + + [Fact] + public static void Test_Multiple_AssemblyLoadContexts() + { + if (typeof(DispatchProxyTests).Assembly.Location == "") + return; + + object proxyDefaultAlc = CreateTestDispatchProxy(typeof(TestDispatchProxy)); + Assert.True(proxyDefaultAlc.GetType().IsAssignableTo(typeof(TestDispatchProxy))); + + Type proxyCustomAlcType = + new AssemblyLoadContext(nameof(Test_Multiple_AssemblyLoadContexts)) + .LoadFromAssemblyPath(typeof(DispatchProxyTests).Assembly.Location) + .GetType(typeof(TestDispatchProxy).FullName, true); + + object proxyCustomAlc = CreateTestDispatchProxy(proxyCustomAlcType); + Assert.True(proxyCustomAlc.GetType().IsAssignableTo(proxyCustomAlcType)); + + static object CreateTestDispatchProxy(Type type) => + typeof(DispatchProxy) + .GetMethod(nameof(DispatchProxy.Create)) + // It has to be a type shared in both ALCs. + .MakeGenericMethod(typeof(IDisposable), type) + .Invoke(null, null); + } } }