The root of a class hierarchy must remain stable, at pain of
invalidating the metaclass hierarchy. Note that a Swift class without an
explicit base class is implicitly rooted in the SwiftObject
Objective-C class.
Structs and tuples currently share the same layout algorithm, noted as the
“Universal” layout algorithm in the compiler implementation. The algorithm
is as follows:
- Start with a size of 0 and an alignment of 1.
- Iterate through the fields, in element order for tuples, or in
var
declaration order for structs. For each field:
- Update size by rounding up to the alignment of the field, that is,
increasing it to the least value greater or equal to size and evenly
divisible by the alignment of the field.
- Assign the offset of the field to the current value of size.
- Update size by adding the size of the field.
- Update alignment to the max of alignment and the
alignment of the field.
- The final size and alignment are the size and alignment of the
aggregate. The stride of the type is the final size rounded up to
alignment.
Note that this differs from C or LLVM’s normal layout rules in that size
and stride are distinct; whereas C layout requires that an embedded struct’s
size be padded out to its alignment and that nothing be laid out there,
Swift layout allows an outer struct to lay out fields in the inner struct’s
tail padding, alignment permitting. Unlike C, zero-sized structs and tuples
are also allowed, and take up no storage in enclosing aggregates. The Swift
compiler emits LLVM packed struct types with manual padding to get the
necessary control over the binary layout. Some examples:
// LLVM <{ i64, i8 }>
struct S {
var x: Int
var y: UInt8
}
// LLVM <{ i8, [7 x i8], <{ i64, i8 }>, i8 }>
struct S2 {
var x: UInt8
var s: S
var y: UInt8
}
// LLVM <{}>
struct Empty {}
// LLVM <{ i64, i64 }>
struct ContainsEmpty {
var x: Int
var y: Empty
var z: Int
}
Swift relies on the following assumptions about the Objective-C runtime,
which are therefore now part of the Objective-C ABI:
- 32-bit platforms never have tagged pointers. ObjC pointer types are
either nil or an object pointer.
- On x86-64, a tagged pointer either sets the lowest bit of the pointer
or the highest bit of the pointer. Therefore, both of these bits are
zero if and only if the value is not a tagged pointer.
- On ARM64, a tagged pointer always sets the highest bit of the pointer.
- 32-bit platforms never perform any isa masking.
object_getClass
is always equivalent to *(Class*)object
.
- 64-bit platforms perform isa masking only if the runtime exports a
symbol
uintptr_t objc_debug_isa_class_mask;
. If this symbol
is exported, object_getClass
on a non-tagged pointer is always
equivalent to (Class)(objc_debug_isa_class_mask & *(uintptr_t*)object)
.
- The superclass field of a class object is always stored immediately
after the isa field. Its value is either nil or a pointer to the
class object for the superclass; it never has other bits set.
The following assumptions are part of the Swift ABI:
- Swift class pointers are never tagged pointers.
TODO
In laying out enum types, the ABI attempts to avoid requiring additional
storage to store the tag for the enum case. The ABI chooses one of five
strategies based on the layout of the enum:
In the degenerate case of an enum with no cases, the enum is an empty type.
enum Empty {} // => empty type
In the degenerate case of an enum with a single case, there is no
discriminator needed, and the enum type has the exact same layout as its
case’s data type, or is empty if the case has no data type.
enum EmptyCase { case X } // => empty type
enum DataCase { case Y(Int, Double) } // => LLVM <{ i64, double }>
Read on →