Skip to content

Commit ff8393d

Browse files
Consider targets of delegates reflection-visible (#70198)
This is to make sure `Delegate.GetMethodInfo` API works in `IlcTrimMetadata=true` mode. In this mode, the presence of code doesn't automatically mean the method is visible to reflection (we only consider results of dataflow analysis or XML inputs, or `DynamicDependency`). Add delegate targets to this list so that `GetMethodInfo` API reliably works. The compiler already tracks delegate creation sequence, so adding a callback to `MetadataManager` to inject the dependencies that make the method reflection-visible. We also handle the situation when the delegate was created to a virtual method and the exact target isn't known until runtime. We do this by injecting conditional dependencies on virtual method implementations. This change causes a 0.3% size regression on ASP.NET WebApi template with IlcTrimMetadata=true. I spot checked the diffs and they all look correct (there's a lot of delegates being created to support various captures and suddenly those things become reflectable - it's what we want).
1 parent 625a9cd commit ff8393d

File tree

7 files changed

+191
-4
lines changed

7 files changed

+191
-4
lines changed

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DelegateCreationInfo.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,16 @@ public MethodDesc TargetMethod
5050
}
5151
}
5252

53+
// The target method might be constrained if this was a "constrained ldftn" IL instruction.
54+
// The real target can be computed after resolving the constraint.
55+
public MethodDesc PossiblyUnresolvedTargetMethod
56+
{
57+
get
58+
{
59+
return _targetMethod;
60+
}
61+
}
62+
5363
private bool TargetMethodIsUnboxingThunk
5464
{
5565
get

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/EETypeNode.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,8 @@ public sealed override IEnumerable<CombinedDependencyListEntry> GetConditionalSt
344344
{
345345
factory.MetadataManager.NoteOverridingMethod(decl, impl);
346346
}
347+
348+
factory.MetadataManager.GetDependenciesForOverridingMethod(ref result, factory, decl, impl);
347349
}
348350

349351
Debug.Assert(
@@ -389,6 +391,8 @@ public sealed override IEnumerable<CombinedDependencyListEntry> GetConditionalSt
389391
}
390392

391393
factory.MetadataManager.NoteOverridingMethod(interfaceMethod, implMethod);
394+
395+
factory.MetadataManager.GetDependenciesForOverridingMethod(ref result, factory, interfaceMethod, implMethod);
392396
}
393397
else
394398
{
@@ -414,6 +418,8 @@ public sealed override IEnumerable<CombinedDependencyListEntry> GetConditionalSt
414418
result.Add(new CombinedDependencyListEntry(factory.MethodEntrypoint(defaultIntfMethod), factory.VirtualMethodUse(interfaceMethod), "Interface method"));
415419

416420
factory.MetadataManager.NoteOverridingMethod(interfaceMethod, implMethod);
421+
422+
factory.MetadataManager.GetDependenciesForOverridingMethod(ref result, factory, interfaceMethod, implMethod);
417423
}
418424
}
419425
}

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ReadyToRunGenericHelperNode.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,12 @@ protected override DependencyList ComputeNonRelocationBasedDependencies(NodeFact
215215
dependencies.Add(new DependencyListEntry(dependency, "GenericLookupResultDependency"));
216216
}
217217

218+
if (_id == ReadyToRunHelperId.DelegateCtor)
219+
{
220+
MethodDesc targetMethod = ((DelegateCreationInfo)_target).PossiblyUnresolvedTargetMethod.GetCanonMethodTarget(CanonicalFormKind.Specific);
221+
factory.MetadataManager.GetDependenciesDueToDelegateCreation(ref dependencies, factory, targetMethod);
222+
}
223+
218224
return dependencies;
219225
}
220226

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ReadyToRunHelperNode.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,23 +142,27 @@ protected override DependencyList ComputeNonRelocationBasedDependencies(NodeFact
142142
}
143143
else if (_id == ReadyToRunHelperId.DelegateCtor)
144144
{
145+
DependencyList dependencyList = null;
146+
145147
var info = (DelegateCreationInfo)_target;
146148
if (info.NeedsVirtualMethodUseTracking)
147149
{
148150
MethodDesc targetMethod = info.TargetMethod;
149151

150-
DependencyList dependencyList = new DependencyList();
151152
#if !SUPPORT_JIT
152153
factory.MetadataManager.GetDependenciesDueToVirtualMethodReflectability(ref dependencyList, factory, targetMethod);
153154

154155
if (!factory.VTable(info.TargetMethod.OwningType).HasFixedSlots)
155156
{
157+
dependencyList ??= new DependencyList();
156158
dependencyList.Add(factory.VirtualMethodUse(info.TargetMethod), "ReadyToRun Delegate to virtual method");
157159
}
158160
#endif
159-
160-
return dependencyList;
161161
}
162+
163+
factory.MetadataManager.GetDependenciesDueToDelegateCreation(ref dependencyList, factory, info.PossiblyUnresolvedTargetMethod);
164+
165+
return dependencyList;
162166
}
163167

164168
return null;

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/MetadataManager.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,22 @@ public virtual void GetDependenciesDueToLdToken(ref DependencyList dependencies,
430430
// RuntimeFieldHandle data structure.
431431
}
432432

433+
/// <summary>
434+
/// This method is an extension point that can provide additional metadata-based dependencies to delegate targets.
435+
/// </summary>
436+
public virtual void GetDependenciesDueToDelegateCreation(ref DependencyList dependencies, NodeFactory factory, MethodDesc target)
437+
{
438+
// MetadataManagers can override this to provide additional dependencies caused by the construction
439+
// of a delegate to a method.
440+
}
441+
442+
/// <summary>
443+
/// This method is an extension point that can provide additional dependencies for overriden methods on constructed types.
444+
/// </summary>
445+
public virtual void GetDependenciesForOverridingMethod(ref CombinedDependencyList dependencies, NodeFactory factory, MethodDesc decl, MethodDesc impl)
446+
{
447+
}
448+
433449
/// <summary>
434450
/// This method is an extension point that can provide additional metadata-based dependencies to generated method bodies.
435451
/// </summary>

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/UsageBasedMetadataManager.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,46 @@ public override void GetDependenciesDueToLdToken(ref DependencyList dependencies
456456
dependencies.Add(factory.ReflectableMethod(method), "LDTOKEN method");
457457
}
458458

459+
public override void GetDependenciesDueToDelegateCreation(ref DependencyList dependencies, NodeFactory factory, MethodDesc target)
460+
{
461+
if (!IsReflectionBlocked(target))
462+
{
463+
dependencies = dependencies ?? new DependencyList();
464+
dependencies.Add(factory.ReflectableMethod(target), "Target of a delegate");
465+
}
466+
}
467+
468+
public override void GetDependenciesForOverridingMethod(ref CombinedDependencyList dependencies, NodeFactory factory, MethodDesc decl, MethodDesc impl)
469+
{
470+
Debug.Assert(decl.IsVirtual && MetadataVirtualMethodAlgorithm.FindSlotDefiningMethodForVirtualMethod(decl) == decl);
471+
472+
// If a virtual method slot is reflection visible, all implementations become reflection visible.
473+
//
474+
// We could technically come up with a weaker position on this because the code below just needs to
475+
// to ensure that delegates to virtual methods can have their GetMethodInfo() called.
476+
// Delegate construction introduces a ReflectableMethod for the slot defining method; it doesn't need to.
477+
// We could have a specialized node type to track that specific thing and introduce a conditional dependency
478+
// on that.
479+
//
480+
// class Base { abstract Boo(); }
481+
// class Derived1 : Base { override Boo() { } }
482+
// class Derived2 : Base { override Boo() { } }
483+
//
484+
// typeof(Derived2).GetMethods(...)
485+
//
486+
// In the above case, we don't really need Derived1.Boo to become reflection visible
487+
// but the below code will do that because ReflectableMethodNode tracks all reflectable methods,
488+
// without keeping information about subtleities like "reflectable delegate".
489+
if (!IsReflectionBlocked(decl) && !IsReflectionBlocked(impl))
490+
{
491+
dependencies ??= new CombinedDependencyList();
492+
dependencies.Add(new DependencyNodeCore<NodeFactory>.CombinedDependencyListEntry(
493+
factory.ReflectableMethod(impl.GetCanonMethodTarget(CanonicalFormKind.Specific)),
494+
factory.ReflectableMethod(decl.GetCanonMethodTarget(CanonicalFormKind.Specific)),
495+
"Virtual method declaration is reflectable"));
496+
}
497+
}
498+
459499
protected override void GetDependenciesDueToMethodCodePresenceInternal(ref DependencyList dependencies, NodeFactory factory, MethodDesc method, MethodIL methodIL)
460500
{
461501
bool scanReflection = (_generationOptions & UsageBasedMetadataGenerationOptions.ReflectionILScanning) != 0;

src/tests/nativeaot/SmokeTests/Reflection/Reflection.cs

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
[assembly: TestAssembly]
1717
[module: TestModule]
1818

19-
internal class ReflectionTest
19+
internal static class ReflectionTest
2020
{
2121
private static int Main()
2222
{
@@ -27,6 +27,8 @@ private static int Main()
2727
//
2828
// Tests for dependency graph in the compiler
2929
//
30+
TestSimpleDelegateTargets.Run();
31+
TestVirtualDelegateTargets.Run();
3032
TestRunClassConstructor.Run();
3133
#if !OPTIMIZED_MODE_WITHOUT_SCANNER
3234
TestContainment.Run();
@@ -1656,6 +1658,104 @@ public static void Run()
16561658
}
16571659
}
16581660

1661+
class TestSimpleDelegateTargets
1662+
{
1663+
class TestClass
1664+
{
1665+
public static void StaticMethod() { }
1666+
public void InstanceMethod() { }
1667+
public static void SimplyCalledMethod() { }
1668+
}
1669+
1670+
class TestClass<T>
1671+
{
1672+
public static void StaticMethod() { }
1673+
}
1674+
1675+
static void CheckGeneric<T>()
1676+
{
1677+
Action staticMethod = TestClass<T>.StaticMethod;
1678+
if (staticMethod.GetMethodInfo().Name != nameof(TestClass<T>.StaticMethod))
1679+
throw new Exception();
1680+
}
1681+
1682+
public static void Run()
1683+
{
1684+
Console.WriteLine("Testing delegate targets are reflectable...");
1685+
1686+
Action staticMethod = TestClass.StaticMethod;
1687+
if (staticMethod.GetMethodInfo().Name != nameof(TestClass.StaticMethod))
1688+
throw new Exception();
1689+
1690+
Action instanceMethod = new TestClass().InstanceMethod;
1691+
if (instanceMethod.GetMethodInfo().Name != nameof(TestClass.InstanceMethod))
1692+
throw new Exception();
1693+
1694+
TestClass.SimplyCalledMethod();
1695+
1696+
Assert.Equal(
1697+
#if REFLECTION_FROM_USAGE
1698+
3,
1699+
#else
1700+
2,
1701+
#endif
1702+
typeof(TestClass).CountMethods());
1703+
1704+
CheckGeneric<object>();
1705+
}
1706+
}
1707+
1708+
class TestVirtualDelegateTargets
1709+
{
1710+
abstract class Base
1711+
{
1712+
public virtual void VirtualMethod() { }
1713+
public abstract void AbstractMethod();
1714+
}
1715+
1716+
class Derived : Base, IBar
1717+
{
1718+
public override void AbstractMethod() { }
1719+
public override void VirtualMethod() { }
1720+
void IFoo.InterfaceMethod() { }
1721+
}
1722+
1723+
interface IFoo
1724+
{
1725+
void InterfaceMethod();
1726+
void DefaultInterfaceMethod() { }
1727+
}
1728+
1729+
interface IBar : IFoo
1730+
{
1731+
void IFoo.DefaultInterfaceMethod() { }
1732+
}
1733+
1734+
static Base s_baseInstance = new Derived();
1735+
static IFoo s_ifooInstance = new Derived();
1736+
1737+
public static void Run()
1738+
{
1739+
Console.WriteLine("Testing virtual delegate targets are reflectable...");
1740+
1741+
Action abstractMethod = s_baseInstance.AbstractMethod;
1742+
if (abstractMethod.GetMethodInfo().Name != nameof(Derived.AbstractMethod))
1743+
throw new Exception();
1744+
1745+
Action virtualMethod = s_baseInstance.VirtualMethod;
1746+
if (virtualMethod.GetMethodInfo().Name != nameof(Derived.VirtualMethod))
1747+
throw new Exception();
1748+
1749+
Action interfaceMethod = s_ifooInstance.InterfaceMethod;
1750+
if (!interfaceMethod.GetMethodInfo().Name.EndsWith("IFoo.InterfaceMethod"))
1751+
throw new Exception();
1752+
1753+
Action defaultMethod = s_ifooInstance.DefaultInterfaceMethod;
1754+
if (!defaultMethod.GetMethodInfo().Name.EndsWith("IFoo.DefaultInterfaceMethod"))
1755+
throw new Exception();
1756+
}
1757+
}
1758+
16591759
class TestRunClassConstructor
16601760
{
16611761
static class TypeWithNoStaticFieldsButACCtor
@@ -1709,6 +1809,11 @@ private static bool HasTypeHandle(Type type)
17091809
return true;
17101810
}
17111811

1812+
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern",
1813+
Justification = "That's the point")]
1814+
public static int CountMethods(this Type t)
1815+
=> t.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly).Length;
1816+
17121817
class Assert
17131818
{
17141819
public static void Equal<T>(T expected, T actual)

0 commit comments

Comments
 (0)