-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Implement Swift's physical lowering pass in the managed type system #98831
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
61b895e
Implement Swift's physical lowering pass for NativeAOT
jkoritzinsky 252ba7c
Add path to ILVerification
jkoritzinsky f732a17
Update API shape to use the future JITInterface type directly now tha…
jkoritzinsky 8e41153
Remove unused using
jkoritzinsky b9da82d
Fix explicit layout validation of byref fields.
jkoritzinsky 4d214a5
Move SwiftPhysicalLowering class to the common section
jkoritzinsky 9cee0a9
Remove usage of range
jkoritzinsky 8e9ac6f
Fix explicit layout test failures
jkoritzinsky acf230f
Remove blank line
jkoritzinsky a47940c
Fix double-counting offsets
jkoritzinsky e8bc0ce
Add more unit tests and fix bugs based on Jakob's repro
jkoritzinsky 8fa6e6c
Add some more tests with explicitly-corrected sizes (since Swift does…
jkoritzinsky cecbefe
Fixed alignment issues and added tests
jkoritzinsky File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
212 changes: 212 additions & 0 deletions
212
src/coreclr/tools/Common/JitInterface/SwiftPhysicalLowering.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,212 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Collections.Immutable; | ||
| using System.Diagnostics; | ||
| using System.Linq; | ||
| using System.Runtime.InteropServices; | ||
| using Internal.TypeSystem; | ||
|
|
||
| namespace Internal.JitInterface | ||
| { | ||
| public static class SwiftPhysicalLowering | ||
| { | ||
| private enum LoweredType | ||
| { | ||
| Empty, | ||
| Opaque, | ||
| Int64, | ||
| Float, | ||
| Double, | ||
| } | ||
|
|
||
| private sealed class LoweringVisitor(int pointerSize) : FieldLayoutIntervalCalculator<LoweredType>(pointerSize) | ||
| { | ||
| protected override LoweredType EmptyIntervalData => LoweredType.Empty; | ||
|
|
||
| protected override bool IntervalsHaveCompatibleTags(LoweredType existingTag, LoweredType nextTag) | ||
| { | ||
| // Adjacent ranges mapped to opaque or empty can be combined. | ||
| return existingTag is LoweredType.Opaque or LoweredType.Empty && nextTag is LoweredType.Opaque or LoweredType.Empty; | ||
| } | ||
|
|
||
| protected override FieldLayoutInterval CombineIntervals(FieldLayoutInterval firstInterval, FieldLayoutInterval nextInterval) | ||
| { | ||
| FieldLayoutInterval resultInterval = firstInterval; | ||
| resultInterval.EndSentinel = nextInterval.EndSentinel; | ||
| if (resultInterval.Tag != nextInterval.Tag) | ||
| { | ||
| resultInterval.Tag = LoweredType.Opaque; | ||
| } | ||
| return resultInterval; | ||
| } | ||
|
|
||
| protected override LoweredType GetIntervalDataForType(int offset, TypeDesc fieldType) | ||
| { | ||
| // Comments here are from the Swift Calling Convention document: | ||
| // In all of these examples, the maximum voluntary integer size is 4 | ||
| // (`i32`) unless otherwise specified. | ||
|
|
||
| // If any range is mapped as a non-empty, non-opaque type, but its start | ||
| // offset is not a multiple of its natural alignment, remap it as opaque. | ||
| // For these purposes, the natural alignment of an integer type is the | ||
| // minimum of its size and the maximum voluntary integer size; the | ||
| // natural alignment of any other type is its C ABI type. | ||
| // | ||
| // TODO: What about 8-byte integers aligned at 4-byte boundaries? | ||
| // Can this even be done in Swift? | ||
| if (fieldType is MetadataType mdType && offset % mdType.InstanceFieldAlignment.AsInt != 0) | ||
| { | ||
| return LoweredType.Opaque; | ||
| } | ||
|
|
||
| if (fieldType.Category is TypeFlags.Single) | ||
| { | ||
| return LoweredType.Float; | ||
| } | ||
|
|
||
| if (fieldType.Category is TypeFlags.Double) | ||
| { | ||
| return LoweredType.Double; | ||
| } | ||
|
|
||
| if (fieldType.Category is TypeFlags.UInt64 or TypeFlags.Int64) | ||
| { | ||
| return LoweredType.Int64; | ||
| } | ||
|
|
||
| Debug.Assert(PointerSize == 8, "Swift interop is only supported on 64-bit platforms."); | ||
|
|
||
| if (fieldType.Category is TypeFlags.IntPtr or TypeFlags.UIntPtr or TypeFlags.Pointer or TypeFlags.FunctionPointer) | ||
| { | ||
| return LoweredType.Int64; | ||
| } | ||
|
|
||
| Debug.Assert(fieldType.IsPrimitiveNumeric); | ||
|
|
||
| // If any range is mapped as an integer type that is not larger than the | ||
| // maximum voluntary size, remap it as opaque. Combine adjacent opaque | ||
| // ranges. | ||
| return LoweredType.Opaque; | ||
| } | ||
|
|
||
| protected override bool NeedsRecursiveLayout(int offset, TypeDesc fieldType) => fieldType.IsValueType && !fieldType.IsPrimitiveNumeric; | ||
|
|
||
| public List<CorInfoType> GetLoweredTypeSequence() | ||
| { | ||
| // We need to track the sequence size to ensure we break up the opaque ranges | ||
| // into correctly-sized integers that do not require padding. | ||
| int loweredSequenceSize = 0; | ||
| List<CorInfoType> loweredTypes = new(); | ||
| foreach (var interval in Intervals) | ||
| { | ||
| // Empty intervals at this point don't need to be represented in the lowered type sequence. | ||
| // We want to skip over them. | ||
| if (interval.Tag == LoweredType.Empty) | ||
| continue; | ||
|
|
||
| if (interval.Tag == LoweredType.Float) | ||
| { | ||
| loweredTypes.Add(CorInfoType.CORINFO_TYPE_FLOAT); | ||
| loweredSequenceSize = loweredSequenceSize.AlignUp(4); | ||
| loweredSequenceSize += 4; | ||
| } | ||
|
|
||
| if (interval.Tag == LoweredType.Double) | ||
| { | ||
| loweredTypes.Add(CorInfoType.CORINFO_TYPE_DOUBLE); | ||
| loweredSequenceSize = loweredSequenceSize.AlignUp(8); | ||
| loweredSequenceSize += 8; | ||
| } | ||
|
|
||
| if (interval.Tag == LoweredType.Int64) | ||
| { | ||
| loweredTypes.Add(CorInfoType.CORINFO_TYPE_LONG); | ||
| loweredSequenceSize = loweredSequenceSize.AlignUp(8); | ||
| loweredSequenceSize += 8; | ||
| } | ||
|
|
||
| if (interval.Tag == LoweredType.Opaque) | ||
| { | ||
| // We need to split the opaque ranges into integer parameters. | ||
| // As part of this splitting, we must ensure that we don't introduce alignment padding. | ||
| // This lowering algorithm should produce a lowered type sequence that would have the same padding for | ||
| // a naturally-aligned struct with the lowered fields as the original type has. | ||
| // This algorithm intends to split the opaque range into the least number of lowered elements that covers the entire range. | ||
| // The lowered range is allowed to extend past the end of the opaque range (including past the end of the struct), | ||
| // but not into the next non-empty interval. | ||
| // However, due to the properties of the lowering (the only non-8 byte elements of the lowering are 4-byte floats), | ||
| // we'll never encounter a scneario where we need would need to account for a correctly-aligned | ||
| // opaque range of > 4 bytes that we must not pad to 8 bytes. | ||
|
|
||
|
|
||
| // As long as we need to fill more than 4 bytes and the sequence is currently 8-byte aligned, we'll split into 8-byte integers. | ||
| // If we have more than 2 bytes but less than 4 and the sequence is 4-byte aligned, we'll use a 4-byte integer to represent the rest of the parameters. | ||
| // If we have 2 bytes and the sequence is 2-byte aligned, we'll use a 2-byte integer to represent the rest of the parameters. | ||
| // If we have 1 byte, we'll use a 1-byte integer to represent the rest of the parameters. | ||
| int remainingIntervalSize = interval.Size; | ||
| while (remainingIntervalSize > 4 && loweredSequenceSize == loweredSequenceSize.AlignUp(8)) | ||
| { | ||
| loweredTypes.Add(CorInfoType.CORINFO_TYPE_LONG); | ||
| loweredSequenceSize += 8; | ||
| remainingIntervalSize -= 8; | ||
| } | ||
|
|
||
| if (remainingIntervalSize > 2 && loweredSequenceSize == loweredSequenceSize.AlignUp(4)) | ||
| { | ||
| loweredTypes.Add(CorInfoType.CORINFO_TYPE_INT); | ||
| loweredSequenceSize += 4; | ||
| remainingIntervalSize -= 4; | ||
| } | ||
|
|
||
| if (remainingIntervalSize > 1 && loweredSequenceSize == loweredSequenceSize.AlignUp(2)) | ||
| { | ||
| loweredTypes.Add(CorInfoType.CORINFO_TYPE_SHORT); | ||
| loweredSequenceSize += 2; | ||
| remainingIntervalSize -= 2; | ||
| } | ||
|
|
||
| if (remainingIntervalSize == 1) | ||
| { | ||
| loweredSequenceSize += 1; | ||
| loweredTypes.Add(CorInfoType.CORINFO_TYPE_BYTE); | ||
| } | ||
| } | ||
| } | ||
| return loweredTypes; | ||
| } | ||
| } | ||
|
|
||
| public static CORINFO_SWIFT_LOWERING LowerTypeForSwiftSignature(TypeDesc type) | ||
| { | ||
| if (!type.IsValueType || type is DefType { ContainsGCPointers: true }) | ||
| { | ||
| Debug.Fail("Non-unmanaged types should not be passed directly to a Swift function."); | ||
| return new() { byReference = true }; | ||
| } | ||
|
|
||
| LoweringVisitor visitor = new(type.Context.Target.PointerSize); | ||
| visitor.AddFields(type, addTrailingEmptyInterval: false); | ||
|
|
||
| List<CorInfoType> loweredTypes = visitor.GetLoweredTypeSequence(); | ||
|
|
||
| // If a type has a primitive sequence with more than 4 elements, Swift passes it by reference. | ||
| if (loweredTypes.Count > 4) | ||
| { | ||
| return new() { byReference = true }; | ||
| } | ||
|
|
||
| CORINFO_SWIFT_LOWERING lowering = new() | ||
| { | ||
| byReference = false, | ||
| numLoweredElements = loweredTypes.Count | ||
| }; | ||
|
|
||
| CollectionsMarshal.AsSpan(loweredTypes).CopyTo(lowering.LoweredElements); | ||
|
|
||
| return lowering; | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make sense to implement an early check in the
GetLoweredTypeSequenceand avoid lowering the full sequence ifloweredTypes.Count > 4?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's fine. The number of frozen structs that would surpass the max size and be passed by-reference is likely very small (as if the type is being passed by-ref, it's not getting most of the benefits that being frozen allows).