InterposeKit
InterposeKit is a modern library to swizzle elegantly in Swift. It is fully written in Swift 5.2+ and works on @objc dynamic
Swift functions or Objective-C instance methods. API documentation available at interposekit.com, and some implementation thoughts on my blog.
Instead of adding new methods and exchanging implementations based on method_exchangeImplementations
, this library replaces the implementation directly using class_replaceMethod
. This avoids some of the usual problems with swizzling.
You can call the original implementation and add code before, instead or after a method call.
This is similar to the Aspects library, but doesn't yet do dynamic subclassing.
Usage
Let's say you want to amend sayHi
from TestClass
:
class TestClass: NSObject {
@objc dynamic func sayHi() -> String {
print("Calling sayHi")
return "Hi there 👋"
}
}
try Interpose(TestClass.self) {
try $0.hook(#selector(TestClass.sayHi), { store in { `self` in
print("Before Interposing \(`self`)")
let string = store((@convention(c) (AnyObject, Selector) -> String).self)(`self`, store.selector)
print("After Interposing \(`self`)")
return string + testSwizzleAddition
}
as @convention(block) (AnyObject) -> String})
}
Here's what we get when calling print(TestClass().sayHi())
[Interposer] Swizzled -[TestClass.sayHi] IMP: 0x000000010d9f4430 -> 0x000000010db36020
Before Interposing <InterposeTests.TestClass: 0x7fa0b160c1e0>
Calling sayHi
After Interposing <InterposeTests.TestClass: 0x7fa0b160c1e0>
Hi there 👋 and Interpose
Key Facts
- Interpose directly modifies the implementaton of a
Method
, which is better than selector-based swizzling. - Pure Swift, no
NSInvocation
, which requires boxing and can be slow. - No Type checking. If you have a typo or forget a
convention
part, this will crash at runtime. - Yes, you have to type the resulting type twice This is a tradeoff, else we need NSInvocation or assembly
- Delayed Interposing helps when a class is loaded at runtime. This is useful for Mac Catalyst
Delayed Hooking
Sometimes it can be necessary to hook a class deep in a system framework, which is loaded at a later time. Interpose has a solution for this and uses a hook in the dynamic linker to be notified whenever new classes are loaded.
try Interpose.whenAvailable(["RTIInput", "SystemSession"]) {
let lock = DispatchQueue(label: "com.steipete.document-state-hack")
try $0.hook("documentState", { store in { `self` in
lock.sync {
store((@convention(c) (AnyObject, Selector) -> AnyObject).self)(`self`, store.selector)
}} as @convention(block) (AnyObject) -> AnyObject})
try $0.hook("setDocumentState:", { store in { `self`, newValue in
lock.sync {
store((@convention(c) (AnyObject, Selector, AnyObject) -> Void).self)(`self`, store.selector, newValue)
}} as @convention(block) (AnyObject, AnyObject) -> Void})
}
FAQ
Why didn't you call it Interpose? "Kit" feels so old-school.
Naming it Interpose was the plan, but then SR-898 came. While having a class with the same name as the module works in most cases, this breaks when you enable build-for-distribution. There's some discussion to get that fixed, but this will be more towards end of 2020, if even.
I want to hook into Swift! You made another ObjC swizzle thingy, why?
UIKit and AppKit won't go away, and the bugs won't go away either. I see this as a rarely-needed instrument to fix system-level issues. There are ways to do some of that in Swift, but that's a separate (and much more difficult!) project.
Can I ship this?
Yes, absolutely. The goal for this one prokect is a simple library that doesn't try to be too smart. I did this in Aspects and while I loved this to no end, it's problematic and can cause side-effects with other code that tries to be clever. InterposeKit is boring, so you don't have to worry about conditions like "We added New Relic to our app and now your thing crashes".
Even security exploits now have logos. Get with the times!
It's 2020 mate.I know, I know. Will be up soon!
It does not do X
Pull Requests welcome! You might wanna open a draft before to lay out what you plan, I want to keep the feature-set minimal so it stays simple and no-magic.
Installation
Building InterposeKit requires Xcode 11.4+ or a Swift 5.2+ toolchain with the Swift Package Manager.
Swift Package Manager
Add .package(url: "https://github.com/steipete/InterposeKit.git", from: "0.0.1")
to your
Package.swift
file's dependencies
.
CocoaPods
Add pod 'InterposeKit'
to your Podfile
.
Carthage
Add github "steipete/InterposeKit"
to your Cartfile
.
Improvement Ideas
- Write proposal to allow to convert the calling convention of existing types.
- Use the C block struct to perfom type checking between Method type and C type (I do that in Aspects library), it's still a runtime crash but could be at hook time, not when we call it.
- Add object-based hooking with dynamic subclassing (Aspects again)
- Add dyld_dynamic_interpose to hook pure C functions
- Combine Promise-API for
Interpose.whenAvailable
for better error bubbling. - Experiment with Swift hooking?
⚡️ - Test against Swift Nightly as Chron Jpb
- I'm sure there's more - Pull Requests or comments very welcome!
Thanks
Special thanks to JP Simard who did such a great job in setting up Yams with GitHub Actions - this was extremely helpful to build CI here fast.
License
InterposeKit is MIT Licensed.