• Stars
    star
    669
  • Rank 64,848 (Top 2 %)
  • Language
    Swift
  • License
    Other
  • Created almost 8 years ago
  • Updated 7 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Trace Swift and Objective-C method invocations

SwiftTrace

Trace Swift and Objective-C method invocations of non-final classes in an app bundle or framework. Think Xtrace but for Swift and Objective-C. You can also add "aspects" to member functions of non-final Swift classes to have a closure called before or after a function implementation executes which in turn can modify incoming arguments or the return value! Apart from the logging functionality, with binary distribution of Swift frameworks on the horizon perhaps this will be of use in the same way "Swizzling" was in days of yore.

SwiftTrace Example

Note: none of these features will work on a class or method that is final or internal in a module compiled with whole module optimisation as the dispatch of the method will be "direct" i.e. linked to a symbol at the call site rather than going through the class' vtable. As such it is possible to trace calls to methods of a struct but only if they are referenced through a protocol as they use a witness table which can be patched.

SwiftTrace can be used with the Swift Package Manager or as a CocoaPod by adding the following to your project's Podfile:

    pod 'SwiftTrace'

Once the project has rebuilt, import SwiftTrace into the application's AppDelegate and add something like the following to the beginning of it's didFinishLaunchingWithOptions method:

    SwiftTrace.traceBundle(containing: type(of: self))

This traces all classes defined in the main application bundle. To trace, for example, all classes in the RxSwift Pod add the following

    SwiftTrace.traceBundle(containing: RxSwift.DisposeBase.self)

This gives output in the Xcode debug console such as that above.

To trace a system framework such as UIKit you can trace classes using a pattern:

    SwiftTrace.traceClasses(matchingPattern:"^UI")

Individual classes can be traced using the underlying api:

    SwiftTrace.trace(aClass: MyClass.self)

Or to trace all methods of instances of a particular class including those of their superclasses use the following:

    SwiftTrace.traceInstances(ofClass: aClass)

Or to trace only a particular instance use the following:

    SwiftTrace.trace(anInstance: anObject)

If you have specified "-Xlinker -interposable" in your project's "Other Linker Flags" it's possible to trace all methods in the application's main bundle at once which can be useful for profiling SwiftUI using the following call:

    SwiftTrace.traceMainBundleMethods()

It is possible to trace methods of a structs or other types if they are messaged through protools as this would then be indirect via what is called a witness table. Tracing protocols is available at the bundle level where the bundle being traced is specified using a class instance. They can be further filtered by an optional regular expression. For example, the following:

SwiftTrace.traceProtocolsInBundle(containing: AClassInTheBundle.self, matchingPattern: "regexp")

For example, to trace internal calls made in the SwiftUI framework you can use the following:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    SwiftTrace.traceProtocolsInBundle(containing: UIHostingController<HomeView>.self)
    return true
}

Which traces are applied can be filtered using method name inclusion and exclusion regexps.

    SwiftTrace.methodInclusionPattern = "TestClass"
    SwiftTrace.methodExclusionPattern = "init|"+SwiftTrace.defaultMethodExclusions

These methods must be called before you start the trace as they are applied during the "Swizzle" phase. There is a default set of exclusions setup as a result of testing by tracing UIKit.

open class var defaultMethodExclusions: String {
    return """
        \\.getter| (?:retain|_tryRetain|release|_isDeallocating|.cxx_destruct|dealloc|description| debugDescription)]|initWithCoder|\
        ^\\+\\[(?:Reader_Base64|UI(?:NibStringIDTable|NibDecoder|CollectionViewData|WebTouchEventsGestureRecognizer)) |\
        ^.\\[(?:UIView|RemoteCapture) |UIDeviceWhiteColor initWithWhite:alpha:|UIButton _defaultBackgroundImageForType:andState:|\
        UIImage _initWithCompositedSymbolImageLayers:name:alignUsingBaselines:|\
        _UIWindowSceneDeviceOrientationSettingsDiffAction _updateDeviceOrientationWithSettingObserverContext:windowScene:transitionContext:|\
        UIColorEffect colorEffectSaturate:|UIWindow _windowWithContextId:|RxSwift.ScheduledDisposable.dispose| ns(?:li|is)_
        """
}

If you want to further process output you can define your own custom tracing sub class:

    class MyTracer: SwiftTrace.Decorated {

        override func onEntry(stack: inout SwiftTrace.EntryStack) {
            print( ">> "+stack )
        }
    }
    
    SwiftTrace.swizzleFactory = MyTracer.self

As the amount of of data logged can quickly get out of hand you can control what is logged by combing traces with the optional subLevels parameter to the above functions. For example, the following puts a trace on all of UIKit but will only log calls to methods of the target instance and up to three levels of calls those method make:

    SwiftTrace.traceBundle(containing: UIView.self)
    SwiftTrace.trace(anInstance: anObject, subLevels: 3)

Or, the following will log methods of the application and calls to RxSwift they make:

    SwiftTrace.traceBundle(containing: RxSwift.DisposeBase.self)
    SwiftTrace.traceMainBundle(subLevels: 3)

If this seems arbitrary the rules are reasonably simple. When you add a trace with a non-zero subLevels parameter all previous traces are inhibited unless they are being made up to subLevels inside a method in the most recent trace or if they where filtered anyway by a class or instance (traceInstances(ofClass:) and trace(anInstance:)).

If you would like to extend SwiftTrace to be able to log one of your app's types there are two steps. First, you may need to extend the type to conform to SwiftTraceFloatArg if it contains only float only float types for example SwiftUI.EdgeInsets.

extension SwiftUI.EdgeInsets: SwiftTraceFloatArg {}

Then, add a handler for the type using the following api:

    SwiftTrace.addFormattedType(SwiftUI.EdgeInsets.self, prefix: "SwiftUI")

Many of these API's are also available as a extension of NSObject which is useful when SwiftTrace is made available by dynamically loading bundle as in (InjectionIII)[https://github.com/johnno1962/InjectionIII].

    SwiftTrace.traceBundle(containing: UIView.class)
    // becomes
    UIView.traceBundle()
    
    SwiftTrace.trace(inInstance: anObject)
    // becomes
    anObject.swiftTraceInstance()

This is useful when SwiftTrace is made available by dynamically loading a bundle such as when using (InjectionIII)[https://github.com/johnno1962/InjectionIII]. Rather than having to include a CocoaPod, all you need to do is add SwiftTrace.h in the InjectionIII application's bundle to your bridging header and dynamically load the bundle.

   Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load()

Benchmarking

To benchmark an app or framework, trace it's methods then you can use one of the following:

   SwiftTrace.sortedElapsedTimes(onlyFirst: 10))
   SwiftTrace.sortedInvocationCounts(onlyFirst: 10))

Object lifetime tracking

You can track the allocations an deallocations of Swift and Objective-C classes using the SwiftTrace.LifetimeTracker class:

SwiftTrace.swizzleFactory = SwiftTrace.LifetimeTracker.self
SwiftTrace.traceMainBundleMethods() == 0 {
    print("⚠️ Tracing Swift methods can only work if you have -Xlinker -interposable to your project's \"Other Linker Flags\"")
}
SwiftTrace.traceMainBundle()

Each time an object is allocated you will see a .__allocating_init message followed by the result and the resulting count of live objects allocated since tracing was started. Each time an object is deallocated you will see a cxx_destruct message followed by the number of objects oustanding for that class.

If you would like to track the lifecycle of Swift structs, create a marker class and add a property to the struct initialised to an instance of it.

class Marker<What> {}

struct MyView: SwiftUI.View {
    var marker = Marker<MyView>()
}

This idea is based on the LifetimeTracker project by Krzysztof Zabłocki.

Aspects

You can add an aspect to a particular method using the method's de-mangled name:

    print(SwiftTrace.addAspect(aClass: TestClass.self,
                      methodName: "SwiftTwaceApp.TestClass.x() -> ()",
                      onEntry: { (_, _) in print("ONE") },
                      onExit: { (_, _) in print("TWO") }))

This will print "ONE" when method "x" of TextClass is called and "TWO when it has exited. The two arguments are the Swizzle which is an object representing the "Swizzle" and the entry or exit stack. The full signature for the entry closure is:

       onEntry: { (swizzle: SwiftTrace.Swizzle, stack: inout SwiftTrace.EntryStack) in

If you understand how registers are allocated to arguments it is possible to poke into the stack to modify the incoming arguments and, for the exit aspect closure you can replace the return value and on a good day log (and prevent) an error being thrown.

Replacing an input argument in the closure is relatively simple:

    stack.intArg1 = 99
    stack.floatArg3 = 77.3

Other types of argument a little more involved. They must be cast and String takes up two integer registers.

    swizzle.rebind(&stack.intArg2).pointee = "Grief"
    swizzle.rebind(&stack.intArg4).pointee = TestClass()

In an exit aspect closure, setting the return type is easier as it is generic:

    stack.setReturn(value: "Phew")

When a function throws you can access NSError objects.

    print(swizzle.rebind(&stack.thrownError, to: NSError.self).pointee)

It is possible to set stack.thrownError to zero to cancel the throw but you will need to set the return value.

If this seems complicated there is a property swizzle.arguments which can be used onEntry which contains the arguments as an Array containing elements of type Any which can be cast to the expected type. Element 0 is self.

Invocation interface

Now we have a trampoline infrastructure, it is possible to implement an invocation api for Swift:

    print("Result: "+SwiftTrace.invoke(target: b,
        methodName: "SwiftTwaceApp.TestClass.zzz(_: Swift.Int, f: Swift.Double, g: Swift.Float, h: Swift.String, f1: Swift.Double, g1: Swift.Float, h1: Swift.Double, f2: Swift.Double, g2: Swift.Float, h2: Swift.Double, e: Swift.Int, ff: Swift.Int, o: SwiftTwaceApp.TestClass) throws -> Swift.String",
        args: 777, 101.0, Float(102.0), "2-2", 103.0, Float(104.0), 105.0, 106.0, Float(107.0), 108.0, 888, 999, TestClass()))

In order to determine the mangled name of a method you can get the full list for a class using this function:

    print(SwiftTrace.methodNames(ofClass: TestClass.self))

There are limitations to this abbreviated interface in that it only supports Double, Float, String, Int, Object, CGRect, CGSize and CGPoint arguments. For other struct types that do not contain floating point values you can conform them to protocol SwiftTraceArg to be able to pass them on the argument list or SwiftTraceFloatArg if they contain only floats. These values and return values must fit into 32 bytes and not contain floats.

How it works

A Swift AnyClass instance has a layout similar to an Objective-C class with some additional data documented in the ClassMetadataSwift in SwiftMeta.swift. After this data there is the vtable of pointers to the class and instance member functions of the class up to the size of the class instance. SwiftTrace replaces these function pointers with a pointer to a unique assembly language "trampoline" entry point which has destination function and data pointers associated with it. Registers are saved and this function is called passing the data pointer to log the method name. The method name is determined by de-mangling the symbol name associated the function address of the implementing method. The registers are then restored and control is passed to the original function implementing the method.

Please file an issue if you encounter a project that doesn't work while tracing. It should be far more reliable as it uses assembly language trampolines rather than Swizzling like Xtrace did. Otherwise, the author can be contacted on Twitter @Injection4Xcode.

Thanks to Oliver Letterer for the imp_implementationForwardingToSelector project adapted to set up the trampolines, included under an MIT license.

The repo includes a very slightly modified version of the very handy https://github.com/facebook/fishhook. See the source and header files for their licensing details.

Thanks also to @twostraws' Unwrap and @artsy's eidolon used extensively during testing.

Enjoy!

$Date: 2022/01/22 $

More Repositories

1

injectionforxcode

Runtime Code Injection for Objective-C & Swift
Objective-C
6,538
star
2

InjectionIII

Re-write of Injection for Xcode in (mostly) Swift
Objective-C
3,835
star
3

Xtrace

Trace Objective-C method calls by class or instance
Objective-C++
1,835
star
4

Refactorator

Xcode Plugin that Refactors Swift & Objective-C
Swift
991
star
5

GitDiff

Highlights deltas against git repo in Xcode
Objective-C
890
star
6

Remote

Control your iPhone from inside Xcode for end-to-end testing.
Objective-C
809
star
7

HotReloading

Hot reloading as a Swift Package
Swift
504
star
8

XprobePlugin

Live Memory Browser for Apps & Xcode
Objective-C++
393
star
9

RefactoratorApp

App version of Refactorator plugin
Swift
255
star
10

Accelerator

Inline frameworks of Swift CocoaPods projects for faster launch
Ruby
174
star
11

InjectionApp

Issue Tracking Repo for Injection as an App
Swift
111
star
12

Fortify

Making Swift more robust
Swift
94
star
13

Diamond

Diamond - Swift scripting made easy
Objective-C
94
star
14

SwiftPython

Experiments in bridging Swift to Python
Swift
88
star
15

HotSwiftUI

Utilities for Hot Reloading SwiftUI apps.
Swift
76
star
16

Dynamo

High Performance (nearly)100% Swift Web server supporting dynamic content.
Swift
68
star
17

SwiftRegex

Some regular expression operators for Swift
Swift
67
star
18

NSLinux

NSString and libdispatch compatibility code for Swift on Linux
Swift
47
star
19

WatchkitCurrency

Swift Currency Convertor for iWatch with flexible interface
Swift
40
star
20

TwoWayMirror

Adapt Swift’s Mirror functionality to make it bidirectional.
Swift
38
star
21

InstantSyntax

SwiftSyntax binary frameworks
Swift
36
star
22

Smuggler

Smuggle code bundles into an app running in the Simulator
Objective-C++
32
star
23

SwiftRegex5

5th incarnation of Swift Regex library using generic subscripts
Swift
32
star
24

SwiftAspects

Experiments in Aspects with Swift (Xtrace for Swift)
Assembly
30
star
25

unhide

export symbols with “hidden” visibility for Swift frameworks
Objective-C++
24
star
26

Symbolicate

Symbolicate for OS X
Objective-C
23
star
27

InjectionLite

Swift package re-write of InjectionIII app
Swift
23
star
28

DLKit

A rather subscript oriented interface to the dynamic linker.
Swift
21
star
29

SwiftTryCatch

Try/Catch for Swift?
Swift
15
star
30

ApportablePlugin

Simple Plugin for work with Apportable
Objective-C
14
star
31

SearchLight

SpotLight on Steroids
Objective-C++
14
star
32

siteify

Build web site from a project’s Swift sources.
HTML
13
star
33

SwiftPlugin

A way to import classes from plugins
Swift
12
star
34

SwiftKeyPath

valueForKeyPath: for Swift
Swift
12
star
35

DynamoLinux

100% Swift Linux Web Server
Swift
11
star
36

Compilertron

InjectionIII for the Swift compiler
C++
10
star
37

SwiftUIPlaygrounds

Alternative to Xcode previews.
Swift
9
star
38

SwiftRegex4

Basic regex operations for Swift4
Swift
9
star
39

StringIndex

Sensible indexing into Swift Strings
Swift
7
star
40

SwiftView

Curated Xcode Project as a means of navigating Swift Sources
7
star
41

Parallel

Some primitives for concurrent processing
Swift
6
star
42

Popen

Reading and writing processes and files
Swift
6
star
43

WatchkitSundial

Sundial for Apple Watch
Objective-C
6
star
44

SwiftierJSON

Memory efficient version of SwiftyJSON
Swift
6
star
45

objectivecpp

HTML
5
star
46

YieldGenerator

Python's "yield" generators for Swift
Swift
5
star
47

opaqueify

Greater use of Opaque types (SE0335)
Swift
5
star
48

SwiftMock

Mock structs and classes without code modification for testing.
Swift
4
star
49

Unwrap

Self documenting alternatives to force unwrap operator.
Swift
4
star
50

InjectionScratch

InjectionScratch
Objective-C++
3
star
51

EasyPointer

Rounding off some of the rough edges of Swift's pointer model
Swift
2
star
52

TestRunner

Example of calling Swift methods using name pattern (XCTest?)
Swift
2
star
53

binary-Swallow0

Swift
1
star
54

Character

Integer conversions and operators for Swift Characters.
Swift
1
star