-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Emit IL for AsyncResumptionStub #121456
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Emit IL for AsyncResumptionStub #121456
Changes from all commits
2abb220
6449d07
a9aead4
c18bc5d
85ff8c7
3da6325
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,8 +18,7 @@ public partial class AsyncResumptionStub : ILStubMethod | |
|
|
||
| public AsyncResumptionStub(MethodDesc owningMethod) | ||
| { | ||
| Debug.Assert(owningMethod.IsAsyncVariant() | ||
| || (owningMethod.IsAsync && !owningMethod.Signature.ReturnsTaskOrValueTask())); | ||
| Debug.Assert(owningMethod.IsAsyncCall()); | ||
| _owningMethod = owningMethod; | ||
| } | ||
|
|
||
|
|
@@ -41,13 +40,62 @@ private MethodSignature InitializeSignature() | |
|
|
||
| public override MethodIL EmitIL() | ||
| { | ||
| var emitter = new ILEmitter(); | ||
| ILCodeStream codeStream = emitter.NewCodeStream(); | ||
| ILEmitter ilEmitter = new ILEmitter(); | ||
| ILCodeStream ilStream = ilEmitter.NewCodeStream(); | ||
|
|
||
| // TODO: match getAsyncResumptionStub from CoreCLR VM | ||
| codeStream.EmitCallThrowHelper(emitter, Context.GetHelperEntryPoint("ThrowHelpers"u8, "ThrowNotSupportedException"u8)); | ||
| // Ported from jitinterface.cpp CEEJitInfo::getAsyncResumptionStub | ||
| if (!_owningMethod.Signature.IsStatic) | ||
| { | ||
| if (_owningMethod.OwningType.IsValueType) | ||
| { | ||
| ilStream.EmitLdc(0); | ||
| ilStream.Emit(ILOpcode.conv_u); | ||
| } | ||
| else | ||
| { | ||
| ilStream.Emit(ILOpcode.ldnull); | ||
| } | ||
| } | ||
|
|
||
| return emitter.Link(this); | ||
| foreach (var param in _owningMethod.Signature) | ||
| { | ||
| var local = ilEmitter.NewLocal(param); | ||
| ilStream.EmitLdLoca(local); | ||
| ilStream.Emit(ILOpcode.initobj, ilEmitter.NewToken(param)); | ||
| ilStream.EmitLdLoc(local); | ||
| } | ||
| ilStream.Emit(ILOpcode.ldftn, ilEmitter.NewToken(_owningMethod)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure. Maybe this is correct in NAOT, but If this is a metadata token for the method that we are resuming, you may end up calling the Task-returning variant. pCode->EmitLDC((DWORD_PTR)m_finalCodeAddressSlot);
pCode->EmitLDIND_I();cc: @MichalStrehovsky - will ldftn do the right thing here?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be emitted as a regular call and the JIT/EE interface and/or the JIT itself should make sure that the call does the right thing. (Once we figure out what to do here, we may want to switch the JIT counterpart to the same plan.)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this should be a regular call. The call will be to a thing with RuntimeAsync calling convention; we can make tokens for that and we will make such token here. We probably don't need to pass generic context explicitly either. But the question is what this will do in the JIT. My expectation is that it will take similar paths as a AsyncExplicitImpl method call.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Runtime async method calls pass a null continuation in the JIT. This needs to pass an actual continuation.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Resume" in a way is the opposite of "Call". In Call we pass formal arguments, generic context, etc.., but continuation is Doing CALLI with a special-crafted signature is the technique to do a low level "unsafe cast" through call conventions in IL. I have seen it in other places (like instantiating stubs?).
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Note - what we are resuming may actually be Anything with Async call conv may need resuming. Whether it has a variant with non-async call convention is unimportant here. Just the part is that it is Async (i.e. emitted as a state machine, has async call conv).
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I do not think that this trick is used anywhere in AOT compilers (just looked through all
We can try going the ldftn route, but I expect that it will hit problems with generics and the generated code won't be the best at the end if we manage to make it work. Managed function pointers in NAOT are tagged pointers. Regular
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could CALLI trick work the same as in JIT case if we clear the tag after LDFTN? (This all assumes that we can know if the calee/resumee has a generic context parameter and can push 0 to its position)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As another wild thought - I think this IL is never exposed outside of internals of VM/JIT/AOT. Perhaps we could use an IL prefix to tag a call as a resuming call? Ex: // load continuation
ldarg 0
// the prefix means "consume one arg as a continuation argument, pass everything else as default"
// could be applied to calli as well
// callee must have async callconv
resume.
call <token>
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
We can certainly try to make LDFTN + CALLI work and special-case it in enough places to make it do what we want. My gut feeling is that it won't be pretty.
I think it is equivalent to |
||
| ilStream.Emit(ILOpcode.calli, ilEmitter.NewToken(this.Signature)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| bool returnsVoid = _owningMethod.Signature.ReturnType != Context.GetWellKnownType(WellKnownType.Void); | ||
| Internal.IL.Stubs.ILLocalVariable resultLocal = default; | ||
| if (!returnsVoid) | ||
| { | ||
| resultLocal = ilEmitter.NewLocal(_owningMethod.Signature.ReturnType); | ||
| ilStream.EmitStLoc(resultLocal); | ||
| } | ||
|
|
||
| MethodDesc asyncCallContinuation = Context.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8) | ||
| .GetKnownMethod("AsyncCallContinuation"u8, null); | ||
|
Comment on lines
+78
to
+79
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For this to work you need to update JIT and the location in CoreCLR's corelib too. (Cc @eduardo-vp since it's used in his PR too and I told him to look into |
||
| TypeDesc continuation = Context.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "Continuation"u8); | ||
| var newContinuationLocal = ilEmitter.NewLocal(continuation); | ||
| ilStream.Emit(ILOpcode.call, ilEmitter.NewToken(asyncCallContinuation)); | ||
| ilStream.EmitStLoc(newContinuationLocal); | ||
|
|
||
| if (!returnsVoid) | ||
| { | ||
| var doneResult = ilEmitter.NewCodeLabel(); | ||
| ilStream.EmitLdLoc(newContinuationLocal); | ||
| ilStream.Emit(ILOpcode.brtrue, doneResult); | ||
| ilStream.EmitLdArg(1); | ||
| ilStream.EmitLdLoc(resultLocal); | ||
| ilStream.Emit(ILOpcode.stobj, ilEmitter.NewToken(_owningMethod.Signature.ReturnType)); | ||
| ilStream.EmitLabel(doneResult); | ||
| } | ||
| ilStream.EmitLdLoc(newContinuationLocal); | ||
| ilStream.Emit(ILOpcode.ret); | ||
|
|
||
| return ilEmitter.Link(this); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the JIT side here, between
thisand other arguments, we also put:It is set to 0, and will not be used by the calee. Continuation has captured the real value and that will be used instead. But we need to match what the calee expects, so we need to push 0 for the context.
it is our arg0. Per the ABI of Async* calls it goes right before the other arguments.
(unless this is x86)
On x86, as usual, there is a difference and the hidden args go after formal ones and in reverse order.
--
*Note - the resume stub is an ordinary function, but what we are resuming is an Async method, thus we shuffle the continuation from our arg0 to its predefined/hidden position in the calee signature.