Skip to content

Commit affc0fc

Browse files
authored
Enable inlining P/Invokes into try blocks with no catch or filter clauses V2 (#73661)
1 parent 789aca1 commit affc0fc

File tree

8 files changed

+125
-47
lines changed

8 files changed

+125
-47
lines changed

src/coreclr/inc/CrstTypes.def

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ Crst Exception
202202
End
203203

204204
Crst ExecutableAllocatorLock
205-
AcquiredAfter LoaderHeap ArgBasedStubCache UMEntryThunkFreeListLock
205+
AcquiredAfter LoaderHeap ArgBasedStubCache UMEntryThunkFreeListLock COMCallWrapper
206206
End
207207

208208
Crst ExecuteManRangeLock

src/coreclr/inc/corinfo.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3253,4 +3253,9 @@ class ICorDynamicInfo : public ICorStaticInfo
32533253
//
32543254
#define IMAGE_REL_BASED_REL_THUMB_MOV32_PCREL 0x14
32553255

3256+
/**********************************************************************************/
3257+
#ifdef TARGET_64BIT
3258+
#define USE_PER_FRAME_PINVOKE_INIT
3259+
#endif
3260+
32563261
#endif // _COR_INFO_H_

src/coreclr/inc/crsttypes.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ int g_rgCrstLevelMap[] =
155155
-1, // CrstClrNotification
156156
6, // CrstCodeFragmentHeap
157157
9, // CrstCodeVersioning
158-
0, // CrstCOMCallWrapper
158+
3, // CrstCOMCallWrapper
159159
5, // CrstCOMWrapperCache
160160
3, // CrstDataTest1
161161
0, // CrstDataTest2

src/coreclr/jit/importer.cpp

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8428,41 +8428,43 @@ bool Compiler::impCanPInvokeInlineCallSite(BasicBlock* block)
84288428
return true;
84298429
}
84308430

8431-
#ifdef TARGET_64BIT
8432-
// On 64-bit platforms, we disable pinvoke inlining inside of try regions.
8433-
// Note that this could be needed on other architectures too, but we
8434-
// haven't done enough investigation to know for sure at this point.
8435-
//
8436-
// Here is the comment from JIT64 explaining why:
8437-
// [VSWhidbey: 611015] - because the jitted code links in the
8438-
// Frame (instead of the stub) we rely on the Frame not being
8439-
// 'active' until inside the stub. This normally happens by the
8440-
// stub setting the return address pointer in the Frame object
8441-
// inside the stub. On a normal return, the return address
8442-
// pointer is zeroed out so the Frame can be safely re-used, but
8443-
// if an exception occurs, nobody zeros out the return address
8444-
// pointer. Thus if we re-used the Frame object, it would go
8445-
// 'active' as soon as we link it into the Frame chain.
8446-
//
8447-
// Technically we only need to disable PInvoke inlining if we're
8448-
// in a handler or if we're in a try body with a catch or
8449-
// filter/except where other non-handler code in this method
8450-
// might run and try to re-use the dirty Frame object.
8451-
//
8452-
// A desktop test case where this seems to matter is
8453-
// jit\jit64\ebvts\mcpp\sources2\ijw\__clrcall\vector_ctor_dtor.02\deldtor_clr.exe
8431+
#ifdef USE_PER_FRAME_PINVOKE_INIT
8432+
// For platforms that use per-P/Invoke InlinedCallFrame initialization,
8433+
// we can't inline P/Invokes inside of try blocks where we can resume execution in the same function.
8434+
// The runtime can correctly unwind out of an InlinedCallFrame and out of managed code. However,
8435+
// it cannot correctly unwind out of an InlinedCallFrame and stop at that frame without also unwinding
8436+
// at least one managed frame. In particular, the runtime struggles to restore non-volatile registers
8437+
// from the top-most unmanaged call before the InlinedCallFrame. As a result, the runtime does not support
8438+
// re-entering the same method frame as the InlinedCallFrame after an exception in unmanaged code.
84548439
if (block->hasTryIndex())
84558440
{
84568441
// This does not apply to the raw pinvoke call that is inside the pinvoke
84578442
// ILStub. In this case, we have to inline the raw pinvoke call into the stub,
84588443
// otherwise we would end up with a stub that recursively calls itself, and end
84598444
// up with a stack overflow.
8445+
// This works correctly because the runtime never emits a catch block in a managed-to-native
8446+
// IL stub. If the runtime ever emits a catch block into a managed-to-native stub when using
8447+
// P/Invoke helpers, this condition will need to be revisited.
84608448
if (opts.jitFlags->IsSet(JitFlags::JIT_FLAG_IL_STUB) && opts.ShouldUsePInvokeHelpers())
84618449
{
84628450
return true;
84638451
}
84648452

8465-
return false;
8453+
// Check if this block's try block or any containing try blocks have catch handlers.
8454+
// If any of the containing try blocks have catch handlers,
8455+
// we cannot inline a P/Invoke for reasons above. If the handler is a fault or finally handler,
8456+
// we can inline a P/Invoke into this block in the try since the code will not resume execution
8457+
// in the same method after throwing an exception if only fault or finally handlers are executed.
8458+
for (unsigned int ehIndex = block->getTryIndex(); ehIndex != EHblkDsc::NO_ENCLOSING_INDEX;
8459+
ehIndex = ehGetEnclosingTryIndex(ehIndex))
8460+
{
8461+
if (ehGetDsc(ehIndex)->HasCatchHandler())
8462+
{
8463+
return false;
8464+
}
8465+
}
8466+
8467+
return true;
84668468
}
84678469
#endif // TARGET_64BIT
84688470

src/coreclr/jit/lower.cpp

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4297,6 +4297,7 @@ GenTree* Lowering::CreateFrameLinkUpdate(FrameLinkAction action)
42974297
// Return Value:
42984298
// none
42994299
//
4300+
// See the usages for USE_PER_FRAME_PINVOKE_INIT for more information.
43004301
void Lowering::InsertPInvokeMethodProlog()
43014302
{
43024303
noway_assert(comp->info.compUnmanagedCallCountWithGCTransition);
@@ -4393,13 +4394,16 @@ void Lowering::InsertPInvokeMethodProlog()
43934394
// --------------------------------------------------------
43944395
// On 32-bit targets, CORINFO_HELP_INIT_PINVOKE_FRAME initializes the PInvoke frame and then pushes it onto
43954396
// the current thread's Frame stack. On 64-bit targets, it only initializes the PInvoke frame.
4397+
// As a result, don't push the frame onto the frame stack here for any 64-bit targets
43964398
CLANG_FORMAT_COMMENT_ANCHOR;
43974399

43984400
#ifdef TARGET_64BIT
4401+
#ifdef USE_PER_FRAME_PINVOKE_INIT
4402+
// For IL stubs, we push the frame once even when we're doing per-pinvoke init.
43994403
if (comp->opts.jitFlags->IsSet(JitFlags::JIT_FLAG_IL_STUB))
4404+
#endif // USE_PER_FRAME_PINVOKE_INIT
44004405
{
4401-
// Push a frame - if we are NOT in an IL stub, this is done right before the call
4402-
// The init routine sets InlinedCallFrame's m_pNext, so we just set the thead's top-of-stack
4406+
// Push a frame. The init routine sets InlinedCallFrame's m_pNext, so we just set the thread's top-of-stack
44034407
GenTree* frameUpd = CreateFrameLinkUpdate(PushFrame);
44044408
firstBlockRange.InsertBefore(insertionPoint, LIR::SeqTree(comp, frameUpd));
44054409
ContainCheckStoreIndir(frameUpd->AsStoreInd());
@@ -4459,9 +4463,10 @@ void Lowering::InsertPInvokeMethodEpilog(BasicBlock* returnBB DEBUGARG(GenTree*
44594463
// this in the epilog for IL stubs; for non-IL stubs the frame is popped after every PInvoke call.
44604464
CLANG_FORMAT_COMMENT_ANCHOR;
44614465

4462-
#ifdef TARGET_64BIT
4466+
#ifdef USE_PER_FRAME_PINVOKE_INIT
4467+
// For IL stubs, we push the frame once even when we're doing per-pinvoke init
44634468
if (comp->opts.jitFlags->IsSet(JitFlags::JIT_FLAG_IL_STUB))
4464-
#endif // TARGET_64BIT
4469+
#endif // USE_PER_FRAME_PINVOKE_INIT
44654470
{
44664471
GenTree* frameUpd = CreateFrameLinkUpdate(PopFrame);
44674472
returnBlockRange.InsertBefore(insertionPoint, LIR::SeqTree(comp, frameUpd));
@@ -4617,7 +4622,7 @@ void Lowering::InsertPInvokeCallProlog(GenTreeCall* call)
46174622
// contains PInvokes; on 64-bit targets this is necessary in non-stubs.
46184623
CLANG_FORMAT_COMMENT_ANCHOR;
46194624

4620-
#ifdef TARGET_64BIT
4625+
#ifdef USE_PER_FRAME_PINVOKE_INIT
46214626
if (!comp->opts.jitFlags->IsSet(JitFlags::JIT_FLAG_IL_STUB))
46224627
{
46234628
// Set the TCB's frame to be the one we just created.
@@ -4629,7 +4634,7 @@ void Lowering::InsertPInvokeCallProlog(GenTreeCall* call)
46294634
BlockRange().InsertBefore(insertBefore, LIR::SeqTree(comp, frameUpd));
46304635
ContainCheckStoreIndir(frameUpd->AsStoreInd());
46314636
}
4632-
#endif // TARGET_64BIT
4637+
#endif // USE_PER_FRAME_PINVOKE_INIT
46334638

46344639
// IMPORTANT **** This instruction must be the last real instruction ****
46354640
// It changes the thread's state to Preemptive mode
@@ -4695,7 +4700,7 @@ void Lowering::InsertPInvokeCallEpilog(GenTreeCall* call)
46954700
// this happens after every PInvoke call in non-stubs. 32-bit targets instead mark the frame as inactive.
46964701
CLANG_FORMAT_COMMENT_ANCHOR;
46974702

4698-
#ifdef TARGET_64BIT
4703+
#ifdef USE_PER_FRAME_PINVOKE_INIT
46994704
if (!comp->opts.jitFlags->IsSet(JitFlags::JIT_FLAG_IL_STUB))
47004705
{
47014706
tree = CreateFrameLinkUpdate(PopFrame);
@@ -4719,7 +4724,7 @@ void Lowering::InsertPInvokeCallEpilog(GenTreeCall* call)
47194724

47204725
BlockRange().InsertBefore(insertionPoint, constantZero, storeCallSiteTracker);
47214726
ContainCheckStoreLoc(storeCallSiteTracker);
4722-
#endif // TARGET_64BIT
4727+
#endif // USE_PER_FRAME_PINVOKE_INIT
47234728
}
47244729

47254730
//------------------------------------------------------------------------

src/coreclr/vm/exceptionhandling.cpp

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "virtualcallstub.h"
1717
#include "utilcode.h"
1818
#include "interoplibinterface.h"
19+
#include "corinfo.h"
1920

2021
#if defined(TARGET_X86)
2122
#define USE_CURRENT_CONTEXT_IN_FILTER
@@ -1773,8 +1774,10 @@ CLRUnwindStatus ExceptionTracker::ProcessOSExceptionNotification(
17731774
// InlinedCallFrames (ICF) are allocated, initialized and linked to the Frame chain
17741775
// by the code generated by the JIT for a method containing a PInvoke.
17751776
//
1776-
// JIT generates code that links in the ICF at the start of the method and unlinks it towards
1777-
// the method end. Thus, ICF is present on the Frame chain at any given point so long as the
1777+
// On platforms where USE_PER_FRAME_PINVOKE_INIT is not defined,
1778+
// the JIT generates code that links in the ICF
1779+
// at the start of the method and unlinks it towards the method end.
1780+
// Thus, ICF is present on the Frame chain at any given point so long as the
17781781
// method containing the PInvoke is on the stack.
17791782
//
17801783
// Now, if the method containing ICF catches an exception, we will reset the Frame chain
@@ -1812,13 +1815,16 @@ CLRUnwindStatus ExceptionTracker::ProcessOSExceptionNotification(
18121815
// below the callerSP for which we will invoke ExceptionUnwind.
18131816
//
18141817
// Thus, ICF::ExceptionUnwind should not do anything significant. If any of these assumptions
1815-
// break, then the next best thing will be to make the JIT link/unlink the frame dynamically.
1818+
// break, then the next best thing will be to make the JIT link/unlink the frame dynamically
18161819
//
1817-
// If the current method executing is from precompiled ReadyToRun code, then the above is no longer
1818-
// applicable because each PInvoke is wrapped by calls to the JIT_PInvokeBegin and JIT_PInvokeEnd
1819-
// helpers, which push and pop the ICF to the current thread. Unlike jitted code, the ICF is not
1820-
// linked during the method prolog, and unlinked at the epilog (it looks more like the X64 case).
1820+
// If the current method executing is from precompiled ReadyToRun code, each PInvoke is wrapped
1821+
// by calls to the JIT_PInvokeBegin and JIT_PInvokeEnd helpers,
1822+
// which push and pop the ICF to the current thread. The ICF is not
1823+
// linked during the method prolog, and unlinked at the epilog.
18211824
// In that case, we need to unlink the ICF during unwinding here.
1825+
// On platforms where USE_PER_FRAME_PINVOKE_INIT is defined, the JIT generates code that links in
1826+
// the ICF immediately before and after a PInvoke in non-IL-stubs, like ReadyToRun.
1827+
// See the usages for USE_PER_FRAME_PINVOKE_INIT for more information.
18221828

18231829
if (fTargetUnwind && (pFrame->GetVTablePtr() == InlinedCallFrame::GetMethodFrameVPtr()))
18241830
{
@@ -1837,9 +1843,20 @@ CLRUnwindStatus ExceptionTracker::ProcessOSExceptionNotification(
18371843
// to the JIT_PInvokeBegin and JIT_PInvokeEnd helpers, which push and pop the ICF on the thread. The
18381844
// ICF is not linked at the method prolog and unlined at the epilog when running R2R code. Since the
18391845
// JIT_PInvokeEnd helper will be skipped, we need to unlink the ICF here. If the executing method
1840-
// has another pinovoke, it will re-link the ICF again when the JIT_PInvokeBegin helper is called
1841-
1842-
if (ExecutionManager::IsReadyToRunCode(((InlinedCallFrame*)pFrame)->m_pCallerReturnAddress))
1846+
// has another pinvoke, it will re-link the ICF again when the JIT_PInvokeBegin helper is called.
1847+
1848+
TADDR returnAddress = ((InlinedCallFrame*)pFrame)->m_pCallerReturnAddress;
1849+
#ifdef USE_PER_FRAME_PINVOKE_INIT
1850+
// If we're setting up the frame for each P/Invoke for the given platform,
1851+
// then we do this for all P/Invokes except ones in IL stubs.
1852+
// IL stubs link the frame in for the whole stub, so if an exception is thrown during marshalling,
1853+
// the ICF will be on the frame chain and inactive.
1854+
if (returnAddress != NULL && !ExecutionManager::GetCodeMethodDesc(returnAddress)->IsILStub())
1855+
#else
1856+
// If we aren't setting up the frame for each P/Invoke (instead setting up once per method),
1857+
// then ReadyToRun code is the only code using the per-P/Invoke logic.
1858+
if (ExecutionManager::IsReadyToRunCode(returnAddress))
1859+
#endif
18431860
{
18441861
pICFForUnwindTarget = pICFForUnwindTarget->Next();
18451862
}

src/coreclr/vm/i386/excepx86.cpp

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include "eeconfig.h"
2929
#include "vars.hpp"
3030
#include "generics.h"
31+
#include "corinfo.h"
3132

3233
#include "asmconstants.h"
3334
#include "virtualcallstub.h"
@@ -2965,6 +2966,8 @@ void ResumeAtJitEH(CrawlFrame* pCf,
29652966
// InlinedCallFrame somewhere up the call chain that is not related to the current exception
29662967
// handling.
29672968

2969+
// See the usages for USE_PER_FRAME_PINVOKE_INIT for more information.
2970+
29682971
#ifdef DEBUG
29692972
TADDR handlerFrameSP = pCf->GetRegisterSet()->SP;
29702973
#endif // DEBUG
@@ -2976,10 +2979,22 @@ void ResumeAtJitEH(CrawlFrame* pCf,
29762979
NULL /* StackwalkCacheUnwindInfo* */);
29772980
_ASSERTE(unwindSuccess);
29782981

2979-
if (((TADDR)pThread->m_pFrame < pCf->GetRegisterSet()->SP) && ExecutionManager::IsReadyToRunCode(((InlinedCallFrame*)pThread->m_pFrame)->m_pCallerReturnAddress))
2982+
if (((TADDR)pThread->m_pFrame < pCf->GetRegisterSet()->SP))
29802983
{
2981-
_ASSERTE((TADDR)pThread->m_pFrame >= handlerFrameSP);
2982-
pThread->m_pFrame->Pop(pThread);
2984+
TADDR returnAddress = ((InlinedCallFrame*)pThread->m_pFrame)->m_pCallerReturnAddress;
2985+
#ifdef USE_PER_FRAME_PINVOKE_INIT
2986+
// If we're setting up the frame for each P/Invoke for the given platform,
2987+
// then we do this for all P/Invokes except ones in IL stubs.
2988+
if (returnAddress != NULL && !ExecutionManager::GetCodeMethodDesc(returnAddress)->IsILStub())
2989+
#else
2990+
// If we aren't setting up the frame for each P/Invoke (instead setting up once per method),
2991+
// then ReadyToRun code is the only code using the per-P/Invoke logic.
2992+
if (ExecutionManager::IsReadyToRunCode(returnAddress))
2993+
#endif
2994+
{
2995+
_ASSERTE((TADDR)pThread->m_pFrame >= handlerFrameSP);
2996+
pThread->m_pFrame->Pop(pThread);
2997+
}
29832998
}
29842999
}
29853000

src/tests/baseservices/exceptions/exceptioninterop/ExceptionInterop.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,38 @@ public static void ThrowNativeExceptionAndCatchInFrameWithFinally()
122122

123123
Assert.True(caughtException);
124124
}
125+
126+
[Fact]
127+
[PlatformSpecific(TestPlatforms.Windows)]
128+
[SkipOnMono("Exception interop not supported on Mono.")]
129+
public static void ThrowNativeExceptionInFrameWithFinallyCatchInOuterFrame()
130+
{
131+
bool caughtException = false;
132+
try
133+
{
134+
ThrowInFrameWithFinally();
135+
}
136+
catch
137+
{
138+
caughtException = true;
139+
}
140+
141+
Assert.True(caughtException);
142+
143+
[MethodImpl(MethodImplOptions.NoInlining)]
144+
static void ThrowInFrameWithFinally()
145+
{
146+
try
147+
{
148+
ThrowException();
149+
}
150+
finally
151+
{
152+
// Try calling another P/Invoke in the finally block before the catch
153+
// to make sure we have everything set up
154+
// to recover from the exceptional control flow.
155+
NativeFunction();
156+
}
157+
}
158+
}
125159
}

0 commit comments

Comments
 (0)