JIT: Support Swift error handling for reverse P/Invokes - #100429

Merged
amanasifkhalid merged 41 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg
Apr 4, 2024
Merged

JIT: Support Swift error handling for reverse P/Invokes#100429
amanasifkhalid merged 41 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg

Conversation

@amanasifkhalid

Copy link
Copy Markdown
Contributor

.NET methods called from Swift update error handling state through a SwiftError* argument; when returning from .NET to Swift, the error value pointed to by the SwiftError* must be loaded into the error register. We use the following IR to support this:

  • The JIT creates a SwiftError "pseudo-local", and transforms all uses of the SwiftError* argument into GT_LCL_ADDR nodes of the pseudo-local.
  • When generating IR for the reverse P/Invoke exit point, we create a GT_SWIFT_ERROR_RET node for loading the SwiftError pseudo-local's value into the error register before returning.

I've added a new test for exercising .NET callbacks with error handling to SwiftErrorHandling.

cc @jkoritzinsky@kotlarmilos, @jakobbotsch PTAL

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 29, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@kotlarmiloskotlarmilos left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@amanasifkhalid

amanasifkhalid commented Mar 29, 2024

Copy link
Copy Markdown
ContributorAuthor

I'm seeing some weird interaction with the SwiftSelf parameter in the reverse P/Invoke test, where its value in the .NET callback is different from the value it was initialized with. The error handling in the callback still works correctly, in that the SwiftError value is set to the SwiftSelf value, but the assert in the test fails because the address the SwiftError was set to isn't correct. I run into the same issue even if I replace the SwiftSelf parameter with an IntPtr containing &expectedValue. I've simplified this test to just hard-code the SwiftError value in the callback so we know the error register is being propagated down the call stack; locally, this works with all the stress modes.

Once I've verified this passes in CI, I'll disable this test for Mono. @kotlarmilos is there any way to disable individual tests for Mono, or do I have to make a separate project in src/tests/Interop/Swift for this new test, and disable it for Mono in issues.targets? Thanks!

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

This change touches some hot paths (such as during importation), so there is some TP impact on Unix x64, though oddly not on arm64.

@kotlarmilos

Copy link
Copy Markdown
Member

I'm seeing some weird interaction with the SwiftSelf parameter in the reverse P/Invoke test, where its value in the .NET callback is different from the value it was initialized with. The error handling in the callback still works correctly, in that the SwiftError value is set to the SwiftSelf value, but the assert in the test fails because the address the SwiftError was set to isn't correct. I run into the same issue even if I replace the SwiftSelf parameter with an IntPtr containing &expectedValue. I've simplified this test to just hard-code the SwiftError value in the callback so we know the error register is being propagated down the call stack; locally, this works with all the stress modes.

According to the Swift calling convention, when passing closures, there is an implicit argument immediately after that is a pointer to the closure context, and it gets stored in the call context register (swiftself). In your test, the address of swifterror gets stored into swiftself, which may lead to unexpected behavior.

Once I've verified this passes in CI, I'll disable this test for Mono. @kotlarmilos is there any way to disable individual tests for Mono, or do I have to make a separate project in src/tests/Interop/Swift for this new test, and disable it for Mono in issues.targets? Thanks!

Try adding the SkipOnMono attribute to the test case.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

According to the Swift calling convention, when passing closures, there is an implicit argument immediately after that is a pointer to the closure context, and it gets stored in the call context register (swiftself). In your test, the address of swifterror gets stored into swiftself, which may lead to unexpected behavior.

I see, thanks for pointing that out -- that explains why the diff in the initial and updated SwiftSelf value was always the same. I guess it makes sense for the SwiftErrorHandling tests to test SwiftError in isolation, anyway.

Try adding the SkipOnMono attribute to the test case.

Thanks! I'll update in the next review iteration.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment on lines +103 to +104
CallArg* swiftErrorArg = nullptr;
CallArgs* callArgs = nullptr;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems like it would be cleaner to return the GenTree* node from impPopArgsForUnmanagedCall and remove the arg in there instead.

Comment threadsrc/coreclr/jit/codegenxarch.cpp Outdated

// If this method returns an error argument in the Swift error register,
// we didn't push the register, and thus shouldn't pop it.
if (compiler->lvaSwiftErrorArg == BAD_VAR_NUM)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems odd given the assert.

@amanasifkhalidamanasifkhalidApr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sorry I meant to remove the assert; doing so seems to fix the jitstressregs failure locally.

I suppose I could remove REG_SWIFT_ERROR from the register mask in the caller, but doing it here means we'll do the removal only if REG_SWIFT_ERROR was ever set in the first place.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Perhaps the caller shouldn't be passing it? Seems we could unify a bit of code by replacing some of the RBM_INT_CALLEE_SAVED occurrences in the backend with a function that takes the error return into account.

@amanasifkhalidamanasifkhalidApr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

That sounds better. Do you think it would make sense for this function to be a member of RegSet -- something like RegSet::rsGetModifiedIntCalleeSavedRegsMask -- since RegSet already has a Compiler* member variable to check for lvaSwiftErrorArg? Or would it be confusing if RegSet::rsGetModifiedIntCalleeSavedRegsMask sometimes doesn't include REG_SWIFT_ERROR, which is technically a callee-save register, even if it was modified?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it would be best to follow the same pattern as get_RBM_FLT_CALLEE_TRASH -- it has the same flavor of "sometimes needs to be determined based on compilation information", and RBM_FLT_CALLEE_TRASH is defined to call that function. It will ensure that everyone gets the consistent view, but will of course result in a much less localized change. If it ends up requiring too many changes or being costly then I would be fine with what you suggest.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thanks for the suggestion! I gave that a try, and I think we use RBM_INT_CALLEE_SAVED in too many places to make this approach manageable; in particular, we use RBM_INT_CALLEE_SAVED in some static functions where we don't have access to the current Compiler's state, so we can't easily figure out whether to include RBM_SWIFT_ERROR in the mask. Also, I think the additional check for lvaSwiftErrorArg would've been more expensive than necessary, since we check RBM_INT_CALLEE_SAVED (directly and indirectly, via other macros) in places irrelevant to method prolog/epilog generation.

I think RegSet provides a decent abstraction, though to make its usage consistent across the JIT, I had to make some trivial changes to architectures irrelevant to this PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok, it seems fine to me for now. Some notes...

and I think we use RBM_INT_CALLEE_SAVED in too many places to make this approach manageable

This doesn't exactly fill me with confidence. It means there are many places getting the wrong and inconsistent view after this PR. Are we sure that none of those need to be updated for correctness sake? If we hit a bug tail around this we should reconsider.

Also, I think the additional check for lvaSwiftErrorArg would've been more expensive than necessary, since we check RBM_INT_CALLEE_SAVED (directly and indirectly, via other macros) in places irrelevant to method prolog/epilog generation.

I think we would design it similar to get_RBM_FLT_CALLEE_TRASH -- the mask is stored in Compiler, so it's just returning the field. There is no dynamic logic to compute it on every invocation.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This doesn't exactly fill me with confidence. It means there are many places getting the wrong and inconsistent view after this PR. Are we sure that none of those need to be updated for correctness sake? If we hit a bug tail around this we should reconsider.

This might be naive, but I think it's ok if we continue to think of REG_SWIFT_ERROR as callee-save elsewhere in the JIT, and limit the error register-specific quirks to prolog/epilog generation. We use RBM_INT_CALLEE_SAVED in the definition of RBM_ALLINT as well, so to ensure the latter includes REG_SWIFT_ERROR, we'd probably need to save the "normal" value of RBM_INT_CALLEE_SAVED to another macro -- the fact that this new macro would only be semantically different from RBM_INT_CALLEE_SAVED on Swift platforms seems messy. We'd also have to use this duplicated macro in the various static asserts that use RBM_INT_CALLEE_SAVED without access to compiler state.

The benefit of the current approach is its behavior is limited in scope. It might be too limited to the point where we are incorrectly treating REG_SWIFT_ERROR as callee-save (though I haven't run into that yet), but that seems easier to debug than the opposite scenario of making RBM_INT_CALLEE_SAVED stateful.

@amanasifkhalid

amanasifkhalid commented Apr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

jitformat is insistent on this odd spacing:

 TEMP_MAX_SIZE = FP_REGSIZE_BYTES,
#endif // defined(TARGET_XARCH) || defined(TARGET_ARM64)
#else // !FEATURE_SIMD
TEMP_MAX_SIZE = sizeof(double),
#endif // !FEATURE_SIMD
TEMP_SLOT_COUNT = (TEMP_MAX_SIZE / sizeof(int))
};

I'll push a fix after approval.

Edit: Bruce just merged in a clang-format/clang-tidy update, so maybe this is fixed...

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
// Swift call isn't going to use the SwiftError* arg, so don't bother emitting it
assert(swiftErrorNode != nullptr);
*swiftErrorNode = swiftErrorArg->GetNode();
call->gtArgs.Remove(swiftErrorArg);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need to potentially adjust swiftSelfIndex here? Or perhaps instead delay the removal until after the loop below?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good catch: If the SwiftSelf arg comes after the SwiftError* arg, then yes, we do need to decrement it. It seems easier to delay the removal. Fixed.

lvaSwiftErrorArg = varDscInfo->varNum;

// Instead, all usages of the SwiftError* parameter will be redirected to this pseudolocal.
lvaSwiftErrorLocal = lvaGrabTempWithImplicitUse(false DEBUGARG("SwiftError pseudolocal"));

@jakobbotschjakobbotschApr 4, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you verify that something somewhere marks this local as either do-not-enreg or address exposed? I don't immediately see where we do that in the JIT when something has implicit uses. Basically, I don't understand for example why liveness doesn't get rid of stores to this local because it cannot see any uses of it. Can you share a jitdump of one of the tests just so I can understand why that doesn't happen?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure thing. I was able to remove the call to lvaSetVarAddrExposed here because fgMarkAddressExposedLocals seems to already do this in LocalAddressVisitor::EscapeAddress for each store to the pseudolocal before running liveness. Here's a JitDump for one of the callbacks (see line 1201, etc.).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we are getting "lucky" there. The JIT is spilling the address of the local to a temp (not really very optimal, we ought to improve this). It only does that because of the constructor call on the RHS of the assignment, and as a consequence we end up address exposing it. If you changed the code slightly, for example to

varx=newSwiftError(...);GC.KeepAlive(null);// avoid Roslyn optimizing it back to the pattern*error=x;

then I would expect that you will hit the issue I describe where nothing marks it as address exposed and we end up removing the store.

@amanasifkhalidamanasifkhalidApr 4, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I tried this, and you're correct that we no longer mark it as address exposed, but we don't end up removing the store -- I think this is because we mark all GT_LCL_ADDR nodes pointing to the pseudolocal as having side effects (perhaps that's what is forcing the JIT to spill the address to a temp?). I'm fine with going back to explicitly marking the pseudolocal as address exposed upon creation. My hope is that is short-lived anyway, once I open a PR for GT_SWIFT_ERROR_RET.

I added an updated dump to the gist with the above code pattern used in ConditionallySetErrorTo21 (though only in the true branch of the if statement).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I'm fine with going back to explicitly marking the pseudolocal as address exposed upon creation

This, along with removing the side effects on the GT_LCL_ADDR nodes of the pseudolocal, improves codegen. For example, before:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06,T01] ( 2, 2 ) long -> rdi "impAppendStmt"
; V07 tmp4 [V07 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
; V09 tmp6 [V09 ] ( 2, 1 ) long -> [rbp-0x10] do-not-enreg[X] addr-exposed "field V03.<Value>k__BackingField (fldOffset=0x0)" P-DEP
;* V10 tmp7 [V10,T03] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
lea rdi, [rbp-0x10]
xor eax, eax
mov qword ptr [rdi], rax
;; size=9 bbWeight=0.50 PerfScore 0.88
G_M3386_IG05: ;; offset=0x003F
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x004C
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 83, prolog size 27, PerfScore 16.58, instruction count 26, allocated bytes for code 83 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

After:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T01] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov qword ptr [rbp-0x10], rdi
;; size=6 bbWeight=0.50 PerfScore 0.62
G_M3386_IG05: ;; offset=0x003C
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x0049
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 80, prolog size 27, PerfScore 16.33, instruction count 25, allocated bytes for code 80 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah, that makes sense. Yeah, the side effect flags on the GT_LCL_ADDR shouldn't be necessary,

My hope is that is short-lived anyway, once I open a PR for GT_SWIFT_ERROR_RET.

Do you plan to do the work? To be honest looking at the codegen there it really doesn't feel all that bad compared to all the other things we end up doing for the reverse pinvokes anyway (like the helper calls), so I'd be perfectly fine with just leaving it like this.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I have some of the implementation saved locally, so I'm interested in trying it out; I think it can be done without polluting the JIT with too many weird edge cases.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
// By adding a well-known "sentinel" argument that uses the error register,
// the JIT will emit code for clearing the error register before the call,
// and will mark the error register as busy so it isn't used to hold the function call's address.
CallArg* const swiftErrorArg = call->gtArgs.GetArgByIndex(swiftErrorIndex);

@jakobbotschjakobbotschApr 4, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The loop above removes/inserts arguments, so this part still has to happen before the loop above.

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM! Thanks for addressing all my feedback.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

LGTM! Thanks for addressing all my feedback.

Thanks for all the reviews!

@amanasifkhalid

amanasifkhalid commented Apr 4, 2024

Copy link
Copy Markdown
ContributorAuthor

SPMI isn't finished running yet, but TP diffs look pretty small. I'm gonna merge to unblock #100344.

@amanasifkhalid
amanasifkhalid merged commit 16492b9 into dotnet:mainApr 4, 2024
@amanasifkhalid
amanasifkhalid deleted the swift-error-reg branch April 4, 2024 19:04
amanasifkhalid added a commit that referenced this pull request Apr 12, 2024
…return (#100692)
Follow-up to #100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
…return (dotnet#100692)
Follow-up to dotnet#100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 5, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@amanasifkhalid@kotlarmilos@jakobbotsch
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

JIT: Support Swift error handling for reverse P/Invokes - #100429

Merged
amanasifkhalid merged 41 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg
Apr 4, 2024
Merged

JIT: Support Swift error handling for reverse P/Invokes#100429
amanasifkhalid merged 41 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg

Conversation

@amanasifkhalid

Copy link
Copy Markdown
Contributor

.NET methods called from Swift update error handling state through a SwiftError* argument; when returning from .NET to Swift, the error value pointed to by the SwiftError* must be loaded into the error register. We use the following IR to support this:

  • The JIT creates a SwiftError "pseudo-local", and transforms all uses of the SwiftError* argument into GT_LCL_ADDR nodes of the pseudo-local.
  • When generating IR for the reverse P/Invoke exit point, we create a GT_SWIFT_ERROR_RET node for loading the SwiftError pseudo-local's value into the error register before returning.

I've added a new test for exercising .NET callbacks with error handling to SwiftErrorHandling.

cc @jkoritzinsky@kotlarmilos, @jakobbotsch PTAL

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 29, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@kotlarmiloskotlarmilos left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@amanasifkhalid

amanasifkhalid commented Mar 29, 2024

Copy link
Copy Markdown
ContributorAuthor

I'm seeing some weird interaction with the SwiftSelf parameter in the reverse P/Invoke test, where its value in the .NET callback is different from the value it was initialized with. The error handling in the callback still works correctly, in that the SwiftError value is set to the SwiftSelf value, but the assert in the test fails because the address the SwiftError was set to isn't correct. I run into the same issue even if I replace the SwiftSelf parameter with an IntPtr containing &expectedValue. I've simplified this test to just hard-code the SwiftError value in the callback so we know the error register is being propagated down the call stack; locally, this works with all the stress modes.

Once I've verified this passes in CI, I'll disable this test for Mono. @kotlarmilos is there any way to disable individual tests for Mono, or do I have to make a separate project in src/tests/Interop/Swift for this new test, and disable it for Mono in issues.targets? Thanks!

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

This change touches some hot paths (such as during importation), so there is some TP impact on Unix x64, though oddly not on arm64.

@kotlarmilos

Copy link
Copy Markdown
Member

I'm seeing some weird interaction with the SwiftSelf parameter in the reverse P/Invoke test, where its value in the .NET callback is different from the value it was initialized with. The error handling in the callback still works correctly, in that the SwiftError value is set to the SwiftSelf value, but the assert in the test fails because the address the SwiftError was set to isn't correct. I run into the same issue even if I replace the SwiftSelf parameter with an IntPtr containing &expectedValue. I've simplified this test to just hard-code the SwiftError value in the callback so we know the error register is being propagated down the call stack; locally, this works with all the stress modes.

According to the Swift calling convention, when passing closures, there is an implicit argument immediately after that is a pointer to the closure context, and it gets stored in the call context register (swiftself). In your test, the address of swifterror gets stored into swiftself, which may lead to unexpected behavior.

Once I've verified this passes in CI, I'll disable this test for Mono. @kotlarmilos is there any way to disable individual tests for Mono, or do I have to make a separate project in src/tests/Interop/Swift for this new test, and disable it for Mono in issues.targets? Thanks!

Try adding the SkipOnMono attribute to the test case.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

According to the Swift calling convention, when passing closures, there is an implicit argument immediately after that is a pointer to the closure context, and it gets stored in the call context register (swiftself). In your test, the address of swifterror gets stored into swiftself, which may lead to unexpected behavior.

I see, thanks for pointing that out -- that explains why the diff in the initial and updated SwiftSelf value was always the same. I guess it makes sense for the SwiftErrorHandling tests to test SwiftError in isolation, anyway.

Try adding the SkipOnMono attribute to the test case.

Thanks! I'll update in the next review iteration.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment on lines +103 to +104
CallArg* swiftErrorArg = nullptr;
CallArgs* callArgs = nullptr;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems like it would be cleaner to return the GenTree* node from impPopArgsForUnmanagedCall and remove the arg in there instead.

Comment threadsrc/coreclr/jit/codegenxarch.cpp Outdated

// If this method returns an error argument in the Swift error register,
// we didn't push the register, and thus shouldn't pop it.
if (compiler->lvaSwiftErrorArg == BAD_VAR_NUM)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems odd given the assert.

@amanasifkhalidamanasifkhalidApr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sorry I meant to remove the assert; doing so seems to fix the jitstressregs failure locally.

I suppose I could remove REG_SWIFT_ERROR from the register mask in the caller, but doing it here means we'll do the removal only if REG_SWIFT_ERROR was ever set in the first place.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Perhaps the caller shouldn't be passing it? Seems we could unify a bit of code by replacing some of the RBM_INT_CALLEE_SAVED occurrences in the backend with a function that takes the error return into account.

@amanasifkhalidamanasifkhalidApr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

That sounds better. Do you think it would make sense for this function to be a member of RegSet -- something like RegSet::rsGetModifiedIntCalleeSavedRegsMask -- since RegSet already has a Compiler* member variable to check for lvaSwiftErrorArg? Or would it be confusing if RegSet::rsGetModifiedIntCalleeSavedRegsMask sometimes doesn't include REG_SWIFT_ERROR, which is technically a callee-save register, even if it was modified?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it would be best to follow the same pattern as get_RBM_FLT_CALLEE_TRASH -- it has the same flavor of "sometimes needs to be determined based on compilation information", and RBM_FLT_CALLEE_TRASH is defined to call that function. It will ensure that everyone gets the consistent view, but will of course result in a much less localized change. If it ends up requiring too many changes or being costly then I would be fine with what you suggest.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thanks for the suggestion! I gave that a try, and I think we use RBM_INT_CALLEE_SAVED in too many places to make this approach manageable; in particular, we use RBM_INT_CALLEE_SAVED in some static functions where we don't have access to the current Compiler's state, so we can't easily figure out whether to include RBM_SWIFT_ERROR in the mask. Also, I think the additional check for lvaSwiftErrorArg would've been more expensive than necessary, since we check RBM_INT_CALLEE_SAVED (directly and indirectly, via other macros) in places irrelevant to method prolog/epilog generation.

I think RegSet provides a decent abstraction, though to make its usage consistent across the JIT, I had to make some trivial changes to architectures irrelevant to this PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok, it seems fine to me for now. Some notes...

and I think we use RBM_INT_CALLEE_SAVED in too many places to make this approach manageable

This doesn't exactly fill me with confidence. It means there are many places getting the wrong and inconsistent view after this PR. Are we sure that none of those need to be updated for correctness sake? If we hit a bug tail around this we should reconsider.

Also, I think the additional check for lvaSwiftErrorArg would've been more expensive than necessary, since we check RBM_INT_CALLEE_SAVED (directly and indirectly, via other macros) in places irrelevant to method prolog/epilog generation.

I think we would design it similar to get_RBM_FLT_CALLEE_TRASH -- the mask is stored in Compiler, so it's just returning the field. There is no dynamic logic to compute it on every invocation.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This doesn't exactly fill me with confidence. It means there are many places getting the wrong and inconsistent view after this PR. Are we sure that none of those need to be updated for correctness sake? If we hit a bug tail around this we should reconsider.

This might be naive, but I think it's ok if we continue to think of REG_SWIFT_ERROR as callee-save elsewhere in the JIT, and limit the error register-specific quirks to prolog/epilog generation. We use RBM_INT_CALLEE_SAVED in the definition of RBM_ALLINT as well, so to ensure the latter includes REG_SWIFT_ERROR, we'd probably need to save the "normal" value of RBM_INT_CALLEE_SAVED to another macro -- the fact that this new macro would only be semantically different from RBM_INT_CALLEE_SAVED on Swift platforms seems messy. We'd also have to use this duplicated macro in the various static asserts that use RBM_INT_CALLEE_SAVED without access to compiler state.

The benefit of the current approach is its behavior is limited in scope. It might be too limited to the point where we are incorrectly treating REG_SWIFT_ERROR as callee-save (though I haven't run into that yet), but that seems easier to debug than the opposite scenario of making RBM_INT_CALLEE_SAVED stateful.

@amanasifkhalid

amanasifkhalid commented Apr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

jitformat is insistent on this odd spacing:

 TEMP_MAX_SIZE = FP_REGSIZE_BYTES,
#endif // defined(TARGET_XARCH) || defined(TARGET_ARM64)
#else // !FEATURE_SIMD
TEMP_MAX_SIZE = sizeof(double),
#endif // !FEATURE_SIMD
TEMP_SLOT_COUNT = (TEMP_MAX_SIZE / sizeof(int))
};

I'll push a fix after approval.

Edit: Bruce just merged in a clang-format/clang-tidy update, so maybe this is fixed...

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
// Swift call isn't going to use the SwiftError* arg, so don't bother emitting it
assert(swiftErrorNode != nullptr);
*swiftErrorNode = swiftErrorArg->GetNode();
call->gtArgs.Remove(swiftErrorArg);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need to potentially adjust swiftSelfIndex here? Or perhaps instead delay the removal until after the loop below?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good catch: If the SwiftSelf arg comes after the SwiftError* arg, then yes, we do need to decrement it. It seems easier to delay the removal. Fixed.

lvaSwiftErrorArg = varDscInfo->varNum;

// Instead, all usages of the SwiftError* parameter will be redirected to this pseudolocal.
lvaSwiftErrorLocal = lvaGrabTempWithImplicitUse(false DEBUGARG("SwiftError pseudolocal"));

@jakobbotschjakobbotschApr 4, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you verify that something somewhere marks this local as either do-not-enreg or address exposed? I don't immediately see where we do that in the JIT when something has implicit uses. Basically, I don't understand for example why liveness doesn't get rid of stores to this local because it cannot see any uses of it. Can you share a jitdump of one of the tests just so I can understand why that doesn't happen?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure thing. I was able to remove the call to lvaSetVarAddrExposed here because fgMarkAddressExposedLocals seems to already do this in LocalAddressVisitor::EscapeAddress for each store to the pseudolocal before running liveness. Here's a JitDump for one of the callbacks (see line 1201, etc.).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we are getting "lucky" there. The JIT is spilling the address of the local to a temp (not really very optimal, we ought to improve this). It only does that because of the constructor call on the RHS of the assignment, and as a consequence we end up address exposing it. If you changed the code slightly, for example to

varx=newSwiftError(...);GC.KeepAlive(null);// avoid Roslyn optimizing it back to the pattern*error=x;

then I would expect that you will hit the issue I describe where nothing marks it as address exposed and we end up removing the store.

@amanasifkhalidamanasifkhalidApr 4, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I tried this, and you're correct that we no longer mark it as address exposed, but we don't end up removing the store -- I think this is because we mark all GT_LCL_ADDR nodes pointing to the pseudolocal as having side effects (perhaps that's what is forcing the JIT to spill the address to a temp?). I'm fine with going back to explicitly marking the pseudolocal as address exposed upon creation. My hope is that is short-lived anyway, once I open a PR for GT_SWIFT_ERROR_RET.

I added an updated dump to the gist with the above code pattern used in ConditionallySetErrorTo21 (though only in the true branch of the if statement).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I'm fine with going back to explicitly marking the pseudolocal as address exposed upon creation

This, along with removing the side effects on the GT_LCL_ADDR nodes of the pseudolocal, improves codegen. For example, before:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06,T01] ( 2, 2 ) long -> rdi "impAppendStmt"
; V07 tmp4 [V07 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
; V09 tmp6 [V09 ] ( 2, 1 ) long -> [rbp-0x10] do-not-enreg[X] addr-exposed "field V03.<Value>k__BackingField (fldOffset=0x0)" P-DEP
;* V10 tmp7 [V10,T03] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
lea rdi, [rbp-0x10]
xor eax, eax
mov qword ptr [rdi], rax
;; size=9 bbWeight=0.50 PerfScore 0.88
G_M3386_IG05: ;; offset=0x003F
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x004C
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 83, prolog size 27, PerfScore 16.58, instruction count 26, allocated bytes for code 83 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

After:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T01] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov qword ptr [rbp-0x10], rdi
;; size=6 bbWeight=0.50 PerfScore 0.62
G_M3386_IG05: ;; offset=0x003C
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x0049
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 80, prolog size 27, PerfScore 16.33, instruction count 25, allocated bytes for code 80 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah, that makes sense. Yeah, the side effect flags on the GT_LCL_ADDR shouldn't be necessary,

My hope is that is short-lived anyway, once I open a PR for GT_SWIFT_ERROR_RET.

Do you plan to do the work? To be honest looking at the codegen there it really doesn't feel all that bad compared to all the other things we end up doing for the reverse pinvokes anyway (like the helper calls), so I'd be perfectly fine with just leaving it like this.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I have some of the implementation saved locally, so I'm interested in trying it out; I think it can be done without polluting the JIT with too many weird edge cases.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
// By adding a well-known "sentinel" argument that uses the error register,
// the JIT will emit code for clearing the error register before the call,
// and will mark the error register as busy so it isn't used to hold the function call's address.
CallArg* const swiftErrorArg = call->gtArgs.GetArgByIndex(swiftErrorIndex);

@jakobbotschjakobbotschApr 4, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The loop above removes/inserts arguments, so this part still has to happen before the loop above.

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM! Thanks for addressing all my feedback.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

LGTM! Thanks for addressing all my feedback.

Thanks for all the reviews!

@amanasifkhalid

amanasifkhalid commented Apr 4, 2024

Copy link
Copy Markdown
ContributorAuthor

SPMI isn't finished running yet, but TP diffs look pretty small. I'm gonna merge to unblock #100344.

@amanasifkhalid
amanasifkhalid merged commit 16492b9 into dotnet:mainApr 4, 2024
@amanasifkhalid
amanasifkhalid deleted the swift-error-reg branch April 4, 2024 19:04
amanasifkhalid added a commit that referenced this pull request Apr 12, 2024
…return (#100692)
Follow-up to #100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
…return (dotnet#100692)
Follow-up to dotnet#100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 5, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@amanasifkhalid@kotlarmilos@jakobbotsch
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

JIT: Support Swift error handling for reverse P/Invokes - #100429

Merged
amanasifkhalid merged 41 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg
Apr 4, 2024
Merged

JIT: Support Swift error handling for reverse P/Invokes#100429
amanasifkhalid merged 41 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg

Conversation

@amanasifkhalid

Copy link
Copy Markdown
Contributor

.NET methods called from Swift update error handling state through a SwiftError* argument; when returning from .NET to Swift, the error value pointed to by the SwiftError* must be loaded into the error register. We use the following IR to support this:

  • The JIT creates a SwiftError "pseudo-local", and transforms all uses of the SwiftError* argument into GT_LCL_ADDR nodes of the pseudo-local.
  • When generating IR for the reverse P/Invoke exit point, we create a GT_SWIFT_ERROR_RET node for loading the SwiftError pseudo-local's value into the error register before returning.

I've added a new test for exercising .NET callbacks with error handling to SwiftErrorHandling.

cc @jkoritzinsky@kotlarmilos, @jakobbotsch PTAL

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 29, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@kotlarmiloskotlarmilos left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@amanasifkhalid

amanasifkhalid commented Mar 29, 2024

Copy link
Copy Markdown
ContributorAuthor

I'm seeing some weird interaction with the SwiftSelf parameter in the reverse P/Invoke test, where its value in the .NET callback is different from the value it was initialized with. The error handling in the callback still works correctly, in that the SwiftError value is set to the SwiftSelf value, but the assert in the test fails because the address the SwiftError was set to isn't correct. I run into the same issue even if I replace the SwiftSelf parameter with an IntPtr containing &expectedValue. I've simplified this test to just hard-code the SwiftError value in the callback so we know the error register is being propagated down the call stack; locally, this works with all the stress modes.

Once I've verified this passes in CI, I'll disable this test for Mono. @kotlarmilos is there any way to disable individual tests for Mono, or do I have to make a separate project in src/tests/Interop/Swift for this new test, and disable it for Mono in issues.targets? Thanks!

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

This change touches some hot paths (such as during importation), so there is some TP impact on Unix x64, though oddly not on arm64.

@kotlarmilos

Copy link
Copy Markdown
Member

I'm seeing some weird interaction with the SwiftSelf parameter in the reverse P/Invoke test, where its value in the .NET callback is different from the value it was initialized with. The error handling in the callback still works correctly, in that the SwiftError value is set to the SwiftSelf value, but the assert in the test fails because the address the SwiftError was set to isn't correct. I run into the same issue even if I replace the SwiftSelf parameter with an IntPtr containing &expectedValue. I've simplified this test to just hard-code the SwiftError value in the callback so we know the error register is being propagated down the call stack; locally, this works with all the stress modes.

According to the Swift calling convention, when passing closures, there is an implicit argument immediately after that is a pointer to the closure context, and it gets stored in the call context register (swiftself). In your test, the address of swifterror gets stored into swiftself, which may lead to unexpected behavior.

Once I've verified this passes in CI, I'll disable this test for Mono. @kotlarmilos is there any way to disable individual tests for Mono, or do I have to make a separate project in src/tests/Interop/Swift for this new test, and disable it for Mono in issues.targets? Thanks!

Try adding the SkipOnMono attribute to the test case.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

According to the Swift calling convention, when passing closures, there is an implicit argument immediately after that is a pointer to the closure context, and it gets stored in the call context register (swiftself). In your test, the address of swifterror gets stored into swiftself, which may lead to unexpected behavior.

I see, thanks for pointing that out -- that explains why the diff in the initial and updated SwiftSelf value was always the same. I guess it makes sense for the SwiftErrorHandling tests to test SwiftError in isolation, anyway.

Try adding the SkipOnMono attribute to the test case.

Thanks! I'll update in the next review iteration.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment on lines +103 to +104
CallArg* swiftErrorArg = nullptr;
CallArgs* callArgs = nullptr;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems like it would be cleaner to return the GenTree* node from impPopArgsForUnmanagedCall and remove the arg in there instead.

Comment threadsrc/coreclr/jit/codegenxarch.cpp Outdated

// If this method returns an error argument in the Swift error register,
// we didn't push the register, and thus shouldn't pop it.
if (compiler->lvaSwiftErrorArg == BAD_VAR_NUM)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems odd given the assert.

@amanasifkhalidamanasifkhalidApr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sorry I meant to remove the assert; doing so seems to fix the jitstressregs failure locally.

I suppose I could remove REG_SWIFT_ERROR from the register mask in the caller, but doing it here means we'll do the removal only if REG_SWIFT_ERROR was ever set in the first place.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Perhaps the caller shouldn't be passing it? Seems we could unify a bit of code by replacing some of the RBM_INT_CALLEE_SAVED occurrences in the backend with a function that takes the error return into account.

@amanasifkhalidamanasifkhalidApr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

That sounds better. Do you think it would make sense for this function to be a member of RegSet -- something like RegSet::rsGetModifiedIntCalleeSavedRegsMask -- since RegSet already has a Compiler* member variable to check for lvaSwiftErrorArg? Or would it be confusing if RegSet::rsGetModifiedIntCalleeSavedRegsMask sometimes doesn't include REG_SWIFT_ERROR, which is technically a callee-save register, even if it was modified?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it would be best to follow the same pattern as get_RBM_FLT_CALLEE_TRASH -- it has the same flavor of "sometimes needs to be determined based on compilation information", and RBM_FLT_CALLEE_TRASH is defined to call that function. It will ensure that everyone gets the consistent view, but will of course result in a much less localized change. If it ends up requiring too many changes or being costly then I would be fine with what you suggest.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thanks for the suggestion! I gave that a try, and I think we use RBM_INT_CALLEE_SAVED in too many places to make this approach manageable; in particular, we use RBM_INT_CALLEE_SAVED in some static functions where we don't have access to the current Compiler's state, so we can't easily figure out whether to include RBM_SWIFT_ERROR in the mask. Also, I think the additional check for lvaSwiftErrorArg would've been more expensive than necessary, since we check RBM_INT_CALLEE_SAVED (directly and indirectly, via other macros) in places irrelevant to method prolog/epilog generation.

I think RegSet provides a decent abstraction, though to make its usage consistent across the JIT, I had to make some trivial changes to architectures irrelevant to this PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok, it seems fine to me for now. Some notes...

and I think we use RBM_INT_CALLEE_SAVED in too many places to make this approach manageable

This doesn't exactly fill me with confidence. It means there are many places getting the wrong and inconsistent view after this PR. Are we sure that none of those need to be updated for correctness sake? If we hit a bug tail around this we should reconsider.

Also, I think the additional check for lvaSwiftErrorArg would've been more expensive than necessary, since we check RBM_INT_CALLEE_SAVED (directly and indirectly, via other macros) in places irrelevant to method prolog/epilog generation.

I think we would design it similar to get_RBM_FLT_CALLEE_TRASH -- the mask is stored in Compiler, so it's just returning the field. There is no dynamic logic to compute it on every invocation.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This doesn't exactly fill me with confidence. It means there are many places getting the wrong and inconsistent view after this PR. Are we sure that none of those need to be updated for correctness sake? If we hit a bug tail around this we should reconsider.

This might be naive, but I think it's ok if we continue to think of REG_SWIFT_ERROR as callee-save elsewhere in the JIT, and limit the error register-specific quirks to prolog/epilog generation. We use RBM_INT_CALLEE_SAVED in the definition of RBM_ALLINT as well, so to ensure the latter includes REG_SWIFT_ERROR, we'd probably need to save the "normal" value of RBM_INT_CALLEE_SAVED to another macro -- the fact that this new macro would only be semantically different from RBM_INT_CALLEE_SAVED on Swift platforms seems messy. We'd also have to use this duplicated macro in the various static asserts that use RBM_INT_CALLEE_SAVED without access to compiler state.

The benefit of the current approach is its behavior is limited in scope. It might be too limited to the point where we are incorrectly treating REG_SWIFT_ERROR as callee-save (though I haven't run into that yet), but that seems easier to debug than the opposite scenario of making RBM_INT_CALLEE_SAVED stateful.

@amanasifkhalid

amanasifkhalid commented Apr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

jitformat is insistent on this odd spacing:

 TEMP_MAX_SIZE = FP_REGSIZE_BYTES,
#endif // defined(TARGET_XARCH) || defined(TARGET_ARM64)
#else // !FEATURE_SIMD
TEMP_MAX_SIZE = sizeof(double),
#endif // !FEATURE_SIMD
TEMP_SLOT_COUNT = (TEMP_MAX_SIZE / sizeof(int))
};

I'll push a fix after approval.

Edit: Bruce just merged in a clang-format/clang-tidy update, so maybe this is fixed...

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
// Swift call isn't going to use the SwiftError* arg, so don't bother emitting it
assert(swiftErrorNode != nullptr);
*swiftErrorNode = swiftErrorArg->GetNode();
call->gtArgs.Remove(swiftErrorArg);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need to potentially adjust swiftSelfIndex here? Or perhaps instead delay the removal until after the loop below?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good catch: If the SwiftSelf arg comes after the SwiftError* arg, then yes, we do need to decrement it. It seems easier to delay the removal. Fixed.

lvaSwiftErrorArg = varDscInfo->varNum;

// Instead, all usages of the SwiftError* parameter will be redirected to this pseudolocal.
lvaSwiftErrorLocal = lvaGrabTempWithImplicitUse(false DEBUGARG("SwiftError pseudolocal"));

@jakobbotschjakobbotschApr 4, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you verify that something somewhere marks this local as either do-not-enreg or address exposed? I don't immediately see where we do that in the JIT when something has implicit uses. Basically, I don't understand for example why liveness doesn't get rid of stores to this local because it cannot see any uses of it. Can you share a jitdump of one of the tests just so I can understand why that doesn't happen?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure thing. I was able to remove the call to lvaSetVarAddrExposed here because fgMarkAddressExposedLocals seems to already do this in LocalAddressVisitor::EscapeAddress for each store to the pseudolocal before running liveness. Here's a JitDump for one of the callbacks (see line 1201, etc.).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we are getting "lucky" there. The JIT is spilling the address of the local to a temp (not really very optimal, we ought to improve this). It only does that because of the constructor call on the RHS of the assignment, and as a consequence we end up address exposing it. If you changed the code slightly, for example to

varx=newSwiftError(...);GC.KeepAlive(null);// avoid Roslyn optimizing it back to the pattern*error=x;

then I would expect that you will hit the issue I describe where nothing marks it as address exposed and we end up removing the store.

@amanasifkhalidamanasifkhalidApr 4, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I tried this, and you're correct that we no longer mark it as address exposed, but we don't end up removing the store -- I think this is because we mark all GT_LCL_ADDR nodes pointing to the pseudolocal as having side effects (perhaps that's what is forcing the JIT to spill the address to a temp?). I'm fine with going back to explicitly marking the pseudolocal as address exposed upon creation. My hope is that is short-lived anyway, once I open a PR for GT_SWIFT_ERROR_RET.

I added an updated dump to the gist with the above code pattern used in ConditionallySetErrorTo21 (though only in the true branch of the if statement).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I'm fine with going back to explicitly marking the pseudolocal as address exposed upon creation

This, along with removing the side effects on the GT_LCL_ADDR nodes of the pseudolocal, improves codegen. For example, before:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06,T01] ( 2, 2 ) long -> rdi "impAppendStmt"
; V07 tmp4 [V07 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
; V09 tmp6 [V09 ] ( 2, 1 ) long -> [rbp-0x10] do-not-enreg[X] addr-exposed "field V03.<Value>k__BackingField (fldOffset=0x0)" P-DEP
;* V10 tmp7 [V10,T03] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
lea rdi, [rbp-0x10]
xor eax, eax
mov qword ptr [rdi], rax
;; size=9 bbWeight=0.50 PerfScore 0.88
G_M3386_IG05: ;; offset=0x003F
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x004C
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 83, prolog size 27, PerfScore 16.58, instruction count 26, allocated bytes for code 83 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

After:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T01] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov qword ptr [rbp-0x10], rdi
;; size=6 bbWeight=0.50 PerfScore 0.62
G_M3386_IG05: ;; offset=0x003C
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x0049
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 80, prolog size 27, PerfScore 16.33, instruction count 25, allocated bytes for code 80 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah, that makes sense. Yeah, the side effect flags on the GT_LCL_ADDR shouldn't be necessary,

My hope is that is short-lived anyway, once I open a PR for GT_SWIFT_ERROR_RET.

Do you plan to do the work? To be honest looking at the codegen there it really doesn't feel all that bad compared to all the other things we end up doing for the reverse pinvokes anyway (like the helper calls), so I'd be perfectly fine with just leaving it like this.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I have some of the implementation saved locally, so I'm interested in trying it out; I think it can be done without polluting the JIT with too many weird edge cases.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
// By adding a well-known "sentinel" argument that uses the error register,
// the JIT will emit code for clearing the error register before the call,
// and will mark the error register as busy so it isn't used to hold the function call's address.
CallArg* const swiftErrorArg = call->gtArgs.GetArgByIndex(swiftErrorIndex);

@jakobbotschjakobbotschApr 4, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The loop above removes/inserts arguments, so this part still has to happen before the loop above.

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM! Thanks for addressing all my feedback.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

LGTM! Thanks for addressing all my feedback.

Thanks for all the reviews!

@amanasifkhalid

amanasifkhalid commented Apr 4, 2024

Copy link
Copy Markdown
ContributorAuthor

SPMI isn't finished running yet, but TP diffs look pretty small. I'm gonna merge to unblock #100344.

@amanasifkhalid
amanasifkhalid merged commit 16492b9 into dotnet:mainApr 4, 2024
@amanasifkhalid
amanasifkhalid deleted the swift-error-reg branch April 4, 2024 19:04
amanasifkhalid added a commit that referenced this pull request Apr 12, 2024
…return (#100692)
Follow-up to #100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
…return (dotnet#100692)
Follow-up to dotnet#100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 5, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@amanasifkhalid@kotlarmilos@jakobbotsch
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

JIT: Support Swift error handling for reverse P/Invokes - #100429

Merged
amanasifkhalid merged 41 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg
Apr 4, 2024
Merged

JIT: Support Swift error handling for reverse P/Invokes#100429
amanasifkhalid merged 41 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg

Conversation

@amanasifkhalid

Copy link
Copy Markdown
Contributor

.NET methods called from Swift update error handling state through a SwiftError* argument; when returning from .NET to Swift, the error value pointed to by the SwiftError* must be loaded into the error register. We use the following IR to support this:

  • The JIT creates a SwiftError "pseudo-local", and transforms all uses of the SwiftError* argument into GT_LCL_ADDR nodes of the pseudo-local.
  • When generating IR for the reverse P/Invoke exit point, we create a GT_SWIFT_ERROR_RET node for loading the SwiftError pseudo-local's value into the error register before returning.

I've added a new test for exercising .NET callbacks with error handling to SwiftErrorHandling.

cc @jkoritzinsky@kotlarmilos, @jakobbotsch PTAL

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 29, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@kotlarmiloskotlarmilos left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@amanasifkhalid

amanasifkhalid commented Mar 29, 2024

Copy link
Copy Markdown
ContributorAuthor

I'm seeing some weird interaction with the SwiftSelf parameter in the reverse P/Invoke test, where its value in the .NET callback is different from the value it was initialized with. The error handling in the callback still works correctly, in that the SwiftError value is set to the SwiftSelf value, but the assert in the test fails because the address the SwiftError was set to isn't correct. I run into the same issue even if I replace the SwiftSelf parameter with an IntPtr containing &expectedValue. I've simplified this test to just hard-code the SwiftError value in the callback so we know the error register is being propagated down the call stack; locally, this works with all the stress modes.

Once I've verified this passes in CI, I'll disable this test for Mono. @kotlarmilos is there any way to disable individual tests for Mono, or do I have to make a separate project in src/tests/Interop/Swift for this new test, and disable it for Mono in issues.targets? Thanks!

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

This change touches some hot paths (such as during importation), so there is some TP impact on Unix x64, though oddly not on arm64.

@kotlarmilos

Copy link
Copy Markdown
Member

I'm seeing some weird interaction with the SwiftSelf parameter in the reverse P/Invoke test, where its value in the .NET callback is different from the value it was initialized with. The error handling in the callback still works correctly, in that the SwiftError value is set to the SwiftSelf value, but the assert in the test fails because the address the SwiftError was set to isn't correct. I run into the same issue even if I replace the SwiftSelf parameter with an IntPtr containing &expectedValue. I've simplified this test to just hard-code the SwiftError value in the callback so we know the error register is being propagated down the call stack; locally, this works with all the stress modes.

According to the Swift calling convention, when passing closures, there is an implicit argument immediately after that is a pointer to the closure context, and it gets stored in the call context register (swiftself). In your test, the address of swifterror gets stored into swiftself, which may lead to unexpected behavior.

Once I've verified this passes in CI, I'll disable this test for Mono. @kotlarmilos is there any way to disable individual tests for Mono, or do I have to make a separate project in src/tests/Interop/Swift for this new test, and disable it for Mono in issues.targets? Thanks!

Try adding the SkipOnMono attribute to the test case.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

According to the Swift calling convention, when passing closures, there is an implicit argument immediately after that is a pointer to the closure context, and it gets stored in the call context register (swiftself). In your test, the address of swifterror gets stored into swiftself, which may lead to unexpected behavior.

I see, thanks for pointing that out -- that explains why the diff in the initial and updated SwiftSelf value was always the same. I guess it makes sense for the SwiftErrorHandling tests to test SwiftError in isolation, anyway.

Try adding the SkipOnMono attribute to the test case.

Thanks! I'll update in the next review iteration.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment on lines +103 to +104
CallArg* swiftErrorArg = nullptr;
CallArgs* callArgs = nullptr;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems like it would be cleaner to return the GenTree* node from impPopArgsForUnmanagedCall and remove the arg in there instead.

Comment threadsrc/coreclr/jit/codegenxarch.cpp Outdated

// If this method returns an error argument in the Swift error register,
// we didn't push the register, and thus shouldn't pop it.
if (compiler->lvaSwiftErrorArg == BAD_VAR_NUM)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems odd given the assert.

@amanasifkhalidamanasifkhalidApr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sorry I meant to remove the assert; doing so seems to fix the jitstressregs failure locally.

I suppose I could remove REG_SWIFT_ERROR from the register mask in the caller, but doing it here means we'll do the removal only if REG_SWIFT_ERROR was ever set in the first place.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Perhaps the caller shouldn't be passing it? Seems we could unify a bit of code by replacing some of the RBM_INT_CALLEE_SAVED occurrences in the backend with a function that takes the error return into account.

@amanasifkhalidamanasifkhalidApr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

That sounds better. Do you think it would make sense for this function to be a member of RegSet -- something like RegSet::rsGetModifiedIntCalleeSavedRegsMask -- since RegSet already has a Compiler* member variable to check for lvaSwiftErrorArg? Or would it be confusing if RegSet::rsGetModifiedIntCalleeSavedRegsMask sometimes doesn't include REG_SWIFT_ERROR, which is technically a callee-save register, even if it was modified?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it would be best to follow the same pattern as get_RBM_FLT_CALLEE_TRASH -- it has the same flavor of "sometimes needs to be determined based on compilation information", and RBM_FLT_CALLEE_TRASH is defined to call that function. It will ensure that everyone gets the consistent view, but will of course result in a much less localized change. If it ends up requiring too many changes or being costly then I would be fine with what you suggest.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thanks for the suggestion! I gave that a try, and I think we use RBM_INT_CALLEE_SAVED in too many places to make this approach manageable; in particular, we use RBM_INT_CALLEE_SAVED in some static functions where we don't have access to the current Compiler's state, so we can't easily figure out whether to include RBM_SWIFT_ERROR in the mask. Also, I think the additional check for lvaSwiftErrorArg would've been more expensive than necessary, since we check RBM_INT_CALLEE_SAVED (directly and indirectly, via other macros) in places irrelevant to method prolog/epilog generation.

I think RegSet provides a decent abstraction, though to make its usage consistent across the JIT, I had to make some trivial changes to architectures irrelevant to this PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok, it seems fine to me for now. Some notes...

and I think we use RBM_INT_CALLEE_SAVED in too many places to make this approach manageable

This doesn't exactly fill me with confidence. It means there are many places getting the wrong and inconsistent view after this PR. Are we sure that none of those need to be updated for correctness sake? If we hit a bug tail around this we should reconsider.

Also, I think the additional check for lvaSwiftErrorArg would've been more expensive than necessary, since we check RBM_INT_CALLEE_SAVED (directly and indirectly, via other macros) in places irrelevant to method prolog/epilog generation.

I think we would design it similar to get_RBM_FLT_CALLEE_TRASH -- the mask is stored in Compiler, so it's just returning the field. There is no dynamic logic to compute it on every invocation.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This doesn't exactly fill me with confidence. It means there are many places getting the wrong and inconsistent view after this PR. Are we sure that none of those need to be updated for correctness sake? If we hit a bug tail around this we should reconsider.

This might be naive, but I think it's ok if we continue to think of REG_SWIFT_ERROR as callee-save elsewhere in the JIT, and limit the error register-specific quirks to prolog/epilog generation. We use RBM_INT_CALLEE_SAVED in the definition of RBM_ALLINT as well, so to ensure the latter includes REG_SWIFT_ERROR, we'd probably need to save the "normal" value of RBM_INT_CALLEE_SAVED to another macro -- the fact that this new macro would only be semantically different from RBM_INT_CALLEE_SAVED on Swift platforms seems messy. We'd also have to use this duplicated macro in the various static asserts that use RBM_INT_CALLEE_SAVED without access to compiler state.

The benefit of the current approach is its behavior is limited in scope. It might be too limited to the point where we are incorrectly treating REG_SWIFT_ERROR as callee-save (though I haven't run into that yet), but that seems easier to debug than the opposite scenario of making RBM_INT_CALLEE_SAVED stateful.

@amanasifkhalid

amanasifkhalid commented Apr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

jitformat is insistent on this odd spacing:

 TEMP_MAX_SIZE = FP_REGSIZE_BYTES,
#endif // defined(TARGET_XARCH) || defined(TARGET_ARM64)
#else // !FEATURE_SIMD
TEMP_MAX_SIZE = sizeof(double),
#endif // !FEATURE_SIMD
TEMP_SLOT_COUNT = (TEMP_MAX_SIZE / sizeof(int))
};

I'll push a fix after approval.

Edit: Bruce just merged in a clang-format/clang-tidy update, so maybe this is fixed...

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
// Swift call isn't going to use the SwiftError* arg, so don't bother emitting it
assert(swiftErrorNode != nullptr);
*swiftErrorNode = swiftErrorArg->GetNode();
call->gtArgs.Remove(swiftErrorArg);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need to potentially adjust swiftSelfIndex here? Or perhaps instead delay the removal until after the loop below?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good catch: If the SwiftSelf arg comes after the SwiftError* arg, then yes, we do need to decrement it. It seems easier to delay the removal. Fixed.

lvaSwiftErrorArg = varDscInfo->varNum;

// Instead, all usages of the SwiftError* parameter will be redirected to this pseudolocal.
lvaSwiftErrorLocal = lvaGrabTempWithImplicitUse(false DEBUGARG("SwiftError pseudolocal"));

@jakobbotschjakobbotschApr 4, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you verify that something somewhere marks this local as either do-not-enreg or address exposed? I don't immediately see where we do that in the JIT when something has implicit uses. Basically, I don't understand for example why liveness doesn't get rid of stores to this local because it cannot see any uses of it. Can you share a jitdump of one of the tests just so I can understand why that doesn't happen?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure thing. I was able to remove the call to lvaSetVarAddrExposed here because fgMarkAddressExposedLocals seems to already do this in LocalAddressVisitor::EscapeAddress for each store to the pseudolocal before running liveness. Here's a JitDump for one of the callbacks (see line 1201, etc.).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we are getting "lucky" there. The JIT is spilling the address of the local to a temp (not really very optimal, we ought to improve this). It only does that because of the constructor call on the RHS of the assignment, and as a consequence we end up address exposing it. If you changed the code slightly, for example to

varx=newSwiftError(...);GC.KeepAlive(null);// avoid Roslyn optimizing it back to the pattern*error=x;

then I would expect that you will hit the issue I describe where nothing marks it as address exposed and we end up removing the store.

@amanasifkhalidamanasifkhalidApr 4, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I tried this, and you're correct that we no longer mark it as address exposed, but we don't end up removing the store -- I think this is because we mark all GT_LCL_ADDR nodes pointing to the pseudolocal as having side effects (perhaps that's what is forcing the JIT to spill the address to a temp?). I'm fine with going back to explicitly marking the pseudolocal as address exposed upon creation. My hope is that is short-lived anyway, once I open a PR for GT_SWIFT_ERROR_RET.

I added an updated dump to the gist with the above code pattern used in ConditionallySetErrorTo21 (though only in the true branch of the if statement).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I'm fine with going back to explicitly marking the pseudolocal as address exposed upon creation

This, along with removing the side effects on the GT_LCL_ADDR nodes of the pseudolocal, improves codegen. For example, before:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06,T01] ( 2, 2 ) long -> rdi "impAppendStmt"
; V07 tmp4 [V07 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
; V09 tmp6 [V09 ] ( 2, 1 ) long -> [rbp-0x10] do-not-enreg[X] addr-exposed "field V03.<Value>k__BackingField (fldOffset=0x0)" P-DEP
;* V10 tmp7 [V10,T03] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
lea rdi, [rbp-0x10]
xor eax, eax
mov qword ptr [rdi], rax
;; size=9 bbWeight=0.50 PerfScore 0.88
G_M3386_IG05: ;; offset=0x003F
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x004C
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 83, prolog size 27, PerfScore 16.58, instruction count 26, allocated bytes for code 83 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

After:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T01] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov qword ptr [rbp-0x10], rdi
;; size=6 bbWeight=0.50 PerfScore 0.62
G_M3386_IG05: ;; offset=0x003C
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x0049
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 80, prolog size 27, PerfScore 16.33, instruction count 25, allocated bytes for code 80 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah, that makes sense. Yeah, the side effect flags on the GT_LCL_ADDR shouldn't be necessary,

My hope is that is short-lived anyway, once I open a PR for GT_SWIFT_ERROR_RET.

Do you plan to do the work? To be honest looking at the codegen there it really doesn't feel all that bad compared to all the other things we end up doing for the reverse pinvokes anyway (like the helper calls), so I'd be perfectly fine with just leaving it like this.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I have some of the implementation saved locally, so I'm interested in trying it out; I think it can be done without polluting the JIT with too many weird edge cases.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
// By adding a well-known "sentinel" argument that uses the error register,
// the JIT will emit code for clearing the error register before the call,
// and will mark the error register as busy so it isn't used to hold the function call's address.
CallArg* const swiftErrorArg = call->gtArgs.GetArgByIndex(swiftErrorIndex);

@jakobbotschjakobbotschApr 4, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The loop above removes/inserts arguments, so this part still has to happen before the loop above.

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM! Thanks for addressing all my feedback.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

LGTM! Thanks for addressing all my feedback.

Thanks for all the reviews!

@amanasifkhalid

amanasifkhalid commented Apr 4, 2024

Copy link
Copy Markdown
ContributorAuthor

SPMI isn't finished running yet, but TP diffs look pretty small. I'm gonna merge to unblock #100344.

@amanasifkhalid
amanasifkhalid merged commit 16492b9 into dotnet:mainApr 4, 2024
@amanasifkhalid
amanasifkhalid deleted the swift-error-reg branch April 4, 2024 19:04
amanasifkhalid added a commit that referenced this pull request Apr 12, 2024
…return (#100692)
Follow-up to #100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
…return (dotnet#100692)
Follow-up to dotnet#100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 5, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@amanasifkhalid@kotlarmilos@jakobbotsch
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

JIT: Support Swift error handling for reverse P/Invokes - #100429

Merged
amanasifkhalid merged 41 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg
Apr 4, 2024
Merged

JIT: Support Swift error handling for reverse P/Invokes#100429
amanasifkhalid merged 41 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg

Conversation

@amanasifkhalid

Copy link
Copy Markdown
Contributor

.NET methods called from Swift update error handling state through a SwiftError* argument; when returning from .NET to Swift, the error value pointed to by the SwiftError* must be loaded into the error register. We use the following IR to support this:

  • The JIT creates a SwiftError "pseudo-local", and transforms all uses of the SwiftError* argument into GT_LCL_ADDR nodes of the pseudo-local.
  • When generating IR for the reverse P/Invoke exit point, we create a GT_SWIFT_ERROR_RET node for loading the SwiftError pseudo-local's value into the error register before returning.

I've added a new test for exercising .NET callbacks with error handling to SwiftErrorHandling.

cc @jkoritzinsky@kotlarmilos, @jakobbotsch PTAL

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 29, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@kotlarmiloskotlarmilos left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@amanasifkhalid

amanasifkhalid commented Mar 29, 2024

Copy link
Copy Markdown
ContributorAuthor

I'm seeing some weird interaction with the SwiftSelf parameter in the reverse P/Invoke test, where its value in the .NET callback is different from the value it was initialized with. The error handling in the callback still works correctly, in that the SwiftError value is set to the SwiftSelf value, but the assert in the test fails because the address the SwiftError was set to isn't correct. I run into the same issue even if I replace the SwiftSelf parameter with an IntPtr containing &expectedValue. I've simplified this test to just hard-code the SwiftError value in the callback so we know the error register is being propagated down the call stack; locally, this works with all the stress modes.

Once I've verified this passes in CI, I'll disable this test for Mono. @kotlarmilos is there any way to disable individual tests for Mono, or do I have to make a separate project in src/tests/Interop/Swift for this new test, and disable it for Mono in issues.targets? Thanks!

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

This change touches some hot paths (such as during importation), so there is some TP impact on Unix x64, though oddly not on arm64.

@kotlarmilos

Copy link
Copy Markdown
Member

I'm seeing some weird interaction with the SwiftSelf parameter in the reverse P/Invoke test, where its value in the .NET callback is different from the value it was initialized with. The error handling in the callback still works correctly, in that the SwiftError value is set to the SwiftSelf value, but the assert in the test fails because the address the SwiftError was set to isn't correct. I run into the same issue even if I replace the SwiftSelf parameter with an IntPtr containing &expectedValue. I've simplified this test to just hard-code the SwiftError value in the callback so we know the error register is being propagated down the call stack; locally, this works with all the stress modes.

According to the Swift calling convention, when passing closures, there is an implicit argument immediately after that is a pointer to the closure context, and it gets stored in the call context register (swiftself). In your test, the address of swifterror gets stored into swiftself, which may lead to unexpected behavior.

Once I've verified this passes in CI, I'll disable this test for Mono. @kotlarmilos is there any way to disable individual tests for Mono, or do I have to make a separate project in src/tests/Interop/Swift for this new test, and disable it for Mono in issues.targets? Thanks!

Try adding the SkipOnMono attribute to the test case.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

According to the Swift calling convention, when passing closures, there is an implicit argument immediately after that is a pointer to the closure context, and it gets stored in the call context register (swiftself). In your test, the address of swifterror gets stored into swiftself, which may lead to unexpected behavior.

I see, thanks for pointing that out -- that explains why the diff in the initial and updated SwiftSelf value was always the same. I guess it makes sense for the SwiftErrorHandling tests to test SwiftError in isolation, anyway.

Try adding the SkipOnMono attribute to the test case.

Thanks! I'll update in the next review iteration.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment on lines +103 to +104
CallArg* swiftErrorArg = nullptr;
CallArgs* callArgs = nullptr;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems like it would be cleaner to return the GenTree* node from impPopArgsForUnmanagedCall and remove the arg in there instead.

Comment threadsrc/coreclr/jit/codegenxarch.cpp Outdated

// If this method returns an error argument in the Swift error register,
// we didn't push the register, and thus shouldn't pop it.
if (compiler->lvaSwiftErrorArg == BAD_VAR_NUM)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems odd given the assert.

@amanasifkhalidamanasifkhalidApr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sorry I meant to remove the assert; doing so seems to fix the jitstressregs failure locally.

I suppose I could remove REG_SWIFT_ERROR from the register mask in the caller, but doing it here means we'll do the removal only if REG_SWIFT_ERROR was ever set in the first place.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Perhaps the caller shouldn't be passing it? Seems we could unify a bit of code by replacing some of the RBM_INT_CALLEE_SAVED occurrences in the backend with a function that takes the error return into account.

@amanasifkhalidamanasifkhalidApr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

That sounds better. Do you think it would make sense for this function to be a member of RegSet -- something like RegSet::rsGetModifiedIntCalleeSavedRegsMask -- since RegSet already has a Compiler* member variable to check for lvaSwiftErrorArg? Or would it be confusing if RegSet::rsGetModifiedIntCalleeSavedRegsMask sometimes doesn't include REG_SWIFT_ERROR, which is technically a callee-save register, even if it was modified?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it would be best to follow the same pattern as get_RBM_FLT_CALLEE_TRASH -- it has the same flavor of "sometimes needs to be determined based on compilation information", and RBM_FLT_CALLEE_TRASH is defined to call that function. It will ensure that everyone gets the consistent view, but will of course result in a much less localized change. If it ends up requiring too many changes or being costly then I would be fine with what you suggest.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thanks for the suggestion! I gave that a try, and I think we use RBM_INT_CALLEE_SAVED in too many places to make this approach manageable; in particular, we use RBM_INT_CALLEE_SAVED in some static functions where we don't have access to the current Compiler's state, so we can't easily figure out whether to include RBM_SWIFT_ERROR in the mask. Also, I think the additional check for lvaSwiftErrorArg would've been more expensive than necessary, since we check RBM_INT_CALLEE_SAVED (directly and indirectly, via other macros) in places irrelevant to method prolog/epilog generation.

I think RegSet provides a decent abstraction, though to make its usage consistent across the JIT, I had to make some trivial changes to architectures irrelevant to this PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok, it seems fine to me for now. Some notes...

and I think we use RBM_INT_CALLEE_SAVED in too many places to make this approach manageable

This doesn't exactly fill me with confidence. It means there are many places getting the wrong and inconsistent view after this PR. Are we sure that none of those need to be updated for correctness sake? If we hit a bug tail around this we should reconsider.

Also, I think the additional check for lvaSwiftErrorArg would've been more expensive than necessary, since we check RBM_INT_CALLEE_SAVED (directly and indirectly, via other macros) in places irrelevant to method prolog/epilog generation.

I think we would design it similar to get_RBM_FLT_CALLEE_TRASH -- the mask is stored in Compiler, so it's just returning the field. There is no dynamic logic to compute it on every invocation.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This doesn't exactly fill me with confidence. It means there are many places getting the wrong and inconsistent view after this PR. Are we sure that none of those need to be updated for correctness sake? If we hit a bug tail around this we should reconsider.

This might be naive, but I think it's ok if we continue to think of REG_SWIFT_ERROR as callee-save elsewhere in the JIT, and limit the error register-specific quirks to prolog/epilog generation. We use RBM_INT_CALLEE_SAVED in the definition of RBM_ALLINT as well, so to ensure the latter includes REG_SWIFT_ERROR, we'd probably need to save the "normal" value of RBM_INT_CALLEE_SAVED to another macro -- the fact that this new macro would only be semantically different from RBM_INT_CALLEE_SAVED on Swift platforms seems messy. We'd also have to use this duplicated macro in the various static asserts that use RBM_INT_CALLEE_SAVED without access to compiler state.

The benefit of the current approach is its behavior is limited in scope. It might be too limited to the point where we are incorrectly treating REG_SWIFT_ERROR as callee-save (though I haven't run into that yet), but that seems easier to debug than the opposite scenario of making RBM_INT_CALLEE_SAVED stateful.

@amanasifkhalid

amanasifkhalid commented Apr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

jitformat is insistent on this odd spacing:

 TEMP_MAX_SIZE = FP_REGSIZE_BYTES,
#endif // defined(TARGET_XARCH) || defined(TARGET_ARM64)
#else // !FEATURE_SIMD
TEMP_MAX_SIZE = sizeof(double),
#endif // !FEATURE_SIMD
TEMP_SLOT_COUNT = (TEMP_MAX_SIZE / sizeof(int))
};

I'll push a fix after approval.

Edit: Bruce just merged in a clang-format/clang-tidy update, so maybe this is fixed...

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
// Swift call isn't going to use the SwiftError* arg, so don't bother emitting it
assert(swiftErrorNode != nullptr);
*swiftErrorNode = swiftErrorArg->GetNode();
call->gtArgs.Remove(swiftErrorArg);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need to potentially adjust swiftSelfIndex here? Or perhaps instead delay the removal until after the loop below?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good catch: If the SwiftSelf arg comes after the SwiftError* arg, then yes, we do need to decrement it. It seems easier to delay the removal. Fixed.

lvaSwiftErrorArg = varDscInfo->varNum;

// Instead, all usages of the SwiftError* parameter will be redirected to this pseudolocal.
lvaSwiftErrorLocal = lvaGrabTempWithImplicitUse(false DEBUGARG("SwiftError pseudolocal"));

@jakobbotschjakobbotschApr 4, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you verify that something somewhere marks this local as either do-not-enreg or address exposed? I don't immediately see where we do that in the JIT when something has implicit uses. Basically, I don't understand for example why liveness doesn't get rid of stores to this local because it cannot see any uses of it. Can you share a jitdump of one of the tests just so I can understand why that doesn't happen?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure thing. I was able to remove the call to lvaSetVarAddrExposed here because fgMarkAddressExposedLocals seems to already do this in LocalAddressVisitor::EscapeAddress for each store to the pseudolocal before running liveness. Here's a JitDump for one of the callbacks (see line 1201, etc.).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we are getting "lucky" there. The JIT is spilling the address of the local to a temp (not really very optimal, we ought to improve this). It only does that because of the constructor call on the RHS of the assignment, and as a consequence we end up address exposing it. If you changed the code slightly, for example to

varx=newSwiftError(...);GC.KeepAlive(null);// avoid Roslyn optimizing it back to the pattern*error=x;

then I would expect that you will hit the issue I describe where nothing marks it as address exposed and we end up removing the store.

@amanasifkhalidamanasifkhalidApr 4, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I tried this, and you're correct that we no longer mark it as address exposed, but we don't end up removing the store -- I think this is because we mark all GT_LCL_ADDR nodes pointing to the pseudolocal as having side effects (perhaps that's what is forcing the JIT to spill the address to a temp?). I'm fine with going back to explicitly marking the pseudolocal as address exposed upon creation. My hope is that is short-lived anyway, once I open a PR for GT_SWIFT_ERROR_RET.

I added an updated dump to the gist with the above code pattern used in ConditionallySetErrorTo21 (though only in the true branch of the if statement).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I'm fine with going back to explicitly marking the pseudolocal as address exposed upon creation

This, along with removing the side effects on the GT_LCL_ADDR nodes of the pseudolocal, improves codegen. For example, before:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06,T01] ( 2, 2 ) long -> rdi "impAppendStmt"
; V07 tmp4 [V07 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
; V09 tmp6 [V09 ] ( 2, 1 ) long -> [rbp-0x10] do-not-enreg[X] addr-exposed "field V03.<Value>k__BackingField (fldOffset=0x0)" P-DEP
;* V10 tmp7 [V10,T03] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
lea rdi, [rbp-0x10]
xor eax, eax
mov qword ptr [rdi], rax
;; size=9 bbWeight=0.50 PerfScore 0.88
G_M3386_IG05: ;; offset=0x003F
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x004C
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 83, prolog size 27, PerfScore 16.58, instruction count 26, allocated bytes for code 83 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

After:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T01] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov qword ptr [rbp-0x10], rdi
;; size=6 bbWeight=0.50 PerfScore 0.62
G_M3386_IG05: ;; offset=0x003C
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x0049
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 80, prolog size 27, PerfScore 16.33, instruction count 25, allocated bytes for code 80 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah, that makes sense. Yeah, the side effect flags on the GT_LCL_ADDR shouldn't be necessary,

My hope is that is short-lived anyway, once I open a PR for GT_SWIFT_ERROR_RET.

Do you plan to do the work? To be honest looking at the codegen there it really doesn't feel all that bad compared to all the other things we end up doing for the reverse pinvokes anyway (like the helper calls), so I'd be perfectly fine with just leaving it like this.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I have some of the implementation saved locally, so I'm interested in trying it out; I think it can be done without polluting the JIT with too many weird edge cases.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
// By adding a well-known "sentinel" argument that uses the error register,
// the JIT will emit code for clearing the error register before the call,
// and will mark the error register as busy so it isn't used to hold the function call's address.
CallArg* const swiftErrorArg = call->gtArgs.GetArgByIndex(swiftErrorIndex);

@jakobbotschjakobbotschApr 4, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The loop above removes/inserts arguments, so this part still has to happen before the loop above.

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM! Thanks for addressing all my feedback.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

LGTM! Thanks for addressing all my feedback.

Thanks for all the reviews!

@amanasifkhalid

amanasifkhalid commented Apr 4, 2024

Copy link
Copy Markdown
ContributorAuthor

SPMI isn't finished running yet, but TP diffs look pretty small. I'm gonna merge to unblock #100344.

@amanasifkhalid
amanasifkhalid merged commit 16492b9 into dotnet:mainApr 4, 2024
@amanasifkhalid
amanasifkhalid deleted the swift-error-reg branch April 4, 2024 19:04
amanasifkhalid added a commit that referenced this pull request Apr 12, 2024
…return (#100692)
Follow-up to #100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
…return (dotnet#100692)
Follow-up to dotnet#100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 5, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@amanasifkhalid@kotlarmilos@jakobbotsch
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

JIT: Support Swift error handling for reverse P/Invokes - #100429

Merged
amanasifkhalid merged 41 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg
Apr 4, 2024
Merged

JIT: Support Swift error handling for reverse P/Invokes#100429
amanasifkhalid merged 41 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg

Conversation

@amanasifkhalid

Copy link
Copy Markdown
Contributor

.NET methods called from Swift update error handling state through a SwiftError* argument; when returning from .NET to Swift, the error value pointed to by the SwiftError* must be loaded into the error register. We use the following IR to support this:

  • The JIT creates a SwiftError "pseudo-local", and transforms all uses of the SwiftError* argument into GT_LCL_ADDR nodes of the pseudo-local.
  • When generating IR for the reverse P/Invoke exit point, we create a GT_SWIFT_ERROR_RET node for loading the SwiftError pseudo-local's value into the error register before returning.

I've added a new test for exercising .NET callbacks with error handling to SwiftErrorHandling.

cc @jkoritzinsky@kotlarmilos, @jakobbotsch PTAL

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 29, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@kotlarmiloskotlarmilos left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@amanasifkhalid

amanasifkhalid commented Mar 29, 2024

Copy link
Copy Markdown
ContributorAuthor

I'm seeing some weird interaction with the SwiftSelf parameter in the reverse P/Invoke test, where its value in the .NET callback is different from the value it was initialized with. The error handling in the callback still works correctly, in that the SwiftError value is set to the SwiftSelf value, but the assert in the test fails because the address the SwiftError was set to isn't correct. I run into the same issue even if I replace the SwiftSelf parameter with an IntPtr containing &expectedValue. I've simplified this test to just hard-code the SwiftError value in the callback so we know the error register is being propagated down the call stack; locally, this works with all the stress modes.

Once I've verified this passes in CI, I'll disable this test for Mono. @kotlarmilos is there any way to disable individual tests for Mono, or do I have to make a separate project in src/tests/Interop/Swift for this new test, and disable it for Mono in issues.targets? Thanks!

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

This change touches some hot paths (such as during importation), so there is some TP impact on Unix x64, though oddly not on arm64.

@kotlarmilos

Copy link
Copy Markdown
Member

I'm seeing some weird interaction with the SwiftSelf parameter in the reverse P/Invoke test, where its value in the .NET callback is different from the value it was initialized with. The error handling in the callback still works correctly, in that the SwiftError value is set to the SwiftSelf value, but the assert in the test fails because the address the SwiftError was set to isn't correct. I run into the same issue even if I replace the SwiftSelf parameter with an IntPtr containing &expectedValue. I've simplified this test to just hard-code the SwiftError value in the callback so we know the error register is being propagated down the call stack; locally, this works with all the stress modes.

According to the Swift calling convention, when passing closures, there is an implicit argument immediately after that is a pointer to the closure context, and it gets stored in the call context register (swiftself). In your test, the address of swifterror gets stored into swiftself, which may lead to unexpected behavior.

Once I've verified this passes in CI, I'll disable this test for Mono. @kotlarmilos is there any way to disable individual tests for Mono, or do I have to make a separate project in src/tests/Interop/Swift for this new test, and disable it for Mono in issues.targets? Thanks!

Try adding the SkipOnMono attribute to the test case.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

According to the Swift calling convention, when passing closures, there is an implicit argument immediately after that is a pointer to the closure context, and it gets stored in the call context register (swiftself). In your test, the address of swifterror gets stored into swiftself, which may lead to unexpected behavior.

I see, thanks for pointing that out -- that explains why the diff in the initial and updated SwiftSelf value was always the same. I guess it makes sense for the SwiftErrorHandling tests to test SwiftError in isolation, anyway.

Try adding the SkipOnMono attribute to the test case.

Thanks! I'll update in the next review iteration.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment on lines +103 to +104
CallArg* swiftErrorArg = nullptr;
CallArgs* callArgs = nullptr;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems like it would be cleaner to return the GenTree* node from impPopArgsForUnmanagedCall and remove the arg in there instead.

Comment threadsrc/coreclr/jit/codegenxarch.cpp Outdated

// If this method returns an error argument in the Swift error register,
// we didn't push the register, and thus shouldn't pop it.
if (compiler->lvaSwiftErrorArg == BAD_VAR_NUM)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems odd given the assert.

@amanasifkhalidamanasifkhalidApr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sorry I meant to remove the assert; doing so seems to fix the jitstressregs failure locally.

I suppose I could remove REG_SWIFT_ERROR from the register mask in the caller, but doing it here means we'll do the removal only if REG_SWIFT_ERROR was ever set in the first place.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Perhaps the caller shouldn't be passing it? Seems we could unify a bit of code by replacing some of the RBM_INT_CALLEE_SAVED occurrences in the backend with a function that takes the error return into account.

@amanasifkhalidamanasifkhalidApr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

That sounds better. Do you think it would make sense for this function to be a member of RegSet -- something like RegSet::rsGetModifiedIntCalleeSavedRegsMask -- since RegSet already has a Compiler* member variable to check for lvaSwiftErrorArg? Or would it be confusing if RegSet::rsGetModifiedIntCalleeSavedRegsMask sometimes doesn't include REG_SWIFT_ERROR, which is technically a callee-save register, even if it was modified?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it would be best to follow the same pattern as get_RBM_FLT_CALLEE_TRASH -- it has the same flavor of "sometimes needs to be determined based on compilation information", and RBM_FLT_CALLEE_TRASH is defined to call that function. It will ensure that everyone gets the consistent view, but will of course result in a much less localized change. If it ends up requiring too many changes or being costly then I would be fine with what you suggest.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thanks for the suggestion! I gave that a try, and I think we use RBM_INT_CALLEE_SAVED in too many places to make this approach manageable; in particular, we use RBM_INT_CALLEE_SAVED in some static functions where we don't have access to the current Compiler's state, so we can't easily figure out whether to include RBM_SWIFT_ERROR in the mask. Also, I think the additional check for lvaSwiftErrorArg would've been more expensive than necessary, since we check RBM_INT_CALLEE_SAVED (directly and indirectly, via other macros) in places irrelevant to method prolog/epilog generation.

I think RegSet provides a decent abstraction, though to make its usage consistent across the JIT, I had to make some trivial changes to architectures irrelevant to this PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok, it seems fine to me for now. Some notes...

and I think we use RBM_INT_CALLEE_SAVED in too many places to make this approach manageable

This doesn't exactly fill me with confidence. It means there are many places getting the wrong and inconsistent view after this PR. Are we sure that none of those need to be updated for correctness sake? If we hit a bug tail around this we should reconsider.

Also, I think the additional check for lvaSwiftErrorArg would've been more expensive than necessary, since we check RBM_INT_CALLEE_SAVED (directly and indirectly, via other macros) in places irrelevant to method prolog/epilog generation.

I think we would design it similar to get_RBM_FLT_CALLEE_TRASH -- the mask is stored in Compiler, so it's just returning the field. There is no dynamic logic to compute it on every invocation.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This doesn't exactly fill me with confidence. It means there are many places getting the wrong and inconsistent view after this PR. Are we sure that none of those need to be updated for correctness sake? If we hit a bug tail around this we should reconsider.

This might be naive, but I think it's ok if we continue to think of REG_SWIFT_ERROR as callee-save elsewhere in the JIT, and limit the error register-specific quirks to prolog/epilog generation. We use RBM_INT_CALLEE_SAVED in the definition of RBM_ALLINT as well, so to ensure the latter includes REG_SWIFT_ERROR, we'd probably need to save the "normal" value of RBM_INT_CALLEE_SAVED to another macro -- the fact that this new macro would only be semantically different from RBM_INT_CALLEE_SAVED on Swift platforms seems messy. We'd also have to use this duplicated macro in the various static asserts that use RBM_INT_CALLEE_SAVED without access to compiler state.

The benefit of the current approach is its behavior is limited in scope. It might be too limited to the point where we are incorrectly treating REG_SWIFT_ERROR as callee-save (though I haven't run into that yet), but that seems easier to debug than the opposite scenario of making RBM_INT_CALLEE_SAVED stateful.

@amanasifkhalid

amanasifkhalid commented Apr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

jitformat is insistent on this odd spacing:

 TEMP_MAX_SIZE = FP_REGSIZE_BYTES,
#endif // defined(TARGET_XARCH) || defined(TARGET_ARM64)
#else // !FEATURE_SIMD
TEMP_MAX_SIZE = sizeof(double),
#endif // !FEATURE_SIMD
TEMP_SLOT_COUNT = (TEMP_MAX_SIZE / sizeof(int))
};

I'll push a fix after approval.

Edit: Bruce just merged in a clang-format/clang-tidy update, so maybe this is fixed...

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
// Swift call isn't going to use the SwiftError* arg, so don't bother emitting it
assert(swiftErrorNode != nullptr);
*swiftErrorNode = swiftErrorArg->GetNode();
call->gtArgs.Remove(swiftErrorArg);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need to potentially adjust swiftSelfIndex here? Or perhaps instead delay the removal until after the loop below?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good catch: If the SwiftSelf arg comes after the SwiftError* arg, then yes, we do need to decrement it. It seems easier to delay the removal. Fixed.

lvaSwiftErrorArg = varDscInfo->varNum;

// Instead, all usages of the SwiftError* parameter will be redirected to this pseudolocal.
lvaSwiftErrorLocal = lvaGrabTempWithImplicitUse(false DEBUGARG("SwiftError pseudolocal"));

@jakobbotschjakobbotschApr 4, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you verify that something somewhere marks this local as either do-not-enreg or address exposed? I don't immediately see where we do that in the JIT when something has implicit uses. Basically, I don't understand for example why liveness doesn't get rid of stores to this local because it cannot see any uses of it. Can you share a jitdump of one of the tests just so I can understand why that doesn't happen?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure thing. I was able to remove the call to lvaSetVarAddrExposed here because fgMarkAddressExposedLocals seems to already do this in LocalAddressVisitor::EscapeAddress for each store to the pseudolocal before running liveness. Here's a JitDump for one of the callbacks (see line 1201, etc.).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we are getting "lucky" there. The JIT is spilling the address of the local to a temp (not really very optimal, we ought to improve this). It only does that because of the constructor call on the RHS of the assignment, and as a consequence we end up address exposing it. If you changed the code slightly, for example to

varx=newSwiftError(...);GC.KeepAlive(null);// avoid Roslyn optimizing it back to the pattern*error=x;

then I would expect that you will hit the issue I describe where nothing marks it as address exposed and we end up removing the store.

@amanasifkhalidamanasifkhalidApr 4, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I tried this, and you're correct that we no longer mark it as address exposed, but we don't end up removing the store -- I think this is because we mark all GT_LCL_ADDR nodes pointing to the pseudolocal as having side effects (perhaps that's what is forcing the JIT to spill the address to a temp?). I'm fine with going back to explicitly marking the pseudolocal as address exposed upon creation. My hope is that is short-lived anyway, once I open a PR for GT_SWIFT_ERROR_RET.

I added an updated dump to the gist with the above code pattern used in ConditionallySetErrorTo21 (though only in the true branch of the if statement).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I'm fine with going back to explicitly marking the pseudolocal as address exposed upon creation

This, along with removing the side effects on the GT_LCL_ADDR nodes of the pseudolocal, improves codegen. For example, before:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06,T01] ( 2, 2 ) long -> rdi "impAppendStmt"
; V07 tmp4 [V07 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
; V09 tmp6 [V09 ] ( 2, 1 ) long -> [rbp-0x10] do-not-enreg[X] addr-exposed "field V03.<Value>k__BackingField (fldOffset=0x0)" P-DEP
;* V10 tmp7 [V10,T03] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
lea rdi, [rbp-0x10]
xor eax, eax
mov qword ptr [rdi], rax
;; size=9 bbWeight=0.50 PerfScore 0.88
G_M3386_IG05: ;; offset=0x003F
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x004C
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 83, prolog size 27, PerfScore 16.58, instruction count 26, allocated bytes for code 83 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

After:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T01] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov qword ptr [rbp-0x10], rdi
;; size=6 bbWeight=0.50 PerfScore 0.62
G_M3386_IG05: ;; offset=0x003C
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x0049
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 80, prolog size 27, PerfScore 16.33, instruction count 25, allocated bytes for code 80 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah, that makes sense. Yeah, the side effect flags on the GT_LCL_ADDR shouldn't be necessary,

My hope is that is short-lived anyway, once I open a PR for GT_SWIFT_ERROR_RET.

Do you plan to do the work? To be honest looking at the codegen there it really doesn't feel all that bad compared to all the other things we end up doing for the reverse pinvokes anyway (like the helper calls), so I'd be perfectly fine with just leaving it like this.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I have some of the implementation saved locally, so I'm interested in trying it out; I think it can be done without polluting the JIT with too many weird edge cases.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
// By adding a well-known "sentinel" argument that uses the error register,
// the JIT will emit code for clearing the error register before the call,
// and will mark the error register as busy so it isn't used to hold the function call's address.
CallArg* const swiftErrorArg = call->gtArgs.GetArgByIndex(swiftErrorIndex);

@jakobbotschjakobbotschApr 4, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The loop above removes/inserts arguments, so this part still has to happen before the loop above.

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM! Thanks for addressing all my feedback.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

LGTM! Thanks for addressing all my feedback.

Thanks for all the reviews!

@amanasifkhalid

amanasifkhalid commented Apr 4, 2024

Copy link
Copy Markdown
ContributorAuthor

SPMI isn't finished running yet, but TP diffs look pretty small. I'm gonna merge to unblock #100344.

@amanasifkhalid
amanasifkhalid merged commit 16492b9 into dotnet:mainApr 4, 2024
@amanasifkhalid
amanasifkhalid deleted the swift-error-reg branch April 4, 2024 19:04
amanasifkhalid added a commit that referenced this pull request Apr 12, 2024
…return (#100692)
Follow-up to #100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
…return (dotnet#100692)
Follow-up to dotnet#100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 5, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@amanasifkhalid@kotlarmilos@jakobbotsch
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

JIT: Support Swift error handling for reverse P/Invokes - #100429

Merged
amanasifkhalid merged 41 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg
Apr 4, 2024
Merged

JIT: Support Swift error handling for reverse P/Invokes#100429
amanasifkhalid merged 41 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg

Conversation

@amanasifkhalid

Copy link
Copy Markdown
Contributor

.NET methods called from Swift update error handling state through a SwiftError* argument; when returning from .NET to Swift, the error value pointed to by the SwiftError* must be loaded into the error register. We use the following IR to support this:

  • The JIT creates a SwiftError "pseudo-local", and transforms all uses of the SwiftError* argument into GT_LCL_ADDR nodes of the pseudo-local.
  • When generating IR for the reverse P/Invoke exit point, we create a GT_SWIFT_ERROR_RET node for loading the SwiftError pseudo-local's value into the error register before returning.

I've added a new test for exercising .NET callbacks with error handling to SwiftErrorHandling.

cc @jkoritzinsky@kotlarmilos, @jakobbotsch PTAL

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 29, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@kotlarmiloskotlarmilos left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@amanasifkhalid

amanasifkhalid commented Mar 29, 2024

Copy link
Copy Markdown
ContributorAuthor

I'm seeing some weird interaction with the SwiftSelf parameter in the reverse P/Invoke test, where its value in the .NET callback is different from the value it was initialized with. The error handling in the callback still works correctly, in that the SwiftError value is set to the SwiftSelf value, but the assert in the test fails because the address the SwiftError was set to isn't correct. I run into the same issue even if I replace the SwiftSelf parameter with an IntPtr containing &expectedValue. I've simplified this test to just hard-code the SwiftError value in the callback so we know the error register is being propagated down the call stack; locally, this works with all the stress modes.

Once I've verified this passes in CI, I'll disable this test for Mono. @kotlarmilos is there any way to disable individual tests for Mono, or do I have to make a separate project in src/tests/Interop/Swift for this new test, and disable it for Mono in issues.targets? Thanks!

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

This change touches some hot paths (such as during importation), so there is some TP impact on Unix x64, though oddly not on arm64.

@kotlarmilos

Copy link
Copy Markdown
Member

I'm seeing some weird interaction with the SwiftSelf parameter in the reverse P/Invoke test, where its value in the .NET callback is different from the value it was initialized with. The error handling in the callback still works correctly, in that the SwiftError value is set to the SwiftSelf value, but the assert in the test fails because the address the SwiftError was set to isn't correct. I run into the same issue even if I replace the SwiftSelf parameter with an IntPtr containing &expectedValue. I've simplified this test to just hard-code the SwiftError value in the callback so we know the error register is being propagated down the call stack; locally, this works with all the stress modes.

According to the Swift calling convention, when passing closures, there is an implicit argument immediately after that is a pointer to the closure context, and it gets stored in the call context register (swiftself). In your test, the address of swifterror gets stored into swiftself, which may lead to unexpected behavior.

Once I've verified this passes in CI, I'll disable this test for Mono. @kotlarmilos is there any way to disable individual tests for Mono, or do I have to make a separate project in src/tests/Interop/Swift for this new test, and disable it for Mono in issues.targets? Thanks!

Try adding the SkipOnMono attribute to the test case.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

According to the Swift calling convention, when passing closures, there is an implicit argument immediately after that is a pointer to the closure context, and it gets stored in the call context register (swiftself). In your test, the address of swifterror gets stored into swiftself, which may lead to unexpected behavior.

I see, thanks for pointing that out -- that explains why the diff in the initial and updated SwiftSelf value was always the same. I guess it makes sense for the SwiftErrorHandling tests to test SwiftError in isolation, anyway.

Try adding the SkipOnMono attribute to the test case.

Thanks! I'll update in the next review iteration.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment on lines +103 to +104
CallArg* swiftErrorArg = nullptr;
CallArgs* callArgs = nullptr;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems like it would be cleaner to return the GenTree* node from impPopArgsForUnmanagedCall and remove the arg in there instead.

Comment threadsrc/coreclr/jit/codegenxarch.cpp Outdated

// If this method returns an error argument in the Swift error register,
// we didn't push the register, and thus shouldn't pop it.
if (compiler->lvaSwiftErrorArg == BAD_VAR_NUM)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems odd given the assert.

@amanasifkhalidamanasifkhalidApr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sorry I meant to remove the assert; doing so seems to fix the jitstressregs failure locally.

I suppose I could remove REG_SWIFT_ERROR from the register mask in the caller, but doing it here means we'll do the removal only if REG_SWIFT_ERROR was ever set in the first place.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Perhaps the caller shouldn't be passing it? Seems we could unify a bit of code by replacing some of the RBM_INT_CALLEE_SAVED occurrences in the backend with a function that takes the error return into account.

@amanasifkhalidamanasifkhalidApr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

That sounds better. Do you think it would make sense for this function to be a member of RegSet -- something like RegSet::rsGetModifiedIntCalleeSavedRegsMask -- since RegSet already has a Compiler* member variable to check for lvaSwiftErrorArg? Or would it be confusing if RegSet::rsGetModifiedIntCalleeSavedRegsMask sometimes doesn't include REG_SWIFT_ERROR, which is technically a callee-save register, even if it was modified?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it would be best to follow the same pattern as get_RBM_FLT_CALLEE_TRASH -- it has the same flavor of "sometimes needs to be determined based on compilation information", and RBM_FLT_CALLEE_TRASH is defined to call that function. It will ensure that everyone gets the consistent view, but will of course result in a much less localized change. If it ends up requiring too many changes or being costly then I would be fine with what you suggest.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thanks for the suggestion! I gave that a try, and I think we use RBM_INT_CALLEE_SAVED in too many places to make this approach manageable; in particular, we use RBM_INT_CALLEE_SAVED in some static functions where we don't have access to the current Compiler's state, so we can't easily figure out whether to include RBM_SWIFT_ERROR in the mask. Also, I think the additional check for lvaSwiftErrorArg would've been more expensive than necessary, since we check RBM_INT_CALLEE_SAVED (directly and indirectly, via other macros) in places irrelevant to method prolog/epilog generation.

I think RegSet provides a decent abstraction, though to make its usage consistent across the JIT, I had to make some trivial changes to architectures irrelevant to this PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok, it seems fine to me for now. Some notes...

and I think we use RBM_INT_CALLEE_SAVED in too many places to make this approach manageable

This doesn't exactly fill me with confidence. It means there are many places getting the wrong and inconsistent view after this PR. Are we sure that none of those need to be updated for correctness sake? If we hit a bug tail around this we should reconsider.

Also, I think the additional check for lvaSwiftErrorArg would've been more expensive than necessary, since we check RBM_INT_CALLEE_SAVED (directly and indirectly, via other macros) in places irrelevant to method prolog/epilog generation.

I think we would design it similar to get_RBM_FLT_CALLEE_TRASH -- the mask is stored in Compiler, so it's just returning the field. There is no dynamic logic to compute it on every invocation.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This doesn't exactly fill me with confidence. It means there are many places getting the wrong and inconsistent view after this PR. Are we sure that none of those need to be updated for correctness sake? If we hit a bug tail around this we should reconsider.

This might be naive, but I think it's ok if we continue to think of REG_SWIFT_ERROR as callee-save elsewhere in the JIT, and limit the error register-specific quirks to prolog/epilog generation. We use RBM_INT_CALLEE_SAVED in the definition of RBM_ALLINT as well, so to ensure the latter includes REG_SWIFT_ERROR, we'd probably need to save the "normal" value of RBM_INT_CALLEE_SAVED to another macro -- the fact that this new macro would only be semantically different from RBM_INT_CALLEE_SAVED on Swift platforms seems messy. We'd also have to use this duplicated macro in the various static asserts that use RBM_INT_CALLEE_SAVED without access to compiler state.

The benefit of the current approach is its behavior is limited in scope. It might be too limited to the point where we are incorrectly treating REG_SWIFT_ERROR as callee-save (though I haven't run into that yet), but that seems easier to debug than the opposite scenario of making RBM_INT_CALLEE_SAVED stateful.

@amanasifkhalid

amanasifkhalid commented Apr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

jitformat is insistent on this odd spacing:

 TEMP_MAX_SIZE = FP_REGSIZE_BYTES,
#endif // defined(TARGET_XARCH) || defined(TARGET_ARM64)
#else // !FEATURE_SIMD
TEMP_MAX_SIZE = sizeof(double),
#endif // !FEATURE_SIMD
TEMP_SLOT_COUNT = (TEMP_MAX_SIZE / sizeof(int))
};

I'll push a fix after approval.

Edit: Bruce just merged in a clang-format/clang-tidy update, so maybe this is fixed...

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
// Swift call isn't going to use the SwiftError* arg, so don't bother emitting it
assert(swiftErrorNode != nullptr);
*swiftErrorNode = swiftErrorArg->GetNode();
call->gtArgs.Remove(swiftErrorArg);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need to potentially adjust swiftSelfIndex here? Or perhaps instead delay the removal until after the loop below?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good catch: If the SwiftSelf arg comes after the SwiftError* arg, then yes, we do need to decrement it. It seems easier to delay the removal. Fixed.

lvaSwiftErrorArg = varDscInfo->varNum;

// Instead, all usages of the SwiftError* parameter will be redirected to this pseudolocal.
lvaSwiftErrorLocal = lvaGrabTempWithImplicitUse(false DEBUGARG("SwiftError pseudolocal"));

@jakobbotschjakobbotschApr 4, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you verify that something somewhere marks this local as either do-not-enreg or address exposed? I don't immediately see where we do that in the JIT when something has implicit uses. Basically, I don't understand for example why liveness doesn't get rid of stores to this local because it cannot see any uses of it. Can you share a jitdump of one of the tests just so I can understand why that doesn't happen?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure thing. I was able to remove the call to lvaSetVarAddrExposed here because fgMarkAddressExposedLocals seems to already do this in LocalAddressVisitor::EscapeAddress for each store to the pseudolocal before running liveness. Here's a JitDump for one of the callbacks (see line 1201, etc.).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we are getting "lucky" there. The JIT is spilling the address of the local to a temp (not really very optimal, we ought to improve this). It only does that because of the constructor call on the RHS of the assignment, and as a consequence we end up address exposing it. If you changed the code slightly, for example to

varx=newSwiftError(...);GC.KeepAlive(null);// avoid Roslyn optimizing it back to the pattern*error=x;

then I would expect that you will hit the issue I describe where nothing marks it as address exposed and we end up removing the store.

@amanasifkhalidamanasifkhalidApr 4, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I tried this, and you're correct that we no longer mark it as address exposed, but we don't end up removing the store -- I think this is because we mark all GT_LCL_ADDR nodes pointing to the pseudolocal as having side effects (perhaps that's what is forcing the JIT to spill the address to a temp?). I'm fine with going back to explicitly marking the pseudolocal as address exposed upon creation. My hope is that is short-lived anyway, once I open a PR for GT_SWIFT_ERROR_RET.

I added an updated dump to the gist with the above code pattern used in ConditionallySetErrorTo21 (though only in the true branch of the if statement).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I'm fine with going back to explicitly marking the pseudolocal as address exposed upon creation

This, along with removing the side effects on the GT_LCL_ADDR nodes of the pseudolocal, improves codegen. For example, before:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06,T01] ( 2, 2 ) long -> rdi "impAppendStmt"
; V07 tmp4 [V07 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
; V09 tmp6 [V09 ] ( 2, 1 ) long -> [rbp-0x10] do-not-enreg[X] addr-exposed "field V03.<Value>k__BackingField (fldOffset=0x0)" P-DEP
;* V10 tmp7 [V10,T03] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
lea rdi, [rbp-0x10]
xor eax, eax
mov qword ptr [rdi], rax
;; size=9 bbWeight=0.50 PerfScore 0.88
G_M3386_IG05: ;; offset=0x003F
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x004C
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 83, prolog size 27, PerfScore 16.58, instruction count 26, allocated bytes for code 83 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

After:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T01] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov qword ptr [rbp-0x10], rdi
;; size=6 bbWeight=0.50 PerfScore 0.62
G_M3386_IG05: ;; offset=0x003C
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x0049
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 80, prolog size 27, PerfScore 16.33, instruction count 25, allocated bytes for code 80 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah, that makes sense. Yeah, the side effect flags on the GT_LCL_ADDR shouldn't be necessary,

My hope is that is short-lived anyway, once I open a PR for GT_SWIFT_ERROR_RET.

Do you plan to do the work? To be honest looking at the codegen there it really doesn't feel all that bad compared to all the other things we end up doing for the reverse pinvokes anyway (like the helper calls), so I'd be perfectly fine with just leaving it like this.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I have some of the implementation saved locally, so I'm interested in trying it out; I think it can be done without polluting the JIT with too many weird edge cases.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
// By adding a well-known "sentinel" argument that uses the error register,
// the JIT will emit code for clearing the error register before the call,
// and will mark the error register as busy so it isn't used to hold the function call's address.
CallArg* const swiftErrorArg = call->gtArgs.GetArgByIndex(swiftErrorIndex);

@jakobbotschjakobbotschApr 4, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The loop above removes/inserts arguments, so this part still has to happen before the loop above.

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM! Thanks for addressing all my feedback.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

LGTM! Thanks for addressing all my feedback.

Thanks for all the reviews!

@amanasifkhalid

amanasifkhalid commented Apr 4, 2024

Copy link
Copy Markdown
ContributorAuthor

SPMI isn't finished running yet, but TP diffs look pretty small. I'm gonna merge to unblock #100344.

@amanasifkhalid
amanasifkhalid merged commit 16492b9 into dotnet:mainApr 4, 2024
@amanasifkhalid
amanasifkhalid deleted the swift-error-reg branch April 4, 2024 19:04
amanasifkhalid added a commit that referenced this pull request Apr 12, 2024
…return (#100692)
Follow-up to #100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
…return (dotnet#100692)
Follow-up to dotnet#100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 5, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@amanasifkhalid@kotlarmilos@jakobbotsch
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

JIT: Support Swift error handling for reverse P/Invokes - #100429

Merged
amanasifkhalid merged 41 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg
Apr 4, 2024
Merged

JIT: Support Swift error handling for reverse P/Invokes#100429
amanasifkhalid merged 41 commits into
dotnet:mainfrom
amanasifkhalid:swift-error-reg

Conversation

@amanasifkhalid

Copy link
Copy Markdown
Contributor

.NET methods called from Swift update error handling state through a SwiftError* argument; when returning from .NET to Swift, the error value pointed to by the SwiftError* must be loaded into the error register. We use the following IR to support this:

  • The JIT creates a SwiftError "pseudo-local", and transforms all uses of the SwiftError* argument into GT_LCL_ADDR nodes of the pseudo-local.
  • When generating IR for the reverse P/Invoke exit point, we create a GT_SWIFT_ERROR_RET node for loading the SwiftError pseudo-local's value into the error register before returning.

I've added a new test for exercising .NET callbacks with error handling to SwiftErrorHandling.

cc @jkoritzinsky@kotlarmilos, @jakobbotsch PTAL

@ghostghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 29, 2024
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@kotlarmiloskotlarmilos left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@amanasifkhalid

amanasifkhalid commented Mar 29, 2024

Copy link
Copy Markdown
ContributorAuthor

I'm seeing some weird interaction with the SwiftSelf parameter in the reverse P/Invoke test, where its value in the .NET callback is different from the value it was initialized with. The error handling in the callback still works correctly, in that the SwiftError value is set to the SwiftSelf value, but the assert in the test fails because the address the SwiftError was set to isn't correct. I run into the same issue even if I replace the SwiftSelf parameter with an IntPtr containing &expectedValue. I've simplified this test to just hard-code the SwiftError value in the callback so we know the error register is being propagated down the call stack; locally, this works with all the stress modes.

Once I've verified this passes in CI, I'll disable this test for Mono. @kotlarmilos is there any way to disable individual tests for Mono, or do I have to make a separate project in src/tests/Interop/Swift for this new test, and disable it for Mono in issues.targets? Thanks!

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

This change touches some hot paths (such as during importation), so there is some TP impact on Unix x64, though oddly not on arm64.

@kotlarmilos

Copy link
Copy Markdown
Member

I'm seeing some weird interaction with the SwiftSelf parameter in the reverse P/Invoke test, where its value in the .NET callback is different from the value it was initialized with. The error handling in the callback still works correctly, in that the SwiftError value is set to the SwiftSelf value, but the assert in the test fails because the address the SwiftError was set to isn't correct. I run into the same issue even if I replace the SwiftSelf parameter with an IntPtr containing &expectedValue. I've simplified this test to just hard-code the SwiftError value in the callback so we know the error register is being propagated down the call stack; locally, this works with all the stress modes.

According to the Swift calling convention, when passing closures, there is an implicit argument immediately after that is a pointer to the closure context, and it gets stored in the call context register (swiftself). In your test, the address of swifterror gets stored into swiftself, which may lead to unexpected behavior.

Once I've verified this passes in CI, I'll disable this test for Mono. @kotlarmilos is there any way to disable individual tests for Mono, or do I have to make a separate project in src/tests/Interop/Swift for this new test, and disable it for Mono in issues.targets? Thanks!

Try adding the SkipOnMono attribute to the test case.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

According to the Swift calling convention, when passing closures, there is an implicit argument immediately after that is a pointer to the closure context, and it gets stored in the call context register (swiftself). In your test, the address of swifterror gets stored into swiftself, which may lead to unexpected behavior.

I see, thanks for pointing that out -- that explains why the diff in the initial and updated SwiftSelf value was always the same. I guess it makes sense for the SwiftErrorHandling tests to test SwiftError in isolation, anyway.

Try adding the SkipOnMono attribute to the test case.

Thanks! I'll update in the next review iteration.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment on lines +103 to +104
CallArg* swiftErrorArg = nullptr;
CallArgs* callArgs = nullptr;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems like it would be cleaner to return the GenTree* node from impPopArgsForUnmanagedCall and remove the arg in there instead.

Comment threadsrc/coreclr/jit/codegenxarch.cpp Outdated

// If this method returns an error argument in the Swift error register,
// we didn't push the register, and thus shouldn't pop it.
if (compiler->lvaSwiftErrorArg == BAD_VAR_NUM)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems odd given the assert.

@amanasifkhalidamanasifkhalidApr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sorry I meant to remove the assert; doing so seems to fix the jitstressregs failure locally.

I suppose I could remove REG_SWIFT_ERROR from the register mask in the caller, but doing it here means we'll do the removal only if REG_SWIFT_ERROR was ever set in the first place.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Perhaps the caller shouldn't be passing it? Seems we could unify a bit of code by replacing some of the RBM_INT_CALLEE_SAVED occurrences in the backend with a function that takes the error return into account.

@amanasifkhalidamanasifkhalidApr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

That sounds better. Do you think it would make sense for this function to be a member of RegSet -- something like RegSet::rsGetModifiedIntCalleeSavedRegsMask -- since RegSet already has a Compiler* member variable to check for lvaSwiftErrorArg? Or would it be confusing if RegSet::rsGetModifiedIntCalleeSavedRegsMask sometimes doesn't include REG_SWIFT_ERROR, which is technically a callee-save register, even if it was modified?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it would be best to follow the same pattern as get_RBM_FLT_CALLEE_TRASH -- it has the same flavor of "sometimes needs to be determined based on compilation information", and RBM_FLT_CALLEE_TRASH is defined to call that function. It will ensure that everyone gets the consistent view, but will of course result in a much less localized change. If it ends up requiring too many changes or being costly then I would be fine with what you suggest.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thanks for the suggestion! I gave that a try, and I think we use RBM_INT_CALLEE_SAVED in too many places to make this approach manageable; in particular, we use RBM_INT_CALLEE_SAVED in some static functions where we don't have access to the current Compiler's state, so we can't easily figure out whether to include RBM_SWIFT_ERROR in the mask. Also, I think the additional check for lvaSwiftErrorArg would've been more expensive than necessary, since we check RBM_INT_CALLEE_SAVED (directly and indirectly, via other macros) in places irrelevant to method prolog/epilog generation.

I think RegSet provides a decent abstraction, though to make its usage consistent across the JIT, I had to make some trivial changes to architectures irrelevant to this PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok, it seems fine to me for now. Some notes...

and I think we use RBM_INT_CALLEE_SAVED in too many places to make this approach manageable

This doesn't exactly fill me with confidence. It means there are many places getting the wrong and inconsistent view after this PR. Are we sure that none of those need to be updated for correctness sake? If we hit a bug tail around this we should reconsider.

Also, I think the additional check for lvaSwiftErrorArg would've been more expensive than necessary, since we check RBM_INT_CALLEE_SAVED (directly and indirectly, via other macros) in places irrelevant to method prolog/epilog generation.

I think we would design it similar to get_RBM_FLT_CALLEE_TRASH -- the mask is stored in Compiler, so it's just returning the field. There is no dynamic logic to compute it on every invocation.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This doesn't exactly fill me with confidence. It means there are many places getting the wrong and inconsistent view after this PR. Are we sure that none of those need to be updated for correctness sake? If we hit a bug tail around this we should reconsider.

This might be naive, but I think it's ok if we continue to think of REG_SWIFT_ERROR as callee-save elsewhere in the JIT, and limit the error register-specific quirks to prolog/epilog generation. We use RBM_INT_CALLEE_SAVED in the definition of RBM_ALLINT as well, so to ensure the latter includes REG_SWIFT_ERROR, we'd probably need to save the "normal" value of RBM_INT_CALLEE_SAVED to another macro -- the fact that this new macro would only be semantically different from RBM_INT_CALLEE_SAVED on Swift platforms seems messy. We'd also have to use this duplicated macro in the various static asserts that use RBM_INT_CALLEE_SAVED without access to compiler state.

The benefit of the current approach is its behavior is limited in scope. It might be too limited to the point where we are incorrectly treating REG_SWIFT_ERROR as callee-save (though I haven't run into that yet), but that seems easier to debug than the opposite scenario of making RBM_INT_CALLEE_SAVED stateful.

@amanasifkhalid

amanasifkhalid commented Apr 3, 2024

Copy link
Copy Markdown
ContributorAuthor

jitformat is insistent on this odd spacing:

 TEMP_MAX_SIZE = FP_REGSIZE_BYTES,
#endif // defined(TARGET_XARCH) || defined(TARGET_ARM64)
#else // !FEATURE_SIMD
TEMP_MAX_SIZE = sizeof(double),
#endif // !FEATURE_SIMD
TEMP_SLOT_COUNT = (TEMP_MAX_SIZE / sizeof(int))
};

I'll push a fix after approval.

Edit: Bruce just merged in a clang-format/clang-tidy update, so maybe this is fixed...

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

/azp run runtime-coreclr jitstress, runtime-coreclr jitstressregs, runtime-coreclr jitstress2-jitstressregs

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
// Swift call isn't going to use the SwiftError* arg, so don't bother emitting it
assert(swiftErrorNode != nullptr);
*swiftErrorNode = swiftErrorArg->GetNode();
call->gtArgs.Remove(swiftErrorArg);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need to potentially adjust swiftSelfIndex here? Or perhaps instead delay the removal until after the loop below?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good catch: If the SwiftSelf arg comes after the SwiftError* arg, then yes, we do need to decrement it. It seems easier to delay the removal. Fixed.

lvaSwiftErrorArg = varDscInfo->varNum;

// Instead, all usages of the SwiftError* parameter will be redirected to this pseudolocal.
lvaSwiftErrorLocal = lvaGrabTempWithImplicitUse(false DEBUGARG("SwiftError pseudolocal"));

@jakobbotschjakobbotschApr 4, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you verify that something somewhere marks this local as either do-not-enreg or address exposed? I don't immediately see where we do that in the JIT when something has implicit uses. Basically, I don't understand for example why liveness doesn't get rid of stores to this local because it cannot see any uses of it. Can you share a jitdump of one of the tests just so I can understand why that doesn't happen?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure thing. I was able to remove the call to lvaSetVarAddrExposed here because fgMarkAddressExposedLocals seems to already do this in LocalAddressVisitor::EscapeAddress for each store to the pseudolocal before running liveness. Here's a JitDump for one of the callbacks (see line 1201, etc.).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we are getting "lucky" there. The JIT is spilling the address of the local to a temp (not really very optimal, we ought to improve this). It only does that because of the constructor call on the RHS of the assignment, and as a consequence we end up address exposing it. If you changed the code slightly, for example to

varx=newSwiftError(...);GC.KeepAlive(null);// avoid Roslyn optimizing it back to the pattern*error=x;

then I would expect that you will hit the issue I describe where nothing marks it as address exposed and we end up removing the store.

@amanasifkhalidamanasifkhalidApr 4, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I tried this, and you're correct that we no longer mark it as address exposed, but we don't end up removing the store -- I think this is because we mark all GT_LCL_ADDR nodes pointing to the pseudolocal as having side effects (perhaps that's what is forcing the JIT to spill the address to a temp?). I'm fine with going back to explicitly marking the pseudolocal as address exposed upon creation. My hope is that is short-lived anyway, once I open a PR for GT_SWIFT_ERROR_RET.

I added an updated dump to the gist with the above code pattern used in ConditionallySetErrorTo21 (though only in the true branch of the if statement).

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I'm fine with going back to explicitly marking the pseudolocal as address exposed upon creation

This, along with removing the side effects on the GT_LCL_ADDR nodes of the pseudolocal, improves codegen. For example, before:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06,T01] ( 2, 2 ) long -> rdi "impAppendStmt"
; V07 tmp4 [V07 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
; V09 tmp6 [V09 ] ( 2, 1 ) long -> [rbp-0x10] do-not-enreg[X] addr-exposed "field V03.<Value>k__BackingField (fldOffset=0x0)" P-DEP
;* V10 tmp7 [V10,T03] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
lea rdi, [rbp-0x10]
xor eax, eax
mov qword ptr [rdi], rax
;; size=9 bbWeight=0.50 PerfScore 0.88
G_M3386_IG05: ;; offset=0x003F
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x004C
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 83, prolog size 27, PerfScore 16.58, instruction count 26, allocated bytes for code 83 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

After:

; Assembly listing for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX - Unix
; FullOpts code
; optimized code
; rbp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 2 single block inlinees; 0 inlinees without PGO data
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) long -> zero-ref single-def
; V01 arg1 [V01,T00] ( 3, 3 ) int -> rbx single-def
;* V02 loc0 [V02 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op <System.Runtime.InteropServices.Swift.SwiftError>
; V03 tmp0 [V03 ] ( 3, 2 ) struct ( 8) [rbp-0x10] do-not-enreg[XS] must-init addr-exposed "SwiftError pseudolocal" <System.Runtime.InteropServices.Swift.SwiftError>
;# V04 OutArgs [V04 ] ( 1, 1 ) struct ( 0) [rsp+0x00] do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;* V05 tmp2 [V05 ] ( 0, 0 ) struct ( 8) zero-ref ld-addr-op "NewObj constructor temp" <System.Runtime.InteropServices.Swift.SwiftError>
; V06 tmp3 [V06 ] ( 3, 3 ) struct (16) [rbp-0x20] do-not-enreg[XS] must-init addr-exposed "Reverse Pinvoke FrameVar"
;* V07 tmp4 [V07,T01] ( 0, 0 ) long -> zero-ref single-def "field V02.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;* V08 tmp5 [V08,T02] ( 0, 0 ) long -> zero-ref single-def "field V05.<Value>k__BackingField (fldOffset=0x0)" P-INDEP
;
; Lcl frame size = 24
G_M3386_IG01: ;; offset=0x0000
push rbp
push rbx
sub rsp, 24
lea rbp, [rsp+0x20]
vxorps xmm8, xmm8, xmm8
vmovdqa xmmword ptr [rbp-0x20], xmm8
xor eax, eax
mov qword ptr [rbp-0x10], rax
mov ebx, edi
;; size=29 bbWeight=1 PerfScore 6.58
G_M3386_IG02: ;; offset=0x001D
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER
test ebx, ebx
je SHORT G_M3386_IG04
;; size=13 bbWeight=1 PerfScore 2.75
G_M3386_IG03: ;; offset=0x002A
xor rdi, rdi
mov qword ptr [rbp-0x10], 21
jmp SHORT G_M3386_IG05
;; size=12 bbWeight=0.50 PerfScore 1.62
G_M3386_IG04: ;; offset=0x0036
xor edi, edi
mov qword ptr [rbp-0x10], rdi
;; size=6 bbWeight=0.50 PerfScore 0.62
G_M3386_IG05: ;; offset=0x003C
lea rdi, [rbp-0x20]
call CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT
mov r12, qword ptr [rbp-0x10]
;; size=13 bbWeight=1 PerfScore 2.50
G_M3386_IG06: ;; offset=0x0049
add rsp, 24
pop rbx
pop rbp
ret ;; size=7 bbWeight=1 PerfScore 2.25
; Total bytes of code 80, prolog size 27, PerfScore 16.33, instruction count 25, allocated bytes for code 80 (MethodHash=763af2c5) for method ErrorHandlingTests:ConditionallySetErrorTo21(ulong,int) (FullOpts)
; ============================================================

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah, that makes sense. Yeah, the side effect flags on the GT_LCL_ADDR shouldn't be necessary,

My hope is that is short-lived anyway, once I open a PR for GT_SWIFT_ERROR_RET.

Do you plan to do the work? To be honest looking at the codegen there it really doesn't feel all that bad compared to all the other things we end up doing for the reverse pinvokes anyway (like the helper calls), so I'd be perfectly fine with just leaving it like this.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I have some of the implementation saved locally, so I'm interested in trying it out; I think it can be done without polluting the JIT with too many weird edge cases.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
// By adding a well-known "sentinel" argument that uses the error register,
// the JIT will emit code for clearing the error register before the call,
// and will mark the error register as busy so it isn't used to hold the function call's address.
CallArg* const swiftErrorArg = call->gtArgs.GetArgByIndex(swiftErrorIndex);

@jakobbotschjakobbotschApr 4, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The loop above removes/inserts arguments, so this part still has to happen before the loop above.

@jakobbotschjakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM! Thanks for addressing all my feedback.

@amanasifkhalid

Copy link
Copy Markdown
ContributorAuthor

LGTM! Thanks for addressing all my feedback.

Thanks for all the reviews!

@amanasifkhalid

amanasifkhalid commented Apr 4, 2024

Copy link
Copy Markdown
ContributorAuthor

SPMI isn't finished running yet, but TP diffs look pretty small. I'm gonna merge to unblock #100344.

@amanasifkhalid
amanasifkhalid merged commit 16492b9 into dotnet:mainApr 4, 2024
@amanasifkhalid
amanasifkhalid deleted the swift-error-reg branch April 4, 2024 19:04
amanasifkhalid added a commit that referenced this pull request Apr 12, 2024
…return (#100692)
Follow-up to #100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
matouskozak pushed a commit to matouskozak/runtime that referenced this pull request Apr 30, 2024
…return (dotnet#100692)
Follow-up to dotnet#100429. If a method has a `SwiftError*` out parameter, a new phase -- `fgAddSwiftErrorReturns` -- converts all `GT_RETURN` nodes into `GT_SWIFT_ERROR_RET` nodes; this new node type is a binop that takes the error value as its first operand, and the normal return value (if there is one) as its second operand. The error value is loaded into the Swift error register upon returning.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 5, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@amanasifkhalid@kotlarmilos@jakobbotsch