Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions src/coreclr/jit/async.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ class AsyncLiveness
return true;
}

if (lclNum == m_comp->info.compLvFrameListRoot)
{
return true;
}

if (lclNum == m_comp->lvaInlinedPInvokeFrameVar)
{
return true;
}

#ifdef FEATURE_EH_WINDOWS_X86
if (lclNum == m_comp->lvaShadowSPslotsVar)
{
Expand Down Expand Up @@ -1296,12 +1306,22 @@ void Async2Transformation::CreateResumptionSwitch()
{
BasicBlock* newEntryBB = BasicBlock::New(m_comp, BBJ_ALWAYS);

bool hadScratchBB = m_comp->fgFirstBBisScratch();

if (m_comp->fgFirstBB->hasProfileWeight())
{
newEntryBB->inheritWeight(m_comp->fgFirstBB);
}
m_comp->fgFirstBB->bbRefs--;

if (hadScratchBB)
{
// If previous first BB was a scratch BB then we have to recreate it
// after since later phases may be relying on it.
// TODO-Cleanup: The later phases should be creating it if they need it.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you plan to address this TODO or this is just a "good to have" comment?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do agree that relying on the presence of scratch is an arbitrary dependency that just happen to hold.
If creating on demand is not too hard, it would be better.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you plan to address this TODO or this is just a "good to have" comment?

I don't plan to address it here, this should rather be cleaned up upstream.

I do agree that relying on the presence of scratch is an arbitrary dependency that just happen to hold.
If creating on demand is not too hard, it would be better.

The frontend does ensure it:

// The backend requires a scratch BB into which it can safely insert a P/Invoke method prolog if one is
// required. Similarly, we need a scratch BB for poisoning and when we have Swift parameters to reassemble.
// Create it here.
if (compMethodRequiresPInvokeFrame() || compShouldPoisonFrame() || lvaHasAnySwiftStackParamToReassemble())
{
madeChanges |= fgEnsureFirstBBisScratch();
fgFirstBB->SetFlags(BBF_DONT_REMOVE);
}

It's simply that this is a very long-term invariant that is error prone and which feels unnecessary to me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly. Such dependencies have negative value by default. They must be justified by advantages or scenarios that they enable. If arranging a scratch block was difficult to do later, it could be a possible justification, but, as I understand, it is not the case.

m_comp->fgFirstBBScratch = nullptr;
}

FlowEdge* toPrevEntryEdge = m_comp->fgAddRefPred(m_comp->fgFirstBB, newEntryBB);
toPrevEntryEdge->setLikelihood(1);

Expand All @@ -1310,10 +1330,6 @@ void Async2Transformation::CreateResumptionSwitch()

m_comp->fgInsertBBbefore(m_comp->fgFirstBB, newEntryBB);

// If previous first BB was a scratch BB, then we must add a new scratch BB
// to create IR before the switch.
m_comp->fgFirstBBScratch = nullptr;

GenTree* continuationArg = m_comp->gtNewLclvNode(m_comp->lvaAsyncContinuationArg, TYP_REF);
GenTree* null = m_comp->gtNewNull();
GenTree* neNull = m_comp->gtNewOperNode(GT_NE, TYP_INT, continuationArg, null);
Expand Down Expand Up @@ -1493,4 +1509,9 @@ void Async2Transformation::CreateResumptionSwitch()
GenTree* jtrue = m_comp->gtNewOperNode(GT_JTRUE, TYP_VOID, ltZero);
LIR::AsRange(checkILOffsetBB).InsertAtEnd(LIR::SeqTree(m_comp, jtrue));
}

if (hadScratchBB)
{
m_comp->fgEnsureFirstBBisScratch();
}
}
2 changes: 1 addition & 1 deletion src/coreclr/jit/compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3368,7 +3368,7 @@ void Compiler::compInitOptions(JitFlags* jitFlags)

if (compIsAsync2())
{
printf("OPTIONS: compilation is an async2\n");
printf("OPTIONS: compilation is an async2 state machine\n");
}
}
#endif
Expand Down
6 changes: 5 additions & 1 deletion src/coreclr/jit/lower.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4888,7 +4888,6 @@ void Lowering::LowerRet(GenTreeOp* ret)
}
}

// Method doing PInvokes has exactly one return block unless it has tail calls.
if (comp->compMethodRequiresPInvokeFrame())
{
InsertPInvokeMethodEpilog(comp->compCurBB DEBUGARG(ret));
Expand Down Expand Up @@ -5332,6 +5331,11 @@ void Lowering::LowerReturnSuspend(GenTree* node)
{
BlockRange().Remove(BlockRange().LastNode(), true);
}

if (comp->compMethodRequiresPInvokeFrame())
{
InsertPInvokeMethodEpilog(comp->compCurBB DEBUGARG(node));
}
}

//----------------------------------------------------------------------------------------------
Expand Down
44 changes: 44 additions & 0 deletions src/tests/async/pinvoke.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Xunit;

public class Async2PInvoke
{
[Fact]
public static void TestEntryPoint()
{
AsyncEntryPoint().Wait();
}

private static async2 Task AsyncEntryPoint()
{
unsafe
{
Assert.Equal(5, GetFPtr()());
}

await Task.Yield();

unsafe
{
Assert.Equal(5, GetFPtr()());
}

await Task.Yield();

unsafe
{
Assert.Equal(5, GetFPtr()());
}
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static unsafe delegate* unmanaged<int> GetFPtr() => &GetValue;

[UnmanagedCallersOnly]
private static int GetValue() => 5;
Copy link
Member

@VSadov VSadov Dec 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A thought: what should happen if UnmanagedCallersOnly is applied to runtime async method.
Does it work today with ordinary async? (I guess, why not, they are just Task-returning methods)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't checked, but I imagine UnmanagedCallersOnly on any function returning an object reference should result in IlegalProgramException. So I would hope that was the case for async1, and we should similarly disallow it for async2.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't checked, but I imagine UnmanagedCallersOnly on any function returning an object reference should result in IlegalProgramException. So I would hope that was the case for async1, and we should similarly disallow it for async2.

I checked.
Even though the documentation only mentions that parameters must be blittable, the runtime requires that the return type is blittable as well. That rules out runtime async signatures. Even nongeneric ValueType, although a struct, is not blittable.

}
8 changes: 8 additions & 0 deletions src/tests/async/pinvoke.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.IL">
<PropertyGroup>
<Optimize>True</Optimize>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildProjectName).cs" />
</ItemGroup>
</Project>