Skip to content

Commit

Permalink
Support unloadability in DispatchProxy. (#62095)
Browse files Browse the repository at this point in the history
* Use separate ProxyAssemblies per ALC in DispatchProxyGenerator.
  • Loading branch information
teo-tsirpanis authored Dec 3, 2021
1 parent aec1f69 commit 2f0341d
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 158 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<Reference Include="System.Reflection.Primitives" />
<Reference Include="System.Resources.ResourceManager" />
<Reference Include="System.Runtime" />
<Reference Include="System.Runtime.Loader" />
<Reference Include="System.Threading" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Type, Dictionary<Type, GeneratedTypeInfo>> s_baseTypeAndInterfaceToGeneratedProxyType = new Dictionary<Type, Dictionary<Type, GeneratedTypeInfo>>();
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<AssemblyLoadContext, ProxyAssembly> 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(
Expand All @@ -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<Type, GeneratedTypeInfo>? interfaceToProxy))
{
interfaceToProxy = new Dictionary<Type, GeneratedTypeInfo>();
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
Expand All @@ -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<Type, Dictionary<Type, GeneratedTypeInfo>> _baseTypeAndInterfaceToGeneratedProxyType = new Dictionary<Type, Dictionary<Type, GeneratedTypeInfo>>();

private readonly AssemblyBuilder _ab;
private readonly ModuleBuilder _mb;
private int _typeId;

private readonly HashSet<string?> _ignoresAccessAssemblyNames = new HashSet<string?>();
private readonly HashSet<string> _ignoresAccessAssemblyNames = new HashSet<string>();
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");
}

Expand All @@ -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<Type, GeneratedTypeInfo>? interfaceToProxy))
{
interfaceToProxy = new Dictionary<Type, GeneratedTypeInfo>();
_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)
{
Expand Down Expand Up @@ -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);
}
}
}
Expand Down Expand Up @@ -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++)
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 2f0341d

Please sign in to comment.