Lesson 11.5 — Aggregates and the ABI: by value, sret, byval, and Clang-style ABI coercion¶
Techniques: passing aggregates by value in registers (scalarized or as first-class aggregates: rustc's scalar pairs, LLVM's first-class structs); returning them through a hidden
sretpointer; passing thembyval(the call sequence makes the copy) or as a pointer to a copy the caller makes; and ABI coercion, Clang's translation of a C type into the LLVM IR types that land in the registers the platform ABI prescribes (System V x86-64 psABI §3.2.3; AAPCS64) · Pebble implements: caller-made copies for aggregate parameters andsretfor aggregate results in the code generator (E5, docs/runtime-abi.md §4);extern fnis restricted to scalars so Pebble never needs coercion · Prerequisites: Ch 9 (attributes,sret,byval); calling conventions (Lesson 21.9) · Time: 4–5 hours
A function call between separately compiled code works only if both sides agree on where every argument and result lives: which register, which stack slot, who makes copies. That agreement is the platform's application binary interface (ABI). LLVM implements the register assignment part in its back end (Ch 21), but it only sees LLVM types. A C struct { int i; double d; } has no LLVM type that the back end would automatically pass "in edi and xmm0"; the front end must translate the source type into the LLVM IR signature that produces that placement. This lesson covers the four techniques and the choice Pebble made. The running examples:
struct Mixed { int i; double d; }; // 16 bytes: one INTEGER and one SSE eightbyte
struct Three { float x, y, z; }; // 12 bytes: SSE, SSE
struct Big { long a, b, c; }; // 24 bytes: MEMORY
and Pebble's fn swap(p: Pair) -> Pair with struct Pair { a: int, b: int }.
1. Problem and motivation¶
By value¶
Aggregates are values in Pebble, C, Rust and Swift: passing one copies it (spec §9.2). The cheapest way to pass a small aggregate is not to have it in memory at all: pass its fields in registers. LLVM lets a front end do this by giving the parameter a first-class aggregate type ({ i64, double }) or by splitting it into scalar parameters; rustc's own ABI passes a two-field struct as a scalar pair (box in §7). The C ABIs formalize the same idea for small structs (psABI §3.2.3 [SysV-ABI]; AAPCS64 [AAPCS64]).
sret¶
A result too big for the return registers is written by the callee into memory the caller provides; the address travels as a hidden first argument. LLVM marks that argument sret(<ty>) [LLVM-LangRef] so that the back end can apply the platform's rule (on x86-64 the callee also returns the address in rax). Pebble returns every aggregate this way (define internal void @swap(ptr noalias sret(%Pair) %result, ptr %p)), which makes the callee's stores go straight into the caller's variable.
byval¶
Passing a large aggregate by value means someone must copy it. With the byval(<ty>) attribute, LLVM's call lowering makes the copy in the outgoing argument area (on the stack), so the callee receives the address of a private copy [LLVM-LangRef]. The alternative, which AArch64's AAPCS, Rust's own ABI and Pebble use, is to copy in the caller's frame and pass a plain pointer: the IR shows the memcpy, and the optimizer can remove it when the callee provably does not write the parameter.
ABI coercion¶
Clang's ABIInfo classes classify each C parameter and result type according to the target ABI and coerce it to an LLVM type whose register assignment by the back end matches the ABI: struct Mixed becomes { i32, double } on x86-64 (in edi and xmm0) and [2 x i64] on AArch64 (in x0 and x1) [CLANG-X86ABI, CLANG-AArch64ABI]. It is the most intricate part of a C front end, and the reason Pebble restricts extern fn to scalars: then the C ABI of every extern call is unambiguous on every target (docs/runtime-abi.md §4).
2. Definitions and algorithms¶
Definition 11.5.1 (Passing modes)
For a parameter or result of type \(T\):
- direct: the value travels in registers as one or more LLVM first-class values;
- indirect (caller copy): the caller copies the value into its own frame and passes a
ptrto the copy; byval: the caller passes aptrto the value markedbyval(T), and the call sequence copies it into the outgoing argument area — the callee sees the copy's address;sret: for a result, the caller passes aptrto storage for it as a hidden first argument markedsret(T), and the function returnsvoid;- coerced: the value is passed direct as an LLVM type \(C \ne \mathrm{lower}(T)\) with the same bytes, chosen so that the back end places it where the ABI requires.
Algorithm 11.5.2 (Pebble's aggregate calling convention)
- Input: a PIR function signature and a call.
- Output: the LLVM signature and call sequence (E5).
- Precondition: the PIR module verifies; callee and caller are both Pebble functions (externs only have scalar parameters, V4).
- Postcondition: value semantics (Theorem 11.5.3).
- Invariant: an aggregate parameter's pointer points to memory that no other argument or the caller reads or writes during the call.
function LowerSignature(f):
params ← []
if f's result is an aggregate: params.add(ptr noalias sret(T)); result ← void
for each parameter of type T:
params.add(ptr if T is an aggregate else ValueType(T)) # bool → i1
return (result, params)
function LowerCall(dest = call f(a1, ..., ak)):
if f's result is an aggregate: r ← alloca T in the entry block; args.add(r)
for each argument a_j:
if a_j is an aggregate: c ← alloca in the entry block; memcpy(c, &a_j); args.add(c)
else: args.add(value of a_j)
call f(args)
if f's result is an aggregate: memcpy(&dest, r) # the destination is evaluated after the call
else if dest: store the result into dest
Theorem 11.5.3 (The Pebble convention preserves value semantics)
With Algorithm 11.5.2, (i) a callee's writes to an aggregate parameter are invisible to the caller, (ii)
the noalias on the sret pointer is correct, and (iii) the caller's destination receives exactly the
returned value, even when the destination is one of the arguments (p = swap(p)).
Proof
(i) The callee's parameter points to the fresh copy \(c\), which the caller never reads after the call;
writes to it are therefore unobservable (PIR passes arguments by value, pir-spec §10). (ii) The sret
buffer \(r\) is a fresh alloca: no argument pointer (each points to its own fresh copy \(c\)) and no memory
the callee can reach otherwise (Pebble has no globals and references are parameters only, already copied
or pointing to the caller's places, which are distinct from \(r\)) aliases it. (iii) The callee writes its
result only into \(r\); the copy into the destination happens after the call returns, so it reads the final
result even if the destination is the argument p, whose copy \(c\) was taken before the call.
Algorithm 11.5.4 (System V x86-64 classification and Clang's coercion, structs of scalars)
- Input: a C struct whose fields are scalars (
char,short,int,long, pointers,float,double) or arrays of them. - Output: the class of each eightbyte (8-byte chunk) and the LLVM type Clang passes it as, or MEMORY.
- Precondition: no unions, bit-fields,
long double, vector or packed/unaligned members (the full rules are in [SysV-ABI §3.2.3]). - Postcondition: the coerced type is assigned the registers that the psABI assigns to the struct (Theorem 11.5.5).
- Invariant: after processing a prefix of the fields, each eightbyte's class is the merge of the classes of the fields in it so far.
function Classify(S):
if size(S) > 16: return MEMORY # passed byval, returned via sret
cls[0 .. ⌈size/8⌉-1] ← NO_CLASS
for each scalar field f at offset o (arrays flattened):
k ← ⌊o / 8⌋
cls[k] ← Merge(cls[k], INTEGER if f is an integer or pointer else SSE)
return cls
function Merge(a, b): # psABI rule 4, restricted to our fields
if a = b: return a
if a = NO_CLASS: return b; if b = NO_CLASS: return a
return INTEGER # INTEGER + SSE → INTEGER
function Coerce(S, cls): # clang/lib/CodeGen/Targets/X86.cpp
for each eightbyte k with used bytes u = min(size(S) - 8k, 8):
if cls[k] = INTEGER:
f ← the field starting at offset 8k
if f is 8 bytes (long, pointer): part_k ← its type (i64 / ptr)
else if f is the only field in the eightbyte: part_k ← its type (i8 / i16 / i32)
else: part_k ← i(8u) # GetINTEGERTypeAtOffset
else: # SSE: GetSSETypeAtOffset
part_k ← double | float | <2 x float> (one double, one float, or two floats)
return part_0 if one eightbyte else { part_0, part_1 }
Theorem 11.5.5 (Coercion places the bytes where the ABI does)
For a struct covered by Algorithm 11.5.4 that is not MEMORY, passing the coerced type directly makes LLVM's x86-64 back end assign eightbyte \(k\) to the next free general-purpose register if \(\mathit{cls}[k] = \mathrm{INTEGER}\) and to the next free SSE register if SSE, holding the same bytes as the struct's eightbyte \(k\) in memory — the psABI's placement.
Proof sketch (full proof: [SysV-ABI, §3.2.3])
The back end's calling convention (X86CallingConv.td, Ch 21) assigns an integer or pointer scalar to the
next GPR and a float/double/<2 x float> to the next XMM register, and passes the elements of a
first-class struct argument as separate arguments in order. Each part of the coerced type has the class of
its eightbyte by construction (an INTEGER eightbyte gets an integer or pointer type, an SSE eightbyte a
floating-point type), so the placement follows the psABI's assignment of eightbytes to register classes.
Clang produces the value of the coerced type by storing the struct to memory and loading the coerced type
from the same address (CreateCoercedLoad), so the bytes are identical; unused bytes of a partial
eightbyte are padding in both. The psABI's rule that an argument needing more registers than remain goes
to memory as a whole is applied by Clang before coercion (it then passes byval).
The three structs, classified
Mixed: int at 0 → eightbyte 0 INTEGER; double at 8 → eightbyte 1 SSE; the int is alone in
eightbyte 0 → { i32, double }. Three: floats at 0 and 4 → eightbyte 0 SSE <2 x float>; float at 8
→ eightbyte 1 SSE float → { <2 x float>, float }. Big: 24 bytes → MEMORY.
3. Worked example¶
By value¶
rustc's Rust ABI passes Mixed as its two fields directly, reordered by the layout algorithm (double first): define { double, i32 } @mixed(double %0, i32 %1) (box in §7). No memory is involved; LLVM's first-class { double, i32 } return is split into xmm0 and eax.
sret¶
Algorithm 11.5.2 on Pebble's swap (pebblec --emit=llvm, box in §7):
| step | caller (pebble_main) |
callee (swap) |
|---|---|---|
| 1 | build the literal Pair { a: 1, b: 2 } in %_0 |
|
| 2 | memcpy(%tmp, %_0, 16) — the argument's copy |
|
| 3 | call void @swap(ptr noalias sret(%Pair) %call.result, ptr %tmp) |
|
| 4 | builds the result in its local %_1, then memcpy(%result, %_1, 16); ret void |
|
| 5 | memcpy(%q.addr, %call.result, 16) — the destination, after the call |
After sroa and instcombine, swap stores its two loads straight into %result and every temporary copy is gone.
byval¶
Clang, struct Big on x86-64: 24 bytes → MEMORY → ptr sret(%struct.Big) for the result and ptr byval(%struct.Big) for the parameter. On AArch64 (AAPCS64: composites over 16 bytes are passed by reference to a caller-made copy), the same parameter is a plain ptr … dead_on_return — Pebble's convention (box in §7).
ABI coercion¶
Algorithm 11.5.4 on the three structs, eightbyte by eightbyte (the abi-classify drill's worked-solution format):
| struct | eightbyte 0: fields → class | eightbyte 1: fields → class | x86-64 coerced type | AArch64 (AAPCS64) |
|---|---|---|---|---|
Mixed |
int@0 → INTEGER, alone |
double@8 → SSE |
{ i32, double } |
[2 x i64] |
Three |
float@0, float@4 → SSE |
float@8 → SSE |
{ <2 x float>, float } |
[3 x float] (an HFA) |
Big |
— | — | MEMORY: sret / byval |
sret / pointer to a copy |
Try it
./course drill abi-classify --seed 12 --difficulty medium --solution classifies a random struct and gives
Clang's coerced type (the oracle matches clang 23.1.2 on 600 random structs, tools/course/tests/test_ch11.py).
4. Invariants and correctness¶
By value¶
Direct passing is correct as long as caller and callee agree on the split, which they do for functions the compiler generates both sides of (rustc's Rust ABI, Pebble's internal functions). Across a language boundary the split must be the platform's — which is what coercion is for.
sret¶
Theorem 11.5.3 (ii) — noalias on sret — is the property that lets LLVM write the result directly into the caller's buffer and optimize the callee's stores. It fails if a caller passes as sret the address of an object the callee can also reach (e.g. p = f(&p) in C++ with copy elision); C++ front ends must then use a temporary, as Algorithm 11.5.2 always does.
byval¶
Correct by LangRef's definition: the callee gets a copy [LLVM-LangRef]. The pitfall is that the copy is invisible in the IR: optimizing the caller cannot remove it, while a caller-made copy (memcpy in the IR) can be removed by memcpyopt when the callee does not write it.
byval is not \"pass by value\" in general
byval is a statement about stack argument passing. A front end that marks a struct byval on AArch64,
or for a struct the x86-64 psABI passes in registers, produces an ABI mismatch with C code compiled by Clang
or GCC: the callee looks for the bytes in registers and finds garbage. Only the ABI classification decides.
ABI coercion¶
Theorem 11.5.5; the preconditions exclude the hard cases (unions merge classes across members; long double is X87; __m256 is SSEUP; packed fields force MEMORY). The drill's oracle is restricted the same way and checked against Clang.
5. Complexity¶
\(f\) = number of scalar fields (after flattening arrays), \(s\) = size in bytes.
| Technique | Compile time | Run-time cost per call | Justification |
|---|---|---|---|
| By value (direct) | \(O(f)\) | fields in registers; spills if registers run out | one IR value per field |
sret |
\(O(1)\) | one pointer argument; callee writes memory | the buffer is the caller's |
byval |
\(O(1)\) in IR; the back end emits the copy | an \(s\)-byte copy on every call | LangRef semantics: the copy is always made |
| Caller copy (Pebble) | \(O(1)\) | an \(s\)-byte memcpy unless optimized away |
visible in IR, removable |
| ABI coercion | \(O(f)\) per type | as direct | Algorithm 11.5.4 inspects each field once; at most 2 eightbytes |
Pathological family. A Pebble function taking an array [int; N] by value copies \(8N\) bytes per call at -O0: sum(xs: [int; 1000]) in the e2e test agg-large-array.pbl copies 8000 bytes on each call — which is why idiomatic Pebble passes &[int; 1000]. For coercion, a struct { float, int, float, int } mixes classes in both eightbytes: both become INTEGER and the floats travel in integer registers, costing moves between register files on each side.
6. Variants and refinements¶
By value¶
- Scalar replacement across calls: LLVM's
argpromotion(Ch 20) turns a pointer parameter of an internal function into its loaded values — Pebble's internal linkage makes that legal. - Multiple return values (first-class aggregates
{ i64, double }as return types, used by rustc and by Swift's calling convention) avoidsretfor results up to a few registers.
sret¶
- Return value optimization / copy elision (C++): the caller passes the final destination as the
sretpointer, eliminating step 5 of §3 when no aliasing is possible. dead_on_unwind,writable,initializesattributes (visible in the Clang box of §7) tell LLVM what the callee does with the buffer, enabling more store elimination.
byval¶
- Caller copy + pointer (AAPCS64, rustc, Pebble) instead of
byval: the copy is IR-visible and optimizable;dead_on_returnmarks that the callee may clobber it. byref(<ty>): a pointer to an argument in memory without the copy semantics, for ABIs (AMDGPU kernels) that pass by reference [LLVM-LangRef].
ABI coercion¶
- rustc's own classifier (
compiler/rustc_target/src/callconv/x86_64.rs,classify) implements the same psABI rules but coerces an INTEGER eightbyte toi64rather than Clang'si32for a loneint(box in §7) — both are correct, since the upper bytes are padding [RUSTC-X86ABI]. - GCC classifies in
classify_argument(gcc/config/i386/i386.cc) directly on RTL modes [GCC-I386]; AAPCS64 adds homogeneous floating-point aggregates (HFAs) of up to four floats or doubles in SIMD registers.
7. In real compilers¶
By value¶
rustc: layouts with BackendRepr::ScalarPair are passed as two scalars in its own ABI (compiler/rustc_target/src/callconv/); LLVM accepts first-class aggregate arguments and returns and splits them in SelectionDAGBuilder. Pebble passes only scalars directly.
rustc: its own ABI vs the C ABI for the same struct
Reproduce (rustc 1.94.1):
cat > abi.rs <<'EOF'
pub struct Mixed { pub i: i32, pub d: f64 }
pub struct Big { pub a: i64, pub b: i64, pub c: i64 }
#[no_mangle] pub fn mixed(mut m: Mixed) -> Mixed { m.i += 1; m }
#[no_mangle] pub fn big(mut b: Big) -> Big { b.a += b.c; b }
#[repr(C)] pub struct CMixed { pub i: i32, pub d: f64 }
#[no_mangle] pub extern "C" fn cmixed(mut m: CMixed) -> CMixed { m.i += 1; m }
EOF
rustc --crate-type=lib -C opt-level=1 -C overflow-checks=off --emit=llvm-ir abi.rs -o abi-rs.ll
grep '^define' abi-rs.ll
Output:
define void @big(ptr dead_on_unwind noalias noundef writable writeonly sret([24 x i8]) align 8 captures(none) dereferenceable(24) initializes((0, 24)) %_0, ptr dead_on_return noalias noundef align 8 captures(none) dereferenceable(24) %b) unnamed_addr #0 {
define { i64, double } @cmixed({ i64, double } %0) unnamed_addr #1 {
define { double, i32 } @mixed(double noundef %0, i32 noundef %1) unnamed_addr #1 {
What to notice: in its own ABI, rustc passes Mixed by value as two scalars (reordered: the
layout put f64 first) and returns a first-class { double, i32 }; Big goes by pointer to a caller copy
(dead_on_return, not byval) with an sret result — Pebble's scheme. With extern "C" the same struct
is coerced to the psABI's { i64, double }.
sret¶
Clang decides sret in its ABIInfo (classifyReturnType) and emits the hidden argument in CodeGenFunction::EmitCall (clang/lib/CodeGen/CGCall.cpp, ClangToLLVMArgMapping) [CLANG-Call]. Pebble: ModuleLowering::declareFunction and FunctionLowering::emitCall in solutions/pebble/lib/CodeGen/PIRToLLVM.cpp; tests/ch11/lit/llvm-abi.pbl checks the shape.
Pebble's sret and caller copies, before and after SROA
Reproduce (pebblec from this repository with -DPEBBLE_USE_SOLUTION=all, LLVM 23.1.2):
cat > pair.pbl <<'EOF'
struct Pair { a: int, b: int }
fn swap(p: Pair) -> Pair { return Pair { a: p.b, b: p.a }; }
fn main() -> int {
let q = swap(Pair { a: 1, b: 2 });
return q.a;
}
EOF
pebblec --emit=llvm pair.pbl -o - | grep -E '^define|call void @swap|memcpy.*(tmp|q.addr|result)'
pebblec -O2 --passes='function(sroa,instcombine)' --emit=llvm pair.pbl -o - | sed -n '/define internal void @swap/,/^}/p'
Output:
define internal void @swap(ptr noalias sret(%Pair) %result, ptr %p) {
call void @llvm.memcpy.p0.p0.i64(ptr align 8 %result, ptr align 8 %_1, i64 16, i1 false)
define i64 @pebble_main() {
call void @llvm.memcpy.p0.p0.i64(ptr align 8 %tmp, ptr align 8 %_0, i64 16, i1 false)
call void @swap(ptr noalias sret(%Pair) %call.result, ptr %tmp) #1
call void @llvm.memcpy.p0.p0.i64(ptr align 8 %q.addr, ptr align 8 %call.result, i64 16, i1 false)
define internal void @swap(ptr noalias sret(%Pair) %result, ptr %p) {
entry:
br label %bb0
bb0: ; preds = %entry
%0 = getelementptr inbounds nuw i8, ptr %p, i64 8
%1 = load i64, ptr %0, align 8
%2 = load i64, ptr %p, align 8
store i64 %1, ptr %result, align 8
%_1.sroa.2.0.result.sroa_idx = getelementptr inbounds nuw i8, ptr %result, i64 8
store i64 %2, ptr %_1.sroa.2.0.result.sroa_idx, align 8
ret void
}
What to notice: the five steps of §3 — the argument copy %tmp, the sret buffer
%call.result, the copy to q after the call. After SROA the callee's local %_1 is gone and the fields
are stored straight into %result (Theorem 11.5.3 (ii) is what makes this rewrite legal).
byval¶
Clang emits byval for x86-64 MEMORY-class arguments from X86_64ABIInfo::classifyArgumentType (clang/lib/CodeGen/Targets/X86.cpp) and caller copies for AArch64 from AArch64ABIInfo::classifyArgumentType [CLANG-X86ABI, CLANG-AArch64ABI]; LLVM's call lowering makes the byval copy (SelectionDAGBuilder::LowerCallTo).
One C file, two targets: coercion, HFAs, byval vs a caller copy
Reproduce (clang 23.1.2):
cat > abi.c <<'EOF'
struct Mixed { int i; double d; };
struct Three { float x, y, z; };
struct Big { long a, b, c; };
struct Mixed mixed(struct Mixed m) { m.i++; return m; }
struct Three three(struct Three t) { t.x = t.z; return t; }
struct Big big(struct Big b) { b.a += b.c; return b; }
EOF
for t in x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do
echo "== $t"; clang-23 --target=$t -O1 -S -emit-llvm abi.c -o - | grep '^define'
done
Output:
== x86_64-unknown-linux-gnu
define dso_local { i32, double } @mixed(i32 %0, double %1) local_unnamed_addr #0 {
define dso_local { <2 x float>, float } @three(<2 x float> %0, float %1) local_unnamed_addr #2 {
define dso_local void @big(ptr dead_on_unwind noalias nofree writable writeonly sret(%struct.Big) align 8 captures(none) initializes((0, 24)) %0, ptr nofree noundef byval(%struct.Big) align 8 captures(none) %1) local_unnamed_addr #3 {
== aarch64-unknown-linux-gnu
define dso_local [2 x i64] @mixed([2 x i64] %0) local_unnamed_addr #0 {
define dso_local %struct.Three @three([3 x float] alignstack(8) %0) local_unnamed_addr #0 {
define dso_local void @big(ptr dead_on_unwind noalias nofree writable writeonly sret(%struct.Big) align 8 captures(none) initializes((0, 24)) %0, ptr nofree noundef align 8 captures(none) dead_on_return %1) local_unnamed_addr #2 {
What to notice: x86-64 passes Big byval (the call sequence copies it to the stack) and returns it
through sret; AArch64 returns it through sret too but passes a plain pointer to a caller copy
(dead_on_return). Mixed and Three are coerced exactly as the table of §3 predicts: { i32, double }
and { <2 x float>, float } on x86-64; [2 x i64] (two X registers) and the HFA [3 x float] on AArch64.
ABI coercion¶
Clang: X86_64ABIInfo::classify, GetINTEGERTypeAtOffset and GetSSETypeAtOffset in clang/lib/CodeGen/Targets/X86.cpp [CLANG-X86ABI]; rustc: classify in compiler/rustc_target/src/callconv/x86_64.rs [RUSTC-X86ABI]; GCC: classify_argument in gcc/config/i386/i386.cc [GCC-I386]. The drill oracle is classify_sysv in tools/course/lib/lowering.py.
The coerced registers in machine code
Reproduce (clang 23.1.2):
cat > abi.c <<'EOF'
struct Mixed { int i; double d; };
struct Mixed mixed(struct Mixed m) { m.i++; return m; }
EOF
clang-23 --target=x86_64-unknown-linux-gnu -O1 -S abi.c -o - | sed -n '/^mixed:/,/retq/p' | grep -v '^\s*\.'
Output:
What to notice: m.i arrives in edi (the INTEGER eightbyte coerced to i32) and is returned
incremented in eax; m.d arrives in xmm0 and is returned unchanged in xmm0, so no instruction
touches it — Theorem 11.5.5 on real registers.
Find where Clang does it. In clang/lib/CodeGen/Targets/X86.cpp, which function chooses i32 rather than i64 for an INTEGER eightbyte that holds only an int? (Quiz clang-where-integer-coerce.)
8. Comparison¶
| Technique | Power / precision | Speed (asymptotic · practical) | Output / error quality | Implementation effort | Typical use |
|---|---|---|---|---|---|
| By value (direct) | Small aggregates; both sides agree on the split | fastest: registers, no memory | Readable IR (fields as values) | Low within one language | rustc's Rust ABI, Swift, LLVM first-class aggregates |
sret |
Any result type | one pointer; callee writes memory directly | Explicit hidden argument | Low | Large results in every C ABI; Pebble's aggregate results |
byval |
Any argument, stack-passed copies | an unavoidable copy per call | Copy hidden in the call lowering | Low in IR; ABI knowledge needed | x86-64 MEMORY-class C arguments |
| Caller copy + pointer | Any argument | a memcpy, removable by the optimizer |
Copy visible in IR | Low | AAPCS64 large composites, rustc, Pebble |
| ABI coercion | Exactly the platform C ABI | as direct | Types look odd ({ i32, double }) but match C |
High (per-target classifiers) | Clang, rustc and Swift extern "C", any FFI |
Choose direct passing when you control both sides (internal functions); sret for large results always; a caller copy when you want the optimizer to see and remove copies (Pebble, AArch64); byval only where the C ABI demands it; and coercion whenever you call or are called by C — or, like Pebble, restrict the foreign-function interface to scalars and avoid it.
9. Assessment¶
- Quiz (
./course quiz 11):abi-direct-rust,abi-direct-regs(tagabi-direct);sret-noalias,sret-steps(tagsret);byval-vs-copy,byval-aarch64(tagbyval);abi-classify-mixed,clang-where-integer-coerce(tagabi-coercion). - Drill:
./course drill abi-classify(sizes, eightbyte classes, Clang's coerced type or MEMORY); Chapter 21'scalling-conventiondrill assigns the registers afterwards. - Flashcards: tags
abi-direct,sret,byval,abi-coercion. - Exercises: E5 (the aggregate calling convention of Algorithm 11.5.2).
References¶
See the chapter references.