From e6611fd9097db57da54a60539ff10781b0ddb9b5 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 23 Sep 2026 09:11:29 +0200 Subject: [PATCH 1/5] [wasm][R2R] Root interpreter thunks for closed static delegate targets --- .../tools/Common/JitInterface/WasmLowering.cs | 15 ++++++ .../WasmArgumentLayoutTests.cs | 53 +++++++++++++++++++ .../JitInterface/CorInfoImpl.ReadyToRun.cs | 7 +++ .../tests/DelegateInDynamicTests.cs | 30 +++++++++++ 4 files changed, 105 insertions(+) diff --git a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs index 04887906ed4120..a5ff2073d734d2 100644 --- a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs +++ b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs @@ -15,6 +15,21 @@ namespace Internal.JitInterface { public static partial class WasmLowering { + public static MethodSignature GetClosedStaticDelegateTargetSignature(MethodSignature signature) + { + Debug.Assert(!signature.IsStatic && !signature.IsExplicitThis); + Debug.Assert(signature.GenericParameterCount == 0); + + TypeDesc[] arguments = new TypeDesc[signature.Length + 1]; + arguments[0] = signature.Context.GetWellKnownType(WellKnownType.Object); + for (int argumentIndex = 0; argumentIndex < signature.Length; argumentIndex++) + { + arguments[argumentIndex + 1] = signature[argumentIndex]; + } + + return new MethodSignature(MethodSignatureFlags.Static, 0, signature.ReturnType, arguments); + } + public static MethodSignature GetStringCtorActualSignature(MethodSignature signature) { Debug.Assert(signature.Context.GetWellKnownType(WellKnownType.String).GetMethod(".ctor"u8, signature) != null); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/WasmArgumentLayoutTests.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/WasmArgumentLayoutTests.cs index f510b504affc6a..4c145b532aa7b6 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/WasmArgumentLayoutTests.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/WasmArgumentLayoutTests.cs @@ -55,6 +55,59 @@ public WasmArgumentLayoutTests(ITestOutputHelper output) _output = output; } + [Theory] + [InlineData("Byte", false)] + [InlineData("Int16", false)] + [InlineData("Int64", false)] + [InlineData("Byte", true)] + [InlineData("Int16", true)] + [InlineData("Int64", true)] + public void ClosedStaticDelegateSignaturePreservesArgumentLayout(string underlyingType, bool returnsStruct) + { + ReadyToRunCompilerContext context = CreateWasmContext(); + TypeDesc objectType = context.GetWellKnownType(WellKnownType.Object); + DefType nullableType = context.SystemModule.GetType("System"u8, "Nullable`1"u8) + .MakeInstantiatedType(GetSystemType(context, underlyingType)); + TypeDesc returnType = returnsStruct ? MakeAlignedEightBlob(context, 16) : objectType; + MethodSignature invokeSignature = new MethodSignature(0, 0, returnType, [objectType, objectType, nullableType]); + + MethodSignature targetSignature = WasmLowering.GetClosedStaticDelegateTargetSignature(invokeSignature); + Assert.True(targetSignature.IsStatic); + Assert.Same(returnType, targetSignature.ReturnType); + Assert.Equal(4, targetSignature.Length); + Assert.Same(objectType, targetSignature[0]); + for (int argumentIndex = 0; argumentIndex < invokeSignature.Length; argumentIndex++) + { + Assert.Same(invokeSignature[argumentIndex], targetSignature[argumentIndex + 1]); + } + + WasmSignature lowered = WasmLowering.GetSignature(targetSignature, WasmLowering.LoweringFlags.None); + Assert.Equal($"{(returnsStruct ? "S16" : "i")}iiiS{nullableType.InstanceFieldSize.AsInt}p", lowered.SignatureString); + Assert.Equal(returnsStruct ? 7 : 6, lowered.FuncType.Params.Types.Length); + MethodSignature raised = WasmLowering.RaiseSignature(lowered, context); + Assert.Equal(GetArgumentOffsets(context, targetSignature), GetArgumentOffsets(context, raised)); + Assert.Equal(new[] { 0, 8, 16, 24 }, GetArgumentOffsets(context, targetSignature)); + } + + [Theory] + [InlineData(WasmLowering.LoweringFlags.None, "viiS16p", 5)] + [InlineData(WasmLowering.LoweringFlags.HasGenericContextArg, "viiiS16p", 6)] + [InlineData(WasmLowering.LoweringFlags.IsAsyncCall, "vaiiS16p", 6)] + [InlineData(WasmLowering.LoweringFlags.HasGenericContextArg | WasmLowering.LoweringFlags.IsAsyncCall, "viaiiS16p", 7)] + public void ClosedStaticDelegateSignaturePreservesHiddenArguments(WasmLowering.LoweringFlags flags, string expectedSignature, int expectedParameters) + { + ReadyToRunCompilerContext context = CreateWasmContext(); + MethodSignature invokeSignature = new MethodSignature(0, 0, context.GetWellKnownType(WellKnownType.Void), + [context.GetWellKnownType(WellKnownType.Int32), MakeAlignedEightBlob(context, 16)]); + + MethodSignature targetSignature = WasmLowering.GetClosedStaticDelegateTargetSignature(invokeSignature); + WasmSignature lowered = WasmLowering.GetSignature(targetSignature, flags); + + Assert.Equal(expectedSignature, lowered.SignatureString); + Assert.Equal(expectedParameters, lowered.FuncType.Params.Types.Length); + Assert.True(lowered.FuncType.Returns.Types.IsEmpty); + } + public static TheoryData V128Types() { TheoryData data = new(); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index d39f244ad798c2..e0e97b30b9e44d 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -2548,6 +2548,13 @@ private void getCallInfo(ref CORINFO_RESOLVED_TOKEN pResolvedToken, CORINFO_RESO // by virtual resolution during getCallInfo (virtual resolution could find a result using type equivalence) ValidateSafetyOfUsingTypeEquivalenceInSignature(targetMethod.GetTypicalMethodDefinition().Signature); + if (_compilation.NodeFactory.Target.IsWasm && targetMethod.OwningType.IsDelegate && targetMethod.Name == "Invoke"u8) + { + MethodSignature closedStaticSignature = WasmLowering.GetClosedStaticDelegateTargetSignature(targetMethod.Signature); + WasmSignature wasmSignature = WasmLowering.GetSignature(closedStaticSignature, WasmLowering.GetLoweringFlags(targetMethod)); + AddAdditionalDependency(_compilation.NodeFactory.WasmR2RToInterpreterThunk(wasmSignature), "R2R-to-interpreter thunk for closed-static delegate target"); + } + // OK, if the EE said we're not doing a stub dispatch then just return the kind to // the caller. No other kinds of virtual calls have extra information attached. switch (pResult->kind) diff --git a/src/libraries/Microsoft.CSharp/tests/DelegateInDynamicTests.cs b/src/libraries/Microsoft.CSharp/tests/DelegateInDynamicTests.cs index 11f351d0462120..b347db2adc6d0b 100644 --- a/src/libraries/Microsoft.CSharp/tests/DelegateInDynamicTests.cs +++ b/src/libraries/Microsoft.CSharp/tests/DelegateInDynamicTests.cs @@ -2,12 +2,42 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Linq.Expressions; using Xunit; namespace Microsoft.CSharp.RuntimeBinder.Tests { public class DelegateInDynamicTests { + [Theory] + [InlineData(null)] + [InlineData((short)0)] + [InlineData((short)7)] + public void ClosedStaticExpressionDelegateWithNullableArgument(short? value) + { + int bias = 37; + Expression> expression = argument => argument.GetValueOrDefault() + bias; + Func compiled = expression.Compile(); + Func openStatic = ReadNullable; + var holder = new NullableDelegateTarget("target"); + Func closedInstance = holder.Read; + + Assert.Equal(value.GetValueOrDefault() + bias, compiled(value)); + Assert.Equal(value.GetValueOrDefault() + 11, openStatic(value)); + Assert.Equal(value.GetValueOrDefault() + 6, closedInstance(value)); + } + + private static int ReadNullable(short? value) => value.GetValueOrDefault() + 11; + + private sealed class NullableDelegateTarget + { + private readonly T _target; + + public NullableDelegateTarget(T target) => _target = target; + + public int Read(short? value) => value.GetValueOrDefault() + _target.ToString().Length; + } + [Fact] public void DelegateInDynamicExplicitInvoke() { From d5f2f23ab9dc9798c78a03a5a47993fb7189ec9f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 23 Sep 2026 09:51:27 +0200 Subject: [PATCH 2/5] [wasm][R2R] Re-enable Microsoft.CSharp library tests --- src/libraries/tests.proj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/tests.proj b/src/libraries/tests.proj index cc791c928b16f6..7d6babed5fbdf8 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -89,7 +89,6 @@ - From a45bef0aa6da7398bdc4552a0bbb2c023d2c9deb Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Wed, 23 Sep 2026 16:06:38 -0500 Subject: [PATCH 3/5] Preserve closed-static delegate signature flags on Wasm Build the target signature with the delegate Invoke flags, and cover default, varargs, and unmanaged calling conventions with a regression test. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bd5418a9-09c6-4017-a1e1-edb2cf396b39 --- .../tools/Common/JitInterface/WasmLowering.cs | 2 +- .../WasmArgumentLayoutTests.cs | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs index a5ff2073d734d2..d2aeae4e12c266 100644 --- a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs +++ b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs @@ -27,7 +27,7 @@ public static MethodSignature GetClosedStaticDelegateTargetSignature(MethodSigna arguments[argumentIndex + 1] = signature[argumentIndex]; } - return new MethodSignature(MethodSignatureFlags.Static, 0, signature.ReturnType, arguments); + return new MethodSignature(signature.Flags | MethodSignatureFlags.Static, 0, signature.ReturnType, arguments); } public static MethodSignature GetStringCtorActualSignature(MethodSignature signature) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/WasmArgumentLayoutTests.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/WasmArgumentLayoutTests.cs index 4c145b532aa7b6..4d7388a8ba0884 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/WasmArgumentLayoutTests.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/WasmArgumentLayoutTests.cs @@ -108,6 +108,24 @@ public void ClosedStaticDelegateSignaturePreservesHiddenArguments(WasmLowering.L Assert.True(lowered.FuncType.Returns.Types.IsEmpty); } + [Theory] + [InlineData(MethodSignatureFlags.None)] + [InlineData(MethodSignatureFlags.CallingConventionVarargs)] + [InlineData(MethodSignatureFlags.UnmanagedCallingConventionCdecl)] + public void ClosedStaticDelegateSignaturePreservesCallingConvention(MethodSignatureFlags flags) + { + ReadyToRunCompilerContext context = CreateWasmContext(); + TypeDesc objectType = context.GetWellKnownType(WellKnownType.Object); + MethodSignature invokeSignature = new MethodSignature(flags, 0, objectType, [objectType]); + + MethodSignature targetSignature = WasmLowering.GetClosedStaticDelegateTargetSignature(invokeSignature); + + Assert.Equal(flags | MethodSignatureFlags.Static, targetSignature.Flags); + Assert.Same(objectType, targetSignature.ReturnType); + Assert.Same(objectType, targetSignature[0]); + Assert.Same(objectType, targetSignature[1]); + } + public static TheoryData V128Types() { TheoryData data = new(); From ca88d7c5385e8a68a1cb915915fd809d1fc1468c Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 24 Sep 2026 09:59:49 +0200 Subject: [PATCH 4/5] Root closed-static delegate thunk from resolved call signature flags Source the interpreter-thunk lowering flags from the resolved call signature instead of the delegate Invoke MethodDesc, so a shared generic delegate's generic-context hidden argument is reflected, and skip unmanaged calls to match the other call-signature recording paths. --- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index e0e97b30b9e44d..8ebe6c338152fe 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -2550,9 +2550,16 @@ private void getCallInfo(ref CORINFO_RESOLVED_TOKEN pResolvedToken, CORINFO_RESO if (_compilation.NodeFactory.Target.IsWasm && targetMethod.OwningType.IsDelegate && targetMethod.Name == "Invoke"u8) { - MethodSignature closedStaticSignature = WasmLowering.GetClosedStaticDelegateTargetSignature(targetMethod.Signature); - WasmSignature wasmSignature = WasmLowering.GetSignature(closedStaticSignature, WasmLowering.GetLoweringFlags(targetMethod)); - AddAdditionalDependency(_compilation.NodeFactory.WasmR2RToInterpreterThunk(wasmSignature), "R2R-to-interpreter thunk for closed-static delegate target"); + // The hidden-argument flags come from the resolved call signature: a shared generic + // delegate supplies its generic context through 'this', which the Invoke method's own + // instantiation flags do not reflect. + WasmLowering.LoweringFlags loweringFlags = WasmLowering.GetLoweringFlags(&pResult->sig); + if (!loweringFlags.HasFlag(WasmLowering.LoweringFlags.IsUnmanagedCallersOnly)) + { + MethodSignature closedStaticSignature = WasmLowering.GetClosedStaticDelegateTargetSignature(targetMethod.Signature); + WasmSignature wasmSignature = WasmLowering.GetSignature(closedStaticSignature, loweringFlags); + AddAdditionalDependency(_compilation.NodeFactory.WasmR2RToInterpreterThunk(wasmSignature), "R2R-to-interpreter thunk for closed-static delegate target"); + } } // OK, if the EE said we're not doing a stub dispatch then just return the kind to From f1a20ce179cffe983dd7bc439e070d6d9eb9a5a2 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 24 Sep 2026 18:46:59 +0200 Subject: [PATCH 5/5] Assert delegate Invoke uses managed calling convention Delegate Invoke cannot be unmanaged, so assert the invariant and root the closed-static interpreter thunk unconditionally. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 8ebe6c338152fe..b91eb07178b170 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -2554,12 +2554,11 @@ private void getCallInfo(ref CORINFO_RESOLVED_TOKEN pResolvedToken, CORINFO_RESO // delegate supplies its generic context through 'this', which the Invoke method's own // instantiation flags do not reflect. WasmLowering.LoweringFlags loweringFlags = WasmLowering.GetLoweringFlags(&pResult->sig); - if (!loweringFlags.HasFlag(WasmLowering.LoweringFlags.IsUnmanagedCallersOnly)) - { - MethodSignature closedStaticSignature = WasmLowering.GetClosedStaticDelegateTargetSignature(targetMethod.Signature); - WasmSignature wasmSignature = WasmLowering.GetSignature(closedStaticSignature, loweringFlags); - AddAdditionalDependency(_compilation.NodeFactory.WasmR2RToInterpreterThunk(wasmSignature), "R2R-to-interpreter thunk for closed-static delegate target"); - } + Debug.Assert(!loweringFlags.HasFlag(WasmLowering.LoweringFlags.IsUnmanagedCallersOnly)); + + MethodSignature closedStaticSignature = WasmLowering.GetClosedStaticDelegateTargetSignature(targetMethod.Signature); + WasmSignature wasmSignature = WasmLowering.GetSignature(closedStaticSignature, loweringFlags); + AddAdditionalDependency(_compilation.NodeFactory.WasmR2RToInterpreterThunk(wasmSignature), "R2R-to-interpreter thunk for closed-static delegate target"); } // OK, if the EE said we're not doing a stub dispatch then just return the kind to