-
Notifications
You must be signed in to change notification settings - Fork 14.3k
[clang][CodeGen] Fix MSVC ABI for classes with a deleted copy assignment operator #90547
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
[clang][CodeGen] Fix MSVC ABI for classes with a deleted copy assignment operator #90547
Conversation
@llvm/pr-subscribers-clang @llvm/pr-subscribers-clang-codegen Author: Max Winkler (MaxEW707) Changeshttps://godbolt.org/z/verKj4cnj for reference. For global functions and static methods the MSVC ABI returns structs/classes with a reference type non static data member indirectly. I tested locally against MSVC 1939 and MSVC 1929. The only reference I could find on MSDN to this ABI constraint is this quote, "and no non-static data members of reference type", here. Pointer fields do not exhibit this behaviour. Full diff: https://github.com/llvm/llvm-project/pull/90547.diff 3 Files Affected:
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 4aedfafcb26aea..8f228bb4d296e0 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -63,6 +63,9 @@ ABI Changes in This Version
MSVC uses a different mangling for these objects, compatibility is not affected.
(#GH85423).
+- Fixed Microsoft calling convention when returning classes that have a reference type
+ as a field. Such a class should be returned indirectly.
+
AST Dumping Potentially Breaking Changes
----------------------------------------
diff --git a/clang/lib/CodeGen/MicrosoftCXXABI.cpp b/clang/lib/CodeGen/MicrosoftCXXABI.cpp
index d38a26940a3cb6..b0798550ba9775 100644
--- a/clang/lib/CodeGen/MicrosoftCXXABI.cpp
+++ b/clang/lib/CodeGen/MicrosoftCXXABI.cpp
@@ -1103,8 +1103,27 @@ bool MicrosoftCXXABI::hasMostDerivedReturn(GlobalDecl GD) const {
return isDeletingDtor(GD);
}
+static bool fieldIsTrivialForMSVC(const FieldDecl *Field,
+ const ASTContext &Context) {
+ if (Field->getType()->isReferenceType())
+ return false;
+
+ const RecordType *RT =
+ Context.getBaseElementType(Field->getType())->getAs<RecordType>();
+ if (!RT)
+ return true;
+
+ CXXRecordDecl *RD = cast<CXXRecordDecl>(RT->getDecl());
+
+ for (const FieldDecl *RDField : RD->fields())
+ if (!fieldIsTrivialForMSVC(RDField, Context))
+ return false;
+
+ return true;
+}
+
static bool isTrivialForMSVC(const CXXRecordDecl *RD, QualType Ty,
- CodeGenModule &CGM) {
+ CodeGenModule &CGM, const ASTContext &Context) {
// On AArch64, HVAs that can be passed in registers can also be returned
// in registers. (Note this is using the MSVC definition of an HVA; see
// isPermittedToBeHomogeneousAggregate().)
@@ -1122,7 +1141,8 @@ static bool isTrivialForMSVC(const CXXRecordDecl *RD, QualType Ty,
// No base classes
// No virtual functions
// Additionally, we need to ensure that there is a trivial copy assignment
- // operator, a trivial destructor and no user-provided constructors.
+ // operator, a trivial destructor, no user-provided constructors and no
+ // non static data members of reference type.
if (RD->hasProtectedFields() || RD->hasPrivateFields())
return false;
if (RD->getNumBases() > 0)
@@ -1136,6 +1156,9 @@ static bool isTrivialForMSVC(const CXXRecordDecl *RD, QualType Ty,
return false;
if (RD->hasNonTrivialDestructor())
return false;
+ for (const FieldDecl *Field : RD->fields())
+ if (!fieldIsTrivialForMSVC(Field, Context))
+ return false;
return true;
}
@@ -1144,11 +1167,13 @@ bool MicrosoftCXXABI::classifyReturnType(CGFunctionInfo &FI) const {
if (!RD)
return false;
- bool isTrivialForABI = RD->canPassInRegisters() &&
- isTrivialForMSVC(RD, FI.getReturnType(), CGM);
-
// MSVC always returns structs indirectly from C++ instance methods.
- bool isIndirectReturn = !isTrivialForABI || FI.isInstanceMethod();
+ bool isIndirectReturn = FI.isInstanceMethod();
+ if (!isIndirectReturn) {
+ isIndirectReturn =
+ !(RD->canPassInRegisters() &&
+ isTrivialForMSVC(RD, FI.getReturnType(), CGM, getContext()));
+ }
if (isIndirectReturn) {
CharUnits Align = CGM.getContext().getTypeAlignInChars(FI.getReturnType());
diff --git a/clang/test/CodeGen/x64-microsoft-arguments.cpp b/clang/test/CodeGen/x64-microsoft-arguments.cpp
new file mode 100644
index 00000000000000..9e89b59924ae49
--- /dev/null
+++ b/clang/test/CodeGen/x64-microsoft-arguments.cpp
@@ -0,0 +1,64 @@
+// RUN: %clang_cc1 -triple x86_64-windows-msvc -ffreestanding -emit-llvm -O0 \
+// RUN: -x c++ -o - %s | FileCheck %s
+
+int global_i = 0;
+
+// Pass and return object with a reference type (pass directly, return indirectly).
+// CHECK: define dso_local void @"?f1@@YA?AUS1@@XZ"(ptr dead_on_unwind noalias writable sret(%struct.S1) align 8 {{.*}})
+// CHECK: call void @"?func1@@YA?AUS1@@U1@@Z"(ptr dead_on_unwind writable sret(%struct.S1) align 8 {{.*}}, i64 {{.*}})
+struct S1 {
+ int& r;
+};
+
+S1 func1(S1 x);
+S1 f1() {
+ S1 x{ global_i };
+ return func1(x);
+}
+
+// Pass and return object with a reference type within an inner struct (pass directly, return indirectly).
+// CHECK: define dso_local void @"?f2@@YA?AUS2@@XZ"(ptr dead_on_unwind noalias writable sret(%struct.S2) align 8 {{.*}})
+// CHECK: call void @"?func2@@YA?AUS2@@U1@@Z"(ptr dead_on_unwind writable sret(%struct.S2) align 8 {{.*}}, i64 {{.*}})
+struct Inner {
+ int& r;
+};
+
+struct S2 {
+ Inner i;
+};
+
+S2 func2(S2 x);
+S2 f2() {
+ S2 x{ { global_i } };
+ return func2(x);
+}
+
+// Pass and return object with a reference type (pass directly, return indirectly).
+// CHECK: define dso_local void @"?f3@@YA?AUS3@@XZ"(ptr dead_on_unwind noalias writable sret(%struct.S3) align 8 {{.*}})
+// CHECK: call void @"?func3@@YA?AUS3@@U1@@Z"(ptr dead_on_unwind writable sret(%struct.S3) align 8 {{.*}}, i64 {{.*}})
+struct S3 {
+ const int& r;
+};
+
+S3 func3(S3 x);
+S3 f3() {
+ S3 x{ global_i };
+ return func3(x);
+}
+
+// Pass and return object with a reference type within an inner struct (pass directly, return indirectly).
+// CHECK: define dso_local void @"?f4@@YA?AUS4@@XZ"(ptr dead_on_unwind noalias writable sret(%struct.S4) align 8 {{.*}})
+// CHECK: call void @"?func4@@YA?AUS4@@U1@@Z"(ptr dead_on_unwind writable sret(%struct.S4) align 8 {{.*}}, i64 {{.*}})
+struct InnerConst {
+ const int& r;
+};
+
+struct S4 {
+ InnerConst i;
+};
+
+S4 func4(S4 x);
+S4 f4() {
+ S4 x{ { global_i } };
+ return func4(x);
+}
|
// MSVC always returns structs indirectly from C++ instance methods. | ||
bool isIndirectReturn = !isTrivialForABI || FI.isInstanceMethod(); | ||
bool isIndirectReturn = FI.isInstanceMethod(); |
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 intentionally moved this around to try to reduce calling isTrivialForMSVC
for methods and potentially now hitting the recursive field loop I added above.
Since methods always return indirectly we can avoid all these extra checks up front.
Let me know if you prefer this being a separate PR.
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'd like to keep ABI changes as simple as possible; please separate.
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.
Maybe we should just be testing if the copy-assignment operator is deleted? For reference, MSVC also returns the following indirectly:
struct Test
{
int x;
Test& operator=(const Test&) = delete;
};
Test foo();
Test test(Test x)
{
return x;
}
// MSVC always returns structs indirectly from C++ instance methods. | ||
bool isIndirectReturn = !isTrivialForABI || FI.isInstanceMethod(); | ||
bool isIndirectReturn = FI.isInstanceMethod(); |
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'd like to keep ABI changes as simple as possible; please separate.
Argh, I didn't even consider trying an explicitly deleted copy assignment operator. I'll give this a try. |
cafb799
to
f351044
Compare
@@ -1131,13 +1132,18 @@ static bool isTrivialForMSVC(const CXXRecordDecl *RD, QualType Ty, | |||
return false; | |||
if (RD->hasNonTrivialCopyAssignment()) | |||
return false; | |||
if (RD->needsImplicitCopyAssignment() && !RD->hasSimpleCopyAssignment()) |
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 didn't find a defaultedCopyAssignmentIsDeleted()
method on CXXRecordDecl
however hasSimpleCopyAssignment()
does what we want here when combined with needsImplicitCopyAssignment()
.
Let me know if there is a better check to capture implicitly declared but deleted copy assignment operators on the record.
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.
If I'm understanding correctly, this is basically split into two parts:
- If we implicitly declared the copy-assignment, just query isDeleted() on it directly.
- If we didn't implicitly declare the copy-assignment, check whether it would have been deleted. (We can't use this in cases where the copy-assignment was declared because the information isn't stored in the CXXRecordDecl.)
This is kind of complicated, but I can't think of a simpler way to do it. That said, it probably deserves a comment to explain the logic.
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.
Correct. The two main cases we need to cover are the following.
I'll make a comment to explain the logic.
struct Test
{
Test& operator=(const Test&) = delete;
int x;
};
In this case DefaultedCopyAssignmentIsDeleted
is false
on the CXXRecordDecl
. So we need to query isDeleted
on the copy assignment operator CXXMethodDecl
.
struct Test
{
int& x;
};
In this case we have no CXXMethodDecl
for the copy assignment operator so we need to check if DefaultedCopyAssignmentIsDeleted
is true
on the CXXRecordDecl
.
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.
LGTM
https://godbolt.org/z/verKj4cnj for reference.
https://godbolt.org/z/z3W9v7o4n for reference.
For global functions and static methods the MSVC ABI returns structs/classes with a reference type non static data member indirectly.
From local testing this is recursively applied to any inner structs/classes.
From local testing this ABI holds true for all currently support architectures including ARM64EC.
I tested locally against MSVC 1939 and MSVC 1929.
The only reference I could find on MSDN to this ABI constraint is this quote, "and no non-static data members of reference type", here.
Pointer fields do not exhibit this behaviour.