diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs index 98e2c4493f1e03..b9b6dbb87947c0 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs @@ -406,5 +406,22 @@ private static Continuation UnwindToPossibleHandler(Continuation continuation) return continuation; } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ExecutionContext? CaptureExecutionContext() + { + return Thread.CurrentThreadAssumedInitialized._executionContext; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void RestoreExecutionContext(ExecutionContext? previousExecutionCtx) + { + Thread thread = Thread.CurrentThreadAssumedInitialized; + ExecutionContext? currentExecutionCtx = thread._executionContext; + if (previousExecutionCtx != currentExecutionCtx) + { + ExecutionContext.RestoreChangedContextToThread(thread, previousExecutionCtx, currentExecutionCtx); + } + } } } diff --git a/src/coreclr/inc/corinfo.h b/src/coreclr/inc/corinfo.h index 266b7c83a6fe07..6182619dbbf77e 100644 --- a/src/coreclr/inc/corinfo.h +++ b/src/coreclr/inc/corinfo.h @@ -1733,6 +1733,10 @@ struct CORINFO_ASYNC_INFO // Whether or not the continuation needs to be allocated through the // helper that also takes a method handle bool continuationsNeedMethodHandle; + // Method handle for AsyncHelpers.CaptureExecutionContext + CORINFO_METHOD_HANDLE captureExecutionContextMethHnd; + // Method handle for AsyncHelpers.RestoreExecutionContext + CORINFO_METHOD_HANDLE restoreExecutionContextMethHnd; }; // Flags passed from JIT to runtime. diff --git a/src/coreclr/inc/jiteeversionguid.h b/src/coreclr/inc/jiteeversionguid.h index 38a900d0178d00..785e4e2a854ab7 100644 --- a/src/coreclr/inc/jiteeversionguid.h +++ b/src/coreclr/inc/jiteeversionguid.h @@ -37,11 +37,11 @@ #include -constexpr GUID JITEEVersionIdentifier = { /* 2004006b-bdff-4357-8e60-3ae950a4f165 */ - 0x2004006b, - 0xbdff, - 0x4357, - {0x8e, 0x60, 0x3a, 0xe9, 0x50, 0xa4, 0xf1, 0x65} +constexpr GUID JITEEVersionIdentifier = { /* ce8cef5e-261f-469a-b599-9f3f3e8b2448 */ + 0xce8cef5e, + 0x261f, + 0x469a, + {0xb5, 0x99, 0x9f, 0x3f, 0x3e, 0x8b, 0x24, 0x48} }; #endif // JIT_EE_VERSIONING_GUID_H diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index 17cf1b989a2d4c..06df957f6c5909 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -3,16 +3,24 @@ // // This file implements the transformation of C# async methods into state -// machines. The transformation takes place late in the JIT pipeline, when most -// optimizations have already been performed, right before lowering. +// machines. The following key operations are performed: // -// The transformation performs the following key operations: +// 1. Early, after import but before inlining: for async calls that require +// ExecutionContext save/restore semantics, ExecutionContext capture and +// restore calls are inserted around the async call site. This ensures proper +// context flow across await boundaries when the continuation may run on +// different threads or synchronization contexts. The captured ExecutionContext +// is stored in a temporary local and restored after the async call completes, +// with special handling for calls inside try regions using try-finally blocks. // -// 1. Each async call becomes a suspension point where execution can pause and +// Later, right before lowering the actual transformation to a state machine is +// performed: +// +// 2. Each async call becomes a suspension point where execution can pause and // return to the caller, accompanied by a resumption point where execution can // continue when the awaited operation completes. // -// 2. When suspending at a suspension point a continuation object is created that contains: +// 3. When suspending at a suspension point a continuation object is created that contains: // - All live local variables // - State number to identify which await is being resumed // - Return value from the awaited operation (filled in by the callee later) @@ -20,10 +28,10 @@ // - Resumption function pointer // - Flags containing additional information // -// 3. The method entry is modified to include dispatch logic that checks for an +// 4. The method entry is modified to include dispatch logic that checks for an // incoming continuation and jumps to the appropriate resumption point. // -// 4. Special handling is included for: +// 5. Special handling is included for: // - Exception propagation across await boundaries // - Return value management for different types (primitives, references, structs) // - Tiered compilation and On-Stack Replacement (OSR) @@ -37,6 +45,304 @@ #include "jitstd/algorithm.h" #include "async.h" +//------------------------------------------------------------------------ +// Compiler::SaveAsyncContexts: +// Insert code to save and restore ExecutionContext around async call sites. +// +// Returns: +// Suitable phase status. +// +// Remarks: +// Runs early, after import but before inlining. Thus RET_EXPRs may be +// present, and async calls may later be inlined. +// +PhaseStatus Compiler::SaveAsyncContexts() +{ + if (!compMustSaveAsyncContexts) + { + JITDUMP("No async calls where execution context capture/restore is necessary\n"); + ValidateNoAsyncSavesNecessary(); + return PhaseStatus::MODIFIED_NOTHING; + } + + PhaseStatus result = PhaseStatus::MODIFIED_NOTHING; + + BasicBlock* curBB = fgFirstBB; + while (curBB != nullptr) + { + BasicBlock* nextBB = curBB->Next(); + + for (Statement* stmt : curBB->Statements()) + { + GenTree* tree = stmt->GetRootNode(); + if (tree->OperIs(GT_STORE_LCL_VAR)) + { + tree = tree->AsLclVarCommon()->Data(); + } + + if (!tree->IsCall() || !tree->AsCall()->IsAsyncAndAlwaysSavesAndRestoresExecutionContext()) + { + ValidateNoAsyncSavesNecessaryInStatement(stmt); + continue; + } + + GenTreeCall* call = tree->AsCall(); + + unsigned lclNum = lvaGrabTemp(false DEBUGARG("ExecutionContext for SaveAndRestore async call")); + + JITDUMP("Saving ExecutionContext in V%02u around [%06u]\n", lclNum, call->gtTreeID); + + CORINFO_ASYNC_INFO* asyncInfo = eeGetAsyncInfo(); + + GenTreeCall* capture = gtNewCallNode(CT_USER_FUNC, asyncInfo->captureExecutionContextMethHnd, TYP_REF); + CORINFO_CALL_INFO callInfo = {}; + callInfo.hMethod = capture->gtCallMethHnd; + callInfo.methodFlags = info.compCompHnd->getMethodAttribs(callInfo.hMethod); + impMarkInlineCandidate(capture, MAKE_METHODCONTEXT(callInfo.hMethod), false, &callInfo, compInlineContext); + + if (capture->IsInlineCandidate()) + { + Statement* captureStmt = fgNewStmtFromTree(capture); + + GenTreeRetExpr* retExpr = gtNewInlineCandidateReturnExpr(capture, TYP_REF); + + capture->GetSingleInlineCandidateInfo()->retExpr = retExpr; + GenTree* storeCapture = gtNewTempStore(lclNum, retExpr); + Statement* storeCaptureStmt = fgNewStmtFromTree(storeCapture); + + fgInsertStmtBefore(curBB, stmt, captureStmt); + fgInsertStmtBefore(curBB, stmt, storeCaptureStmt); + + JITDUMP("Inserted capture:\n"); + DISPSTMT(captureStmt); + DISPSTMT(storeCaptureStmt); + } + else + { + GenTree* storeCapture = gtNewTempStore(lclNum, capture); + Statement* storeCaptureStmt = fgNewStmtFromTree(storeCapture); + + fgInsertStmtBefore(curBB, stmt, storeCaptureStmt); + + JITDUMP("Inserted capture:\n"); + DISPSTMT(storeCaptureStmt); + } + + BasicBlock* restoreBB = curBB; + Statement* restoreAfterStmt = stmt; + + if (call->IsInlineCandidate() && (call->gtReturnType != TYP_VOID)) + { + restoreAfterStmt = stmt->GetNextStmt(); + assert(restoreAfterStmt->GetRootNode()->OperIs(GT_RET_EXPR) || + (restoreAfterStmt->GetRootNode()->OperIs(GT_STORE_LCL_VAR) && + restoreAfterStmt->GetRootNode()->AsLclVarCommon()->Data()->OperIs(GT_RET_EXPR))); + } + + if (curBB->hasTryIndex()) + { +#ifdef FEATURE_EH_WINDOWS_X86 + IMPL_LIMITATION("Cannot handle insertion of try-finally without funclets"); +#else + // Await is inside a try, need to insert try-finally around it. + restoreBB = InsertTryFinallyForContextRestore(curBB, stmt, restoreAfterStmt); + restoreAfterStmt = nullptr; +#endif + } + + GenTreeCall* restore = gtNewCallNode(CT_USER_FUNC, asyncInfo->restoreExecutionContextMethHnd, TYP_VOID); + restore->gtArgs.PushFront(this, NewCallArg::Primitive(gtNewLclVarNode(lclNum))); + + callInfo = {}; + callInfo.hMethod = restore->gtCallMethHnd; + callInfo.methodFlags = info.compCompHnd->getMethodAttribs(callInfo.hMethod); + impMarkInlineCandidate(restore, MAKE_METHODCONTEXT(callInfo.hMethod), false, &callInfo, compInlineContext); + + Statement* restoreStmt = fgNewStmtFromTree(restore); + if (restoreAfterStmt == nullptr) + { + fgInsertStmtNearEnd(restoreBB, restoreStmt); + } + else + { + fgInsertStmtAfter(restoreBB, restoreAfterStmt, restoreStmt); + } + + JITDUMP("Inserted restore:\n"); + DISPSTMT(restoreStmt); + + result = PhaseStatus::MODIFIED_EVERYTHING; + } + + curBB = nextBB; + } + + return result; +} + +//------------------------------------------------------------------------ +// Compiler::ValidateNoAsyncSavesNecessary: +// Check that there are no async calls requiring saving of ExecutionContext +// in the method. +// +void Compiler::ValidateNoAsyncSavesNecessary() +{ +#ifdef DEBUG + for (BasicBlock* block : Blocks()) + { + for (Statement* stmt : block->Statements()) + { + ValidateNoAsyncSavesNecessaryInStatement(stmt); + } + } +#endif +} + +//------------------------------------------------------------------------ +// Compiler::ValidateNoAsyncSavesNecessaryInStatement: +// Check that there are no async calls requiring saving of ExecutionContext +// in the statement. +// +// Parameters: +// stmt - The statement +// +void Compiler::ValidateNoAsyncSavesNecessaryInStatement(Statement* stmt) +{ +#ifdef DEBUG + struct Visitor : GenTreeVisitor + { + enum + { + DoPreOrder = true, + }; + + Visitor(Compiler* comp) + : GenTreeVisitor(comp) + { + } + + fgWalkResult PreOrderVisit(GenTree** use, GenTree* user) + { + if (((*use)->gtFlags & GTF_CALL) == 0) + { + return WALK_SKIP_SUBTREES; + } + + if ((*use)->IsCall()) + { + assert(!(*use)->AsCall()->IsAsyncAndAlwaysSavesAndRestoresExecutionContext()); + } + + return WALK_CONTINUE; + } + }; + + Visitor visitor(this); + visitor.WalkTree(stmt->GetRootNodePointer(), nullptr); +#endif +} + +//------------------------------------------------------------------------ +// Compiler::InsertTryFinallyForContextRestore: +// Insert a try-finally around the specified statements in the specified +// block. +// +// Returns: +// Finally block of inserted try-finally. +// +BasicBlock* Compiler::InsertTryFinallyForContextRestore(BasicBlock* block, Statement* firstStmt, Statement* lastStmt) +{ + assert(!block->hasHndIndex()); + EHblkDsc* ebd = fgTryAddEHTableEntries(block->bbTryIndex - 1, 1); + if (ebd == nullptr) + { + IMPL_LIMITATION("Awaits require insertion of too many EH clauses"); + } + + if (firstStmt == block->firstStmt()) + { + block = fgSplitBlockAtBeginning(block); + } + else + { + block = fgSplitBlockAfterStatement(block, firstStmt->GetPrevStmt()); + } + + BasicBlock* tailBB = fgSplitBlockAfterStatement(block, lastStmt); + + BasicBlock* callFinally = fgNewBBafter(BBJ_CALLFINALLY, block, false); + BasicBlock* callFinallyRet = fgNewBBafter(BBJ_CALLFINALLYRET, callFinally, false); + BasicBlock* finallyRet = fgNewBBafter(BBJ_EHFINALLYRET, callFinallyRet, false); + BasicBlock* goToTailBlock = fgNewBBafter(BBJ_ALWAYS, finallyRet, false); + + callFinally->inheritWeight(block); + callFinallyRet->inheritWeight(block); + finallyRet->inheritWeight(block); + goToTailBlock->inheritWeight(block); + + // Set some info the starting blocks like fgFindBasicBlocks does + block->SetFlags(BBF_DONT_REMOVE); + finallyRet->SetFlags(BBF_DONT_REMOVE); + finallyRet->bbRefs++; // Artificial ref count on handler begins + + fgRemoveRefPred(block->GetTargetEdge()); + // Wire up the control flow for the new blocks + block->SetTargetEdge(fgAddRefPred(callFinally, block)); + callFinally->SetTargetEdge(fgAddRefPred(finallyRet, callFinally)); + + BBehfDesc* ehfDesc = new (this, CMK_BasicBlock) BBehfDesc; + ehfDesc->bbeCount = 1; + ehfDesc->bbeSuccs = new (this, CMK_BasicBlock) FlowEdge* [1] { + fgAddRefPred(callFinallyRet, finallyRet) + }; + ehfDesc->bbeSuccs[0]->setLikelihood(1.0); + finallyRet->SetEhfTargets(ehfDesc); + + callFinallyRet->SetTargetEdge(fgAddRefPred(goToTailBlock, callFinallyRet)); + goToTailBlock->SetTargetEdge(fgAddRefPred(tailBB, goToTailBlock)); + + // Most of these blocks go in the old EH region + callFinally->bbTryIndex = block->bbTryIndex; + callFinallyRet->bbTryIndex = block->bbTryIndex; + finallyRet->bbTryIndex = block->bbTryIndex; + goToTailBlock->bbTryIndex = block->bbTryIndex; + + callFinally->bbHndIndex = block->bbHndIndex; + callFinallyRet->bbHndIndex = block->bbHndIndex; + finallyRet->bbHndIndex = block->bbHndIndex; + goToTailBlock->bbHndIndex = block->bbHndIndex; + + // block goes into the inserted EH clause and the finally becomes the handler + block->bbTryIndex--; + finallyRet->bbHndIndex = block->bbTryIndex; + + ebd->ebdID = impInlineRoot()->compEHID++; + ebd->ebdHandlerType = EH_HANDLER_FINALLY; + + ebd->ebdTryBeg = block; + ebd->ebdTryLast = block; + + ebd->ebdHndBeg = finallyRet; + ebd->ebdHndLast = finallyRet; + + ebd->ebdTyp = 0; + ebd->ebdEnclosingTryIndex = (unsigned short)goToTailBlock->getTryIndex(); + ebd->ebdEnclosingHndIndex = EHblkDsc::NO_ENCLOSING_INDEX; + + ebd->ebdTryBegOffset = block->bbCodeOffs; + ebd->ebdTryEndOffset = block->bbCodeOffsEnd; + ebd->ebdFilterBegOffset = 0; + ebd->ebdHndBegOffset = 0; + ebd->ebdHndEndOffset = 0; + + finallyRet->bbCatchTyp = BBCT_FINALLY; + GenTree* retFilt = gtNewOperNode(GT_RETFILT, TYP_VOID, nullptr); + Statement* retFiltStmt = fgNewStmtFromTree(retFilt); + fgInsertStmtAtEnd(finallyRet, retFiltStmt); + + return finallyRet; +} + class AsyncLiveness { Compiler* m_comp; @@ -320,7 +626,7 @@ PhaseStatus AsyncTransformation::Run() m_newContinuationVar = m_comp->lvaGrabTemp(false DEBUGARG("new continuation")); m_comp->lvaGetDesc(m_newContinuationVar)->lvType = TYP_REF; - m_comp->info.compCompHnd->getAsyncInfo(&m_asyncInfo); + m_asyncInfo = m_comp->eeGetAsyncInfo(); #ifdef JIT32_GCENCODER // Due to a hard cap on epilogs we need a shared return here. @@ -476,7 +782,7 @@ void AsyncTransformation::Transform( unsigned stateNum = (unsigned)m_resumptionBBs.size(); JITDUMP(" Assigned state %u\n", stateNum); - BasicBlock* suspendBB = CreateSuspension(block, stateNum, life, layout); + BasicBlock* suspendBB = CreateSuspension(block, call, stateNum, life, layout); CreateCheckAndSuspendAfterCall(block, callDefInfo, life, suspendBB, remainder); @@ -734,6 +1040,14 @@ ContinuationLayout AsyncTransformation::LayOutContinuation(BasicBlock* block->getTryIndex(), layout.ExceptionGCDataIndex); } + if (call->GetAsyncInfo().ExecutionContextHandling == ExecutionContextHandling::AsyncSaveAndRestore) + { + layout.ExecContextGCDataIndex = layout.GCRefsCount++; + JITDUMP( + " Call has async-only save and restore of ExecutionContext; ExecutionContext will be at GC@+%02u in GC data\n", + layout.ExecContextGCDataIndex); + } + for (LiveLocalInfo& inf : liveLocals) { layout.DataSize = roundUp(layout.DataSize, inf.Alignment); @@ -828,6 +1142,7 @@ CallDefinitionInfo AsyncTransformation::CanonicalizeCallDefinition(BasicBlock* // // Parameters: // block - The block containing the async call +// call - The async call // stateNum - State number assigned to this suspension point // life - Liveness information about live locals // layout - Layout information for the continuation object @@ -835,10 +1150,8 @@ CallDefinitionInfo AsyncTransformation::CanonicalizeCallDefinition(BasicBlock* // Returns: // The new basic block that was created. // -BasicBlock* AsyncTransformation::CreateSuspension(BasicBlock* block, - unsigned stateNum, - AsyncLiveness& life, - const ContinuationLayout& layout) +BasicBlock* AsyncTransformation::CreateSuspension( + BasicBlock* block, GenTreeCall* call, unsigned stateNum, AsyncLiveness& life, const ContinuationLayout& layout) { if (m_lastSuspensionBB == nullptr) { @@ -874,14 +1187,14 @@ BasicBlock* AsyncTransformation::CreateSuspension(BasicBlock* bloc // Fill in 'Resume' GenTree* newContinuation = m_comp->gtNewLclvNode(m_newContinuationVar, TYP_REF); - unsigned resumeOffset = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo.continuationResumeFldHnd); + unsigned resumeOffset = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo->continuationResumeFldHnd); GenTree* resumeStubAddr = CreateResumptionStubAddrTree(); GenTree* storeResume = StoreAtOffset(newContinuation, resumeOffset, resumeStubAddr, TYP_I_IMPL); LIR::AsRange(suspendBB).InsertAtEnd(LIR::SeqTree(m_comp, storeResume)); // Fill in 'state' newContinuation = m_comp->gtNewLclvNode(m_newContinuationVar, TYP_REF); - unsigned stateOffset = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo.continuationStateFldHnd); + unsigned stateOffset = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo->continuationStateFldHnd); GenTree* stateNumNode = m_comp->gtNewIconNode((ssize_t)stateNum, TYP_INT); GenTree* storeState = StoreAtOffset(newContinuation, stateOffset, stateNumNode, TYP_INT); LIR::AsRange(suspendBB).InsertAtEnd(LIR::SeqTree(m_comp, storeState)); @@ -896,14 +1209,14 @@ BasicBlock* AsyncTransformation::CreateSuspension(BasicBlock* bloc continuationFlags |= CORINFO_CONTINUATION_OSR_IL_OFFSET_IN_DATA; newContinuation = m_comp->gtNewLclvNode(m_newContinuationVar, TYP_REF); - unsigned flagsOffset = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo.continuationFlagsFldHnd); + unsigned flagsOffset = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo->continuationFlagsFldHnd); GenTree* flagsNode = m_comp->gtNewIconNode((ssize_t)continuationFlags, TYP_INT); GenTree* storeFlags = StoreAtOffset(newContinuation, flagsOffset, flagsNode, TYP_INT); LIR::AsRange(suspendBB).InsertAtEnd(LIR::SeqTree(m_comp, storeFlags)); if (layout.GCRefsCount > 0) { - FillInGCPointersOnSuspension(layout.Locals, suspendBB); + FillInGCPointersOnSuspension(layout, suspendBB); } if (layout.DataSize > 0) @@ -955,7 +1268,7 @@ GenTreeCall* AsyncTransformation::CreateAllocContinuationCall(AsyncLiveness& lif { classHandleArg = m_comp->gtNewLclvNode(m_comp->info.compTypeCtxtArg, TYP_I_IMPL); } - else if (m_asyncInfo.continuationsNeedMethodHandle) + else if (m_asyncInfo->continuationsNeedMethodHandle) { methodHandleArg = m_comp->gtNewIconEmbMethHndNode(m_comp->info.compMethodHnd); } @@ -983,21 +1296,20 @@ GenTreeCall* AsyncTransformation::CreateAllocContinuationCall(AsyncLiveness& lif // parts that need to be stored. // // Parameters: -// liveLocals - Information about each live local. -// suspendBB - Basic block to add IR to. +// layout - Layout information +// suspendBB - Basic block to add IR to. // -void AsyncTransformation::FillInGCPointersOnSuspension(const jitstd::vector& liveLocals, - BasicBlock* suspendBB) +void AsyncTransformation::FillInGCPointersOnSuspension(const ContinuationLayout& layout, BasicBlock* suspendBB) { unsigned objectArrLclNum = GetGCDataArrayVar(); GenTree* newContinuation = m_comp->gtNewLclvNode(m_newContinuationVar, TYP_REF); - unsigned gcDataOffset = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo.continuationGCDataFldHnd); + unsigned gcDataOffset = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo->continuationGCDataFldHnd); GenTree* gcDataInd = LoadFromOffset(newContinuation, gcDataOffset, TYP_REF); GenTree* storeAllocedObjectArr = m_comp->gtNewStoreLclVarNode(objectArrLclNum, gcDataInd); LIR::AsRange(suspendBB).InsertAtEnd(LIR::SeqTree(m_comp, storeAllocedObjectArr)); - for (const LiveLocalInfo& inf : liveLocals) + for (const LiveLocalInfo& inf : layout.Locals) { if (inf.GCDataCount <= 0) { @@ -1073,6 +1385,22 @@ void AsyncTransformation::FillInGCPointersOnSuspension(const jitstd::vectorgtNewCallNode(CT_USER_FUNC, m_asyncInfo->captureExecutionContextMethHnd, TYP_REF); + + m_comp->compCurBB = suspendBB; + m_comp->fgMorphTree(captureExecContext); + + GenTree* objectArr = m_comp->gtNewLclvNode(objectArrLclNum, TYP_REF); + GenTree* store = + StoreAtOffset(objectArr, + OFFSETOF__CORINFO_Array__data + (layout.ExecContextGCDataIndex * TARGET_POINTER_SIZE), + captureExecContext, TYP_REF); + LIR::AsRange(suspendBB).InsertAtEnd(LIR::SeqTree(m_comp, store)); + } } //------------------------------------------------------------------------ @@ -1088,7 +1416,7 @@ void AsyncTransformation::FillInDataOnSuspension(const jitstd::vectorgtNewLclvNode(m_newContinuationVar, TYP_REF); - unsigned dataOffset = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo.continuationDataFldHnd); + unsigned dataOffset = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo->continuationDataFldHnd); GenTree* dataInd = LoadFromOffset(newContinuation, dataOffset, TYP_REF); GenTree* storeAllocedByteArr = m_comp->gtNewStoreLclVarNode(byteArrLclNum, dataInd); LIR::AsRange(suspendBB).InsertAtEnd(LIR::SeqTree(m_comp, storeAllocedByteArr)); @@ -1241,7 +1569,7 @@ BasicBlock* AsyncTransformation::CreateResumption(BasicBlock* bloc resumeByteArrLclNum = GetDataArrayVar(); GenTree* newContinuation = m_comp->gtNewLclvNode(m_comp->lvaAsyncContinuationArg, TYP_REF); - unsigned dataOffset = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo.continuationDataFldHnd); + unsigned dataOffset = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo->continuationDataFldHnd); GenTree* dataInd = LoadFromOffset(newContinuation, dataOffset, TYP_REF); GenTree* storeAllocedByteArr = m_comp->gtNewStoreLclVarNode(resumeByteArrLclNum, dataInd); @@ -1257,13 +1585,13 @@ BasicBlock* AsyncTransformation::CreateResumption(BasicBlock* bloc { resumeObjectArrLclNum = GetGCDataArrayVar(); - GenTree* newContinuation = m_comp->gtNewLclvNode(m_comp->lvaAsyncContinuationArg, TYP_REF); - unsigned gcDataOffset = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo.continuationGCDataFldHnd); - GenTree* gcDataInd = LoadFromOffset(newContinuation, gcDataOffset, TYP_REF); + GenTree* newContinuation = m_comp->gtNewLclvNode(m_comp->lvaAsyncContinuationArg, TYP_REF); + unsigned gcDataOffset = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo->continuationGCDataFldHnd); + GenTree* gcDataInd = LoadFromOffset(newContinuation, gcDataOffset, TYP_REF); GenTree* storeAllocedObjectArr = m_comp->gtNewStoreLclVarNode(resumeObjectArrLclNum, gcDataInd); LIR::AsRange(resumeBB).InsertAtEnd(LIR::SeqTree(m_comp, storeAllocedObjectArr)); - RestoreFromGCPointersOnResumption(resumeObjectArrLclNum, layout.Locals, resumeBB); + RestoreFromGCPointersOnResumption(resumeObjectArrLclNum, layout, resumeBB); if (layout.ExceptionGCDataIndex != UINT_MAX) { @@ -1271,7 +1599,6 @@ BasicBlock* AsyncTransformation::CreateResumption(BasicBlock* bloc } } - // Copy call return value. if ((layout.ReturnSize > 0) && (callDefInfo.DefinitionNode != nullptr)) { CopyReturnValueOnResumption(call, callDefInfo, resumeByteArrLclNum, resumeObjectArrLclNum, layout, @@ -1346,11 +1673,38 @@ void AsyncTransformation::RestoreFromDataOnResumption(unsigned // liveLocals - Information about each live local. // resumeBB - Basic block to append IR to // -void AsyncTransformation::RestoreFromGCPointersOnResumption(unsigned resumeObjectArrLclNum, - const jitstd::vector& liveLocals, - BasicBlock* resumeBB) +void AsyncTransformation::RestoreFromGCPointersOnResumption(unsigned resumeObjectArrLclNum, + const ContinuationLayout& layout, + BasicBlock* resumeBB) { - for (const LiveLocalInfo& inf : liveLocals) + if (layout.ExecContextGCDataIndex != BAD_VAR_NUM) + { + GenTree* valuePlaceholder = m_comp->gtNewZeroConNode(TYP_REF); + GenTreeCall* restoreCall = + m_comp->gtNewCallNode(CT_USER_FUNC, m_asyncInfo->restoreExecutionContextMethHnd, TYP_VOID); + restoreCall->gtArgs.PushFront(m_comp, NewCallArg::Primitive(valuePlaceholder)); + + m_comp->compCurBB = resumeBB; + m_comp->fgMorphTree(restoreCall); + + LIR::AsRange(resumeBB).InsertAtEnd(LIR::SeqTree(m_comp, restoreCall)); + + LIR::Use valueUse; + bool gotUse = LIR::AsRange(resumeBB).TryGetUse(valuePlaceholder, &valueUse); + assert(gotUse); + + GenTree* objectArr = m_comp->gtNewLclvNode(resumeObjectArrLclNum, TYP_REF); + unsigned execContextOffset = + OFFSETOF__CORINFO_Array__data + (layout.ExecContextGCDataIndex * TARGET_POINTER_SIZE); + GenTree* execContextValue = LoadFromOffset(objectArr, execContextOffset, TYP_REF); + + LIR::AsRange(resumeBB).InsertBefore(valuePlaceholder, LIR::SeqTree(m_comp, execContextValue)); + valueUse.ReplaceWith(execContextValue); + + LIR::AsRange(resumeBB).Remove(valuePlaceholder); + } + + for (const LiveLocalInfo& inf : layout.Locals) { if (inf.GCDataCount <= 0) { @@ -1845,7 +2199,7 @@ void AsyncTransformation::CreateResumptionSwitch() newEntryBB->bbNum, condBB->bbNum); continuationArg = m_comp->gtNewLclvNode(m_comp->lvaAsyncContinuationArg, TYP_REF); - unsigned stateOffset = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo.continuationStateFldHnd); + unsigned stateOffset = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo->continuationStateFldHnd); GenTree* stateOffsetNode = m_comp->gtNewIconNode((ssize_t)stateOffset, TYP_I_IMPL); GenTree* stateAddr = m_comp->gtNewOperNode(GT_ADD, TYP_BYREF, continuationArg, stateOffsetNode); GenTree* stateInd = m_comp->gtNewIndir(TYP_INT, stateAddr, GTF_IND_NONFAULTING); @@ -1867,7 +2221,7 @@ void AsyncTransformation::CreateResumptionSwitch() newEntryBB->bbNum, switchBB->bbNum, m_resumptionBBs.size()); continuationArg = m_comp->gtNewLclvNode(m_comp->lvaAsyncContinuationArg, TYP_REF); - unsigned stateOffset = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo.continuationStateFldHnd); + unsigned stateOffset = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo->continuationStateFldHnd); GenTree* stateOffsetNode = m_comp->gtNewIconNode((ssize_t)stateOffset, TYP_I_IMPL); GenTree* stateAddr = m_comp->gtNewOperNode(GT_ADD, TYP_BYREF, continuationArg, stateOffsetNode); GenTree* stateInd = m_comp->gtNewIndir(TYP_INT, stateAddr, GTF_IND_NONFAULTING); @@ -1932,7 +2286,7 @@ void AsyncTransformation::CreateResumptionSwitch() // We need to dispatch to the OSR version if the IL offset is non-negative. continuationArg = m_comp->gtNewLclvNode(m_comp->lvaAsyncContinuationArg, TYP_REF); - unsigned offsetOfData = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo.continuationDataFldHnd); + unsigned offsetOfData = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo->continuationDataFldHnd); GenTree* dataArr = LoadFromOffset(continuationArg, offsetOfData, TYP_REF); unsigned offsetOfIlOffset = OFFSETOF__CORINFO_Array__data; GenTree* ilOffset = LoadFromOffset(dataArr, offsetOfIlOffset, TYP_INT); @@ -1987,7 +2341,7 @@ void AsyncTransformation::CreateResumptionSwitch() JITDUMP(" Created " FMT_BB " to check for Tier-0 continuations\n", checkILOffsetBB->bbNum); continuationArg = m_comp->gtNewLclvNode(m_comp->lvaAsyncContinuationArg, TYP_REF); - unsigned offsetOfData = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo.continuationDataFldHnd); + unsigned offsetOfData = m_comp->info.compCompHnd->getFieldOffset(m_asyncInfo->continuationDataFldHnd); GenTree* dataArr = LoadFromOffset(continuationArg, offsetOfData, TYP_REF); unsigned offsetOfIlOffset = OFFSETOF__CORINFO_Array__data; GenTree* ilOffset = LoadFromOffset(dataArr, offsetOfIlOffset, TYP_INT); diff --git a/src/coreclr/jit/async.h b/src/coreclr/jit/async.h index 63e1db0a636ed0..83732fd241187c 100644 --- a/src/coreclr/jit/async.h +++ b/src/coreclr/jit/async.h @@ -18,13 +18,14 @@ struct LiveLocalInfo struct ContinuationLayout { - unsigned DataSize = 0; - unsigned GCRefsCount = 0; - ClassLayout* ReturnStructLayout = nullptr; - unsigned ReturnSize = 0; - bool ReturnInGCData = false; - unsigned ReturnValDataOffset = UINT_MAX; - unsigned ExceptionGCDataIndex = UINT_MAX; + unsigned DataSize = 0; + unsigned GCRefsCount = 0; + ClassLayout* ReturnStructLayout = nullptr; + unsigned ReturnSize = 0; + bool ReturnInGCData = false; + unsigned ReturnValDataOffset = UINT_MAX; + unsigned ExceptionGCDataIndex = UINT_MAX; + unsigned ExecContextGCDataIndex = UINT_MAX; const jitstd::vector& Locals; explicit ContinuationLayout(const jitstd::vector& locals) @@ -47,7 +48,7 @@ class AsyncTransformation Compiler* m_comp; jitstd::vector m_liveLocalsScratch; - CORINFO_ASYNC_INFO m_asyncInfo; + CORINFO_ASYNC_INFO* m_asyncInfo; jitstd::vector m_resumptionBBs; CORINFO_METHOD_HANDLE m_resumeStub = NO_METHOD_HANDLE; CORINFO_CONST_LOOKUP m_resumeStubLookup; @@ -84,15 +85,13 @@ class AsyncTransformation CallDefinitionInfo CanonicalizeCallDefinition(BasicBlock* block, GenTreeCall* call, AsyncLiveness& life); - BasicBlock* CreateSuspension(BasicBlock* block, - unsigned stateNum, - AsyncLiveness& life, - const ContinuationLayout& layout); + BasicBlock* CreateSuspension( + BasicBlock* block, GenTreeCall* call, unsigned stateNum, AsyncLiveness& life, const ContinuationLayout& layout); GenTreeCall* CreateAllocContinuationCall(AsyncLiveness& life, GenTree* prevContinuation, unsigned gcRefsCount, unsigned int dataSize); - void FillInGCPointersOnSuspension(const jitstd::vector& liveLocals, BasicBlock* suspendBB); + void FillInGCPointersOnSuspension(const ContinuationLayout& layout, BasicBlock* suspendBB); void FillInDataOnSuspension(const jitstd::vector& liveLocals, BasicBlock* suspendBB); void CreateCheckAndSuspendAfterCall(BasicBlock* block, const CallDefinitionInfo& callDefInfo, @@ -109,9 +108,9 @@ class AsyncTransformation void RestoreFromDataOnResumption(unsigned resumeByteArrLclNum, const jitstd::vector& liveLocals, BasicBlock* resumeBB); - void RestoreFromGCPointersOnResumption(unsigned resumeObjectArrLclNum, - const jitstd::vector& liveLocals, - BasicBlock* resumeBB); + void RestoreFromGCPointersOnResumption(unsigned resumeObjectArrLclNum, + const ContinuationLayout& layout, + BasicBlock* resumeBB); BasicBlock* RethrowExceptionOnResumption(BasicBlock* block, BasicBlock* remainder, unsigned resumeObjectArrLclNum, diff --git a/src/coreclr/jit/compiler.cpp b/src/coreclr/jit/compiler.cpp index db283b444cd7e7..ca4bd2a1366010 100644 --- a/src/coreclr/jit/compiler.cpp +++ b/src/coreclr/jit/compiler.cpp @@ -2508,7 +2508,7 @@ void Compiler::compInitOptions(JitFlags* jitFlags) } // Stash pointers to PGO info on the context so - // we can access contextually it later. + // we can access it contextually later. // compInlineContext->SetPgoInfo(PgoInfo(this)); } @@ -4377,6 +4377,10 @@ void Compiler::compCompile(void** methodCodePtr, uint32_t* methodCodeSize, JitFl // DoPhase(this, PHASE_POST_IMPORT, &Compiler::fgPostImportationCleanup); + // Capture and restore contexts around awaited calls, if needed. + // + DoPhase(this, PHASE_ASYNC_SAVE_CONTEXTS, &Compiler::SaveAsyncContexts); + // If we're importing for inlining, we're done. if (compIsForInlining()) { diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index 5a123c8f0a6a6a..a87b83e6b24c04 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -4428,6 +4428,8 @@ class Compiler #ifdef DEBUG PREFIX_TAILCALL_STRESS = 0x00000040, // call doesn't "tail" IL prefix but is treated as explicit because of tail call stress #endif + // This call is a task await + PREFIX_IS_TASK_AWAIT = 0x00000080, }; static void impValidateMemoryAccessOpcode(const BYTE* codeAddr, const BYTE* codeEndp, bool volatilePrefix); @@ -4844,7 +4846,7 @@ class Compiler bool impMatchIsInstBooleanConversion(const BYTE* codeAddr, const BYTE* codeEndp, int* consumed); - bool impMatchAwaitPattern(const BYTE * codeAddr, const BYTE * codeEndp, int* configVal); + bool impMatchTaskAwaitPattern(const BYTE * codeAddr, const BYTE * codeEndp, int* configVal); GenTree* impCastClassOrIsInstToTree( GenTree* op1, GenTree* op2, CORINFO_RESOLVED_TOKEN* pResolvedToken, bool isCastClass, bool* booleanCheck, IL_OFFSET ilOffset); @@ -5525,6 +5527,10 @@ class Compiler PhaseStatus placeLoopAlignInstructions(); #endif + PhaseStatus SaveAsyncContexts(); + BasicBlock* InsertTryFinallyForContextRestore(BasicBlock* block, Statement* firstStmt, Statement* lastStmt); + void ValidateNoAsyncSavesNecessary(); + void ValidateNoAsyncSavesNecessaryInStatement(Statement* stmt); PhaseStatus TransformAsync(); // This field keep the R2R helper call that would be inserted to trigger the constructor @@ -8376,6 +8382,11 @@ class Compiler CORINFO_EE_INFO* eeGetEEInfo(); + CORINFO_ASYNC_INFO asyncInfo; + bool asyncInfoInitialized = false; + + CORINFO_ASYNC_INFO* eeGetAsyncInfo(); + // Gets the offset of a SDArray's first element static unsigned eeGetArrayDataOffset(); @@ -9834,6 +9845,8 @@ class Compiler bool compSwitchedToMinOpts = false; // Codegen initially was Tier1/FullOpts but jit switched to MinOpts bool compSuppressedZeroInit = false; // There are vars with lvSuppressedZeroInit set bool compMaskConvertUsed = false; // Does the method have Convert Mask To Vector nodes. + bool compUsesThrowHelper = false; // There is a call to a THROW_HELPER for the compiled method. + bool compMustSaveAsyncContexts = false; // There is an async call that needs capture/restore of async contexts. // NOTE: These values are only reliable after // the importing is completely finished. @@ -9860,8 +9873,6 @@ class Compiler bool compLSRADone = false; bool compRationalIRForm = false; - bool compUsesThrowHelper = false; // There is a call to a THROW_HELPER for the compiled method. - bool compGeneratingProlog = false; bool compGeneratingEpilog = false; bool compGeneratingUnwindProlog = false; diff --git a/src/coreclr/jit/compphases.h b/src/coreclr/jit/compphases.h index 03a7df476e65df..035bfc65118820 100644 --- a/src/coreclr/jit/compphases.h +++ b/src/coreclr/jit/compphases.h @@ -28,6 +28,7 @@ CompPhaseNameMacro(PHASE_IMPORTATION, "Importation", CompPhaseNameMacro(PHASE_INDXCALL, "Indirect call transform", false, -1, true) CompPhaseNameMacro(PHASE_PATCHPOINTS, "Expand patchpoints", false, -1, true) CompPhaseNameMacro(PHASE_POST_IMPORT, "Post-import", false, -1, false) +CompPhaseNameMacro(PHASE_ASYNC_SAVE_CONTEXTS, "Save contexts around async calls",false, -1, false) CompPhaseNameMacro(PHASE_IBCPREP, "Profile instrumentation prep", false, -1, false) CompPhaseNameMacro(PHASE_IBCINSTR, "Profile instrumentation", false, -1, false) CompPhaseNameMacro(PHASE_INCPROFILE, "Profile incorporation", false, -1, false) diff --git a/src/coreclr/jit/ee_il_dll.hpp b/src/coreclr/jit/ee_il_dll.hpp index 32289aff62dac2..303340a446612b 100644 --- a/src/coreclr/jit/ee_il_dll.hpp +++ b/src/coreclr/jit/ee_il_dll.hpp @@ -176,6 +176,17 @@ inline CORINFO_EE_INFO* Compiler::eeGetEEInfo() return &eeInfo; } +inline CORINFO_ASYNC_INFO* Compiler::eeGetAsyncInfo() +{ + if (!asyncInfoInitialized) + { + info.compCompHnd->getAsyncInfo(&asyncInfo); + asyncInfoInitialized = true; + } + + return &asyncInfo; +} + /***************************************************************************** * * Convert the type returned from the VM to a var_type. diff --git a/src/coreclr/jit/fgbasic.cpp b/src/coreclr/jit/fgbasic.cpp index a0b4cd8d7b6c09..958aa3cff0eb22 100644 --- a/src/coreclr/jit/fgbasic.cpp +++ b/src/coreclr/jit/fgbasic.cpp @@ -3857,9 +3857,6 @@ void Compiler::fgFindBasicBlocks() filtBB->bbRefs++; // The first block of a filter gets an extra, "artificial" reference count. } - tryBegBB->SetFlags(BBF_DONT_REMOVE); - hndBegBB->SetFlags(BBF_DONT_REMOVE); - // // Store the info to the table of EH block handlers // diff --git a/src/coreclr/jit/fgdiagnostic.cpp b/src/coreclr/jit/fgdiagnostic.cpp index 08ed93adbf9775..5203c36b8e1d69 100644 --- a/src/coreclr/jit/fgdiagnostic.cpp +++ b/src/coreclr/jit/fgdiagnostic.cpp @@ -2764,7 +2764,14 @@ bool BBPredsChecker::CheckJump(BasicBlock* blockPred, BasicBlock* block) case BBJ_CALLFINALLYRET: case BBJ_EHCATCHRET: case BBJ_EHFILTERRET: - assert(blockPred->TargetIs(block)); + if (!blockPred->TargetIs(block)) + { + JITDUMP(FMT_BB " -> " FMT_BB " from pred links does not match " FMT_BB " -> " FMT_BB + " from succ links\n", + blockPred->bbNum, block->bbNum, blockPred->bbNum, + blockPred->GetTarget() == nullptr ? 0 : blockPred->GetTarget()->bbNum); + assert(!"Invalid block preds"); + } assert(blockPred->GetTargetEdge()->getLikelihood() == 1.0); return true; diff --git a/src/coreclr/jit/fginline.cpp b/src/coreclr/jit/fginline.cpp index 1439d544c680dd..8d9919ee895093 100644 --- a/src/coreclr/jit/fginline.cpp +++ b/src/coreclr/jit/fginline.cpp @@ -1721,9 +1721,7 @@ void Compiler::fgInsertInlineeBlocks(InlineInfo* pInlineInfo) JITDUMP("Inlinee is not nested inside any EH region\n"); } - // Grow the EH table. - // - // TODO: verify earlier that this won't fail... + // Grow the EH table. We verified in fgFindBasicBlocks that this won't fail. // EHblkDsc* const outermostEbd = fgTryAddEHTableEntries(insertBeforeIndex, inlineeRegionCount, /* deferAdding */ false); @@ -2397,7 +2395,7 @@ Statement* Compiler::fgInlinePrependStatements(InlineInfo* inlineInfo) // If the call we're inlining is in tail position then // we skip nulling the locals, since it can interfere // with tail calls introduced by the local. - +// void Compiler::fgInlineAppendStatements(InlineInfo* inlineInfo, BasicBlock* block, Statement* stmtAfter) { // Null out any gc ref locals diff --git a/src/coreclr/jit/gentree.cpp b/src/coreclr/jit/gentree.cpp index ee57e958e439f9..5b3ec103373e8a 100644 --- a/src/coreclr/jit/gentree.cpp +++ b/src/coreclr/jit/gentree.cpp @@ -2236,6 +2236,24 @@ bool GenTreeCall::IsAsync() const return (gtCallMoreFlags & GTF_CALL_M_ASYNC) != 0; } +//------------------------------------------------------------------------- +// IsAsyncAndAlwaysSavesAndRestoresExecutionContext: +// Check if this is an async call that always saves and restores the +// ExecutionContext around it. +// +// Return Value: +// True if so. +// +// Remarks: +// Normal user await calls have this behavior, while custom awaiters (via +// AsyncHelpers.AwaitAwaiter) only saves and restores the ExecutionContext if +// actual suspension happens. +// +bool GenTreeCall::IsAsyncAndAlwaysSavesAndRestoresExecutionContext() const +{ + return IsAsync() && (GetAsyncInfo().ExecutionContextHandling == ExecutionContextHandling::SaveAndRestore); +} + //------------------------------------------------------------------------- // HasNonStandardAddedArgs: Return true if the method has non-standard args added to the call // argument list during argument morphing (fgMorphArgs), e.g., passed in R10 or R11 on AMD64. @@ -9887,9 +9905,20 @@ GenTreeCall* Compiler::gtCloneExprCallHelper(GenTreeCall* tree) // because the inlinee still uses the inliner's memory allocator anyway.) INDEBUG(copy->callSig = tree->callSig;) - // The tail call info does not change after it is allocated, so for the same reasons as above - // a shallow copy suffices. - copy->tailCallInfo = tree->tailCallInfo; + if (tree->IsUnmanaged()) + { + copy->unmgdCallConv = tree->unmgdCallConv; + } + else if (tree->IsAsync()) + { + copy->asyncInfo = tree->asyncInfo; + } + else if (tree->IsTailPrefixedCall()) + { + // The tail call info does not change after it is allocated, so for the same reasons as above + // a shallow copy suffices. + copy->tailCallInfo = tree->tailCallInfo; + } copy->gtRetClsHnd = tree->gtRetClsHnd; copy->gtControlExpr = gtCloneExpr(tree->gtControlExpr); diff --git a/src/coreclr/jit/gentree.h b/src/coreclr/jit/gentree.h index 148cc40d0ba97b..982dd41c611ec3 100644 --- a/src/coreclr/jit/gentree.h +++ b/src/coreclr/jit/gentree.h @@ -4303,6 +4303,24 @@ inline GenTreeCallDebugFlags& operator &=(GenTreeCallDebugFlags& a, GenTreeCallD // clang-format on +enum class ExecutionContextHandling +{ + // No special handling of execution context is required. + None, + // Always save and restore ExecutionContext around this await. + // Used for task awaits. + SaveAndRestore, + // Save and restore execution context on suspension/resumption only. + // Used for custom awaitables. + AsyncSaveAndRestore, +}; + +// Additional async call info. +struct AsyncCallInfo +{ + ExecutionContextHandling ExecutionContextHandling = ExecutionContextHandling::None; +}; + // Return type descriptor of a GT_CALL node. // x64 Unix, Arm64, Arm32 and x86 allow a value to be returned in multiple // registers. For such calls this struct provides the following info @@ -4955,9 +4973,12 @@ struct GenTreeCall final : public GenTree union { + // Used for explicit tail prefixed calls TailCallSiteInfo* tailCallInfo; // Only used for unmanaged calls, which cannot be tail-called CorInfoCallConvExtension unmgdCallConv; + // Used for async calls + const AsyncCallInfo* asyncInfo; }; #if FEATURE_MULTIREG_RET @@ -5015,13 +5036,23 @@ struct GenTreeCall final : public GenTree #endif } - void SetIsAsync() + void SetIsAsync(const AsyncCallInfo* info) { + assert(info != nullptr); gtCallMoreFlags |= GTF_CALL_M_ASYNC; + asyncInfo = info; } bool IsAsync() const; + const AsyncCallInfo& GetAsyncInfo() const + { + assert(IsAsync()); + return *asyncInfo; + } + + bool IsAsyncAndAlwaysSavesAndRestoresExecutionContext() const; + //--------------------------------------------------------------------------- // GetRegNumByIdx: get i'th return register allocated to this call node. // diff --git a/src/coreclr/jit/gtlist.h b/src/coreclr/jit/gtlist.h index 67964f7a3865c7..0ec112781c050e 100644 --- a/src/coreclr/jit/gtlist.h +++ b/src/coreclr/jit/gtlist.h @@ -317,7 +317,7 @@ GTNODE(NO_OP , GenTree ,0,0,GTK_LEAF|GTK_NOVALUE) // A NOP // Suspend an async method, returning a continuation. // Before lowering this is a seemingly normal TYP_VOID node with a lot of side effects (GTF_CALL | GTF_GLOB_REF | GTF_ORDER_SIDEEFF). // Lowering then removes all successor nodes and leaves it as the terminator node. -GTNODE(RETURN_SUSPEND , GenTreeOp ,0,1,GTK_UNOP|GTK_NOVALUE) // Return a continuation in an async method +GTNODE(RETURN_SUSPEND , GenTreeOp ,0,1,GTK_UNOP|GTK_NOVALUE) GTNODE(START_NONGC , GenTree ,0,0,GTK_LEAF|GTK_NOVALUE|DBK_NOTHIR) // Starts a new instruction group that will be non-gc interruptible. GTNODE(START_PREEMPTGC , GenTree ,0,0,GTK_LEAF|GTK_NOVALUE|DBK_NOTHIR) // Starts a new instruction group where preemptive GC is enabled. diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp index be1873657fa8dc..a578fa9dd41b4f 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -6012,8 +6012,9 @@ bool Compiler::impBlockIsInALoop(BasicBlock* block) } //------------------------------------------------------------------------ -// impMatchAwaitPattern: check if a method call starts an Await pattern -// that can be optimized for runtime async +// impMatchTaskAwaitPattern: +// Check if a method call starts an a task await pattern that can be +// optimized for runtime async // // Arguments: // codeAddr - IL after call[virt] @@ -6023,7 +6024,7 @@ bool Compiler::impBlockIsInALoop(BasicBlock* block) // Returns: // true if this is an Await that we can optimize // -bool Compiler::impMatchAwaitPattern(const BYTE* codeAddr, const BYTE* codeEndp, int* configVal) +bool Compiler::impMatchTaskAwaitPattern(const BYTE* codeAddr, const BYTE* codeEndp, int* configVal) { // If we see the following code pattern in runtime async methods: // @@ -9127,7 +9128,11 @@ void Compiler::impImportBlockCode(BasicBlock* block) int configVal = -1; // -1 not configured, 0/1 configured to false/true if (compIsAsync() && JitConfig.JitOptimizeAwait()) { - isAwait = impMatchAwaitPattern(codeAddr, codeEndp, &configVal); + if (impMatchTaskAwaitPattern(codeAddr, codeEndp, &configVal)) + { + isAwait = true; + prefixFlags |= PREFIX_IS_TASK_AWAIT; + } } if (isAwait) @@ -9138,7 +9143,9 @@ void Compiler::impImportBlockCode(BasicBlock* block) // There is a runtime async variant that is implicitly awaitable, just call that. // if configured, skip {ldc call ConfigureAwait} if (configVal >= 0) + { codeAddr += 2 + sizeof(mdToken); + } // Skip the call to `Await` codeAddr += 1 + sizeof(mdToken); @@ -9147,7 +9154,7 @@ void Compiler::impImportBlockCode(BasicBlock* block) { // This can happen in rare cases when the Task-returning method is not a runtime Async // function. For example "T M1(T arg) => arg" when called with a Task argument. Treat - // that as a regualr call that is Awaited + // that as a regular call that is Awaited _impResolveToken(CORINFO_TOKENKIND_Method); } } diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 10ac50c6dabbad..f1714a45e64113 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -699,7 +699,51 @@ var_types Compiler::impImportCall(OPCODE opcode, if (sig->isAsyncCall()) { - call->AsCall()->SetIsAsync(); + AsyncCallInfo asyncInfo; + + JITDUMP("Call is an async "); + + if ((prefixFlags & PREFIX_IS_TASK_AWAIT) != 0) + { + JITDUMP("task await\n"); + + asyncInfo.ExecutionContextHandling = ExecutionContextHandling::SaveAndRestore; + } + else + { + JITDUMP("non-task await\n"); + // Only expected non-task await to see in IL is one of the AsyncHelpers.AwaitAwaiter variants. + // These are awaits of custom awaitables, and they come with the behavior that the execution context + // is captured and restored on suspension/resumption. + // We could perhaps skip this for AwaitAwaiter (but not for UnsafeAwaitAwaiter) since it is expected + // that the safe INotifyCompletion will take care of flowing ExecutionContext. + asyncInfo.ExecutionContextHandling = ExecutionContextHandling::AsyncSaveAndRestore; + } + + // For tailcalls the context does not need saving/restoring: it will be + // overwritten by the caller anyway. + // + // More specifically, if we can show that + // Thread.CurrentThread._executionContext is not accessed between the + // call and returning then we can omit save/restore of the execution + // context. We do not do that optimization yet. + if (tailCallFlags != 0) + { + asyncInfo.ExecutionContextHandling = ExecutionContextHandling::None; + } + + call->AsCall()->SetIsAsync(new (this, CMK_Async) AsyncCallInfo(asyncInfo)); + + if (asyncInfo.ExecutionContextHandling == ExecutionContextHandling::SaveAndRestore) + { + compMustSaveAsyncContexts = true; + + // In this case we will need to save the context after the arguments are evaluated. + // Spill the arguments to accomplish that. + // (We could do this via splitting in SaveAsyncContexts, but since we need to + // handle inline candidates we won't gain much.) + impSpillSideEffects(true, CHECK_SPILL_ALL DEBUGARG("Async await with execution context save and restore")); + } } // Now create the argument list. @@ -1445,32 +1489,53 @@ var_types Compiler::impImportCall(OPCODE opcode, // Propagate retExpr as the placeholder for the call. call = retExpr; + + if (origCall->IsAsyncAndAlwaysSavesAndRestoresExecutionContext()) + { + // Async calls that require save/restore of + // ExecutionContext need to be top most so that we can + // insert try-finally around them. We can inline these, so + // we need to ensure that the RET_EXPR is findable when we + // later expand this. + + unsigned resultLcl = lvaGrabTemp(true DEBUGARG("async")); + LclVarDsc* varDsc = lvaGetDesc(resultLcl); + // Keep the information about small typedness to avoid + // inserting unnecessary casts around normalization. + if (varTypeIsSmall(origCall->gtReturnType)) + { + assert(origCall->NormalizesSmallTypesOnReturn()); + varDsc->lvType = origCall->gtReturnType; + } + + impStoreToTemp(resultLcl, call, CHECK_SPILL_ALL); + // impStoreToTemp can change src arg list and return type for call that returns struct. + var_types type = genActualType(lvaGetDesc(resultLcl)->TypeGet()); + call = gtNewLclvNode(resultLcl, type); + } } else { - if (isFatPointerCandidate) + if (call->IsCall() && + (isFatPointerCandidate || call->AsCall()->IsAsyncAndAlwaysSavesAndRestoresExecutionContext())) { - // fatPointer candidates should be in statements of the form call() or var = call(). + // these calls should be in statements of the form call() or var = call(). // Such form allows to find statements with fat calls without walking through whole trees // and removes problems with cutting trees. - assert(IsTargetAbi(CORINFO_NATIVEAOT_ABI)); - if (!call->OperIs(GT_LCL_VAR)) // can be already converted by impFixupCallStructReturn. + unsigned resultLcl = lvaGrabTemp(true DEBUGARG(isFatPointerCandidate ? "calli" : "async")); + LclVarDsc* varDsc = lvaGetDesc(resultLcl); + // Keep the information about small typedness to avoid + // inserting unnecessary casts around normalization. + if (varTypeIsSmall(call->AsCall()->gtReturnType)) { - unsigned calliSlot = lvaGrabTemp(true DEBUGARG("calli")); - LclVarDsc* varDsc = lvaGetDesc(calliSlot); - // Keep the information about small typedness to avoid - // inserting unnecessary casts around normalization. - if (call->IsCall() && varTypeIsSmall(call->AsCall()->gtReturnType)) - { - assert(call->AsCall()->NormalizesSmallTypesOnReturn()); - varDsc->lvType = call->AsCall()->gtReturnType; - } - - impStoreToTemp(calliSlot, call, CHECK_SPILL_ALL); - // impStoreToTemp can change src arg list and return type for call that returns struct. - var_types type = genActualType(lvaTable[calliSlot].TypeGet()); - call = gtNewLclvNode(calliSlot, type); + assert(call->AsCall()->NormalizesSmallTypesOnReturn()); + varDsc->lvType = call->AsCall()->gtReturnType; } + + impStoreToTemp(resultLcl, call, CHECK_SPILL_ALL); + // impStoreToTemp can change src arg list and return type for call that returns struct. + var_types type = genActualType(lvaGetDesc(resultLcl)->TypeGet()); + call = gtNewLclvNode(resultLcl, type); } // For non-candidates we must also spill, since we diff --git a/src/coreclr/jit/inline.cpp b/src/coreclr/jit/inline.cpp index 7c862c8744d130..33474bbbfda689 100644 --- a/src/coreclr/jit/inline.cpp +++ b/src/coreclr/jit/inline.cpp @@ -1309,9 +1309,8 @@ InlineContext* InlineStrategy::NewContext(InlineContext* parentContext, Statemen context->m_Sibling = parentContext->m_Child; parentContext->m_Child = context; - // In debug builds we record inline contexts in all produced calls to be - // able to show all failed inlines in the inline tree, even non-candidates. - // These should always match the parent context we are seeing here. + // The inline context should always match the parent context we are seeing + // here. assert(parentContext == call->gtInlineContext); if (call->IsInlineCandidate()) diff --git a/src/coreclr/jit/jiteh.cpp b/src/coreclr/jit/jiteh.cpp index 50fa24a24cc48d..b1dbebdd0c0513 100644 --- a/src/coreclr/jit/jiteh.cpp +++ b/src/coreclr/jit/jiteh.cpp @@ -1742,7 +1742,7 @@ void Compiler::fgRemoveEHTableEntry(unsigned XTnum) // // Notes: // -// Note that changes the size of the exception table. +// Note that this changes the size of the exception table. // All the blocks referring to the various index values are updated. // The new table entries are not filled in. // @@ -1868,7 +1868,7 @@ EHblkDsc* Compiler::fgTryAddEHTableEntries(unsigned XTnum, unsigned count, bool // yet, such as when we add an EH region for synchronized methods that don't already have one, // we start at zero, so we need to make sure the new table has at least one entry. // - unsigned newHndBBtabAllocCount = max(1u, compHndBBtabAllocCount + newCount); + unsigned newHndBBtabAllocCount = max(1u, newCount); noway_assert(compHndBBtabAllocCount < newHndBBtabAllocCount); // check for overflow if (newHndBBtabAllocCount > MAX_XCPTN_INDEX) @@ -2131,7 +2131,7 @@ void Compiler::fgSortEHTable() // // The benefit of this is that adding a block to an EH region will not require examining every EH region, // looking for possible shared "first" blocks to adjust. It also makes it easier to put code at the top -// of a particular EH region, especially for loop optimizations. +// of a particular EH region. // // These empty blocks (BB08, BB09) will generate no code (unless some code is subsequently placed into them), // and will have the same native code offset as BB01 after code is generated. There may be labels generated diff --git a/src/coreclr/jit/lower.cpp b/src/coreclr/jit/lower.cpp index e1025850ef7c8f..398ee64eb14fe8 100644 --- a/src/coreclr/jit/lower.cpp +++ b/src/coreclr/jit/lower.cpp @@ -5875,7 +5875,7 @@ GenTree* Lowering::LowerAsyncContinuation(GenTree* asyncCont) { JITDUMP("Marking the call [%06u] before async continuation [%06u] as an async call\n", Compiler::dspTreeID(node), Compiler::dspTreeID(asyncCont)); - node->AsCall()->SetIsAsync(); + node->AsCall()->SetIsAsync(new (comp, CMK_Async) AsyncCallInfo); } BlockRange().Remove(asyncCont); diff --git a/src/coreclr/tools/superpmi/superpmi-shared/agnostic.h b/src/coreclr/tools/superpmi/superpmi-shared/agnostic.h index 3dc7e0d5533472..1a04979bad37a5 100644 --- a/src/coreclr/tools/superpmi/superpmi-shared/agnostic.h +++ b/src/coreclr/tools/superpmi/superpmi-shared/agnostic.h @@ -203,6 +203,8 @@ struct Agnostic_CORINFO_ASYNC_INFO DWORDLONG continuationDataFldHnd; DWORDLONG continuationGCDataFldHnd; DWORD continuationsNeedMethodHandle; + DWORDLONG captureExecutionContextMethHnd; + DWORDLONG restoreExecutionContextMethHnd; }; struct Agnostic_GetOSRInfo diff --git a/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp b/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp index 55d5f732db35fb..bfdab057de446b 100644 --- a/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp +++ b/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp @@ -4484,6 +4484,8 @@ void MethodContext::recGetAsyncInfo(const CORINFO_ASYNC_INFO* pAsyncInfo) value.continuationDataFldHnd = CastHandle(pAsyncInfo->continuationDataFldHnd); value.continuationGCDataFldHnd = CastHandle(pAsyncInfo->continuationGCDataFldHnd); value.continuationsNeedMethodHandle = pAsyncInfo->continuationsNeedMethodHandle ? 1 : 0; + value.captureExecutionContextMethHnd = CastHandle(pAsyncInfo->captureExecutionContextMethHnd); + value.restoreExecutionContextMethHnd = CastHandle(pAsyncInfo->restoreExecutionContextMethHnd); GetAsyncInfo->Add(0, value); DEBUG_REC(dmpGetAsyncInfo(0, value)); @@ -4507,6 +4509,8 @@ void MethodContext::repGetAsyncInfo(CORINFO_ASYNC_INFO* pAsyncInfoOut) pAsyncInfoOut->continuationDataFldHnd = (CORINFO_FIELD_HANDLE)value.continuationDataFldHnd; pAsyncInfoOut->continuationGCDataFldHnd = (CORINFO_FIELD_HANDLE)value.continuationGCDataFldHnd; pAsyncInfoOut->continuationsNeedMethodHandle = value.continuationsNeedMethodHandle != 0; + pAsyncInfoOut->captureExecutionContextMethHnd = (CORINFO_METHOD_HANDLE)value.captureExecutionContextMethHnd; + pAsyncInfoOut->restoreExecutionContextMethHnd = (CORINFO_METHOD_HANDLE)value.restoreExecutionContextMethHnd; DEBUG_REP(dmpGetAsyncInfo(0, value)); } diff --git a/src/coreclr/vm/corelib.h b/src/coreclr/vm/corelib.h index 40525a6f65c731..16cd41fd7e90f4 100644 --- a/src/coreclr/vm/corelib.h +++ b/src/coreclr/vm/corelib.h @@ -726,6 +726,8 @@ DEFINE_METHOD(ASYNC_HELPERS, FINALIZE_TASK_RETURNING_THUNK_1, FinalizeTaskR DEFINE_METHOD(ASYNC_HELPERS, FINALIZE_VALUETASK_RETURNING_THUNK, FinalizeValueTaskReturningThunk, SM_Continuation_RetValueTask) DEFINE_METHOD(ASYNC_HELPERS, FINALIZE_VALUETASK_RETURNING_THUNK_1, FinalizeValueTaskReturningThunk, GM_Continuation_RetValueTaskOfT) DEFINE_METHOD(ASYNC_HELPERS, UNSAFE_AWAIT_AWAITER_1, UnsafeAwaitAwaiter, GM_T_RetVoid) +DEFINE_METHOD(ASYNC_HELPERS, CAPTURE_EXECUTION_CONTEXT, CaptureExecutionContext, NoSig) +DEFINE_METHOD(ASYNC_HELPERS, RESTORE_EXECUTION_CONTEXT, RestoreExecutionContext, NoSig) DEFINE_CLASS(SPAN_HELPERS, System, SpanHelpers) DEFINE_METHOD(SPAN_HELPERS, MEMSET, Fill, SM_RefByte_Byte_UIntPtr_RetVoid) diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 784043ae4b4cb4..7516fb8da883e1 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -10234,6 +10234,8 @@ void CEEInfo::getAsyncInfo(CORINFO_ASYNC_INFO* pAsyncInfoOut) pAsyncInfoOut->continuationDataFldHnd = CORINFO_FIELD_HANDLE(CoreLibBinder::GetField(FIELD__CONTINUATION__DATA)); pAsyncInfoOut->continuationGCDataFldHnd = CORINFO_FIELD_HANDLE(CoreLibBinder::GetField(FIELD__CONTINUATION__GCDATA)); pAsyncInfoOut->continuationsNeedMethodHandle = m_pMethodBeingCompiled->GetLoaderAllocator()->CanUnload(); + pAsyncInfoOut->captureExecutionContextMethHnd = CORINFO_METHOD_HANDLE(CoreLibBinder::GetMethod(METHOD__ASYNC_HELPERS__CAPTURE_EXECUTION_CONTEXT)); + pAsyncInfoOut->restoreExecutionContextMethHnd = CORINFO_METHOD_HANDLE(CoreLibBinder::GetMethod(METHOD__ASYNC_HELPERS__RESTORE_EXECUTION_CONTEXT)); EE_TO_JIT_TRANSITION(); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs index 83c3cd0698b55b..7b2a94ce56b175 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs @@ -23,13 +23,13 @@ public static partial class AsyncHelpers [RequiresPreviewFeatures] public static void AwaitAwaiter(TAwaiter awaiter) where TAwaiter : INotifyCompletion { - ref AsyncHelpers.RuntimeAsyncAwaitState state = ref AsyncHelpers.t_runtimeAsyncAwaitState; + ref RuntimeAsyncAwaitState state = ref t_runtimeAsyncAwaitState; Continuation? sentinelContinuation = state.SentinelContinuation; if (sentinelContinuation == null) state.SentinelContinuation = sentinelContinuation = new Continuation(); state.Notifier = awaiter; - AsyncHelpers.AsyncSuspend(sentinelContinuation); + AsyncSuspend(sentinelContinuation); } // Must be NoInlining because we use AsyncSuspend to manufacture an explicit suspension point. @@ -39,13 +39,13 @@ public static void AwaitAwaiter(TAwaiter awaiter) where TAwaiter : INo [RequiresPreviewFeatures] public static void UnsafeAwaitAwaiter(TAwaiter awaiter) where TAwaiter : ICriticalNotifyCompletion { - ref AsyncHelpers.RuntimeAsyncAwaitState state = ref AsyncHelpers.t_runtimeAsyncAwaitState; + ref RuntimeAsyncAwaitState state = ref t_runtimeAsyncAwaitState; Continuation? sentinelContinuation = state.SentinelContinuation; if (sentinelContinuation == null) state.SentinelContinuation = sentinelContinuation = new Continuation(); state.Notifier = awaiter; - AsyncHelpers.AsyncSuspend(sentinelContinuation); + AsyncSuspend(sentinelContinuation); } [Intrinsic] diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs index 25c20786826a01..9539b7182eba9b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs @@ -391,6 +391,16 @@ public static void Sleep(int millisecondsTimeout) internal static void FastPollGC() => FastPollGC(); #endif + internal static Thread CurrentThreadAssumedInitialized + { + get + { + Thread? thread = t_currentThread; + Debug.Assert(thread != null); + return thread; + } + } + public ExecutionContext? ExecutionContext => ExecutionContext.Capture(); public string? Name diff --git a/src/tests/async/execution-context/execution-context.cs b/src/tests/async/execution-context/execution-context.cs new file mode 100644 index 00000000000000..6455792a0d0f50 --- /dev/null +++ b/src/tests/async/execution-context/execution-context.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +public class Async2ExecutionContext +{ + [Fact] + public static void TestEntryPoint() + { + Test().GetAwaiter().GetResult(); + } + + public static AsyncLocal s_local = new AsyncLocal(); + private static async Task Test() + { + s_local.Value = 42; + await ChangeThenReturn(); + Assert.Equal(42, s_local.Value); + + try + { + s_local.Value = 43; + await ChangeThenThrow(); + } + catch (Exception) + { + Assert.Equal(43, s_local.Value); + } + + s_local.Value = 44; + await ChangeThenReturnInlined(); + Assert.Equal(44, s_local.Value); + + try + { + s_local.Value = 45; + await ChangeThenThrowInlined(); + } + catch (Exception) + { + Assert.Equal(45, s_local.Value); + } + + s_local.Value = 46; + await Task.Yield(); + Assert.Equal(46, s_local.Value); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task ChangeThenThrow() + { + s_local.Value = 123; + throw new Exception(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task ChangeThenReturn() + { + s_local.Value = 123; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static async Task ChangeThenThrowInlined() + { + s_local.Value = 123; + throw new Exception(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static async Task ChangeThenReturnInlined() + { + s_local.Value = 123; + } +} diff --git a/src/tests/async/execution-context/execution-context.csproj b/src/tests/async/execution-context/execution-context.csproj new file mode 100644 index 00000000000000..1ae294349c376f --- /dev/null +++ b/src/tests/async/execution-context/execution-context.csproj @@ -0,0 +1,8 @@ + + + True + + + + + diff --git a/src/tests/async/fibonacci-with-yields_struct_return/fibonacci-with-yields_struct_return.cs b/src/tests/async/fibonacci-with-yields-struct-return/fibonacci-with-yields-struct-return.cs similarity index 100% rename from src/tests/async/fibonacci-with-yields_struct_return/fibonacci-with-yields_struct_return.cs rename to src/tests/async/fibonacci-with-yields-struct-return/fibonacci-with-yields-struct-return.cs diff --git a/src/tests/async/fibonacci-with-yields_struct_return/fibonacci-with-yields_struct_return.csproj b/src/tests/async/fibonacci-with-yields-struct-return/fibonacci-with-yields-struct-return.csproj similarity index 100% rename from src/tests/async/fibonacci-with-yields_struct_return/fibonacci-with-yields_struct_return.csproj rename to src/tests/async/fibonacci-with-yields-struct-return/fibonacci-with-yields-struct-return.csproj