Skip to content

Commit d541340

Browse files
committed
Initial README rework
1 parent c128b85 commit d541340

File tree

2 files changed

+158
-170
lines changed

2 files changed

+158
-170
lines changed

README-legacy.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
Instead of [adding new methods and exchanging implementations](https://nshipster.com/method-swizzling/) based on [`method_exchangeImplementations`](https://developer.apple.com/documentation/objectivec/1418769-method_exchangeimplementations), this library replaces the implementation directly using [`class_replaceMethod`](https://developer.apple.com/documentation/objectivec/1418677-class_replacemethod). This avoids some of [the usual problems with swizzling](https://pspdfkit.com/blog/2019/swizzling-in-swift/).
2+
3+
## Usage
4+
5+
Let's say you want to amend `sayHi` from `TestClass`:
6+
7+
```swift
8+
class TestClass: NSObject {
9+
// Functions need to be marked as `@objc dynamic` or written in Objective-C.
10+
@objc dynamic func sayHi() -> String {
11+
print("Calling sayHi")
12+
return "Hi there 👋"
13+
}
14+
}
15+
16+
let interposer = try Interpose(TestClass.self) {
17+
try $0.prepareHook(
18+
#selector(TestClass.sayHi),
19+
methodSignature: (@convention(c) (AnyObject, Selector) -> String).self,
20+
hookSignature: (@convention(block) (AnyObject) -> String).self) {
21+
store in { `self` in
22+
print("Before Interposing \(`self`)")
23+
let string = store.original(`self`, store.selector) // free to skip
24+
print("After Interposing \(`self`)")
25+
return string + "and Interpose"
26+
}
27+
}
28+
}
29+
30+
// Don't need the hook anymore? Undo is built-in!
31+
interposer.revert()
32+
```
33+
34+
Want to hook just a single instance? No problem!
35+
36+
```swift
37+
let hook = try testObj.hook(
38+
#selector(TestClass.sayHi),
39+
methodSignature: (@convention(c) (AnyObject, Selector) -> String).self,
40+
hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { `self` in
41+
return store.original(`self`, store.selector) + "just this instance"
42+
}
43+
}
44+
```
45+
46+
Here's what we get when calling `print(TestClass().sayHi())`
47+
```
48+
[Interposer] Swizzled -[TestClass.sayHi] IMP: 0x000000010d9f4430 -> 0x000000010db36020
49+
Before Interposing <InterposeTests.TestClass: 0x7fa0b160c1e0>
50+
Calling sayHi
51+
After Interposing <InterposeTests.TestClass: 0x7fa0b160c1e0>
52+
Hi there 👋 and Interpose
53+
```
54+
55+
## Object Hooking
56+
57+
InterposeKit can hook classes and object. Class hooking is similar to swizzling, but object-based hooking offers a variety of new ways to set hooks. This is achieved via creating a dynamic subclass at runtime.
58+
59+
Caveat: Hooking will fail with an error if the object uses KVO. The KVO machinery is fragile and it's to easy to cause a crash. Using KVO after a hook was created is supported and will not cause issues.
60+
61+
## Various ways to define the signature
62+
63+
Next to using `methodSignature` and `hookSignature`, following variants to define the signature are also possible:
64+
65+
### methodSignature + casted block
66+
```
67+
let interposer = try Interpose(testObj) {
68+
try $0.hook(
69+
#selector(TestClass.sayHi),
70+
methodSignature: (@convention(c) (AnyObject, Selector) -> String).self) { store in { `self` in
71+
let string = store.original(`self`, store.selector)
72+
return string + testString
73+
} as @convention(block) (AnyObject) -> String }
74+
}
75+
```
76+
77+
### Define type via store object
78+
```
79+
// Functions need to be `@objc dynamic` to be hookable.
80+
let interposer = try Interpose(testObj) {
81+
try $0.hook(#selector(TestClass.returnInt)) { (store: TypedHook<@convention(c) (AnyObject, Selector) -> Int, @convention(block) (AnyObject) -> Int>) in {
82+
83+
// You're free to skip calling the original implementation.
84+
let int = store.original($0, store.selector)
85+
return int + returnIntOverrideOffset
86+
}
87+
}
88+
}
89+
```
90+
91+
## FAQ
92+
93+
### Why didn't you call it Interpose? "Kit" feels so old-school.
94+
Naming it Interpose was the plan, but then [SR-898](https://bugs.swift.org/browse/SR-898) came. While having a class with the same name as the module works [in most cases](https://forums.swift.org/t/frameworkname-is-not-a-member-type-of-frameworkname-errors-inside-swiftinterface/28962), [this breaks](https://twitter.com/BalestraPatrick/status/1260928023357878273) when you enable build-for-distribution. There's some [discussion](https://forums.swift.org/t/pitch-fully-qualified-name-syntax/28482/81) to get that fixed, but this will be more towards end of 2020, if even.
95+
96+
### I want to hook into Swift! You made another ObjC swizzle thingy, why?
97+
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. (See [Dynamic function replacement #20333](https://github.com/apple/swift/pull/20333) aka `@_dynamicReplacement` for details.)
98+
99+
### Can I ship this?
100+
Yes, absolutely. The goal for this one project is a simple library that doesn't try to be too smart. I did this in [Aspects](https://github.com/steipete/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](https://github.com/steipete/Aspects/issues/21)".
101+
102+
### It does not do X!
103+
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.
104+
105+
## Installation
106+
107+
Building InterposeKit requires Xcode 15+ or a Swift 5.9+ toolchain with the Swift Package Manager.
108+
109+
### Swift Package Manager
110+
111+
Add `.package(url: "https://github.com/steipete/InterposeKit.git", from: "0.0.1")` to your
112+
`Package.swift` file's `dependencies`.
113+
114+
## Improvement Ideas
115+
116+
- Write proposal to allow to [convert the calling convention of existing types](https://twitter.com/steipete/status/1266799174563041282?s=21).
117+
- Use the C block struct to perform type checking between Method type and C type (I do that in [Aspects library](https://github.com/steipete/Aspects)), it's still a runtime crash but could be at hook time, not when we call it.
118+
- Add a way to get all current hooks from an object/class.
119+
- Add a way to revert hooks without super helper.
120+
- Add a way to apply multiple hooks to classes
121+
- Enable hooking of class methods.
122+
- Add [dyld_dynamic_interpose](https://twitter.com/steipete/status/1258482647933870080) to hook pure C functions
123+
- Combine Promise-API for `Interpose.whenAvailable` for better error bubbling.
124+
- Experiment with [Swift function hooking](https://github.com/rodionovd/SWRoute/wiki/Function-hooking-in-Swift)? ⚡️
125+
- Test against Swift Nightly as Cron Job
126+
- Switch to Trampolines to manage cases where other code overrides super, so we end up with a super call that's [not on top of the class hierarchy](https://github.com/steipete/InterposeKit/pull/15#discussion_r439871752).

0 commit comments

Comments
 (0)