Guardsquare Devblog

Technical notes from the Guardsquare engineering teams.

Swift Native method swizzling

Posted at — Sep 10, 2019

In this post we’ll focus on the newly introduced Swift native method swizzling and provide an overview of its syntax, related compilation options and the generated code.

Method swizzling is the process of replacing the implementation of a function at runtime. Swift, as a static, strongly typed language, did not previously have any built-in mechanism that would allow to dynamically change the implementation of a function. Objective-C, on the other hand, has had this since its early days through class posing (deprecated) and method swizzling.

Classes inheriting from the Objective-C base class NSObject can be bridged between both languages, and thus allow the Swift compiler to leverage the Objective-C runtime for method swizzling. Methods implemented in Swift must have the @objc attribute in order to be swizzled when invoked from Objective-C code. Additionally, the dynamic modifier can be used to force Swift methods to always be invoked through message passing, even in Swift-to-Swift interactions. Up until now, however, the modifier was only available for declarations that were bridged to Objective-C.

Starting from version 5.1, Swift provides its own native version of method swizzling that does not rely on Objective-C’s message passing. When using the new Xcode 11, the dynamic modifier can be used on arbitrary functions and methods. This is the reason we call it ‘native’ method swizzling, because up until now it was not possible to do in pure Swift runtime.

Language syntax and swizzling options

In Swift 5.1 a new attribute has been introduced to the language; @_dynamicReplacement. It allows to leverage the swizzling feature. The attribute can be used on a replacement function, and takes as an argument the name of the function that it should replace.

The function that is being replaced must be marked with the dynamic modifier, unless -enable-implicit-dynamic compilation flag is used, which makes the compiler assume that every eligible entity has been marked with the modifier.

For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
dynamic func foo() {
  print("foo")
}

@_dynamicReplacement(for: foo)
func bar() {
  print("bar")
}

foo() // prints 'bar'

This basic @_dynamicReplacement example however does not fully demonstrate all use-cases.

Besides facilitating complete function replacement, native method swizzling also enables the extension of existing functions. Code can be added to run before and/or after the original implementation. In addition Swift offers two different approaches to native method swizzling: the default replacement behaviour and the chaining behaviour. Chaining can only be enabled per compilation unit using -enable-dynamic-replacement-chaining compilation flag. There is no way to opt-in on a per function level.

In short, native method swizzling can facilitate:

  1. Complete replacement
  2. Adding behaviour before existing functions
  3. Adding behaviour after existing functions

For the last two, there is the choice between the default replacement behaviour and the chaining behaviour.

In order to understand the difference between the default replacement behaviour and the chaining behaviour, let’s consider two functions A and B. Consider B replacing the implementation of A. Note that calls to A will have a different meaning depending on where they take place. When A is called from any other function than B, method swizzling takes place and B’s implementation is used instead. However, when A is called from within B itself, the compiler generates an invocation to the original implementation of A.

When we add an additional function C to our example, the two available behaviours become relevant. Consider C similar to B, i.e. replacing A’s implementation. Also assume C to be processed by the Swift runtime after B. There are now two options:

  1. By default, if A is called from the body of function C, the behavior is the same as for B: the original implementation of A is used.
  2. Optionally, the chaining behaviour can be enabled. In this mode, when A is called from C, the compiler generates an invocation to B instead.

To illustrate this, consider this minimal program matching our previous description.

1
2
3
4
5
6
7
dynamic
func A() {}

@_dynamicReplacement(for: A)
func B() { A() }

func entry() { A() }

The call graph for this snippet clearly shows both, regular and swizzled, situations.

Note that with swizzling enabled the entry function calls B, even though its body originally contained a call to A. Also notice the context-aware behaviour of the Swift compiler, as B still calls the original A implementation, instead of recursively calling itself.

Let’s now consider a slightly more complex example that contains our additional function C:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
dynamic 
func A() {}

@_dynamicReplacement(for: A)
func B() { A() }

@_dynamicReplacement(for: A)
func C() { A() }

func entry() { A() } 

Again, we use the various call graphs of this program to clearly illustrate the difference between the default and the chaining behaviour. Note how with chaining enabled, C, B and A’s original implementations are used respectively.

Generated code analysis

It is clear that this kind of behaviour requires some code generation changes from the compiler. In this section, we have a look under the hood and explain what those required changes were and how they facilitate dynamic replacement. Our analysis is performed on the LLVM IR. This is one of the intermediary representations in the Swift compiler. For our purpose of revealing the inner workings of dynamic replacement, the LLVM IR abstraction level is well suited. It shows all the mechanics involved but doesn’t require us to deal with CPU-architecture specific details in the explanation.

We’ll first have a look at the generated code for both sides of this replacement relation, functions ready to be replaced and functions intended to replace others. Then we will analyse changes in the generated code and describe how the Swift runtime propagates new replacement entries when code is loaded into the running process.

Emitting functions ready to be replaced

First, let’s see how the generated code (LLVM IR) differs when a standalone function is compiled with and without the dynamic modifier. Let’s consider a very minimal Swift function:

1
2
3
4
// a.swift
public func foo() -> Int {
  return 10
}

Without the modifier, only the function symbol is generated:

1
2
3
4
define swiftcc i64 @"$s4main3fooSiyF"() #0 {
entry:
  ret i64 10
}

After adding the dynamic modifier, effectively marking the foo function a candidate for replacement, three additional symbols are emitted. Symbols generated by the Swift compiler preserve information about certain properties by encoding them in their names, this is called mangling. We will use swift-demangle tool to decode that information.

Symbol name swift-demangle output
$s1a3fooSiyFTX dynamically replaceable variable for a.foo() -> Swift.Int
$s1a3fooSiyFTx dynamically replaceable key for a.foo() -> Swift.Int
$s1a3fooSiyFTI dynamically replaceable thunk for a.foo() -> Swift.Int

The replaceable key is a constant structure. It stores a reference to the replaceable variable and has one field reserved for flags (the key symbol is generated but, in this sample, not yet used):

%swift.dyn_repl_key = type { i32, i32 }

The replaceable variable is a global non-constant structure i.e. it can be mutated. The first structure member is used as a pointer to a function, whilst the other recursively references another replaceable variable, in effect forming a ‘chain’:

%swift.dyn_repl_link_entry = type { i8*, %swift.dyn_repl_link_entry* }

The thunk function contains the original function implementation. The generated code for the user-defined foo function now instead implements indirection logic for dynamic resolution and no longer the previous ret i64 10 statement. This generated indirection logic shows the address of the replacement variable being loaded and used to invoke the real implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
define swiftcc i64 @"$s1a3fooSiyF"() #0 { ; original function
entry:
  ; load active implementation fn pointer from replaceable variable
  %0 = load i8*, i8** getelementptr inbounds (%swift.dyn_repl_link_entry, %swift.dyn_repl_link_entry* @"$s1a3fooSiyFTX", i32 0, i32 0), align 8

  ; cast and call the function retrieved from the variable
  %1 = bitcast i8* %0 to i64 ()*
  %2 = tail call swiftcc i64 %1()

  ; propagate the called function’s return value
  ret i64 %2
}

Emitting replacement functions

In order to inspect the code generation for the other part of the equation, replacement functions, we’ll use another minimal Swift function:

1
2
3
4
5
6
7
// b.swift
import a

@_dynamicReplacement(for: foo)
func bar() -> Int {
  return 10
}

Similarly as in the case of a replaceable function, additional symbols are generated:

Symbol name swift-demangle output
$s1b3barSiyFTX dynamically replaceable variable for b.bar() -> Swift.Int
$s1b3barSiyFTI dynamically replaceable thunk for b.bar() -> Swift.Int

Names of those new symbols have the same mangled form as those that were generated with the replaceable function. Their meaning, however, differs despite names resemblance. The emitted replacement functions also need their own replaceable variables and thunk functions in order to implement chaining behaviour. In this case, the thunk function is one that implements the indirection and resolves its replacement variable. This is exactly the opposite of the previous scenario, in which the thunk actually contained the original implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
define hidden swiftcc i64 @"$s1b3barSiyFTI"() #1 { ; thunk function
entry:
  ; load active implementation fn pointer from replaceable variable
  %0 = load i8*, i8** getelementptr inbounds (%swift.dyn_repl_link_entry, %swift.dyn_repl_link_entry* @"$s1b3barSiyFTX", i32 0, i32 0), align 8

  ; cast and call the function retrieved from the variable
  %1 = bitcast i8* %0 to i64 ()*
  %2 = call swiftcc i64 %1()

  ; propagate the called function’s return value
  ret i64 %2
}

By reusing the same naming convention for a different purpose, the replacing functions themselves can’t be replaced.

Implementation analysis

Earlier, we have separately considered the code generated for two modules, the module a with the ready to be replaced function foo, and the module b with the replacement function bar. In this section, we will have a look at both modules side by side to understand how method swizzling is implemented in the Swift runtime.

The dynamic replacement variable for the foo function is directly initialised with the address of the foo thunk function (which has the actual implementation of foo). The variable for the bar function, on the other hand, is zero-initialised and will be dynamically initialized as the code is being processed by the Swift runtime, followed by the dynamic linker loading.

a.ir

1
2
3
4
5
6
7
; variable for foo
@"$s1a3fooSiyFTX" = global %swift.dyn_repl_link_entry {
  i8* bitcast (
    i64 ()* @"$s1a3fooSiyFTI" to i8*
  ), 
  %swift.dyn_repl_link_entry* null 
}, align 8

b.ir

1
2
; variable for bar
@"$s1b3barSiyFTX" = hidden global %swift.dyn_repl_link_entry zeroinitializer, align 8

The table below presents the initial state of the dynamically replaceable variables and how their values change as new replacement functions are being processed by the Swift runtime. To get a complete picture, we consider one more replacement function quux that - similarly to function bar - replaces function foo.

replaceable variable binary runtime after processing bar runtime after processing quux
foo {foo’s thunk, nullptr} {bar, bar’s replacement variable} {quux, quux’s replacement variable}
bar {nullptr, nullptr} {foo’s thunk, nullptr} {foo’s thunk, nullptr}
quux {nullptr, nullptr} {nullptr, nullptr} {bar, bar’s replacement variable}
quux without chaining {nullptr, nullptr} {nullptr, nullptr} {foo’s thunk, nullptr}
Dynamic replacement variable state

The replaceable variable of the function foo always stores a pointer to the active implementation, whether it is the original function or one of its replacements. In the case of replacement functions, the value of their replaceable variables is different based on whether chaining is enabled or not. If it is, they will point to the replacement predecessor, in the other case they point directly to the function they replace (here foo).

In section Language Syntax and swizzling options, we have noticed that the Swift compiler can be context-aware and generates different function invocations depending on where the replaced function is called from. Extending our previous examples modules a and b with a new module c, let’s see what the context-aware generated code looks like. The c module consists of a replacing function bar and a regular, unrelated, function other:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// c.swift
import a

@_dynamicReplacement(for: foo)
func bar() -> Int {
  return foo() + 10
}

func other() -> Int {
  return foo()
}

A call to the replaceable function foo, from the function other does not differ from a non-replaceable function call:

1
2
3
4
5
6
7
8
// c.ir
; implementation of function other
define hidden swiftcc i64 @"$s1c5otherSiyF"() #0 {
entry:
  ; call to function foo
  %0 = call swiftcc i64 @"$s1a3fooSiyF"()
  ret i64 %0
}

As we have noticed earlier, the replaceable function foo is responsible for resolving its active replacement function, in our case; bar. This allows the caller of foo, the function other, to remain unaware of foo’s dynamic properties and invoke it just as any other function. However, when invoked from bar, foo is translated into a call to bar’s thunk function, which in turn uses bar’s replacement variable and results in a call to foo’s original implementation:

1
2
3
4
5
6
7
8
// c.ir
; implementation of function bar
define hidden swiftcc i64 @"$s1c3barSiyF"() #0 {
entry:
  ; call to bar’s thunk function
  %0 = call swiftcc i64 @"$s1c3barSiyFTI"()
  ...
}

Runtime bootstrap process

So far, we have covered aspects of the dynamic function replacement starting from the language syntax to it’s behaviour and the code that the compiler generates to support it. The part of the swizzling mechanism that was not discussed yet is how the runtime bootstraps the dynamic variables as code is loaded into memory. The runtime uses information embedded in binaries to resolve the swizzling information as additional binaries are loaded. This happens gradually as shown earlier in the Dynamic replacement variable state table.

To understand the process, notice two global values in a non-writable memory segment and the first one in a new section:

  1. @”\01l_auto_dynamic_replacements” = … section “TEXT, **swift5_replace**, regular, no_dead_strip”
  2. @”\01l_unnamed_dynamic_replacements” = … section “TEXT,const”

Through the analysis of the types of those constants and the values they are initialised with, we will describe how the runtime bootstraps function replacements.

We will start with the auto_dynamic_replacements.

Type (C syntax)

1
2
3
4
5
typedef struct {
  int32_t flag;
  int32_t nEntries;
  ptrdiff_t entries[1];
} auto_dynamic_replacements_t;

Initial value

{
  0,
  1,
  { (ptrdiff_t)&unnamed_dynamic_replacements - (ptrdiff_t)&replacements.entries[0] }
}

Besides this, we have another global value unnamed_dynamic_replacements.

Type (C syntax)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
typedef struct {
  ptrdiff_t replacedFnKey;
  ptrdiff_t replacementFn;
  ptrdiff_t replacementFnKey;
  int32_t flags;
} replacement_entry_t;
typedef struct {
  int32_t flag;
  int32_t nEntries;
  replacement_entry_t entries[1];
} unnamed_entry_t;

Initial value

{
  0,
  1,
  {
    (ptrdiff_t)&fooFnReplKeyAddrs - (ptrdiff_t)&anEntry.entries[0].replacedFnKey,
    (ptrdiff_t)&barFn - (ptrdiff_t)&anEntry.entries[0].replacementFn,
    (ptrdiff_t)&barFnReplVar - (ptrdiff_t)&anEntry.entries[0].replacementFnKey,
    0
  }
}

The unnamed_entry_t structure follows exactly the same pattern - a flag field, a second field indicating the length of an array element and a third field, the actual array. The array elements are the interesting part and finally lead to the core of the replacement metadata. If we were to discard all the implementation detail information, a single entry could be read as:

The replaceable key for function foo is used to find the function and its replaceable variable. The second and the third field reference the bar function and its variable respectively. A single entry, therefore, provides access to all elements of the dynamic replacement relationship. Earlier, we have analysed how the elements are used by the runtime to propagate newly loaded replacement functions.

Notice how both the global constants merely introduce new levels of indirection, each adding additional flag fields. All the final entries in which actual functions and their replaceable variables and keys are referenced could be included in a single array. The additional indirection simplifies the grouping of the elements and allows for higher extensibility through the flag fields. Using the new binary section, __swift5_replace to include this information makes it easier to access the replacement entries as the binary is processed by the runtime. Adding a new section also has no ABI compatibility implications with older runtime versions, since they simply won’t be processed.

The dynamic replacements are extended whenever a new dynamic library is processed by the runtime and there is no constructor function present in the generated IR, that indicates that the runtime is using image loading callbacks. To verify the usage of image loading callbacks, you place a watchpoint, using a debugger, on a replaceable variable. Make sure to do so before loading dynamic library that contains functions replacements is loaded using dlopen. Here is a stacktrace example as a watchpoint triggers during a call to dlopen:

* thread #1, queue = 'com.apple.main-thread', stop reason = watchpoint 1
  * frame #0: … libswiftCore.dylib`swift::addImageDynamicReplacementBlockCallback(void const*, unsigned long, void const*, unsigned long) + 180
    frame #1: … libswiftCore.dylib`
      void (anonymous namespace)::addImageCallback2Sections<
          &((anonymous namespace)::TextSegment), 
          &((anonymous namespace)::DynamicReplacementSection), 
          &((anonymous namespace)::TextSegment),
          &((anonymous namespace)::DynamicReplacementSomeSection),
          &(swift::addImageDynamicReplacementBlockCallback(void const*, unsigned long, void const*, unsigned long))
      >(mach_header const*) + 107
    frame #2: … libobjc.A.dylib`map_images_nolock + 6984
    frame #3: … libobjc.A.dylib`map_images + 59
    frame #4: … dyld`dyld::notifyBatchPartial(dyld_image_states, bool, char const* (*)(dyld_image_states, unsigned int, dyld_image_info const*), bool, bool) + 1767
    frame #5: … dyld`ImageLoader::link(ImageLoader::LinkContext const&, bool, bool, bool, ImageLoader::RPathChain const&, char const*) + 516
    frame #6: … dyld`dyld::link(ImageLoader*, bool, bool, ImageLoader::RPathChain const&, unsigned int) + 161
    frame #7: … dyld`dlopen_internal + 477
    frame #8: … libdyld.dylib`dlopen + 171
    frame #9: … exec`main + 108
    frame #10: … libdyld.dylib`start + 1

Summary

Our analysis of the native Swift method swizzling shows a modern alternative to the Objective-C approach. The Swift language runtime moves the responsibility of dynamic function dispatch from the caller to the callee. The need for an intermediate, objc_msgSend-like function is replaced by the use of global data structures. This design guarantees better runtime performance for the dynamic functions and has no performance impact on a code that doesn’t use this feature. With the addition of new dynamic features, the gap between Swift and Objective-C becomes even smaller.