Jacob’s Tech Tavern

Jacob’s Tech Tavern

The Swift Runtime: Your Silent Partner

Fun and games with the Swift source code

Jacob Bartlett's avatar
Jacob Bartlett
Dec 16, 2024

The Swift Runtime, a.k.a libswiftCore, is a C++ library that runs alongside Swift programs to facilitates core language features. I set out to understand what we mean by “runs alongside” and “facilitates core language features”.


Subscribe to Jacob’s Tech Tavern for free to get ludicrously in-depth articles on iOS, Swift, tech, & indie projects in your inbox every week.

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:

  • Memory management

  • Error handling

  • Dynamic dispatch

  • Types, generics, & protocols

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”!?

Let’s find out with a quest into the Swift source code.

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.

Memory Management

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.

  • stdlib/public/runtime/HeapObject.cpp

  • stdlib/public/runtime/Heap.cpp

  • stdlib/public/SwiftShims/swift/shims/HeapObject.h

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:

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
[%1: noescape **]
[global: read,write,copy,allocate,deinit_barrier]
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @$s4main3memAA6MemoryCvp           // id: %2
  %3 = global_addr @$s4main3memAA6MemoryCvp : $*Memory // user: %6
  %4 = alloc_ref $Memory                          // users: %6, %5
  debug_value %4 : $Memory, let, name "self", argno 1, implicit // id: %5
  store %4 to %3 : $*Memory                       // id: %6
  %7 = integer_literal $Builtin.Int32, 0          // user: %8
  %8 = struct $Int32 (%7 : $Builtin.Int32)        // user: %9
  return %8 : $Int32                              // id: %9
} // end sil function 'main'

Let’s study the following line in more detail:

%4 = alloc_ref $Memory

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

Subscribe for free to continue reading

Enter your email and get every post from Jacob’s Tech Tavern.
Already have an account? Sign in
© 2026 Jacob Bartlett · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture