From 28fbc2b01f79dd00ca4c51316e520787016fd77c Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Sun, 27 Sep 2026 12:13:02 -0500 Subject: [PATCH] [interpreter] Resolve open virtual delegates invoked through the shuffle thunk (wasm, Apple mobile) (#134699) (cherry picked from commit 01a32f307cf916d453fe8d6203eef498fd31fa45) --- src/coreclr/vm/comdelegate.cpp | 2 + src/coreclr/vm/interpexec.cpp | 38 +++++++++++++++---- .../System/DelegateTests.cs | 26 +++++++++++++ 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/coreclr/vm/comdelegate.cpp b/src/coreclr/vm/comdelegate.cpp index dc7f140946e6a8..76af73921ec6e0 100644 --- a/src/coreclr/vm/comdelegate.cpp +++ b/src/coreclr/vm/comdelegate.cpp @@ -845,6 +845,8 @@ static PCODE CreateILDelegateShuffleThunk(MethodDesc* pDelegateMD, bool callTarg pCode->EmitLoadThis(); pCode->EmitLDFLD(pCode->GetToken(CoreLibBinder::GetField(FIELD__DELEGATE__METHOD_PTR_AUX))); + // TODO: For open virtual delegates, _methodPtrAux is a dispatch stub that expects the address of _methodPtrAux in a + // hidden argument, which calli does not pass: https://github.com/dotnet/runtime/issues/134733 pCode->EmitCALLI(TOKEN_ILSTUB_TARGET_SIG, sig.NumFixedArgs(), sig.IsReturnTypeVoid() ? 0 : 1); pCode->EmitRET(); diff --git a/src/coreclr/vm/interpexec.cpp b/src/coreclr/vm/interpexec.cpp index ae2b5d51edc5e1..154878e4352bb7 100644 --- a/src/coreclr/vm/interpexec.cpp +++ b/src/coreclr/vm/interpexec.cpp @@ -1332,6 +1332,18 @@ static void ShiftDelegateCallArgs(int8_t* stack, int32_t callArgsOffset, int32_t } } +// Resolves the target of an open virtual delegate for the given 'this' argument. +static MethodDesc* ResolveOpenVirtualDelegateTarget(DELEGATEREF delegateObj, OBJECTREF* pThisArg) +{ + MethodDesc* pDeclMD = COMDelegate::GetMethodDescForOpenVirtualDelegate(delegateObj); + return CallWithSEHWrapper( + [pDeclMD, pThisArg]() { + MethodTable* pMT = (*pThisArg)->GetMethodTable(); + GCX_PREEMP(); + return pDeclMD->GetMethodDescOfVirtualizedCode(pThisArg, pMT, pDeclMD->GetMethodTable()); + }); +} + static void UpdateFrameForTailCall(InterpMethodContextFrame *pFrame, PTR_InterpByteCodeStart targetIp, int8_t *callArgsAddress) { InterpMethod *pTargetMethod = targetIp->Method; @@ -3307,6 +3319,24 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr InvokeUnmanagedCalliWithTransition(calliFunctionPointer, cookie, stack, pFrame, callArgsAddress, returnValueAddress); } } +#ifdef FEATURE_CACHED_INTERFACE_DISPATCH + else if (calliFunctionPointer == (PCODE)CID_VirtualOpenDelegateDispatch) + { + // For an open virtual delegate, _methodPtrAux is CID_VirtualOpenDelegateDispatch, + // which expects the address of _methodPtrAux in a hidden argument that calli cannot + // express; resolve the target as INTOP_CALLDELEGATE does. + // Workaround for https://github.com/dotnet/runtime/issues/134733. + + // The shuffle thunk's 'this' is the delegate. + DELEGATEREF delegateObj = LOCAL_VAR(0, DELEGATEREF); + _ASSERTE(((MethodDesc*)pMethod->methodHnd)->IsILStub() && ((MethodDesc*)pMethod->methodHnd)->AsDynamicMethodDesc()->IsDelegateShuffleThunk()); + _ASSERTE(delegateObj != NULL && delegateObj->GetMethodPtrAux() == calliFunctionPointer); + OBJECTREF *pThisArg = (OBJECTREF*)callArgsAddress; + NULL_CHECK(*pThisArg); + targetMethod = ResolveOpenVirtualDelegateTarget(delegateObj, pThisArg); + goto CALL_INTERP_METHOD; + } +#endif // FEATURE_CACHED_INTERFACE_DISPATCH #ifndef FEATURE_PORTABLE_ENTRYPOINTS // If we're not using portable entrypoints, we can use NonVirtualEntry2MethodDesc to figure out where tailcalls go. Since this is // somewhat expensive, we only do it for tailcalls which are relatively rare. @@ -3432,15 +3462,9 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr if (isOpenVirtual) { - targetMethod = COMDelegate::GetMethodDescForOpenVirtualDelegate(*delegateObj); OBJECTREF *pThisArg = LOCAL_VAR_ADDR(callArgsOffset + INTERP_STACK_SLOT_SIZE, OBJECTREF); NULL_CHECK(*pThisArg); - targetMethod = CallWithSEHWrapper( - [&targetMethod, &pThisArg]() { - MethodTable* pMT = (*pThisArg)->GetMethodTable(); - GCX_PREEMP(); - return targetMethod->GetMethodDescOfVirtualizedCode(pThisArg, pMT, targetMethod->GetMethodTable()); - }); + targetMethod = ResolveOpenVirtualDelegateTarget(*delegateObj, pThisArg); } else { diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DelegateTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DelegateTests.cs index c0a2037e79ce3c..7cd6b35b1a618e 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DelegateTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DelegateTests.cs @@ -545,6 +545,24 @@ public static void DifferentOpenVirtualDelegates() Assert.Equal(m2, b.Method); } + [Fact] + public static void OpenVirtualDelegates_InvokeResolvesOverride() + { + Func toString = typeof(object).GetMethod(nameof(object.ToString)).CreateDelegate>(); + Assert.Equal(nameof(OpenVirtualDerived), toString(new OpenVirtualDerived())); + Assert.Equal(typeof(Struct).ToString(), toString(new Struct())); + Assert.Equal(nameof(DayOfWeek.Monday), toString(DayOfWeek.Monday)); + } + + [Fact] + [ActiveIssue("https://github.com/dotnet/runtime/issues/134707", typeof(PlatformDetection), nameof(PlatformDetection.IsBrowser), nameof(PlatformDetection.IsMonoAOT))] + public static void OpenVirtualDelegates_InterfaceMethod_InvokeResolvesImplementation() + { + Func interfaceMethod = typeof(IOpenVirtual).GetMethod(nameof(IOpenVirtual.M)).CreateDelegate>(); + Assert.Equal(1, interfaceMethod(new OpenVirtualDerived())); + Assert.Equal(2, interfaceMethod(new OpenVirtualStruct())); + } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsTypeEquivalenceSupported))] public static void TypeEquivalentDelegatesPointingToSameMethod_AreEqualAndHaveSameHashCode() { @@ -580,6 +598,14 @@ internal virtual void M1() { } internal virtual void M2() { } } + interface IOpenVirtual { int M(); } + class OpenVirtualDerived : IOpenVirtual + { + public int M() => 1; + public override string ToString() => nameof(OpenVirtualDerived); + } + struct OpenVirtualStruct : IOpenVirtual { public int M() => 2; } + class Base { public virtual void M() { } } class Derived : Base { public override void M() { } }