Behind the Curtain - How Swift Classes Are Stored in Memory
Background
Coder Orangu has always been curious: “Why can’t extensions support stored properties in Swift?”
1
2
3
extension LoginViewModel {
var isFirstLogin: Bool
}
A practical scenario often arises when he creates multiple extensions for a class across different files. He wishes each extension could have its own independent stored property, allowing every extension to maintain its state, offering a clearer separation of concerns.
For instance, an extension like LoginViewModel+FirstLogin could focus solely on handling the first-launch flow, without being entangled with the rest of the class.
Coder Orangu realised that to uncover the reasoning behind this limitation, he first needed to understand how a Swift class is laid out in memory. This curiosity set the stage for exploring Swift’s storage model and why extensions behave this way.
Memory Layout Basics
Coder Orangu first needs to investigate when and how instance memory is defined. In Swift, a class’s memory layout is determined at compile time. This means the compiler knows the structure of the class—what properties it has and their sizes—before the program runs.
However, the actual storage for those instance variables is only allocated at runtime, when an object is created.
Let’s see how the compiler decides the layout for a simple LoginViewModel
:
1
2
3
4
class LoginViewModel {
var isFirstLogin: Bool
var username: String
}
When you declare a Swift class, the compiler generates several low-level data structures that are used by the Swift runtime. One of the most important is the instance memory layout, which contains both metadata and the stored properties of the class. The compiler will generate an instance layout similar to this:
Components of the Memory Layout
ISA Pointer:
A pointer to the class’s metadata. Metadata is another compiler-generated low-level structure that contains information used for method dispatch, type information, and reflection.
(We’ll discuss metadata in detail in the next section.)
Reference Count:
Every class instance in Swift is managed by Automatic Reference Counting (ARC). This field stores the retain count for the object at runtime, allowing the system to know when to deallocate memory.
Stored Property:
This section holds the storage required for the class’s instance variables. In our case, that’s isFirstLogin
and username
.
The compiler determines their offsets and alignment based on type requirements. The Swift compiler may also insert padding to maintain alignment rules, ensuring efficient memory access. For example, Bool
here is only 1 byte, but it is padded when followed by a String
to align correctly on 8-byte boundaries.
Conceptually, you can think of the compiler laying out these properties as if it were generating a C-style struct. This isn’t the precise layout Swift uses internally. Still, this mental model helps you understand why offsets increase the way they do, how padding fills the gaps, and why alignment always pushes the next field to the proper boundary. A simplified example might look like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct LoginViewModelInstance {
// Offset: 0x00
void* isa; // 8 bytes - pointer to class metadata
// Offset: 0x08
int refCount; // 4 bytes
int padding1; // 4 bytes - padding
// Offset: 0x10
bool isFirstLogin; // 1 byte
char padding2[7]; // 7 bytes padding
// Offset: 0x18
void* username; // 8 bytes
}
With this foundation, we’re ready to move to the next layer: metadata—where Swift keeps track of what the object is and its behaviour, not just how it’s laid out in memory.
It should already feel clearer now why extensions cannot add stored properties—the object’s memory layout is fixed at compile time.
Class Metadata Layout
The other important data structure generated by the compiler is the class metadata, also known as the type descriptor for a class. The isa
pointer in an object’s memory points directly to this metadata.
A class metadata primarily contains the following components:
Major Components of Class Metadata
Type Descriptor:
It provides the runtime with static information about the type, including the class name, property details, generic context, and protocol conformance records. This is essential for features like introspection, dynamic casting, and generic specialisation.
Superclass Reference:
A pointer linking to the metadata of the parent class, which allows Swift to build the inheritance chain. If the class is final or has no base class, this pointer is nil.
Method Table (Virtual Method Table):
A key part of class metadata that ensures the correct method runs at runtime, especially with inheritance and overriding. It works like a list of pointers, each pointing to a specific method implementation, so the right function is called when you invoke a method on an object. A complete blog can be written on how this method call mechanism works — known as dynamic dispatch — I’ll cover this in a separate blog.
Property Descriptors:
A reference to another piece of metadata that describes the class’s stored and computed properties. It tells the runtime what data exists inside each object and how it should be accessed or managed.
Protocol Conformances:
When a type (class, struct, or enum) conforms to a protocol, the compiler creates a conformance record that links the type to the protocol and provides the runtime with the needed implementations. Class metadata holds references to these conformance records, which describe the mapping between the type and the protocol.
Nature of Extensions
A pure class extension in Swift does not alter the instance memory layout or the class metadata. When we say pure extension, it means an extension that does not implement protocol conformances or include @objc methods. For this blog, we will focus solely on pure extensions and their behaviour at compile time.
In such cases, extensions are a compile-time feature: they allow you to add new methods and computed properties into an existing class without touching its stored properties or memory layout.
Here’s what actually happens under the hood:
The class definition is compiled first, and its memory layout, metadata, and vtable are finalised.
The extension is compiled separately. The compiler generates additional function symbols for its methods and computed properties, and places them in the type’s namespace.
These functions do not exist as a separate block at runtime, nor do they alter the class metadata.
Methods in pure extensions are resolved through static dispatch, since they are not part of the class vtable.
Compiler Action Example
Take this pure extension:
1
2
3
4
5
6
// Pure extension
extension LoginViewModel {
func isFirstLogin() -> Bool {
//Implementation
}
}
//Usage:
1
2
let vm = LoginViewModel(username: "")
let result = vm.isFirstLogin()
Behind the scenes, the compiler might lower it into something like this:
1
2
3
func LoginViewModel_isFirstLogin(_ self: LoginViewModel) -> Bool {
//Implementation
}
//And at the call site:
1
2
let vm = LoginViewModel(username: "")
let result = LoginViewModel_isFirstLogin(vm)
Takeaway
The method call looks like an instance method to us, but the compiler rewrites it into a static function call under the hood. That’s why pure extensions don’t touch memory layout or class metadata—they’re just syntactic sugar for namespaced free functions.
If Extensions Had Memory
Swift deliberately disallows stored properties inside extensions. If it were allowed, a few deep changes would have been necessary under the hood:
ABI & Binary Compatibility Issues
The Application Binary Interface (ABI) is a “contract” between compiled Swift code and the runtime/OS. It fixes the memory layout of a type and how class metadata, vtables, and dynamic dispatch work. Now this contract will be breached if a library defines a class and later someone extends it with a stored property; all binaries depending on the original class would suddenly have mismatched assumptions about its memory layout.
Memory Layout Explosion
If extensions could add stored properties, every new extension would force the compiler to reshape the object’s memory layout. That would invalidate pointer offsets, reference counting, and even previously compiled machine code. The only conceivable workaround would be a hidden indirection — such as a side table or a dedicated “extension storage” reference. But that design adds runtime lookups, memory overhead, and subtle performance traps, contradicting Swift’s philosophy of predictable, zero-cost abstractions.
Dynamic Linking Conflicts
Imagine two different libraries, both adding stored properties to UIView. Which one “owns” the memory slot? How would the runtime patch up these additions without crashing or corrupting memory? Swift avoids this chaos by making extensions purely compile-time sugar, not runtime memory changes.
Preserving Swift’s Core Predictability
By restricting extensions to methods, computed properties, and protocol conformances, Swift ensures they are safe, lightweight, and predictable — without hidden runtime costs.
Coder Orangu now understands why Swift never allowed stored properties in extensions. What seemed like a small convenience would, in reality, unravel memory safety, ABI stability, and add unwanted runtime cost.
Final Thought
Swift extensions give us flexibility without compromising type integrity. By forbidding stored properties, Swift keeps memory models consistent and extensions efficient.
However, sometimes when Coder Orangu really needs to associate extra state with a type, he can use the Objective-C runtime’s objc_setAssociatedObject mechanism. It isn’t as clean or type-safe as true stored properties, but it provides a practical workaround. And that curiosity—about a missing feature—ultimately reveals how carefully Swift balances convenience with safety.
Comments powered by Disqus.