Skip to content

Commit 38333d5

Browse files
ostannardtstellar
authored andcommitted
[ARM] Empty structs are 1-byte for C++ ABI (llvm#124762)
For C++ (but not C), empty structs should be passed to functions as if they are a 1 byte object with 1 byte alignment. This is defined in Arm's CPPABI32: https://github.com/ARM-software/abi-aa/blob/main/cppabi32/cppabi32.rst For the purposes of parameter passing in AAPCS32, a parameter whose type is an empty class shall be treated as if its type were an aggregate with a single member of type unsigned byte. The AArch64 equivalent of this has an exception for structs containing an array of size zero, I've kept that logic for ARM. I've not found a reason for this exception, but I've checked that GCC does have the same behaviour for ARM as it does for AArch64. The AArch64 version has an Apple ABI with different rules, which ignores empty structs in both C and C++. This is documented at https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms. The ARM equivalent of that appears to be AAPCS16_VFP, used for WatchOS, but I can't find any documentation for that ABI, so I'm not sure what rules it should follow. For now I've left it following the AArch64 Apple rules.
1 parent 04d5513 commit 38333d5

File tree

4 files changed

+178
-5
lines changed

4 files changed

+178
-5
lines changed

clang/docs/ReleaseNotes.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,11 @@ Arm and AArch64 Support
11991199
- Runtime detection of depended-on Function Multi Versioning features has been added
12001200
in accordance with the Arm C Language Extensions (ACLE).
12011201

1202+
- The ARM calling convention for empty structs in C++ mode was changed to pass
1203+
them as if they have a size of 1 byte, matching the AAPCS32 specification and
1204+
GCC's implementation. The previous behaviour of ignoring the argument can be
1205+
restored using the -fclang-abi-compat=19 (or earlier) option.
1206+
12021207
Android Support
12031208
^^^^^^^^^^^^^^^
12041209

clang/include/clang/Basic/LangOptions.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,8 @@ class LangOptionsBase {
246246
/// construction vtable because it hasn't added 'type' as a substitution.
247247
/// - Skip mangling enclosing class templates of member-like friend
248248
/// function templates.
249+
/// - Ignore empty struct arguments in C++ mode for ARM, instead of
250+
/// passing them as if they had a size of 1 byte.
249251
Ver19,
250252

251253
/// Conform to the underlying platform's C and C++ ABIs as closely

clang/lib/CodeGen/Targets/ARM.cpp

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class ARMABIInfo : public ABIInfo {
7171
unsigned functionCallConv) const;
7272
ABIArgInfo classifyHomogeneousAggregate(QualType Ty, const Type *Base,
7373
uint64_t Members) const;
74+
bool shouldIgnoreEmptyArg(QualType Ty) const;
7475
ABIArgInfo coerceIllegalVector(QualType Ty) const;
7576
bool isIllegalVectorType(QualType Ty) const;
7677
bool containsAnyFP16Vectors(QualType Ty) const;
@@ -328,6 +329,31 @@ ABIArgInfo ARMABIInfo::classifyHomogeneousAggregate(QualType Ty,
328329
return ABIArgInfo::getDirect(nullptr, 0, nullptr, false, Align);
329330
}
330331

332+
bool ARMABIInfo::shouldIgnoreEmptyArg(QualType Ty) const {
333+
uint64_t Size = getContext().getTypeSize(Ty);
334+
assert((isEmptyRecord(getContext(), Ty, true) || Size == 0) &&
335+
"Arg is not empty");
336+
337+
// Empty records are ignored in C mode, and in C++ on WatchOS.
338+
if (!getContext().getLangOpts().CPlusPlus ||
339+
getABIKind() == ARMABIKind::AAPCS16_VFP)
340+
return true;
341+
342+
// In C++ mode, arguments which have sizeof() == 0 are ignored. This is not a
343+
// situation which is defined by any C++ standard or ABI, but this matches
344+
// GCC's de facto ABI.
345+
if (Size == 0)
346+
return true;
347+
348+
// Clang 19.0 and earlier always ignored empty struct arguments in C++ mode.
349+
if (getContext().getLangOpts().getClangABICompat() <=
350+
LangOptions::ClangABI::Ver19)
351+
return true;
352+
353+
// Otherwise, they are passed as if they have a size of 1 byte.
354+
return false;
355+
}
356+
331357
ABIArgInfo ARMABIInfo::classifyArgumentType(QualType Ty, bool isVariadic,
332358
unsigned functionCallConv) const {
333359
// 6.1.2.1 The following argument types are VFP CPRCs:
@@ -366,9 +392,15 @@ ABIArgInfo ARMABIInfo::classifyArgumentType(QualType Ty, bool isVariadic,
366392
return getNaturalAlignIndirect(Ty, RAA == CGCXXABI::RAA_DirectInMemory);
367393
}
368394

369-
// Ignore empty records.
370-
if (isEmptyRecord(getContext(), Ty, true))
371-
return ABIArgInfo::getIgnore();
395+
// Empty records are either ignored completely or passed as if they were a
396+
// 1-byte object, depending on the ABI and language standard.
397+
if (isEmptyRecord(getContext(), Ty, true) ||
398+
getContext().getTypeSize(Ty) == 0) {
399+
if (shouldIgnoreEmptyArg(Ty))
400+
return ABIArgInfo::getIgnore();
401+
else
402+
return ABIArgInfo::getDirect(llvm::Type::getInt8Ty(getVMContext()));
403+
}
372404

373405
if (IsAAPCS_VFP) {
374406
// Homogeneous Aggregates need to be expanded when we can fit the aggregate
@@ -588,7 +620,8 @@ ABIArgInfo ARMABIInfo::classifyReturnType(QualType RetTy, bool isVariadic,
588620

589621
// Otherwise this is an AAPCS variant.
590622

591-
if (isEmptyRecord(getContext(), RetTy, true))
623+
if (isEmptyRecord(getContext(), RetTy, true) ||
624+
getContext().getTypeSize(RetTy) == 0)
592625
return ABIArgInfo::getIgnore();
593626

594627
// Check for homogeneous aggregates with AAPCS-VFP.
@@ -752,7 +785,9 @@ RValue ARMABIInfo::EmitVAArg(CodeGenFunction &CGF, Address VAListAddr,
752785
CharUnits SlotSize = CharUnits::fromQuantity(4);
753786

754787
// Empty records are ignored for parameter passing purposes.
755-
if (isEmptyRecord(getContext(), Ty, true))
788+
uint64_t Size = getContext().getTypeSize(Ty);
789+
bool IsEmpty = isEmptyRecord(getContext(), Ty, true);
790+
if ((IsEmpty || Size == 0) && shouldIgnoreEmptyArg(Ty))
756791
return Slot.asRValue();
757792

758793
CharUnits TySize = getContext().getTypeSizeInChars(Ty);

clang/test/CodeGen/arm-empty-args.cpp

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// RUN: %clang_cc1 -triple armv7a-linux-gnueabi -emit-llvm -o - -x c %s | FileCheck %s --check-prefixes=CHECK,C
2+
// RUN: %clang_cc1 -triple armv7a-linux-gnueabi -emit-llvm -o - %s | FileCheck %s --check-prefixes=CHECK,CXX
3+
// RUN: %clang_cc1 -triple armv7a-linux-gnueabi -emit-llvm -o - %s -fclang-abi-compat=19 | FileCheck %s --check-prefixes=CHECK,CXXCLANG19
4+
// RUN: %clang_cc1 -triple thumbv7k-apple-watchos2.0 -target-abi aapcs16 -emit-llvm -o - %s | FileCheck %s --check-prefixes=CHECK,WATCHOS
5+
6+
// Empty structs are ignored for PCS purposes on WatchOS and in C mode
7+
// elsewhere. In C++ mode they consume a register slot though. Functions are
8+
// slightly bigger than minimal to make confirmation against actual GCC
9+
// behaviour easier.
10+
11+
#if __cplusplus
12+
#define EXTERNC extern "C"
13+
#else
14+
#define EXTERNC
15+
#endif
16+
17+
struct Empty {};
18+
19+
// C: define{{.*}} i32 @empty_arg(i32 noundef %a)
20+
// CXX: define{{.*}} i32 @empty_arg(i8 %e.coerce, i32 noundef %a)
21+
// CXXCLANG19: define{{.*}} i32 @empty_arg(i32 noundef %a)
22+
// WATCHOS: define{{.*}} i32 @empty_arg(i32 noundef %a)
23+
EXTERNC int empty_arg(struct Empty e, int a) {
24+
return a;
25+
}
26+
27+
// C: define{{.*}} void @empty_ret()
28+
// CXX: define{{.*}} void @empty_ret()
29+
// CXXCLANG19: define{{.*}} void @empty_ret()
30+
// WATCHOS: define{{.*}} void @empty_ret()
31+
EXTERNC struct Empty empty_ret(void) {
32+
struct Empty e;
33+
return e;
34+
}
35+
36+
// However, what counts as "empty" is a baroque mess. This is super-empty, it's
37+
// ignored even in C++ mode. It also has sizeof == 0, violating C++, but that's
38+
// legacy for you:
39+
40+
struct SuperEmpty {
41+
int arr[0];
42+
};
43+
44+
// C: define{{.*}} i32 @super_empty_arg(i32 noundef %a)
45+
// CXX: define{{.*}} i32 @super_empty_arg(i32 noundef %a)
46+
// CXXCLANG19: define{{.*}} i32 @super_empty_arg(i32 noundef %a)
47+
// WATCHOS: define{{.*}} i32 @super_empty_arg(i32 noundef %a)
48+
EXTERNC int super_empty_arg(struct SuperEmpty e, int a) {
49+
return a;
50+
}
51+
52+
struct SortOfEmpty {
53+
struct SuperEmpty e;
54+
};
55+
56+
// C: define{{.*}} i32 @sort_of_empty_arg(i32 noundef %a)
57+
// CXX: define{{.*}} i32 @sort_of_empty_arg(i8 %e.coerce, i32 noundef %a)
58+
// CXXCLANG19: define{{.*}} i32 @sort_of_empty_arg(i32 noundef %a)
59+
// WATCHOS: define{{.*}} i32 @sort_of_empty_arg(i32 noundef %a)
60+
EXTERNC int sort_of_empty_arg(struct Empty e, int a) {
61+
return a;
62+
}
63+
64+
// C: define{{.*}} void @sort_of_empty_ret()
65+
// CXX: define{{.*}} void @sort_of_empty_ret()
66+
// CXXCLANG19: define{{.*}} void @sort_of_empty_ret()
67+
// WATCHOS: define{{.*}} void @sort_of_empty_ret()
68+
EXTERNC struct SortOfEmpty sort_of_empty_ret(void) {
69+
struct SortOfEmpty e;
70+
return e;
71+
}
72+
73+
#include <stdarg.h>
74+
75+
// va_arg matches the above rules, consuming an incoming argument in cases
76+
// where one would be passed, and not doing so when the argument should be
77+
// ignored.
78+
79+
EXTERNC int empty_arg_variadic(int a, ...) {
80+
// CHECK-LABEL: @empty_arg_variadic(
81+
// C: %argp.next = getelementptr inbounds i8, ptr %argp.cur, i32 4
82+
// C-NOT: {{ getelementptr }}
83+
// CXX: %argp.next = getelementptr inbounds i8, ptr %argp.cur, i32 4
84+
// CXX: %argp.next2 = getelementptr inbounds i8, ptr %argp.cur1, i32 4
85+
// CXXCLANG19: %argp.next = getelementptr inbounds i8, ptr %argp.cur, i32 4
86+
// CXXCLANG19-NOT: {{ getelementptr }}
87+
// WATCHOS: %argp.next = getelementptr inbounds i8, ptr %argp.cur, i32 4
88+
// WATCHOS-NOT: {{ getelementptr }}
89+
va_list vl;
90+
va_start(vl, a);
91+
struct Empty b = va_arg(vl, struct Empty);
92+
int c = va_arg(vl, int);
93+
va_end(vl);
94+
return c;
95+
}
96+
97+
EXTERNC int super_empty_arg_variadic(int a, ...) {
98+
// CHECK-LABEL: @super_empty_arg_variadic(
99+
// C: %argp.next = getelementptr inbounds i8, ptr %argp.cur, i32 4
100+
// C-NOT: {{ getelementptr }}
101+
// CXX: %argp.next = getelementptr inbounds i8, ptr %argp.cur, i32 4
102+
// CXX-NOT: {{ getelementptr }}
103+
// CXXCLANG19: %argp.next = getelementptr inbounds i8, ptr %argp.cur, i32 4
104+
// CXXCLANG19-NOT: {{ getelementptr }}
105+
// WATCHOS: %argp.next = getelementptr inbounds i8, ptr %argp.cur, i32 4
106+
// WATCHOS-NOT: {{ getelementptr }}
107+
va_list vl;
108+
va_start(vl, a);
109+
struct SuperEmpty b = va_arg(vl, struct SuperEmpty);
110+
int c = va_arg(vl, int);
111+
va_end(vl);
112+
return c;
113+
}
114+
115+
EXTERNC int sort_of_empty_arg_variadic(int a, ...) {
116+
// CHECK-LABEL: @sort_of_empty_arg_variadic(
117+
// C: %argp.next = getelementptr inbounds i8, ptr %argp.cur, i32 4
118+
// C-NOT: {{ getelementptr }}
119+
// CXX: %argp.next = getelementptr inbounds i8, ptr %argp.cur, i32 4
120+
// CXX-NOT: {{ getelementptr }}
121+
// CXXCLANG19: %argp.next = getelementptr inbounds i8, ptr %argp.cur, i32 4
122+
// CXXCLANG19-NOT: {{ getelementptr }}
123+
// WATCHOS: %argp.next = getelementptr inbounds i8, ptr %argp.cur, i32 4
124+
// WATCHOS-NOT: {{ getelementptr }}
125+
va_list vl;
126+
va_start(vl, a);
127+
struct SortOfEmpty b = va_arg(vl, struct SortOfEmpty);
128+
int c = va_arg(vl, int);
129+
va_end(vl);
130+
return c;
131+
}

0 commit comments

Comments
 (0)