Understanding Swift SIL and Method Dispatch Mechanisms
Swift inserts the high‑level SIL intermediate representation between source code and LLVM, exposing type declarations, method blocks, and virtual tables, while supporting three dispatch strategies—direct for value types and final methods, VTable for regular class methods, and Objective‑C message dispatch for @objc dynamic methods—crucial for debugging mixed Swift/Objective‑C and protocol‑extension behavior.
1. Introduction to SIL
SIL (Swift Intermediate Language) is an SSA‑form IR designed specifically for the Swift language. It carries high‑level semantic information and sits between the Swift front‑end and LLVM back‑end. The Swift compiler (swiftc) parses source code into an AST, then emits LLVM IR; before that, it inserts SIL to retain richer Swift‑specific details.
SIL is an SSA-form IR with high-level semantic information designed to implement the Swift programming languageSIL provides better readability for developers and enables more powerful optimizations compared to raw LLVM IR.
2. Generating SIL
To generate SIL for a Swift source file, use the following command:
swiftc -emit-silgen -Onone Contents.swift | xcrun swift-demangle >> result.silThe -Onone flag disables optimizations, allowing a full view of the generated SIL. The xcrun swift-demangle tool improves readability of mangled symbols.
Declaration and Definition
The top part of a SIL file contains type declarations and definitions.
// 1
sil_stage raw
// 2
import Builtin
import Swift
import SwiftShims
import Foundation
class Cat {
func speak()
@objc deinit
init()
}
@_hasStorage @_hasInitialValue let cat: Cat { get }
// 3
// cat
sil_global hidden [let] @Contents.cat : Contents.Cat : $CatKey points:
sil_stage can be raw (unoptimized) or canonical (optimized).
Type declarations mirror the source code.
sil_global hidden declares a module‑private global variable.
Code Blocks
Each method in the source generates a corresponding SIL code block.
// main
sil [ossa] @main : $@convention(c)(Int32, UnsafeMutablePointer
>>) -> Int32 { ... }
// Cat.speak()
sil hidden [ossa] @Contents.Cat.speak() -> () : $@convention(method)(@guaranteed Cat) -> () { ... }
// Cat.deinit
sil hidden [ossa] @Contents.Cat.deinit : $@convention(method)(@guaranteed Cat) -> @owned Builtin.NativeObject { ... }The main function is the entry point, using @convention(c) to follow C calling conventions. Instance methods use @convention(method) , where the instance (self) is passed as the first argument.
Function Table
At the end of the SIL file, a virtual method table (VTable) lists the concrete implementations for a class.
sil_vtable Cat {
#Cat.speak: (Cat) -> () -> () : @Contents.Cat.speak()
#Cat.init!allocator: (Cat.Type) -> () -> Cat : @Contents.Cat.__allocating_init()
#Cat.deinit!deallocator: @Contents.Cat.__deallocating_deinit
}3. Swift Method Dispatch
Direct Dispatch
Value‑type methods (e.g., structs) are dispatched directly. In SIL this appears as a function_ref followed by an apply .
// function_ref Dog.speak()
%9 = function_ref @Contents.Dog.speak() -> () : $@convention(method)(Dog) -> ()
%10 = apply %9(%8) : $@convention(method)(Dog) -> ()Adding final to a class method or implementing a method in an extension also results in direct dispatch.
VTable (Function‑Table) Dispatch
Non‑final class methods are looked up in the class’s VTable using the class_method instruction.
// Cat.speak()
%9 = class_method %8 : $Cat, #Cat.speak : (Cat) -> () -> (), $@convention(method)(@guaranteed Cat) -> ()
%10 = apply %9(%8) : $@convention(method)(@guaranteed Cat) -> ()Witness Table Dispatch (Protocol Methods)
When a method is defined in a protocol, the call goes through a witness table (WTable). The SIL uses witness_method to retrieve the implementation.
%10 = witness_method $@opened("...") Animal, #Animal.speak :
(Self) -> () -> (), %9 : $*@opened("...") Animal : $@convention(witness_method: Animal)<τ_0_0 where τ_0_0 : Animal>(@in_guaranteed τ_0_0) -> ()
%11 = apply %10<@opened("...") Animal>(%9) : $@convention(witness_method: Animal)<τ_0_0 where τ_0_0 : Animal>(@in_guaranteed τ_0_0) -> ()Message (Objective‑C) Dispatch
Adding @objc makes a Swift method visible to Objective‑C, generating a thunk that uses @convention(objc_method) . However, the normal Swift call still goes through the VTable.
// @objc Cat.speak()
sil hidden [thunk] [ossa] @objc @Contents.Cat.speak() -> () : $@convention(objc_method)(Cat) -> () {
%3 = function_ref @Contents.Cat.speak() -> () : $@convention(method)(@guaranteed Cat) -> ()
%4 = apply %3(%2) : $@convention(method)(@guaranteed Cat) -> ()
}To force true message dispatch, the method must be marked dynamic . The SIL then uses objc_method instead of class_method .
%9 = objc_method %8 : $Cat, #Cat.speak!foreign : (Cat) -> () -> (), $@convention(objc_method)(Cat) -> ()
%10 = apply %9(%8) : $@convention(objc_method)(Cat) -> ()4. Practical Scenarios
Scenario 1 – Protocol Extension
If a method is provided only in a protocol extension (not declared in the protocol), a variable typed as the protocol will call the extension implementation via static dispatch.
protocol Animal {}
extension Animal { func speak() { print("adhansxkjaw") } }
class Cat: Animal { func speak() { print("喵喵") } }
let cat: Animal = Cat()
cat.speak() // prints "adhansxkjaw"The SIL shows a static call to the extension’s function; no witness table entry is generated.
Scenario 2 – Subclass Implements Protocol Method
When a subclass implements a protocol method but the superclass does not, a variable typed as the protocol still uses the superclass’s witness table, resulting in the extension’s implementation being called.
protocol Animal { func speak() }
extension Animal { func speak() { print("adhansxkjaw") } }
class Cat: Animal {}
class PetCat: Cat { func speak() { print("meow~") } }
let cat: Animal = PetCat()
cat.speak() // prints "adhansxkjaw"The SIL uses witness_method that resolves to the witness table of Cat , not PetCat , because only explicit conformances generate witness tables.
Scenario 3 – Objective‑C Mixed Compilation
An Objective‑C protocol implemented in a Swift subclass works via message dispatch. However, if the Swift base class is generic or resides in a static library, the compiler may not emit the @objc thunk, breaking the call. Explicitly adding @objc to the method restores compatibility.
5. Summary
Swift inserts SIL between the source and LLVM IR, providing a readable, high‑level IR that reveals method‑dispatch details. Three dispatch mechanisms exist:
Static (direct) dispatch – fastest, used for value types, final methods, and extension methods not declared in a protocol.
VTable dispatch – used for regular class methods; the method is looked up in the class’s virtual table.
Message (dynamic) dispatch – achieved with @objc dynamic , generating an Objective‑C thunk that ultimately calls objc_msgSend .
Understanding SIL helps diagnose unexpected behavior, especially when mixing Swift with Objective‑C or dealing with protocol extensions.
NetEase Cloud Music Tech Team
Official account of NetEase Cloud Music Tech Team
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.