Understanding Swift Object Creation, Method Dispatch, and Memory Layout – A C‑Style Pseudocode Exploration
This article explains how Swift objects are created and destroyed, how method calls are implemented compared with Objective‑C, the memory layout of Swift classes, the role of virtual tables, the impact of compiler optimizations, and the feasibility of runtime method replacement, illustrated with extensive C‑style pseudocode examples.
Swift’s runtime capabilities are intentionally limited compared with Objective‑C, making method calls static and determined at compile‑time, which improves speed and reduces binary size but restricts dynamic features such as method swizzling.
Object Method Invocation in Objective‑C
In Objective‑C, every method call goes through objc_msgSend , which uses the object’s class structure and the selector to locate the implementation at runtime, supporting dynamic dispatch and method replacement.
Swift Class Object Creation and Destruction
Swift defines two kinds of classes: those derived from NSObject and those derived from the hidden base class SwiftObject . Objects are allocated on the heap, and the compiler generates an allocating initializer like Module.Class.__allocating_init and a deallocating deinit function.
// Assume a class CA is defined.
class CA {
init(_ a:Int){}
}
// Compiler‑generated allocation and initialization function
CA * XXX.CA.__allocating_init(swift_class classCA, int a) {
CA *obj = swift_allocObject(classCA); // allocate memory
obj->init(a); // call initializer
}
// Compiler‑generated deallocation function
XXX.CA.__deallocating_deinit(CA *obj) {
obj->deinit(); // call destructor
swift_deallocClassInstance(obj); // free memory
}Reference counting is used for lifetime management: swift_retain increments the count, swift_release decrements it, and when it reaches zero the deallocating deinit is invoked.
// Swift source
let obj1:CA = CA(20);
let obj2 = obj1
// Corresponding C pseudocode
CA *obj1 = XXX.CA.__allocating_init(classCA, 20);
CA *obj2 = obj1;
swift_retain(obj1);
swift_release(obj1);
swift_release(obj2);Swift Class Method Dispatch
Swift method dispatch resembles C++ virtual function calls. The compiler builds a virtual table (vtable) inside the class’s metadata; each regular method’s address is stored there. Calls are compiled to load the function pointer from the vtable and invoke it indirectly.
Overridden Objective‑C Methods in Swift Subclasses
When a Swift class subclasses an Objective‑C class and overrides a method, the call still goes through objc_msgSend . New methods defined only in Swift use the vtable.
// Swift source
class MyUIView:UIView {
open func foo(){}
override func layoutSubviews() {}
}
func main(){
let obj = MyUIView()
obj.layoutSubviews() // OC‑style call
obj.foo() // Swift vtable call
}
// C pseudocode (simplified)
struct method_t { SEL name; IMP imp; };
struct swift_class { struct method_t methods[1]; IMP vtable[1]; };
void main(){
MyUIView *obj = MyUIView.__allocating_init(classMyUIView);
obj->isa = &classMyUIView
objc_msgSend(obj, @selector(layoutSubviews)); // overridden OC method
obj->isa->vtable[0](); // Swift method foo via vtable
}Methods Defined in Extensions
Extension methods are compiled to direct calls; their symbols are not placed in the class’s vtable, so they cannot be overridden at runtime.
// Swift source
class CA { open func foo(){} }
extension CA { open func extfoo(){} }
func main(){ let obj = CA(); obj.foo(); obj.extfoo(); }
// C pseudocode
struct swift_class { IMP vtable[1]; };
struct CA { struct swift_class *isa; };
void main(){
CA *obj = CA.__allocating_init(classCA);
obj->isa = &classCA
obj->isa->vtable[0](); // foo via vtable
extfoo(); // direct call, no vtable entry
}Regular Swift Methods
Regular methods are stored in the vtable; each call loads the function pointer from the table using a compile‑time index, similar to C++ virtual calls.
// Swift source (simplified)
class CA { open func foo1(_ a:Int){} open func foo2(){} }
class CB:CA { open func foo3(){} override open func foo1(_ a:Int){} }
func testfunc(_ obj:CA){ obj.foo1(10) }
func main(){
let a = CA(); a.foo1(10); a.foo2(); a.extfoo();
let b = CB(); b.foo1(10); b.foo3();
testfunc(a); testfunc(b);
}
// C pseudocode (vtable layout)
struct swift_class { IMP vtable[4]; };
struct CA { struct swift_class *isa; };
struct CB { struct swift_class *isa; };
void main(){
CA *objA = CA.__allocating_init(classCA);
objA->isa->vtable[0](10); // foo1
objA->isa->vtable[2](); // foo2
_$s3XXX2CAC6extfooyyF(); // extfoo direct call
CB *objB = CB.__allocating_init(classCB);
objB->isa->vtable[0](10); // overridden foo1
objB->isa->vtable[3](); // foo3
_$s3XXX2CAC6extfooyyF(); // extfoo
testfunc(objA);
testfunc(objB);
}Member Variable Access
Both Objective‑C and Swift objects start with an isa pointer. Swift generates getter and setter functions for each stored property and hard‑codes their offsets, eliminating the need for a separate ivar offset table.
// Swift source
class CA { var a:Int = 10; var b:Int = 20 }
func main(){ let obj = CA(); obj.b = obj.a }
// C pseudocode
struct CA { struct swift_class *isa; long reserve; int a; int b; };
int getA(){ struct CA *obj = x20; return obj->a; }
void setA(int a){ struct CA *obj = x20; obj->a = a; }
int getB(){ struct CA *obj = x20; return obj->b; }
void setB(int b){ struct CA *obj = x20; obj->b = b; }
void main(){
CA *obj = CA.__allocating_init(classCA);
obj->isa = &classCA
obj->reserve = 2;
obj->a = 10;
obj->b = 20;
asm("mov x20, obj");
obj->isa->vtable[2](obj->isa->vtable[0]()); // b = a via getters/setters
}Struct Methods
Swift structs have no isa and therefore do not support polymorphic dispatch; all method calls are compiled as direct calls.
Class Methods and Global Functions
Class methods and global functions are compiled as ordinary C functions; they are not passed an object pointer and are invoked via hard‑coded addresses.
Calling Swift from Objective‑C
Objective‑C can call Swift methods only if they are marked @objc . The compiler generates a trampoline function that converts the Objective‑C calling convention to Swift’s (moving the object from x0 to x20 ) and then calls the real Swift implementation.
// Swift source
class MyUIView:UIView { @objc open func foo(){} }
// Objective‑C source
#import "Project-Swift.h"
void main(){ MyUIView *obj = [MyUIView new]; [obj foo]; }
// C pseudocode (trampoline)
void foo(){ /* Swift implementation */ }
void trampoline_foo(id self, SEL _cmd){ asm("mov x20, x0"); self->isa->vtable[0](); }
// Runtime metadata
struct method_t { SEL name; IMP imp; };
struct swift_class { struct method_t methods[1]; IMP vtable[1]; };
void main(){
MyUIView *obj = MyUIView.__allocating_init(classMyUIView);
obj->isa = &classMyUIView
asm("mov x20, obj");
obj->isa->vtable[0](); // Swift call via vtable
// OC side
objc_msgSend(obj, @selector(foo)); // goes through trampoline_foo
}Feasibility of Runtime Method Swizzling in Swift
Swift’s metadata resides in a writable segment, suggesting that vtable entries could be patched at runtime, but three major obstacles exist:
Swift hides memory‑manipulation APIs, making direct metadata modification difficult.
The ABI passes the object in x20 instead of x0 , so a replacement function must follow Swift’s calling convention.
Swift no longer supports inline assembly, preventing easy trampoline creation.
Using an extension method as a replacement can solve the ABI issue, but locating the extension’s address and preserving the original implementation remain challenging.
Effect of Compiler Optimizations
When optimization is enabled, many short methods are inlined, and the compiler may replace virtual‑table dispatch with type‑checked direct calls, reducing binary size and improving speed but further limiting runtime method replacement.
// Optimized Swift (inlined)
class CA { open func foo(_ a:Int, _ b:Int) -> Int { return a + b } }
// C pseudocode after optimization
struct swift_class { /* no vtable */ };
struct CA { struct swift_class *isa; };
void main(){
CA *obj = CA.__allocating_init(classCA);
int a = 10 + 20; // inlined
int b = a + 40; // inlined
}For polymorphic calls, the compiler may generate a series of if (obj->isa == &Class) checks instead of virtual dispatch, which further prevents vtable patching.
// Polymorphic call without vtable
void testfunc(CA *obj){
asm("mov x20, obj");
if (obj->isa == &classCA) fooForA();
else if (obj->isa == &classCB) fooForB();
}In summary, Swift’s static dispatch model, aggressive inlining, and ABI differences make runtime method swapping far more difficult than in Objective‑C, especially when compiler optimizations are enabled.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.