My wife thought I looked so good after taking this photo, she asked me to wear the tape all day
The Swift Runtime, a.k.a libswiftCore, is a C++ library that runs alongside all Swift programs. The runtime library is dynamically linked to your app binary at launch and facilitates core language features such as:
That is the standard elevator pitch, but there’s just one problem: What the h*ck do I mean by “runs alongside” and “facilitates core language features”!?
Here there be dragons (well, wyverns). Many a brave explorer has been lost, or lost their mind, trying to comprehend the labyrinthine code paths of its forbidden texts (more commonly referred to as C++).
To keep us tethered to reality, we will start with some familiar Swift code you’ll know and love. I’ll demonstrate a core Swift language feature in the most basic way possible.
We’ll convert this into Swift Intermediate Language, and follow how these instructions compile all the way down to calls into the Swift Runtime ABI. This will ultimately show exactly how the runtime program runs alongside your own code.
Finally, we will look inside libswiftCore — the runtime library code itself — to understand how core language features are implemented.
Enjoying Jacob’s Tech Tavern? Share it! Refer a friend as a free subscriber to unlock a complimentary month of full subscription.
The Swift runtime is in charge of allocating blocks of memory, cleaning it up, and everything in-between.
It captures memory from the heap, tracks the reference counts of your objects, creates side tables to store weak references, and kills your app if you mess up with unowned references.
There are few really important files relating to handling memory in the Swift runtime code.
This isn’t exhaustive — SwiftObject.mm, RefCount.h, and WeakReference.h are also vital pieces of the runtime memory puzzle. But we can get an understanding of how the runtime system works just by analysing a single component in detail: memory allocation.
Let’s narrow our weave and start with 2 lines of code.
Your Swift Code
We’ll begin with a dead-simple Swift program.
class Memory { } let mem = Memory()
We’re defining a class, then instantiating an instance.
If the going gets tough down below, just remember these 2 lines of Swift.
Swift Intermediate Language
The first few steps of the compiler convert our code into Swift Intermediate Language, a special syntax which allows the compiler to perform optimisations on the code. We can convert our main.swift file into optimised SIL using this command:
swiftc -emit-sil -O main.swift > sil.txt
This generates a hefty 87 lines of SIL, but we can find what we’re looking for in the explicitly-generated main() function:
alloc_ref is the SIL instruction for allocating the memory required to create an instance of our Memory class.
The Swift Compiler
When pulling instructions down from the Swift compiler’s front-end into the guts of the LLVM toolchain, the instructions are visited to prepare them for conversion to LLVM Intermediate Representation.
This is simply a design pattern for traversing each SIL instruction. They are individually compiled down into the representation required for the next stage of the compiler. The functions that implement this visitor pattern follow the format:
visit[SIL-instruction-name]Inst
This pattern is applied in each function call.
Understanding these patterns and naming conventions is paramount to learning to traverse the Swift source code yourself.
In IRGenSIL.cpp, the IRGenSILFunction::visitAllocRefInst function translates the alloc_ref instruction into LLVM IR.
void IRGenSILFunction::visitAllocRefInst(swift::AllocRefInst *i) { int StackAllocSize = -1; if (i->canAllocOnStack()) { estimateStackSize(); // Is there enough space for stack allocation? StackAllocSize = IGM.IRGen.Opts.StackPromotionSizeLimit - EstimatedStackSize; } SmallVector<std::pair<SILType, llvm::Value *>, 4> TailArrays; buildTailArrays(*this, TailArrays, i); llvm::Value *alloced = emitClassAllocation(*this, i->getType(), i->isObjC(), i->isBare(), StackAllocSize, TailArrays); if (StackAllocSize >= 0) { // Remember that this alloc_ref allocates the object on the stack. StackAllocs.insert(i); EstimatedStackSize += StackAllocSize; } Explosion e; e.add(alloced); setLoweredExplosion(i, e); }
i->getType() fetches the type metadata used with the alloc_ref instruction, which returns the $Memory class from the SIL.
The emitClassAllocation function leads to GenClass.cpp:
llvm::Value *irgen::emitClassAllocation(IRGenFunction &IGF, SILType selfType, bool objc, bool isBare, int &StackAllocSize, TailArraysRef TailArrays) { auto &classTI = IGF.getTypeInfo(selfType).as<ClassTypeInfo>(); auto classType = selfType.getASTType(); // (a lot of) code emitted for brevity ... std::tie(size, alignMask) = appendSizeForTailAllocatedArrays(IGF, size, alignMask, TailArrays); val = IGF.emitAllocObjectCall(metadata, size, alignMask, "reference.new"); StackAllocSize = -1; } return IGF.Builder.CreateBitCast(val, destType); }
IGF.emitAllocObjectCall takes us close to our final prize in IRGenFunction.cpp:
/// Emit a heap allocation. llvm::Value *IRGenFunction::emitAllocObjectCall(llvm::Value *metadata, llvm::Value *size, llvm::Value *alignMask, const llvm::Twine &name) { // For now, all we have is swift_allocObject. return emitAllocatingCall(*this, IGM.getAllocObjectFunctionPointer(), {metadata, size, alignMask}, name); } static llvm::Value *emitAllocatingCall(IRGenFunction &IGF, FunctionPointer fn, ArrayRef<llvm::Value *> args, const llvm::Twine &name) { auto allocAttrs = IGF.IGM.getAllocAttrs(); llvm::CallInst *call = IGF.Builder.CreateCall(fn, llvm::ArrayRef(args.begin(), args.size())); call->setAttributes(allocAttrs); return call; }
emitAllocatingCall demonstrates the actual function call being emitted as an LLVM instruction. But the piece we are really interested in is one of its arguments:
IGM.getAllocObjectFunctionPointer()
When fully compiled into machine code, this resolves as a pointer to the runtime ABI memory address of swift_allocObject.
We can apply the consistent compiler function naming format to track down any runtime ABI instruction we like in the compiler code IRGen system, and perform the same analysis:
get[runtime-abi-instruction]FunctionPointer
The Swift Runtime ABI
The Swift Runtime ABI, or Application Binary Interface, is the API contract, or protocol definition, for the Swift Runtime.
We can read this contract in Runtime.md — it’s an unassuming list of memory addresses and function names.
The pre-compiled binary for libswiftCore is dynamically linked to your Swift executable at launch. This contract lists the memory addresses of the public functions it contains.
getAllocObjectFunctionPointer() does exactly that: it returns a pointer to the memory location of the swift_allocObject function.
Thanks to the runtime contract, the compiler knows that it can rely on the swift_allocObject function being at the address 0x1c990 in the runtime library, whenever it’s needed in the running program.
This is what I meant when I said the runtime “runs alongside” your own code.
The Swift Runtime
LLVM relies on the runtime ABI contract to define the memory address of _swift_allocObject:
000000000001c990 T _swift_allocObject
This allows the Swift code to transition from our initial Memory() class initialisation, into SIL code with the alloc_refinstruction, through to a call into the runtime’s memory management system which actually allocates the object.
HeapObject
We can look at this implementation of _swift_allocObject in HeapObject.cpp to learn what’s really going on.