Skip to content

Commit d2fae90

Browse files
authored
Respect IsDynamicCodeSupported in more places in Linq.Expressions (#88539)
* Respect IsDynamicCodeSupported in more places in Linq.Expressions * CanEmitObjectArrayDelegate * CanCreateArbitraryDelegates These properties are all set to `false` when running on NativeAOT, so have them respect RuntimeFeature.IsDynamicCodeSupported. However, CanEmitObjectArrayDelegate needs a work around because DynamicDelegateAugments.CreateObjectArrayDelegate does not exist in CoreClr's System.Private.CoreLib. Allow System.Linq.Expressions to create an object[] delegate using Ref.Emit even though RuntimeFeature.IsDynamicCodeSupported is set to false (ex. using a feature switch). To enable this, add an internal method in CoreLib that temporarily allows the current thread to skip the RuntimeFeature check and allows DynamicMethod instances to be created. When System.Linq.Expressions needs to generate one of these delegates, it calls the internal method through Reflection and continues to use Ref.Emit to generate the delegate. Fix #81803
1 parent f98389c commit d2fae90

File tree

5 files changed

+113
-15
lines changed

5 files changed

+113
-15
lines changed

src/libraries/System.Linq.Expressions/src/System/Dynamic/Utils/DelegateHelpers.cs

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
using System.Diagnostics.CodeAnalysis;
55
using System.Reflection;
66
using System.Reflection.Emit;
7+
using System.Runtime.CompilerServices;
78
using System.Text;
89
using System.Threading;
910

1011
namespace System.Dynamic.Utils
1112
{
1213
internal static class DelegateHelpers
1314
{
14-
// This can be flipped to true using feature switches at publishing time
15+
// This can be flipped to false using feature switches at publishing time
1516
internal static bool CanEmitObjectArrayDelegate => true;
1617

1718
// Separate class so that the it can be trimmed away and doesn't get conflated
@@ -21,14 +22,23 @@ private static class DynamicDelegateLightup
2122
public static Func<Type, Func<object?[], object?>, Delegate> CreateObjectArrayDelegate { get; }
2223
= CreateObjectArrayDelegateInternal();
2324

24-
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern",
25-
Justification = "Works around https://github.com/dotnet/linker/issues/2392")]
2625
private static Func<Type, Func<object?[], object?>, Delegate> CreateObjectArrayDelegateInternal()
2726
=> Type.GetType("Internal.Runtime.Augments.DynamicDelegateAugments")!
2827
.GetMethod("CreateObjectArrayDelegate")!
2928
.CreateDelegate<Func<Type, Func<object?[], object?>, Delegate>>();
3029
}
3130

31+
private static class ForceAllowDynamicCodeLightup
32+
{
33+
public static Func<IDisposable>? ForceAllowDynamicCodeDelegate { get; }
34+
= ForceAllowDynamicCodeDelegateInternal();
35+
36+
private static Func<IDisposable>? ForceAllowDynamicCodeDelegateInternal()
37+
=> typeof(AssemblyBuilder)
38+
.GetMethod("ForceAllowDynamicCode", BindingFlags.NonPublic | BindingFlags.Static)
39+
?.CreateDelegate<Func<IDisposable>>();
40+
}
41+
3242
internal static Delegate CreateObjectArrayDelegate(Type delegateType, Func<object?[], object?> handler)
3343
{
3444
if (CanEmitObjectArrayDelegate)
@@ -186,6 +196,23 @@ private static Delegate CreateObjectArrayDelegateRefEmit(Type delegateType, Func
186196

187197
if (thunkMethod == null)
188198
{
199+
static IDisposable? CreateForceAllowDynamicCodeScope()
200+
{
201+
if (!RuntimeFeature.IsDynamicCodeSupported)
202+
{
203+
// Force 'new DynamicMethod' to not throw even though RuntimeFeature.IsDynamicCodeSupported is false.
204+
// If we are running on a runtime that supports dynamic code, even though the feature switch is off,
205+
// for example when running on CoreClr with PublishAot=true, this will allow IL to be emitted.
206+
// If we are running on a runtime that really doesn't support dynamic code, like NativeAOT,
207+
// CanEmitObjectArrayDelegate will be flipped to 'false', and this method won't be invoked.
208+
return ForceAllowDynamicCodeLightup.ForceAllowDynamicCodeDelegate?.Invoke();
209+
}
210+
211+
return null;
212+
}
213+
214+
using IDisposable? forceAllowDynamicCodeScope = CreateForceAllowDynamicCodeScope();
215+
189216
int thunkIndex = Interlocked.Increment(ref s_ThunksCreated);
190217
Type[] paramTypes = new Type[parameters.Length + 1];
191218
paramTypes[0] = typeof(Func<object[], object>);
@@ -270,8 +297,8 @@ private static Delegate CreateObjectArrayDelegateRefEmit(Type delegateType, Func
270297
ilgen.BeginFinallyBlock();
271298
for (int i = 0; i < parameters.Length; i++)
272299
{
273-
if (parameters[i].ParameterType.IsByRef)
274-
{
300+
if (parameters[i].ParameterType.IsByRef)
301+
{
275302
Type byrefToType = parameters[i].ParameterType.GetElementType()!;
276303

277304
// update parameter

src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/Interpreter/CallInstruction.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ internal abstract partial class CallInstruction : Instruction
1616
/// </summary>
1717
public abstract int ArgumentCount { get; }
1818

19-
private static bool CanCreateArbitraryDelegates => true;
19+
private static bool CanCreateArbitraryDelegates => RuntimeFeature.IsDynamicCodeSupported;
2020

2121
#region Construction
2222

src/libraries/System.Linq.Expressions/tests/CompilerTests.cs

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Reflection;
45
using System.Runtime.CompilerServices;
56
using System.Text.RegularExpressions;
67
using Microsoft.DotNet.RemoteExecutor;
@@ -442,16 +443,51 @@ public static void CompileWorksWhenDynamicCodeNotSupported()
442443

443444
using RemoteInvokeHandle remoteHandle = RemoteExecutor.Invoke(static () =>
444445
{
445-
ParameterExpression param = Expression.Parameter(typeof(int));
446+
CompileWorksWhenDynamicCodeNotSupportedInner();
447+
}, options);
448+
}
446449

447-
Func<int, int> typedDel =
448-
Expression.Lambda<Func<int, int>>(Expression.Add(param, Expression.Constant(4)), param).Compile();
449-
Assert.Equal(304, typedDel(300));
450+
// run the same test code as the above CompileWorksWhenDynamicCodeNotSupported test
451+
// to ensure this test works correctly on all platforms - even if RemoteExecutor is not supported
452+
[Fact]
453+
public static void CompileWorksWhenDynamicCodeNotSupportedInner()
454+
{
455+
ParameterExpression param = Expression.Parameter(typeof(int));
450456

451-
Delegate del =
452-
Expression.Lambda(Expression.Add(param, Expression.Constant(5)), param).Compile();
453-
Assert.Equal(305, del.DynamicInvoke(300));
454-
}, options);
457+
Func<int, int> typedDel =
458+
Expression.Lambda<Func<int, int>>(Expression.Add(param, Expression.Constant(4)), param).Compile();
459+
Assert.Equal(304, typedDel(300));
460+
461+
Delegate del =
462+
Expression.Lambda(Expression.Add(param, Expression.Constant(5)), param).Compile();
463+
Assert.Equal(305, del.DynamicInvoke(300));
464+
465+
// testing more than 2 parameters is important because because it follows a different code path in Compile.
466+
Expression<Func<int, int, int, int, int, int>> fiveParameterExpression = (a, b, c, d, e) => a + b + c + d + e;
467+
Func<int, int, int, int, int, int> fiveParameterFunc = fiveParameterExpression.Compile();
468+
Assert.Equal(6, fiveParameterFunc(2, 2, 1, 1, 0));
469+
470+
Expression<Func<int, int, int>> callExpression = (a, b) => Add(a, b);
471+
Func<int, int, int> callFunc = callExpression.Compile();
472+
Assert.Equal(29, callFunc(20, 9));
473+
474+
MethodCallExpression methodCallExpression = Expression.Call(
475+
instance: null,
476+
typeof(CompilerTests).GetMethod("Add4", BindingFlags.NonPublic | BindingFlags.Static),
477+
Expression.Constant(5), Expression.Constant(6), Expression.Constant(7), Expression.Constant(8));
478+
479+
Func<int> methodCallDelegate = Expression.Lambda<Func<int>>(methodCallExpression).Compile();
480+
Assert.Equal(26, methodCallDelegate());
481+
}
482+
483+
private static int Add(int a, int b)
484+
{
485+
return a + b;
486+
}
487+
488+
private static int Add4(int a, int b, int c, int d)
489+
{
490+
return a + b + c + d;
455491
}
456492
}
457493

src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Descriptors.LibraryBuild.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
<!-- Used by VS4Mac via reflection to symbolize stack traces -->
99
<method name="GetMethodFromNativeIP" />
1010
</type>
11+
<type fullname="System.Reflection.Emit.AssemblyBuilder">
12+
<!-- Used by System.Linq.Expressions via reflection -->
13+
<method name="ForceAllowDynamicCode" />
14+
</type>
1115
<type fullname="System.Runtime.Serialization.SerializationInfo">
1216
<!-- Used by System.Runtime.Serialization.Formatters via reflection -->
1317
<method name="UpdateValue" />

src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/AssemblyBuilder.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ namespace System.Reflection.Emit
99
{
1010
public abstract partial class AssemblyBuilder : Assembly
1111
{
12+
[ThreadStatic]
13+
private static bool t_allowDynamicCode;
14+
1215
protected AssemblyBuilder()
1316
{
1417
}
@@ -81,12 +84,40 @@ public override string[] GetManifestResourceNames() =>
8184

8285
internal static void EnsureDynamicCodeSupported()
8386
{
84-
if (!RuntimeFeature.IsDynamicCodeSupported)
87+
if (!RuntimeFeature.IsDynamicCodeSupported && !t_allowDynamicCode)
8588
{
8689
ThrowDynamicCodeNotSupported();
8790
}
8891
}
8992

93+
/// <summary>
94+
/// Allows dynamic code even though RuntimeFeature.IsDynamicCodeSupported is false.
95+
/// </summary>
96+
/// <returns>An object that, when disposed, will revert allowing dynamic code back to its initial state.</returns>
97+
/// <remarks>
98+
/// This is useful for cases where RuntimeFeature.IsDynamicCodeSupported returns false, but
99+
/// the runtime is still capable of emitting dynamic code. For example, when generating delegates
100+
/// in System.Linq.Expressions while PublishAot=true is set in the project. At debug time, the app
101+
/// uses the non-AOT runtime with the IsDynamicCodeSupported feature switch set to false.
102+
/// </remarks>
103+
internal static IDisposable ForceAllowDynamicCode() => new ForceAllowDynamicCodeScope();
104+
105+
private sealed class ForceAllowDynamicCodeScope : IDisposable
106+
{
107+
private readonly bool _previous;
108+
109+
public ForceAllowDynamicCodeScope()
110+
{
111+
_previous = t_allowDynamicCode;
112+
t_allowDynamicCode = true;
113+
}
114+
115+
public void Dispose()
116+
{
117+
t_allowDynamicCode = _previous;
118+
}
119+
}
120+
90121
private static void ThrowDynamicCodeNotSupported() =>
91122
throw new PlatformNotSupportedException(SR.PlatformNotSupported_ReflectionEmit);
92123
}

0 commit comments

Comments
 (0)