Skip to content

Commit fc93a70

Browse files
Cleanup hook blocks (#14)
Co-authored-by: Victor Pavlychko <[email protected]>
1 parent ff5f424 commit fc93a70

File tree

5 files changed

+113
-1
lines changed

5 files changed

+113
-1
lines changed
File renamed without changes.

Interpose.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist renamed to InterposeKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

File renamed without changes.

Interpose.xcworkspace/xcshareddata/swiftpm/Package.resolved renamed to InterposeKit.xcworkspace/xcshareddata/swiftpm/Package.resolved

File renamed without changes.

Sources/InterposeKit/InterposeKit.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ final public class Interpose {
2929
}
3030
}
3131

32+
deinit {
33+
tasks.forEach({ $0.cleanup() })
34+
}
35+
3236
/// Hook an `@objc dynamic` instance method via selector name on the current class.
3337
@discardableResult public func hook(_ selName: String,
3438
_ implementation: (Task) -> Any) throws -> Task {
@@ -146,6 +150,19 @@ extension Interpose {
146150
try execute(newState: .prepared) { try resetImplementation() }
147151
}
148152

153+
/// Release the hook block if possible.
154+
fileprivate func cleanup() {
155+
switch state {
156+
case .prepared:
157+
Interpose.log("Releasing -[\(`class`).\(selector)] IMP: \(replacementIMP!)")
158+
imp_removeBlock(replacementIMP)
159+
case .interposed:
160+
Interpose.log("Keeping -[\(`class`).\(selector)] IMP: \(replacementIMP!)")
161+
case let .error(error):
162+
Interpose.log("Leaking -[\(`class`).\(selector)] IMP: \(replacementIMP!) due to error: \(error)")
163+
}
164+
}
165+
149166
private func execute(newState: State, task: () throws -> Void) throws {
150167
do {
151168
try task()
@@ -324,4 +341,5 @@ func method_getTypeEncoding(_ m: Method) -> UnsafePointer<Int8>? { return nil }
324341
// swiftlint:disable:next identifier_name
325342
func _dyld_register_func_for_add_image(_ func: (@convention(c) (UnsafePointer<Int8>?, Int) -> Void)!) {}
326343
func imp_implementationWithBlock(_ block: Any) -> IMP { IMP() }
344+
func imp_removeBlock(_ anImp: IMP) -> Bool { false }
327345
#endif

Tests/InterposeKitTests/InterposeKitTests.swift

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ class TestClass: NSObject {
1010
print(testClassHi)
1111
return testClassHi
1212
}
13+
14+
@objc dynamic func doNothing() { }
1315
}
1416

1517
class TestSubclass: TestClass {
@@ -93,8 +95,100 @@ final class InterposeKitTests: XCTestCase {
9395
XCTAssertEqual(testObj.sayHi(), testClassHi + testSubclass)
9496
}
9597

98+
func testInterposedCleanup() throws {
99+
var deallocated = false
100+
101+
try autoreleasepool {
102+
let tracker = LifetimeTracker {
103+
deallocated = true
104+
}
105+
106+
// Swizzle test class
107+
let interposer = try Interpose(TestClass.self) {
108+
try $0.hook(#selector(TestClass.doNothing), { store in { `self` in
109+
tracker.keep()
110+
let origCall = store((@convention(c) (AnyObject, Selector) -> Void).self)
111+
return origCall(`self`, store.selector)
112+
} as @convention(block) (AnyObject) -> Void })
113+
}
114+
115+
// Dealloc interposer without removing hooks
116+
_ = interposer
117+
}
118+
119+
// Unreverted block should not be deallocated
120+
XCTAssertFalse(deallocated)
121+
}
122+
123+
func testRevertedCleanup() throws {
124+
var deallocated = false
125+
126+
try autoreleasepool {
127+
let tracker = LifetimeTracker {
128+
deallocated = true
129+
}
130+
131+
// Swizzle test class
132+
let interposer = try Interpose(TestClass.self) {
133+
try $0.hook(#selector(TestClass.doNothing), { store in { `self` in
134+
tracker.keep()
135+
let origCall = store((@convention(c) (AnyObject, Selector) -> Void).self)
136+
return origCall(`self`, store.selector)
137+
} as @convention(block) (AnyObject) -> Void })
138+
}
139+
140+
try interposer.revert()
141+
}
142+
143+
// Verify that the block was deallocated
144+
XCTAssertTrue(deallocated)
145+
}
146+
147+
func testImpRemoveBlockWorks() {
148+
var deallocated = false
149+
150+
let imp: IMP = autoreleasepool {
151+
let tracker = LifetimeTracker {
152+
deallocated = true
153+
}
154+
155+
let block: @convention(block) (AnyObject) -> Void = { _ in
156+
// retain `tracker` inside a block
157+
tracker.keep()
158+
}
159+
160+
return imp_implementationWithBlock(block)
161+
}
162+
163+
// `imp` retains `block` which retains `tracker`
164+
XCTAssertFalse(deallocated)
165+
166+
// Detach `block` from `imp`
167+
imp_removeBlock(imp)
168+
169+
// `block` and `tracker` should be deallocated now
170+
XCTAssertTrue(deallocated)
171+
}
172+
173+
class LifetimeTracker {
174+
let deinitCalled: () -> Void
175+
176+
init(deinitCalled: @escaping () -> Void) {
177+
self.deinitCalled = deinitCalled
178+
}
179+
180+
deinit {
181+
deinitCalled()
182+
}
183+
184+
func keep() { }
185+
}
186+
96187
static var allTests = [
97188
("testClassOverrideAndRevert", testClassOverrideAndRevert),
98-
("testSubclassOverride", testSubclassOverride)
189+
("testSubclassOverride", testSubclassOverride),
190+
("testInterposedCleanup", testInterposedCleanup),
191+
("testRevertedCleanup", testRevertedCleanup),
192+
("testImpRemoveBlockWorks", testImpRemoveBlockWorks)
99193
]
100194
}

0 commit comments

Comments
 (0)