Skip to content

Commit ba9df1e

Browse files
[release/8.0-staging] Call the Copy Constructor for stack arguments in C++/CLI on x86 (#100221)
* Call the Copy Constructor for stack arguments in C++/CLI on Windows-x86 (#100050) * Add repro test case * Directly load the argument address using ldarga to avoid making a copy * Reimplement the "Copy Constructor Cookie" logic in a more modern and maintainable style to get the test passing again * Narrow support to Windows only --------- Co-authored-by: Jeremy Koritzinsky <jekoritz@microsoft.com>
1 parent f36dd88 commit ba9df1e

File tree

10 files changed

+361
-7
lines changed

10 files changed

+361
-7
lines changed

src/coreclr/System.Private.CoreLib/src/System/StubHelpers.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,6 +1111,75 @@ public IntPtr AddRef()
11111111
}
11121112
} // class CleanupWorkListElement
11131113

1114+
internal unsafe struct CopyConstructorCookie
1115+
{
1116+
private void* m_source;
1117+
1118+
private nuint m_destinationOffset;
1119+
1120+
public delegate*<void*, void*, void> m_copyConstructor;
1121+
1122+
public delegate*<void*, void> m_destructor;
1123+
1124+
public CopyConstructorCookie* m_next;
1125+
1126+
[StackTraceHidden]
1127+
public void ExecuteCopy(void* destinationBase)
1128+
{
1129+
if (m_copyConstructor != null)
1130+
{
1131+
m_copyConstructor((byte*)destinationBase + m_destinationOffset, m_source);
1132+
}
1133+
1134+
if (m_destructor != null)
1135+
{
1136+
m_destructor(m_source);
1137+
}
1138+
}
1139+
}
1140+
1141+
internal unsafe struct CopyConstructorChain
1142+
{
1143+
public void* m_realTarget;
1144+
public CopyConstructorCookie* m_head;
1145+
1146+
public void Add(CopyConstructorCookie* cookie)
1147+
{
1148+
cookie->m_next = m_head;
1149+
m_head = cookie;
1150+
}
1151+
1152+
[ThreadStatic]
1153+
private static CopyConstructorChain s_copyConstructorChain;
1154+
1155+
public void Install(void* realTarget)
1156+
{
1157+
m_realTarget = realTarget;
1158+
s_copyConstructorChain = this;
1159+
}
1160+
1161+
[StackTraceHidden]
1162+
private void ExecuteCopies(void* destinationBase)
1163+
{
1164+
for (CopyConstructorCookie* current = m_head; current != null; current = current->m_next)
1165+
{
1166+
current->ExecuteCopy(destinationBase);
1167+
}
1168+
}
1169+
1170+
[UnmanagedCallersOnly]
1171+
[StackTraceHidden]
1172+
public static void* ExecuteCurrentCopiesAndGetTarget(void* destinationBase)
1173+
{
1174+
void* target = s_copyConstructorChain.m_realTarget;
1175+
s_copyConstructorChain.ExecuteCopies(destinationBase);
1176+
// Reset this instance to ensure we don't accidentally execute the copies again.
1177+
// All of the pointers point to the stack, so we don't need to free any memory.
1178+
s_copyConstructorChain = default;
1179+
return target;
1180+
}
1181+
}
1182+
11141183
internal static class StubHelpers
11151184
{
11161185
[MethodImpl(MethodImplOptions.InternalCall)]

src/coreclr/vm/corelib.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,21 @@ DEFINE_METHOD(HANDLE_MARSHALER, CONVERT_SAFEHANDLE_TO_NATIVE,ConvertSaf
10681068
DEFINE_METHOD(HANDLE_MARSHALER, THROW_SAFEHANDLE_FIELD_CHANGED, ThrowSafeHandleFieldChanged, SM_RetVoid)
10691069
DEFINE_METHOD(HANDLE_MARSHALER, THROW_CRITICALHANDLE_FIELD_CHANGED, ThrowCriticalHandleFieldChanged, SM_RetVoid)
10701070

1071+
#ifdef TARGET_WINDOWS
1072+
#ifdef TARGET_X86
1073+
DEFINE_CLASS(COPY_CONSTRUCTOR_CHAIN, StubHelpers, CopyConstructorChain)
1074+
DEFINE_METHOD(COPY_CONSTRUCTOR_CHAIN, EXECUTE_CURRENT_COPIES_AND_GET_TARGET, ExecuteCurrentCopiesAndGetTarget, SM_PtrVoid_RetPtrVoid)
1075+
DEFINE_METHOD(COPY_CONSTRUCTOR_CHAIN, INSTALL, Install, IM_PtrVoid_RetVoid)
1076+
DEFINE_METHOD(COPY_CONSTRUCTOR_CHAIN, ADD, Add, IM_PtrCopyConstructorCookie_RetVoid)
1077+
1078+
DEFINE_CLASS(COPY_CONSTRUCTOR_COOKIE, StubHelpers, CopyConstructorCookie)
1079+
DEFINE_FIELD(COPY_CONSTRUCTOR_COOKIE, SOURCE, m_source)
1080+
DEFINE_FIELD(COPY_CONSTRUCTOR_COOKIE, DESTINATION_OFFSET, m_destinationOffset)
1081+
DEFINE_FIELD(COPY_CONSTRUCTOR_COOKIE, COPY_CONSTRUCTOR, m_copyConstructor)
1082+
DEFINE_FIELD(COPY_CONSTRUCTOR_COOKIE, DESTRUCTOR, m_destructor)
1083+
#endif // TARGET_X86
1084+
#endif // TARGET_WINDOWS
1085+
10711086
DEFINE_CLASS(NATIVEVARIANT, StubHelpers, NativeVariant)
10721087
DEFINE_CLASS(NATIVEDECIMAL, StubHelpers, NativeDecimal)
10731088

src/coreclr/vm/dllimport.cpp

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1623,6 +1623,10 @@ NDirectStubLinker::NDirectStubLinker(
16231623
m_pcsSetup->EmitSTLOC(m_dwTargetInterfacePointerLocalNum);
16241624
}
16251625
#endif // FEATURE_COMINTEROP
1626+
1627+
#if defined(TARGET_X86) && defined(TARGET_WINDOWS)
1628+
m_dwCopyCtorChainLocalNum = (DWORD)-1;
1629+
#endif // defined(TARGET_X86) && defined(TARGET_WINDOWS)
16261630
}
16271631

16281632
void NDirectStubLinker::SetCallingConvention(CorInfoCallConvExtension unmngCallConv, BOOL fIsVarArg)
@@ -1835,6 +1839,23 @@ DWORD NDirectStubLinker::GetReturnValueLocalNum()
18351839
return m_dwRetValLocalNum;
18361840
}
18371841

1842+
#if defined(TARGET_X86) && defined(TARGET_WINDOWS)
1843+
DWORD NDirectStubLinker::GetCopyCtorChainLocalNum()
1844+
{
1845+
STANDARD_VM_CONTRACT;
1846+
1847+
if (m_dwCopyCtorChainLocalNum == (DWORD)-1)
1848+
{
1849+
// The local is created and initialized lazily when first asked.
1850+
m_dwCopyCtorChainLocalNum = NewLocal(CoreLibBinder::GetClass(CLASS__COPY_CONSTRUCTOR_CHAIN));
1851+
m_pcsSetup->EmitLDLOCA(m_dwCopyCtorChainLocalNum);
1852+
m_pcsSetup->EmitINITOBJ(m_pcsSetup->GetToken(CoreLibBinder::GetClass(CLASS__COPY_CONSTRUCTOR_CHAIN)));
1853+
}
1854+
1855+
return m_dwCopyCtorChainLocalNum;
1856+
}
1857+
#endif // defined(TARGET_X86) && defined(TARGET_WINDOWS)
1858+
18381859
BOOL NDirectStubLinker::IsCleanupNeeded()
18391860
{
18401861
LIMITED_METHOD_CONTRACT;
@@ -2064,6 +2085,10 @@ void NDirectStubLinker::End(DWORD dwStubFlags)
20642085
}
20652086
}
20662087

2088+
#if defined(TARGET_X86) && defined(TARGET_WINDOWS)
2089+
EXTERN_C void STDCALL CopyConstructorCallStub(void);
2090+
#endif // defined(TARGET_X86) && defined(TARGET_WINDOWS)
2091+
20672092
void NDirectStubLinker::DoNDirect(ILCodeStream *pcsEmit, DWORD dwStubFlags, MethodDesc * pStubMD)
20682093
{
20692094
STANDARD_VM_CONTRACT;
@@ -2150,6 +2175,21 @@ void NDirectStubLinker::DoNDirect(ILCodeStream *pcsEmit, DWORD dwStubFlags, Meth
21502175
}
21512176
}
21522177

2178+
#if defined(TARGET_X86) && defined(TARGET_WINDOWS)
2179+
if (m_dwCopyCtorChainLocalNum != (DWORD)-1)
2180+
{
2181+
// If we have a copy constructor chain local, we need to call the copy constructor stub
2182+
// to ensure that the chain is called correctly.
2183+
// Let's install the stub chain here and redirect the call to the stub.
2184+
DWORD targetLoc = NewLocal(ELEMENT_TYPE_I);
2185+
pcsEmit->EmitSTLOC(targetLoc);
2186+
pcsEmit->EmitLDLOCA(m_dwCopyCtorChainLocalNum);
2187+
pcsEmit->EmitLDLOC(targetLoc);
2188+
pcsEmit->EmitCALL(METHOD__COPY_CONSTRUCTOR_CHAIN__INSTALL, 2, 0);
2189+
pcsEmit->EmitLDC((DWORD_PTR)&CopyConstructorCallStub);
2190+
}
2191+
#endif // defined(TARGET_X86) && defined(TARGET_WINDOWS)
2192+
21532193
// For managed-to-native calls, the rest of the work is done by the JIT. It will
21542194
// erect InlinedCallFrame, flip GC mode, and use the specified calling convention
21552195
// to call the target. For native-to-managed calls, this is an ordinary managed
@@ -6078,5 +6118,21 @@ PCODE GetILStubForCalli(VASigCookie *pVASigCookie, MethodDesc *pMD)
60786118
RETURN pVASigCookie->pNDirectILStub;
60796119
}
60806120

6121+
#if defined(TARGET_X86) && defined(TARGET_WINDOWS)
6122+
// Copy constructor support for C++/CLI
6123+
EXTERN_C void* STDCALL CallCopyConstructorsWorker(void* esp)
6124+
{
6125+
STATIC_CONTRACT_THROWS;
6126+
STATIC_CONTRACT_GC_TRIGGERS;
6127+
STATIC_CONTRACT_MODE_PREEMPTIVE; // we've already switched to preemptive
6128+
6129+
using ExecuteCallback = void*(STDMETHODCALLTYPE*)(void*);
6130+
6131+
MethodDesc* pMD = CoreLibBinder::GetMethod(METHOD__COPY_CONSTRUCTOR_CHAIN__EXECUTE_CURRENT_COPIES_AND_GET_TARGET);
6132+
ExecuteCallback pExecute = (ExecuteCallback)pMD->GetMultiCallableAddrOfCode();
6133+
6134+
return pExecute(esp);
6135+
}
6136+
#endif // defined(TARGET_X86) && defined(TARGET_WINDOWS)
60816137

60826138
#endif // #ifndef DACCESS_COMPILE

src/coreclr/vm/dllimport.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,9 @@ class NDirectStubLinker : public ILStubLinker
484484
DWORD GetCleanupWorkListLocalNum();
485485
DWORD GetThreadLocalNum();
486486
DWORD GetReturnValueLocalNum();
487+
#if defined(TARGET_X86) && defined(TARGET_WINDOWS)
488+
DWORD GetCopyCtorChainLocalNum();
489+
#endif // defined(TARGET_X86) && defined(TARGET_WINDOWS)
487490
void SetCleanupNeeded();
488491
void SetExceptionCleanupNeeded();
489492
BOOL IsCleanupWorkListSetup();
@@ -553,6 +556,10 @@ class NDirectStubLinker : public ILStubLinker
553556
DWORD m_dwTargetEntryPointLocalNum;
554557
#endif // FEATURE_COMINTEROP
555558

559+
#if defined(TARGET_X86) && defined(TARGET_WINDOWS)
560+
DWORD m_dwCopyCtorChainLocalNum;
561+
#endif // defined(TARGET_X86) && defined(TARGET_WINDOWS)
562+
556563
BOOL m_fHasCleanupCode;
557564
BOOL m_fHasExceptionCleanupCode;
558565
BOOL m_fCleanupWorkListIsSetup;

src/coreclr/vm/i386/asmhelpers.asm

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ EXTERN _NDirectImportWorker@4:PROC
4141

4242
EXTERN _VarargPInvokeStubWorker@12:PROC
4343
EXTERN _GenericPInvokeCalliStubWorker@12:PROC
44+
EXTERN _CallCopyConstructorsWorker@4:PROC
4445

4546
EXTERN _PreStubWorker@8:PROC
4647
EXTERN _TheUMEntryPrestubWorker@4:PROC
@@ -1062,6 +1063,29 @@ GoCallCalliWorker:
10621063

10631064
_GenericPInvokeCalliHelper@0 endp
10641065

1066+
;==========================================================================
1067+
; This is small stub whose purpose is to record current stack pointer and
1068+
; call CallCopyConstructorsWorker to invoke copy constructors and destructors
1069+
; as appropriate. This stub operates on arguments already pushed to the
1070+
; stack by JITted IL stub and must not create a new frame, i.e. it must tail
1071+
; call to the target for it to see the arguments that copy ctors have been
1072+
; called on.
1073+
;
1074+
_CopyConstructorCallStub@0 proc public
1075+
; there may be an argument in ecx - save it
1076+
push ecx
1077+
1078+
; push pointer to arguments
1079+
lea edx, [esp + 8]
1080+
push edx
1081+
1082+
call _CallCopyConstructorsWorker@4
1083+
1084+
; restore ecx and tail call to the target
1085+
pop ecx
1086+
jmp eax
1087+
_CopyConstructorCallStub@0 endp
1088+
10651089
ifdef FEATURE_COMINTEROP
10661090

10671091
;==========================================================================

src/coreclr/vm/ilmarshalers.cpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3459,6 +3459,38 @@ MarshalerOverrideStatus ILBlittableValueClassWithCopyCtorMarshaler::ArgumentOver
34593459
#ifdef TARGET_X86
34603460
pslIL->SetStubTargetArgType(&locDesc); // native type is the value type
34613461
pslILDispatch->EmitLDLOC(dwNewValueTypeLocal); // we load the local directly
3462+
3463+
#if defined(TARGET_WINDOWS)
3464+
// Record this argument's stack slot in the copy constructor chain so we can correctly invoke the copy constructor.
3465+
DWORD ctorCookie = pslIL->NewLocal(CoreLibBinder::GetClass(CLASS__COPY_CONSTRUCTOR_COOKIE));
3466+
pslIL->EmitLDLOCA(ctorCookie);
3467+
pslIL->EmitINITOBJ(pslIL->GetToken(CoreLibBinder::GetClass(CLASS__COPY_CONSTRUCTOR_COOKIE)));
3468+
pslIL->EmitLDLOCA(ctorCookie);
3469+
pslIL->EmitLDLOCA(dwNewValueTypeLocal);
3470+
pslIL->EmitSTFLD(pslIL->GetToken(CoreLibBinder::GetField(FIELD__COPY_CONSTRUCTOR_COOKIE__SOURCE)));
3471+
pslIL->EmitLDLOCA(ctorCookie);
3472+
pslIL->EmitLDC(nativeStackOffset);
3473+
pslIL->EmitSTFLD(pslIL->GetToken(CoreLibBinder::GetField(FIELD__COPY_CONSTRUCTOR_COOKIE__DESTINATION_OFFSET)));
3474+
3475+
if (pargs->mm.m_pCopyCtor)
3476+
{
3477+
pslIL->EmitLDLOCA(ctorCookie);
3478+
pslIL->EmitLDFTN(pslIL->GetToken(pargs->mm.m_pCopyCtor));
3479+
pslIL->EmitSTFLD(pslIL->GetToken(CoreLibBinder::GetField(FIELD__COPY_CONSTRUCTOR_COOKIE__COPY_CONSTRUCTOR)));
3480+
}
3481+
3482+
if (pargs->mm.m_pDtor)
3483+
{
3484+
pslIL->EmitLDLOCA(ctorCookie);
3485+
pslIL->EmitLDFTN(pslIL->GetToken(pargs->mm.m_pDtor));
3486+
pslIL->EmitSTFLD(pslIL->GetToken(CoreLibBinder::GetField(FIELD__COPY_CONSTRUCTOR_COOKIE__DESTRUCTOR)));
3487+
}
3488+
3489+
pslIL->EmitLDLOCA(psl->GetCopyCtorChainLocalNum());
3490+
pslIL->EmitLDLOCA(ctorCookie);
3491+
pslIL->EmitCALL(METHOD__COPY_CONSTRUCTOR_CHAIN__ADD, 2, 0);
3492+
#endif // defined(TARGET_WINDOWS)
3493+
34623494
#else
34633495
pslIL->SetStubTargetArgType(ELEMENT_TYPE_I); // native type is a pointer
34643496
EmitLoadNativeLocalAddrForByRefDispatch(pslILDispatch, dwNewValueTypeLocal);
@@ -3477,9 +3509,13 @@ MarshalerOverrideStatus ILBlittableValueClassWithCopyCtorMarshaler::ArgumentOver
34773509

34783510
DWORD dwNewValueTypeLocal;
34793511
dwNewValueTypeLocal = pslIL->NewLocal(locDesc);
3512+
#ifdef TARGET_WINDOWS
3513+
pslILDispatch->EmitLDARGA(argidx);
3514+
#else // !TARGET_WINDOWS
34803515
pslILDispatch->EmitLDARG(argidx);
34813516
pslILDispatch->EmitSTLOC(dwNewValueTypeLocal);
34823517
pslILDispatch->EmitLDLOCA(dwNewValueTypeLocal);
3518+
#endif // TARGET_WINDOWS
34833519
#else
34843520
LocalDesc locDesc(pargs->mm.m_pMT);
34853521
locDesc.MakeCopyConstructedPointer();

src/coreclr/vm/metasig.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,13 @@ DEFINE_METASIG_T(SM(RefCleanupWorkListElement_RetVoid, r(C(CLEANUP_WORK_LIST_ELE
578578
DEFINE_METASIG_T(SM(RefCleanupWorkListElement_SafeHandle_RetIntPtr, r(C(CLEANUP_WORK_LIST_ELEMENT)) C(SAFE_HANDLE), I))
579579
DEFINE_METASIG_T(SM(RefCleanupWorkListElement_Obj_RetVoid, r(C(CLEANUP_WORK_LIST_ELEMENT)) j, v))
580580

581+
DEFINE_METASIG(SM(PtrVoid_RetPtrVoid, P(v), P(v)))
582+
DEFINE_METASIG(IM(PtrVoid_RetVoid, P(v), v))
583+
#if defined(TARGET_X86) && defined(TARGET_WINDOWS)
584+
DEFINE_METASIG_T(IM(PtrCopyConstructorCookie_RetVoid, P(g(COPY_CONSTRUCTOR_COOKIE)), v))
585+
#endif // defined(TARGET_X86) && defined(TARGET_WINDOWS)
586+
587+
581588
#ifdef FEATURE_ICASTABLE
582589
DEFINE_METASIG_T(SM(ICastable_RtType_RefException_RetBool, C(ICASTABLE) C(CLASS) r(C(EXCEPTION)), F))
583590
DEFINE_METASIG_T(SM(ICastable_RtType_RetRtType, C(ICASTABLE) C(CLASS), C(CLASS)))

src/tests/Interop/IJW/CopyConstructorMarshaler/CopyConstructorMarshaler.cs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,34 +25,62 @@ static int Main()
2525
object testInstance = Activator.CreateInstance(testType);
2626
MethodInfo testMethod = testType.GetMethod("PInvokeNumCopies");
2727

28+
// On x86, we have an additional copy on every P/Invoke from the "native" parameter to the actual location on the stack.
29+
int platformExtra = 0;
30+
if (RuntimeInformation.ProcessArchitecture == Architecture.X86)
31+
{
32+
platformExtra = 1;
33+
}
34+
2835
// PInvoke will copy twice. Once from argument to parameter, and once from the managed to native parameter.
29-
Assert.Equal(2, (int)testMethod.Invoke(testInstance, null));
36+
Assert.Equal(2 + platformExtra, (int)testMethod.Invoke(testInstance, null));
3037

3138
testMethod = testType.GetMethod("ReversePInvokeNumCopies");
3239

3340
// Reverse PInvoke will copy 3 times. Two are from the same paths as the PInvoke,
3441
// and the third is from the reverse P/Invoke call.
35-
Assert.Equal(3, (int)testMethod.Invoke(testInstance, null));
42+
Assert.Equal(3 + platformExtra, (int)testMethod.Invoke(testInstance, null));
3643

3744
testMethod = testType.GetMethod("PInvokeNumCopiesDerivedType");
3845

3946
// PInvoke will copy twice. Once from argument to parameter, and once from the managed to native parameter.
40-
Assert.Equal(2, (int)testMethod.Invoke(testInstance, null));
47+
Assert.Equal(2 + platformExtra, (int)testMethod.Invoke(testInstance, null));
4148

4249
testMethod = testType.GetMethod("ReversePInvokeNumCopiesDerivedType");
4350

4451
// Reverse PInvoke will copy 3 times. Two are from the same paths as the PInvoke,
4552
// and the third is from the reverse P/Invoke call.
46-
Assert.Equal(3, (int)testMethod.Invoke(testInstance, null));
53+
Assert.Equal(3 + platformExtra, (int)testMethod.Invoke(testInstance, null));
4754
}
4855
catch (Exception ex)
4956
{
5057
Console.WriteLine(ex);
5158
return 101;
5259
}
60+
61+
try
62+
{
63+
CopyConstructorsInArgumentStackSlots();
64+
}
65+
catch (Exception ex)
66+
{
67+
Console.WriteLine(ex);
68+
return 101;
69+
}
70+
5371
return 100;
5472
}
5573

74+
public static void CopyConstructorsInArgumentStackSlots()
75+
{
76+
Assembly ijwNativeDll = Assembly.Load("IjwCopyConstructorMarshaler");
77+
Type testType = ijwNativeDll.GetType("TestClass");
78+
object testInstance = Activator.CreateInstance(testType);
79+
MethodInfo testMethod = testType.GetMethod("ExposedThisCopyConstructorScenario");
80+
81+
Assert.Equal(0, (int)testMethod.Invoke(testInstance, null));
82+
}
83+
5684
[DllImport("kernel32.dll")]
5785
static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hReservedNull, int dwFlags);
5886

0 commit comments

Comments
 (0)