From f4287f3fe829dabd92b9161ca1ca6636576fe0e7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 6 Jun 2020 13:29:11 +0200 Subject: [PATCH 01/77] add sample for segfault --- InterposeKit.xcodeproj/project.pbxproj | 12 +- Sources/InterposeKit/ClassTask.swift | 84 ++++++++ Sources/InterposeKit/InterposeKit.swift | 165 ++++++--------- Sources/InterposeKit/ObjectTask.swift | 193 ++++++++++++++++++ .../ObjectInterposeTests.swift | 45 ++++ 5 files changed, 395 insertions(+), 104 deletions(-) create mode 100644 Sources/InterposeKit/ClassTask.swift create mode 100644 Sources/InterposeKit/ObjectTask.swift create mode 100644 Tests/InterposeKitTests/ObjectInterposeTests.swift diff --git a/InterposeKit.xcodeproj/project.pbxproj b/InterposeKit.xcodeproj/project.pbxproj index 5974105..1b83021 100644 --- a/InterposeKit.xcodeproj/project.pbxproj +++ b/InterposeKit.xcodeproj/project.pbxproj @@ -11,7 +11,9 @@ 78C39D8F2483164500B46395 /* InterposeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 78C39D8E2483164500B46395 /* InterposeKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 78C39D912483165600B46395 /* InterposeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C39D902483165600B46395 /* InterposeKit.swift */; }; 78C39D932483169300B46395 /* InterposeKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C39D922483169300B46395 /* InterposeKitTests.swift */; }; - 78EDB8D5248B9BB500D2F6C1 /* TestClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */; }; + 78EDB8DA248BA9B300D2F6C1 /* TestClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */; }; + 78EDB8DB248BA9BB00D2F6C1 /* ObjectInterposeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */; }; + 78EDB8DD248BAA5600D2F6C1 /* ClassTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8DC248BAA5600D2F6C1 /* ClassTask.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -37,6 +39,8 @@ 78C39DC0248317B400B46395 /* Defaults.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Defaults.xcconfig; sourceTree = ""; }; 78C39DC1248317B400B46395 /* Defaults-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Defaults-Debug.xcconfig"; sourceTree = ""; }; 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TestClass.swift; path = Tests/InterposeKitTests/TestClass.swift; sourceTree = SOURCE_ROOT; }; + 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ObjectInterposeTests.swift; path = Tests/InterposeKitTests/ObjectInterposeTests.swift; sourceTree = SOURCE_ROOT; }; + 78EDB8DC248BAA5600D2F6C1 /* ClassTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ClassTask.swift; path = Sources/InterposeKit/ClassTask.swift; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -82,6 +86,7 @@ children = ( 78863ECA2464B2F900BA3762 /* Info.plist */, 78C39D8E2483164500B46395 /* InterposeKit.h */, + 78EDB8DC248BAA5600D2F6C1 /* ClassTask.swift */, 78C39D902483165600B46395 /* InterposeKit.swift */, ); path = InterposeKit; @@ -93,6 +98,7 @@ 78C39D922483169300B46395 /* InterposeKitTests.swift */, 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */, 78C39D7B2482CC7D00B46395 /* Info-Tests.plist */, + 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */, ); path = InterposeTests; sourceTree = ""; @@ -219,8 +225,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 78EDB8D5248B9BB500D2F6C1 /* TestClass.swift in Sources */, 78C39D912483165600B46395 /* InterposeKit.swift in Sources */, + 78EDB8DD248BAA5600D2F6C1 /* ClassTask.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -229,6 +235,8 @@ buildActionMask = 2147483647; files = ( 78C39D932483169300B46395 /* InterposeKitTests.swift in Sources */, + 78EDB8DA248BA9B300D2F6C1 /* TestClass.swift in Sources */, + 78EDB8DB248BA9BB00D2F6C1 /* ObjectInterposeTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/InterposeKit/ClassTask.swift b/Sources/InterposeKit/ClassTask.swift new file mode 100644 index 0000000..dd9bf21 --- /dev/null +++ b/Sources/InterposeKit/ClassTask.swift @@ -0,0 +1,84 @@ +import Foundation + +/// A task represents a hook to an instance method and stores both the original and new implementation. +final public class ClassTask: ValidatableTask { + public let `class`: AnyClass + public let selector: Selector + public private(set) var origIMP: IMP? // fetched at apply time, changes late, thus class requirement + public private(set) var replacementIMP: IMP! // else we validate init order + public private(set) var state = Interpose.State.prepared + + /// Initialize a new task to interpose an instance method. + public init(`class`: AnyClass, selector: Selector, implementation: (Task) -> Any) throws { + self.selector = selector + self.class = `class` + // Check if method exists + try validate() + replacementIMP = imp_implementationWithBlock(implementation(self)) + } + + /// Validate that the selector exists on the active class. + @discardableResult public func validate(expectedState: Interpose.State = .prepared) throws -> Method { + guard let method = class_getInstanceMethod(`class`, selector) else { throw Interpose.Error.methodNotFound } + guard state == expectedState else { throw Interpose.Error.invalidState } + return method + } + + public func apply() throws { + try execute(newState: .interposed) { try replaceImplementation() } + } + + public func revert() throws { + try execute(newState: .prepared) { try resetImplementation() } + } + + /// Release the hook block if possible. + public func cleanup() { + switch state { + case .prepared: + Interpose.log("Releasing -[\(`class`).\(selector)] IMP: \(replacementIMP!)") + imp_removeBlock(replacementIMP) + case .interposed: + Interpose.log("Keeping -[\(`class`).\(selector)] IMP: \(replacementIMP!)") + case let .error(error): + Interpose.log("Leaking -[\(`class`).\(selector)] IMP: \(replacementIMP!) due to error: \(error)") + } + } + + private func execute(newState: Interpose.State, task: () throws -> Void) throws { + do { + try task() + state = newState + } catch let error as Interpose.Error { + state = .error(error) + throw error + } + } + + private func replaceImplementation() throws { + let method = try validate() + origIMP = class_replaceMethod(`class`, selector, replacementIMP, method_getTypeEncoding(method)) + guard origIMP != nil else { throw Interpose.Error.nonExistingImplementation } + Interpose.log("Swizzled -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") + } + + private func resetImplementation() throws { + let method = try validate(expectedState: .interposed) + precondition(origIMP != nil) + let previousIMP = class_replaceMethod(`class`, selector, origIMP!, method_getTypeEncoding(method)) + guard previousIMP == replacementIMP else { throw Interpose.Error.unexpectedImplementation } + Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") + } + + public func callAsFunction(_ type: U.Type) -> U { + unsafeBitCast(origIMP, to: type) + } +} + +#if DEBUG +extension ClassTask: CustomDebugStringConvertible { + public var debugDescription: String { + return "\(selector) -> \(String(describing: origIMP))" + } +} +#endif diff --git a/Sources/InterposeKit/InterposeKit.swift b/Sources/InterposeKit/InterposeKit.swift index 9fac914..a05e5b0 100644 --- a/Sources/InterposeKit/InterposeKit.swift +++ b/Sources/InterposeKit/InterposeKit.swift @@ -18,10 +18,25 @@ final public class Interpose { /// Lists all tasks for the current interpose class object. public private(set) var tasks: [Task] = [] + /// If Interposing is object-based, this is set. + public let object: AnyObject? + /// Initializes an instance of Interpose for a specific class. /// If `builder` is present, `apply()` is automatically called. public init(_ `class`: AnyClass, builder: ((Interpose) throws -> Void)? = nil) throws { self.class = `class` + self.object = nil + + // Only apply if a builder is present + if let builder = builder { + try apply(builder) + } + } + + /// Initialize with a single object to interpose. + public init(_ object: AnyObject, builder: ((Interpose) throws -> Void)? = nil) throws { + self.object = object + self.class = type(of: object) // Only apply if a builder is present if let builder = builder { @@ -30,7 +45,8 @@ final public class Interpose { } deinit { - tasks.forEach({ $0.cleanup() }) + guard let validatableTasks = tasks as? [ValidatableTask] else { return } + validatableTasks.forEach({ $0.cleanup() }) } /// Hook an `@objc dynamic` instance method via selector name on the current class. @@ -42,7 +58,13 @@ final public class Interpose { /// Hook an `@objc dynamic` instance method via selector on the current class. @discardableResult public func hook(_ selector: Selector, _ implementation: (Task) -> Any) throws -> Task { - let task = try Task(class: `class`, selector: selector, implementation: implementation) + + var task: ValidatableTask + if let object = self.object { + task = try ObjectTask(object: object, selector: selector, implementation: implementation) + } else { + task = try ClassTask(class: `class`, selector: selector, implementation: implementation) + } tasks.append(task) return task } @@ -58,14 +80,14 @@ final public class Interpose { } private func execute(_ task: ((Interpose) throws -> Void)? = nil, - expectedState: Task.State = .prepared, + expectedState: Interpose.State = .prepared, executor: ((Task) throws -> Void)) throws -> Interpose { // Run pre-apply code first if let task = task { try task(self) } // Validate all tasks, stop if anything is not valid - guard tasks.allSatisfy({ + guard let validatableTasks = tasks as? [ValidatableTask], validatableTasks.allSatisfy({ (try? $0.validate(expectedState: expectedState)) != nil }) else { throw Error.invalidState @@ -87,112 +109,59 @@ final public class Interpose { /// This is bad, likely someone else also hooked this method. If you are in such a codebase, do not use revert. case unexpectedImplementation + case failedToAllocateClassPair + /// Can't revert or apply if already done so. case invalidState } -} - -// MARK: Interpose Task - -extension Interpose { - /// A task represents a hook to an instance method and stores both the original and new implementation. - final public class Task { - /// The class this tasks operates on. - public let `class`: AnyClass - - /// The selector this tasks operates on. - public let selector: Selector - - /// The original implementation is set once the swizzling is complete. - public private(set) var origIMP: IMP? // fetched at apply time, changes late, thus class requirement - /// The replacement implementation is created on initialization time. - public private(set) var replacementIMP: IMP! // else we validate init order + /// The possible task states. + public enum State: Equatable { + /// The task is prepared to be interposed. + case prepared - /// The state of the interpose operation. - public private(set) var state = State.prepared + /// The method has been successfully interposed. + case interposed - /// The possible task states. - public enum State: Equatable { - /// The task is prepared to be interposed. - case prepared + /// An error happened while interposing a method. + case error(Interpose.Error) + } +} - /// The method has been successfully interposed. - case interposed +// MARK: Interpose Task - /// An error happened while interposing a method. - case error(Error) - } +public protocol Task { + /// The class this tasks operates on. + var `class`: AnyClass { get } - /// Initialize a new task to interpose an instance method. - public init(`class`: AnyClass, selector: Selector, implementation: (Task) -> Any) throws { - self.selector = selector - self.class = `class` - // Check if method exists - try validate() - replacementIMP = imp_implementationWithBlock(implementation(self)) - } + /// If Interposing is object-based, this is set. + //var object: AnyObject? { get } - /// Validate that the selector exists on the active class. - @discardableResult func validate(expectedState: State = .prepared) throws -> Method { - guard let method = class_getInstanceMethod(`class`, selector) else { throw Error.methodNotFound } - guard state == expectedState else { throw Error.invalidState } - return method - } + /// The selector this tasks operates on. + var selector: Selector { get } - /// Apply the interpose hook. - public func apply() throws { - try execute(newState: .interposed) { try replaceImplementation() } - } + /// The original implementation is set once the swizzling is complete. + var origIMP: IMP? { get } - /// Revert the interpose hoook. - public func revert() throws { - try execute(newState: .prepared) { try resetImplementation() } - } + /// The replacement implementation is created on initialization time. + var replacementIMP: IMP! { get } - /// Release the hook block if possible. - fileprivate func cleanup() { - switch state { - case .prepared: - Interpose.log("Releasing -[\(`class`).\(selector)] IMP: \(replacementIMP!)") - imp_removeBlock(replacementIMP) - case .interposed: - Interpose.log("Keeping -[\(`class`).\(selector)] IMP: \(replacementIMP!)") - case let .error(error): - Interpose.log("Leaking -[\(`class`).\(selector)] IMP: \(replacementIMP!) due to error: \(error)") - } - } + /// The state of the interpose operation. + var state: Interpose.State { get } - private func execute(newState: State, task: () throws -> Void) throws { - do { - try task() - state = newState - } catch let error as Error { - state = .error(error) - throw error - } - } + /// Apply the interpose hook. + func apply() throws - private func replaceImplementation() throws { - let method = try validate() - origIMP = class_replaceMethod(`class`, selector, replacementIMP, method_getTypeEncoding(method)) - guard origIMP != nil else { throw Error.nonExistingImplementation } - Interpose.log("Swizzled -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") - } + /// Revert the interpose hoook. + func revert() throws - private func resetImplementation() throws { - let method = try validate(expectedState: .interposed) - precondition(origIMP != nil) - let previousIMP = class_replaceMethod(`class`, selector, origIMP!, method_getTypeEncoding(method)) - guard previousIMP == replacementIMP else { throw Error.unexpectedImplementation } - Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") - } + /// Convenience to call the original implementation. + func callAsFunction(_ type: U.Type) -> U +} - /// Convenience to call the original implementation. - public func callAsFunction(_ type: U.Type) -> U { - unsafeBitCast(origIMP, to: type) - } - } +public protocol ValidatableTask: Task { + func validate(expectedState: Interpose.State) throws -> Method + func cleanup() } // MARK: Logging @@ -202,7 +171,7 @@ extension Interpose { public static var isLoggingEnabled = false /// Simple log wrapper for print. - fileprivate class func log(_ object: Any) { + class func log(_ object: Any) { if isLoggingEnabled { print("[Interposer] \(object)") } @@ -316,14 +285,6 @@ private struct InterposeWatcher { // MARK: Debug Helper -#if DEBUG -extension Interpose.Task: CustomDebugStringConvertible { - public var debugDescription: String { - return "\(selector) -> \(String(describing: origIMP))" - } -} -#endif - #if os(Linux) // Linux is used to create Jazzy docs /// :nodoc: Selector diff --git a/Sources/InterposeKit/ObjectTask.swift b/Sources/InterposeKit/ObjectTask.swift new file mode 100644 index 0000000..7aabf77 --- /dev/null +++ b/Sources/InterposeKit/ObjectTask.swift @@ -0,0 +1,193 @@ +import Foundation + +private enum Constants { + static let subclassSuffix = "_InterposeKit_" +} + +internal enum ObjCSelector { +// static let forwardInvocation = Selector((("forwardInvocation:"))) +// static let methodSignatureForSelector = Selector((("methodSignatureForSelector:"))) + static let getClass = Selector((("class"))) +} + +internal enum ObjCMethodEncoding { +// static let forwardInvocation = extract("v@:@") +// static let methodSignatureForSelector = extract("v@::") + static let getClass = extract("#@:") + + private static func extract(_ string: StaticString) -> UnsafePointer { + return UnsafeRawPointer(string.utf8Start).assumingMemoryBound(to: CChar.self) + } +} + + +/// A task represents a hook to an instance method of a single object and stores both the original and new implementation. +final public class ObjectTask: ValidatableTask { + public let `class`: AnyClass + public let object: AnyObject + public let selector: Selector + public private(set) var origIMP: IMP? // fetched at apply time, changes late, thus class requirement + public private(set) var replacementIMP: IMP! // else we validate init order + public private(set) var state = Interpose.State.prepared + + /// Initialize a new task to interpose an instance method. + public init(object: AnyObject, selector: Selector, implementation: (Task) -> Any) throws { + self.selector = selector + self.object = object + self.class = type(of: object) + // Check if method exists + try validate() + replacementIMP = imp_implementationWithBlock(implementation(self)) + } + + class KVOObserver: NSObject { + @objc var objectToObserve: AnyObject + var observation: NSKeyValueObservation? + + init(object: AnyObject) { + objectToObserve = object + super.init() + + observation = observe( + \.objectToObserve.description, + options: [.new] + ) { object, change in + print("myDate changed to: \(String(describing: change.newValue))") + } + } + } + + // Before creating our subclass, we trigger KVO. + // KVO also creates a subclass at runtime. If we do this prior, then KVO fails. + // If KVO runs prior, and then we sub-subclass, everything works. + var kvoObserver: KVOObserver? + private func registerKVO() { + kvoObserver = KVOObserver(object: object) + //object.addObserver(self, forKeyPath: "description", options: .new, context: nil) +// kvoToken = observe(\.object.description, options: .new) { (obj, change) in +// guard let description = change.new else { return } +// print("New description is: \(description)") +// } + } + + private func createSubclass() throws -> AnyClass { + let perceivedClass = `class` + let className = NSStringFromClass(perceivedClass) + let subclassName = Constants.subclassSuffix + className + + let subclass: AnyClass? = subclassName.withCString { cString in + if let existingClass = objc_getClass(cString) as! AnyClass? { + return existingClass + } else { + if let subclass: AnyClass = objc_allocateClassPair(perceivedClass, cString, 0) { + replaceGetClass(in: subclass, decoy: perceivedClass) + objc_registerClassPair(subclass) + return subclass + } else { + return nil + } + } + } + + guard let nonnullSubclass = subclass else { + throw Interpose.Error.failedToAllocateClassPair + } + + object_setClass(object, nonnullSubclass) + return nonnullSubclass + + } + + private func replaceGetClass(in class: AnyClass, decoy perceivedClass: AnyClass) { + let getClass: @convention(block) (UnsafeRawPointer?) -> AnyClass = { _ in + perceivedClass + } + + let impl = imp_implementationWithBlock(getClass as Any) + + _ = class_replaceMethod(`class`, ObjCSelector.getClass, + impl, + ObjCMethodEncoding.getClass) + + _ = class_replaceMethod(object_getClass(`class`), + ObjCSelector.getClass, + impl, + ObjCMethodEncoding.getClass) + } + + + + + + + + + /// Validate that the selector exists on the active class. + @discardableResult public func validate(expectedState: Interpose.State = .prepared) throws -> Method { + guard let method = class_getInstanceMethod(`class`, selector) else { throw Interpose.Error.methodNotFound } + guard state == expectedState else { throw Interpose.Error.invalidState } + return method + } + + public func apply() throws { + try execute(newState: .interposed) { try replaceImplementation() } + } + + public func revert() throws { + try execute(newState: .prepared) { try resetImplementation() } + } + + /// Release the hook block if possible. + public func cleanup() { + switch state { + case .prepared: + Interpose.log("Releasing -[\(`class`).\(selector)] IMP: \(replacementIMP!)") + imp_removeBlock(replacementIMP) + case .interposed: + Interpose.log("Keeping -[\(`class`).\(selector)] IMP: \(replacementIMP!)") + case let .error(error): + Interpose.log("Leaking -[\(`class`).\(selector)] IMP: \(replacementIMP!) due to error: \(error)") + } + } + + private func execute(newState: Interpose.State, task: () throws -> Void) throws { + do { + try task() + state = newState + } catch let error as Interpose.Error { + state = .error(error) + throw error + } + } + + private func replaceImplementation() throws { + let method = try validate() + + registerKVO() + let subclass = try createSubclass() + + origIMP = class_replaceMethod(subclass, selector, replacementIMP, method_getTypeEncoding(method)) + guard origIMP != nil else { throw Interpose.Error.nonExistingImplementation } + Interpose.log("Swizzled -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") + } + + private func resetImplementation() throws { + let method = try validate(expectedState: .interposed) + precondition(origIMP != nil) + let previousIMP = class_replaceMethod(`class`, selector, origIMP!, method_getTypeEncoding(method)) + guard previousIMP == replacementIMP else { throw Interpose.Error.unexpectedImplementation } + Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") + } + + public func callAsFunction(_ type: U.Type) -> U { + unsafeBitCast(origIMP, to: type) + } +} + +#if DEBUG +extension ObjectTask: CustomDebugStringConvertible { + public var debugDescription: String { + return "\(selector) -> \(String(describing: origIMP))" + } +} +#endif diff --git a/Tests/InterposeKitTests/ObjectInterposeTests.swift b/Tests/InterposeKitTests/ObjectInterposeTests.swift new file mode 100644 index 0000000..44fc09d --- /dev/null +++ b/Tests/InterposeKitTests/ObjectInterposeTests.swift @@ -0,0 +1,45 @@ +import Foundation +import XCTest +@testable import InterposeKit + +final class ObjectInterposeTests: XCTestCase { + + override func setUpWithError() throws { + Interpose.isLoggingEnabled = true + } + + func testInterposeSingleObject() throws { + let testObj = TestClass() + let testObj2 = TestClass() + + XCTAssertEqual(testObj.sayHi(), testClassHi) + XCTAssertEqual(testObj2.sayHi(), testClassHi) + + // Functions need to be `@objc dynamic` to be hookable. + let interposer = try Interpose(testObj) { + try $0.hook(#selector(TestClass.sayHi), { store in { `self` in + + print("Before Interposing \(`self`)") + + // Calling convention and passing selector is important! + // You're free to skip calling the original implementation. + let origCall = store((@convention(c) (AnyObject, Selector) -> String).self) + let string = origCall(`self`, store.selector) + + print("After Interposing \(`self`)") + + return string + testSwizzleAddition + + // Similar signature cast as above, but without selector. + } as @convention(block) (AnyObject) -> String}) + } + XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition) + XCTAssertEqual(testObj2.sayHi(), testClassHi) + try interposer.revert() + XCTAssertEqual(testObj.sayHi(), testClassHi) + XCTAssertEqual(testObj2.sayHi(), testClassHi) + try interposer.apply() + XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition) + XCTAssertEqual(testObj2.sayHi(), testClassHi) + } +} From 807a04755baceeef05d8d9cc6c5dbf92b9d94f31 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 6 Jun 2020 14:23:12 +0200 Subject: [PATCH 02/77] push changes for abort trap 6 --- Sources/InterposeKit/ObjectTask.swift | 45 +++++++++++++-------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/Sources/InterposeKit/ObjectTask.swift b/Sources/InterposeKit/ObjectTask.swift index 7aabf77..815c6bd 100644 --- a/Sources/InterposeKit/ObjectTask.swift +++ b/Sources/InterposeKit/ObjectTask.swift @@ -48,12 +48,8 @@ final public class ObjectTask: ValidatableTask { objectToObserve = object super.init() - observation = observe( - \.objectToObserve.description, - options: [.new] - ) { object, change in - print("myDate changed to: \(String(describing: change.newValue))") - } + // Can't use modern syntax cause https://bugs.swift.org/browse/SR-12944 + objectToObserve.addObserver(self, forKeyPath: "description", options: .new, context: nil) } } @@ -71,7 +67,7 @@ final public class ObjectTask: ValidatableTask { } private func createSubclass() throws -> AnyClass { - let perceivedClass = `class` + let perceivedClass: AnyClass = `class` let className = NSStringFromClass(perceivedClass) let subclassName = Constants.subclassSuffix + className @@ -104,24 +100,26 @@ final public class ObjectTask: ValidatableTask { } let impl = imp_implementationWithBlock(getClass as Any) - - _ = class_replaceMethod(`class`, ObjCSelector.getClass, - impl, - ObjCMethodEncoding.getClass) - - _ = class_replaceMethod(object_getClass(`class`), - ObjCSelector.getClass, - impl, - ObjCMethodEncoding.getClass) + _ = class_replaceMethod(`class`, ObjCSelector.getClass, impl, ObjCMethodEncoding.getClass) + _ = class_replaceMethod(object_getClass(`class`), ObjCSelector.getClass, impl, ObjCMethodEncoding.getClass) } + private func addSuperTrampolineMethod(subclass: AnyClass, method: Method) { + let typeEncoding = method_getTypeEncoding(method) + + let handle = dlopen(nil, RTLD_LAZY); + // https://opensource.apple.com/source/objc4/objc4-493.9/runtime/objc-abi.h + // objc_msgSendSuper2() takes the current search class, not its superclass. + // OBJC_EXPORT id objc_msgSendSuper2(struct objc_super *super, SEL op, ...) + let sendSuper2 = dlsym(handle, "objc_msgSendSuper2"); + let block: @convention(block) (AnyObject, va_list) -> Void = { obj, vaList in + let superStruct = objc_super(receiver: obj as! Unmanaged, super_class: subclass) + unsafeBitCast(sendSuper2, to: (@convention(c) (objc_super, Selector, va_list) -> Void).self)(superStruct, self.selector, vaList) + } + class_addMethod(subclass, self.selector, imp_implementationWithBlock(block), typeEncoding) + } - - - - - - + /// Validate that the selector exists on the active class. @discardableResult public func validate(expectedState: Interpose.State = .prepared) throws -> Method { guard let method = class_getInstanceMethod(`class`, selector) else { throw Interpose.Error.methodNotFound } @@ -164,7 +162,8 @@ final public class ObjectTask: ValidatableTask { let method = try validate() registerKVO() - let subclass = try createSubclass() + let subclass: AnyClass = try createSubclass() + addSuperTrampolineMethod(subclass: subclass, method: method) origIMP = class_replaceMethod(subclass, selector, replacementIMP, method_getTypeEncoding(method)) guard origIMP != nil else { throw Interpose.Error.nonExistingImplementation } From e8a63b89247e2e09e5659e9f83b02d0bc5300605 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 6 Jun 2020 15:22:31 +0200 Subject: [PATCH 03/77] Cursed first version --- .../xcschemes/InterposeExample.xcscheme | 7 +++ Sources/InterposeKit/ObjectTask.swift | 46 ++++++++++++++----- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/Example/InterposeExample.xcodeproj/xcshareddata/xcschemes/InterposeExample.xcscheme b/Example/InterposeExample.xcodeproj/xcshareddata/xcschemes/InterposeExample.xcscheme index 4bacafa..3ca0587 100644 --- a/Example/InterposeExample.xcodeproj/xcshareddata/xcschemes/InterposeExample.xcscheme +++ b/Example/InterposeExample.xcodeproj/xcshareddata/xcschemes/InterposeExample.xcscheme @@ -28,6 +28,13 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" codeCoverageEnabled = "YES"> + + + + diff --git a/Sources/InterposeKit/ObjectTask.swift b/Sources/InterposeKit/ObjectTask.swift index 815c6bd..086463d 100644 --- a/Sources/InterposeKit/ObjectTask.swift +++ b/Sources/InterposeKit/ObjectTask.swift @@ -30,6 +30,9 @@ final public class ObjectTask: ValidatableTask { public private(set) var replacementIMP: IMP! // else we validate init order public private(set) var state = Interpose.State.prepared + // Subclass that we create on the fly + var dynamicSubclass: AnyClass? + /// Initialize a new task to interpose an instance method. public init(object: AnyObject, selector: Selector, implementation: (Task) -> Any) throws { self.selector = selector @@ -59,11 +62,6 @@ final public class ObjectTask: ValidatableTask { var kvoObserver: KVOObserver? private func registerKVO() { kvoObserver = KVOObserver(object: object) - //object.addObserver(self, forKeyPath: "description", options: .new, context: nil) -// kvoToken = observe(\.object.description, options: .new) { (obj, change) in -// guard let description = change.new else { return } -// print("New description is: \(description)") -// } } private func createSubclass() throws -> AnyClass { @@ -104,6 +102,11 @@ final public class ObjectTask: ValidatableTask { _ = class_replaceMethod(object_getClass(`class`), ObjCSelector.getClass, impl, ObjCMethodEncoding.getClass) } + struct objc_super_fake { + public var receiver: Unmanaged + public var super_class: AnyClass + } + private func addSuperTrampolineMethod(subclass: AnyClass, method: Method) { let typeEncoding = method_getTypeEncoding(method) @@ -111,10 +114,20 @@ final public class ObjectTask: ValidatableTask { // https://opensource.apple.com/source/objc4/objc4-493.9/runtime/objc-abi.h // objc_msgSendSuper2() takes the current search class, not its superclass. // OBJC_EXPORT id objc_msgSendSuper2(struct objc_super *super, SEL op, ...) + // TODO: This should be cached. let sendSuper2 = dlsym(handle, "objc_msgSendSuper2"); - let block: @convention(block) (AnyObject, va_list) -> Void = { obj, vaList in - let superStruct = objc_super(receiver: obj as! Unmanaged, super_class: subclass) - unsafeBitCast(sendSuper2, to: (@convention(c) (objc_super, Selector, va_list) -> Void).self)(superStruct, self.selector, vaList) + + let block: @convention(block) (AnyObject, va_list) -> AnyObject = { obj, vaList in + let raw = Unmanaged.passUnretained(obj) + let superStruct = objc_super_fake(receiver: raw, super_class: subclass) + let realSuperStruct = unsafeBitCast(superStruct, to: objc_super.self) + // This is extremely cursed: https://bugs.swift.org/browse/SR-12945 + // let realSuperStruct = objc_super(receiver: raw, super_class: subclass) + return withUnsafePointer(to: realSuperStruct) { realSuperStructPointer -> AnyObject in + return unsafeBitCast(sendSuper2, to: (@convention(c) (UnsafePointer, Selector, va_list) -> AnyObject).self)(realSuperStructPointer, self.selector, vaList) + } + // Equivalent in C: + // return ((id(*)(struct objc_super *, SEL, va_list))objc_msgSendSuper2)(&super, selector, argp); } class_addMethod(subclass, self.selector, imp_implementationWithBlock(block), typeEncoding) } @@ -122,6 +135,7 @@ final public class ObjectTask: ValidatableTask { /// Validate that the selector exists on the active class. @discardableResult public func validate(expectedState: Interpose.State = .prepared) throws -> Method { + // We need to validate on class, not the subclass guard let method = class_getInstanceMethod(`class`, selector) else { throw Interpose.Error.methodNotFound } guard state == expectedState else { throw Interpose.Error.invalidState } return method @@ -161,11 +175,17 @@ final public class ObjectTask: ValidatableTask { private func replaceImplementation() throws { let method = try validate() + // Register a KVO to work around any KVO issues with opposite order registerKVO() - let subclass: AnyClass = try createSubclass() - addSuperTrampolineMethod(subclass: subclass, method: method) - origIMP = class_replaceMethod(subclass, selector, replacementIMP, method_getTypeEncoding(method)) + // Register subclass at runtime if we haven't already + if dynamicSubclass == nil { + dynamicSubclass = try createSubclass() + } + // Add empty trampoline that we then replace the IMP! + addSuperTrampolineMethod(subclass: dynamicSubclass!, method: method) + + origIMP = class_replaceMethod(dynamicSubclass!, selector, replacementIMP, method_getTypeEncoding(method)) guard origIMP != nil else { throw Interpose.Error.nonExistingImplementation } Interpose.log("Swizzled -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") } @@ -173,7 +193,9 @@ final public class ObjectTask: ValidatableTask { private func resetImplementation() throws { let method = try validate(expectedState: .interposed) precondition(origIMP != nil) - let previousIMP = class_replaceMethod(`class`, selector, origIMP!, method_getTypeEncoding(method)) + precondition(dynamicSubclass != nil) + + let previousIMP = class_replaceMethod(dynamicSubclass!, selector, origIMP!, method_getTypeEncoding(method)) guard previousIMP == replacementIMP else { throw Interpose.Error.unexpectedImplementation } Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") } From 333ba4ce85385f3292c7466df103a8377474a7dd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 6 Jun 2020 15:25:59 +0200 Subject: [PATCH 04/77] cleanup --- Sources/InterposeKit/ClassTask.swift | 1 + Sources/InterposeKit/ObjectTask.swift | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Sources/InterposeKit/ClassTask.swift b/Sources/InterposeKit/ClassTask.swift index dd9bf21..62145e7 100644 --- a/Sources/InterposeKit/ClassTask.swift +++ b/Sources/InterposeKit/ClassTask.swift @@ -34,6 +34,7 @@ final public class ClassTask: ValidatableTask { /// Release the hook block if possible. public func cleanup() { + // TODO: remove subclass! switch state { case .prepared: Interpose.log("Releasing -[\(`class`).\(selector)] IMP: \(replacementIMP!)") diff --git a/Sources/InterposeKit/ObjectTask.swift b/Sources/InterposeKit/ObjectTask.swift index 086463d..30427f1 100644 --- a/Sources/InterposeKit/ObjectTask.swift +++ b/Sources/InterposeKit/ObjectTask.swift @@ -1,7 +1,7 @@ import Foundation private enum Constants { - static let subclassSuffix = "_InterposeKit_" + static let subclassSuffix = "InterposeKit_" } internal enum ObjCSelector { @@ -67,7 +67,9 @@ final public class ObjectTask: ValidatableTask { private func createSubclass() throws -> AnyClass { let perceivedClass: AnyClass = `class` let className = NSStringFromClass(perceivedClass) - let subclassName = Constants.subclassSuffix + className + // Right now we are wasteful. Might be able to optimize for shared IMP? + let uuid = UUID().uuidString.replacingOccurrences(of: "-", with: "") + let subclassName = Constants.subclassSuffix + className + uuid let subclass: AnyClass? = subclassName.withCString { cString in if let existingClass = objc_getClass(cString) as! AnyClass? { @@ -82,26 +84,25 @@ final public class ObjectTask: ValidatableTask { } } } - + guard let nonnullSubclass = subclass else { throw Interpose.Error.failedToAllocateClassPair } object_setClass(object, nonnullSubclass) return nonnullSubclass - } private func replaceGetClass(in class: AnyClass, decoy perceivedClass: AnyClass) { let getClass: @convention(block) (UnsafeRawPointer?) -> AnyClass = { _ in perceivedClass } - let impl = imp_implementationWithBlock(getClass as Any) _ = class_replaceMethod(`class`, ObjCSelector.getClass, impl, ObjCMethodEncoding.getClass) _ = class_replaceMethod(object_getClass(`class`), ObjCSelector.getClass, impl, ObjCMethodEncoding.getClass) } + // https://bugs.swift.org/browse/SR-12945 struct objc_super_fake { public var receiver: Unmanaged public var super_class: AnyClass From 4f85b46ff994fc227ed6d394ef5c29c0a3dc65f6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 7 Jun 2020 13:06:29 +0200 Subject: [PATCH 05/77] Remove object retain to allow integer returns --- Sources/InterposeKit/ObjectTask.swift | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Sources/InterposeKit/ObjectTask.swift b/Sources/InterposeKit/ObjectTask.swift index 30427f1..d27217a 100644 --- a/Sources/InterposeKit/ObjectTask.swift +++ b/Sources/InterposeKit/ObjectTask.swift @@ -5,14 +5,10 @@ private enum Constants { } internal enum ObjCSelector { -// static let forwardInvocation = Selector((("forwardInvocation:"))) -// static let methodSignatureForSelector = Selector((("methodSignatureForSelector:"))) static let getClass = Selector((("class"))) } internal enum ObjCMethodEncoding { -// static let forwardInvocation = extract("v@:@") -// static let methodSignatureForSelector = extract("v@::") static let getClass = extract("#@:") private static func extract(_ string: StaticString) -> UnsafePointer { @@ -20,7 +16,6 @@ internal enum ObjCMethodEncoding { } } - /// A task represents a hook to an instance method of a single object and stores both the original and new implementation. final public class ObjectTask: ValidatableTask { public let `class`: AnyClass @@ -84,7 +79,7 @@ final public class ObjectTask: ValidatableTask { } } } - + guard let nonnullSubclass = subclass else { throw Interpose.Error.failedToAllocateClassPair } @@ -118,14 +113,14 @@ final public class ObjectTask: ValidatableTask { // TODO: This should be cached. let sendSuper2 = dlsym(handle, "objc_msgSendSuper2"); - let block: @convention(block) (AnyObject, va_list) -> AnyObject = { obj, vaList in + let block: @convention(block) (AnyObject, va_list) -> Unmanaged = { obj, vaList in let raw = Unmanaged.passUnretained(obj) let superStruct = objc_super_fake(receiver: raw, super_class: subclass) let realSuperStruct = unsafeBitCast(superStruct, to: objc_super.self) // This is extremely cursed: https://bugs.swift.org/browse/SR-12945 // let realSuperStruct = objc_super(receiver: raw, super_class: subclass) - return withUnsafePointer(to: realSuperStruct) { realSuperStructPointer -> AnyObject in - return unsafeBitCast(sendSuper2, to: (@convention(c) (UnsafePointer, Selector, va_list) -> AnyObject).self)(realSuperStructPointer, self.selector, vaList) + return withUnsafePointer(to: realSuperStruct) { realSuperStructPointer -> Unmanaged in + return unsafeBitCast(sendSuper2, to: (@convention(c) (UnsafePointer, Selector, va_list) -> Unmanaged).self)(realSuperStructPointer, self.selector, vaList) } // Equivalent in C: // return ((id(*)(struct objc_super *, SEL, va_list))objc_msgSendSuper2)(&super, selector, argp); From 839a2e50ea63b2357434ef1f872462c59167d6f5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 7 Jun 2020 13:06:36 +0200 Subject: [PATCH 06/77] Add integer test case --- .../ObjectInterposeTests.swift | 27 +++++++++++++++++++ Tests/InterposeKitTests/TestClass.swift | 4 +++ 2 files changed, 31 insertions(+) diff --git a/Tests/InterposeKitTests/ObjectInterposeTests.swift b/Tests/InterposeKitTests/ObjectInterposeTests.swift index 44fc09d..b805bda 100644 --- a/Tests/InterposeKitTests/ObjectInterposeTests.swift +++ b/Tests/InterposeKitTests/ObjectInterposeTests.swift @@ -42,4 +42,31 @@ final class ObjectInterposeTests: XCTestCase { XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition) XCTAssertEqual(testObj2.sayHi(), testClassHi) } + + func testInterposeSingleObjectInt() throws { + let testObj = TestClass() + let returnIntDefault = testObj.returnInt() + let returnIntOverrideOffset = 2 + XCTAssertEqual(testObj.returnInt(), returnIntDefault) + + // Functions need to be `@objc dynamic` to be hookable. + let interposer = try Interpose(testObj) { + try $0.hook(#selector(TestClass.returnInt), { store in { `self` in + // Calling convention and passing selector is important! + // You're free to skip calling the original implementation. + let origCall = store((@convention(c) (AnyObject, Selector) -> Int).self) + let int = origCall(`self`, store.selector) + return int + returnIntOverrideOffset + + // Similar signature cast as above, but without selector. + } as @convention(block) (AnyObject) -> Int}) + } + XCTAssertEqual(testObj.returnInt(), returnIntDefault + returnIntOverrideOffset) + try interposer.revert() + XCTAssertEqual(testObj.returnInt(), returnIntDefault) + try interposer.apply() + XCTAssertEqual(testObj.returnInt(), returnIntDefault + returnIntOverrideOffset) + try interposer.revert() + XCTAssertEqual(testObj.returnInt(), returnIntDefault) + } } diff --git a/Tests/InterposeKitTests/TestClass.swift b/Tests/InterposeKitTests/TestClass.swift index aeff1e2..e2f0efe 100644 --- a/Tests/InterposeKitTests/TestClass.swift +++ b/Tests/InterposeKitTests/TestClass.swift @@ -11,6 +11,10 @@ class TestClass: NSObject { } @objc dynamic func doNothing() { } + + @objc dynamic func returnInt() -> Int { + 7 + } } class TestSubclass: TestClass { From a053337a5d8313130c1d8265861fb322c933c26e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 7 Jun 2020 13:21:07 +0200 Subject: [PATCH 07/77] Remove zombies --- .../xcshareddata/xcschemes/InterposeExample.xcscheme | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Example/InterposeExample.xcodeproj/xcshareddata/xcschemes/InterposeExample.xcscheme b/Example/InterposeExample.xcodeproj/xcshareddata/xcschemes/InterposeExample.xcscheme index 3ca0587..4bacafa 100644 --- a/Example/InterposeExample.xcodeproj/xcshareddata/xcschemes/InterposeExample.xcscheme +++ b/Example/InterposeExample.xcodeproj/xcshareddata/xcschemes/InterposeExample.xcscheme @@ -28,13 +28,6 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" codeCoverageEnabled = "YES"> - - - - From 7d349a88f1fdec714b9f2d5bb2594385cb6d1ac7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 7 Jun 2020 13:21:17 +0200 Subject: [PATCH 08/77] Add combined test --- .../ObjectInterposeTests.swift | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/Tests/InterposeKitTests/ObjectInterposeTests.swift b/Tests/InterposeKitTests/ObjectInterposeTests.swift index b805bda..8019778 100644 --- a/Tests/InterposeKitTests/ObjectInterposeTests.swift +++ b/Tests/InterposeKitTests/ObjectInterposeTests.swift @@ -65,8 +65,57 @@ final class ObjectInterposeTests: XCTestCase { try interposer.revert() XCTAssertEqual(testObj.returnInt(), returnIntDefault) try interposer.apply() + // ensure we really don't leak into another object + let testObj2 = TestClass() + XCTAssertEqual(testObj2.returnInt(), returnIntDefault) XCTAssertEqual(testObj.returnInt(), returnIntDefault + returnIntOverrideOffset) try interposer.revert() XCTAssertEqual(testObj.returnInt(), returnIntDefault) } + + func testDoubleIntegerInterpose() throws { + let testObj = TestClass() + let returnIntDefault = testObj.returnInt() + let returnIntOverrideOffset = 2 + let returnIntClassMultiplier = 4 + XCTAssertEqual(testObj.returnInt(), returnIntDefault) + + // Functions need to be `@objc dynamic` to be hookable. + let interposer = try Interpose(testObj) { + try $0.hook(#selector(TestClass.returnInt), { store in { `self` in + // Calling convention and passing selector is important! + // You're free to skip calling the original implementation. + let origCall = store((@convention(c) (AnyObject, Selector) -> Int).self) + let int = origCall(`self`, store.selector) + return int + returnIntOverrideOffset + + // Similar signature cast as above, but without selector. + } as @convention(block) (AnyObject) -> Int}) + } + XCTAssertEqual(testObj.returnInt(), returnIntDefault + returnIntOverrideOffset) + + // Interpose on TestClass itself! + let classInterposer = try Interpose(TestClass.self) { + try $0.hook(#selector(TestClass.returnInt), { store in { `self` in + // Calling convention and passing selector is important! + // You're free to skip calling the original implementation. + let origCall = store((@convention(c) (AnyObject, Selector) -> Int).self) + let int = origCall(`self`, store.selector) + return int * returnIntClassMultiplier + + // Similar signature cast as above, but without selector. + } as @convention(block) (AnyObject) -> Int}) + } + + XCTAssertEqual(testObj.returnInt(), (returnIntDefault * returnIntClassMultiplier) + returnIntOverrideOffset) + + // ensure we really don't leak into another object + let testObj2 = TestClass() + XCTAssertEqual(testObj2.returnInt(), returnIntDefault * returnIntClassMultiplier) + + try interposer.revert() + XCTAssertEqual(testObj.returnInt(), returnIntDefault * returnIntClassMultiplier) + try classInterposer.revert() + XCTAssertEqual(testObj.returnInt(), returnIntDefault) + } } From 463dc8192fd24c59c73ab5cee93217ace49860ec Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 7 Jun 2020 13:21:27 +0200 Subject: [PATCH 09/77] Add swiftlint rule --- Sources/InterposeKit/ObjectTask.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/InterposeKit/ObjectTask.swift b/Sources/InterposeKit/ObjectTask.swift index d27217a..c978f51 100644 --- a/Sources/InterposeKit/ObjectTask.swift +++ b/Sources/InterposeKit/ObjectTask.swift @@ -67,6 +67,7 @@ final public class ObjectTask: ValidatableTask { let subclassName = Constants.subclassSuffix + className + uuid let subclass: AnyClass? = subclassName.withCString { cString in + // swiftlint:disable:next force_cast if let existingClass = objc_getClass(cString) as! AnyClass? { return existingClass } else { From 1fb6c6c7e78cf7b6ee2ca02d8c19405afb8199eb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 7 Jun 2020 13:21:58 +0200 Subject: [PATCH 10/77] Illegal Instruction 4 --- Sources/InterposeKit/ObjectTask.swift | 31 +++++++++++++++------------ 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/Sources/InterposeKit/ObjectTask.swift b/Sources/InterposeKit/ObjectTask.swift index c978f51..4375c24 100644 --- a/Sources/InterposeKit/ObjectTask.swift +++ b/Sources/InterposeKit/ObjectTask.swift @@ -99,36 +99,39 @@ final public class ObjectTask: ValidatableTask { } // https://bugs.swift.org/browse/SR-12945 - struct objc_super_fake { + struct objcSuperFake { public var receiver: Unmanaged - public var super_class: AnyClass + public var superClass: AnyClass } - private func addSuperTrampolineMethod(subclass: AnyClass, method: Method) { - let typeEncoding = method_getTypeEncoding(method) - - let handle = dlopen(nil, RTLD_LAZY); + typealias MsgSendSuperType = @convention(c) (UnsafePointer, Selector, va_list) -> Unmanaged + private lazy var msgSendSuper2 : MsgSendSuperType = { + let handle = dlopen(nil, RTLD_LAZY) // https://opensource.apple.com/source/objc4/objc4-493.9/runtime/objc-abi.h // objc_msgSendSuper2() takes the current search class, not its superclass. // OBJC_EXPORT id objc_msgSendSuper2(struct objc_super *super, SEL op, ...) // TODO: This should be cached. - let sendSuper2 = dlsym(handle, "objc_msgSendSuper2"); + let sendSuper2 = dlsym(handle, "objc_msgSendSuper2") + return unsafeBitCast(sendSuper2, to: MsgSendSuperType.self) + }() + + private func addSuperTrampolineMethod(subclass: AnyClass, method: Method) { + let typeEncoding = method_getTypeEncoding(method) let block: @convention(block) (AnyObject, va_list) -> Unmanaged = { obj, vaList in + // This is an extremely cursed workaround for following crashing the compiler: + // let realSuperStruct = objc_super(receiver: raw, super_class: subclass) + // https://bugs.swift.org/browse/SR-12945 let raw = Unmanaged.passUnretained(obj) - let superStruct = objc_super_fake(receiver: raw, super_class: subclass) + let superStruct = objcSuperFake(receiver: raw, superClass: subclass) let realSuperStruct = unsafeBitCast(superStruct, to: objc_super.self) - // This is extremely cursed: https://bugs.swift.org/browse/SR-12945 - // let realSuperStruct = objc_super(receiver: raw, super_class: subclass) + // C: return ((id(*)(struct objc_super *, SEL, va_list))objc_msgSendSuper2)(&super, selector, argp); return withUnsafePointer(to: realSuperStruct) { realSuperStructPointer -> Unmanaged in - return unsafeBitCast(sendSuper2, to: (@convention(c) (UnsafePointer, Selector, va_list) -> Unmanaged).self)(realSuperStructPointer, self.selector, vaList) + return self.msgSendSuper2(realSuperStructPointer, self.selector, vaList) } - // Equivalent in C: - // return ((id(*)(struct objc_super *, SEL, va_list))objc_msgSendSuper2)(&super, selector, argp); } class_addMethod(subclass, self.selector, imp_implementationWithBlock(block), typeEncoding) } - /// Validate that the selector exists on the active class. @discardableResult public func validate(expectedState: Interpose.State = .prepared) throws -> Method { From faa3e5331aa2723f82c1abe74ddcb8ba58f0be45 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 7 Jun 2020 13:34:05 +0200 Subject: [PATCH 11/77] Work around compiler crash --- Sources/InterposeKit/ObjectTask.swift | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Sources/InterposeKit/ObjectTask.swift b/Sources/InterposeKit/ObjectTask.swift index 4375c24..f84ab8f 100644 --- a/Sources/InterposeKit/ObjectTask.swift +++ b/Sources/InterposeKit/ObjectTask.swift @@ -104,15 +104,12 @@ final public class ObjectTask: ValidatableTask { public var superClass: AnyClass } - typealias MsgSendSuperType = @convention(c) (UnsafePointer, Selector, va_list) -> Unmanaged - private lazy var msgSendSuper2 : MsgSendSuperType = { + // https://opensource.apple.com/source/objc4/objc4-493.9/runtime/objc-abi.h + // objc_msgSendSuper2() takes the current search class, not its superclass. + // OBJC_EXPORT id objc_msgSendSuper2(struct objc_super *super, SEL op, ...) + private lazy var msgSendSuper2 : UnsafeMutableRawPointer = { let handle = dlopen(nil, RTLD_LAZY) - // https://opensource.apple.com/source/objc4/objc4-493.9/runtime/objc-abi.h - // objc_msgSendSuper2() takes the current search class, not its superclass. - // OBJC_EXPORT id objc_msgSendSuper2(struct objc_super *super, SEL op, ...) - // TODO: This should be cached. - let sendSuper2 = dlsym(handle, "objc_msgSendSuper2") - return unsafeBitCast(sendSuper2, to: MsgSendSuperType.self) + return dlsym(handle, "objc_msgSendSuper2") }() private func addSuperTrampolineMethod(subclass: AnyClass, method: Method) { @@ -127,7 +124,8 @@ final public class ObjectTask: ValidatableTask { let realSuperStruct = unsafeBitCast(superStruct, to: objc_super.self) // C: return ((id(*)(struct objc_super *, SEL, va_list))objc_msgSendSuper2)(&super, selector, argp); return withUnsafePointer(to: realSuperStruct) { realSuperStructPointer -> Unmanaged in - return self.msgSendSuper2(realSuperStructPointer, self.selector, vaList) + let msgSendSuper2 = unsafeBitCast(self.msgSendSuper2, to: (@convention(c) (UnsafePointer, Selector, va_list) -> Unmanaged).self) + return msgSendSuper2(realSuperStructPointer, self.selector, vaList) } } class_addMethod(subclass, self.selector, imp_implementationWithBlock(block), typeEncoding) From 55cc408624db88336cc7f5c866886b69ae29308e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 7 Jun 2020 13:44:43 +0200 Subject: [PATCH 12/77] add object task --- InterposeKit.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InterposeKit.xcodeproj/project.pbxproj b/InterposeKit.xcodeproj/project.pbxproj index 1b83021..26adbb0 100644 --- a/InterposeKit.xcodeproj/project.pbxproj +++ b/InterposeKit.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 78EDB8DA248BA9B300D2F6C1 /* TestClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */; }; 78EDB8DB248BA9BB00D2F6C1 /* ObjectInterposeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */; }; 78EDB8DD248BAA5600D2F6C1 /* ClassTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8DC248BAA5600D2F6C1 /* ClassTask.swift */; }; + 78EDB8FF248D0A9900D2F6C1 /* ObjectTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8FE248D0A9900D2F6C1 /* ObjectTask.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -41,6 +42,7 @@ 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TestClass.swift; path = Tests/InterposeKitTests/TestClass.swift; sourceTree = SOURCE_ROOT; }; 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ObjectInterposeTests.swift; path = Tests/InterposeKitTests/ObjectInterposeTests.swift; sourceTree = SOURCE_ROOT; }; 78EDB8DC248BAA5600D2F6C1 /* ClassTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ClassTask.swift; path = Sources/InterposeKit/ClassTask.swift; sourceTree = SOURCE_ROOT; }; + 78EDB8FE248D0A9900D2F6C1 /* ObjectTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ObjectTask.swift; path = Sources/InterposeKit/ObjectTask.swift; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -87,6 +89,7 @@ 78863ECA2464B2F900BA3762 /* Info.plist */, 78C39D8E2483164500B46395 /* InterposeKit.h */, 78EDB8DC248BAA5600D2F6C1 /* ClassTask.swift */, + 78EDB8FE248D0A9900D2F6C1 /* ObjectTask.swift */, 78C39D902483165600B46395 /* InterposeKit.swift */, ); path = InterposeKit; @@ -226,6 +229,7 @@ buildActionMask = 2147483647; files = ( 78C39D912483165600B46395 /* InterposeKit.swift in Sources */, + 78EDB8FF248D0A9900D2F6C1 /* ObjectTask.swift in Sources */, 78EDB8DD248BAA5600D2F6C1 /* ClassTask.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; From d822fc08a151a124a35f2082c150235fcb5ce2dd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 7 Jun 2020 17:38:13 +0200 Subject: [PATCH 13/77] Try to make watchOS compile --- Sources/InterposeKit/ObjectTask.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/InterposeKit/ObjectTask.swift b/Sources/InterposeKit/ObjectTask.swift index f84ab8f..ab84129 100644 --- a/Sources/InterposeKit/ObjectTask.swift +++ b/Sources/InterposeKit/ObjectTask.swift @@ -210,3 +210,8 @@ extension ObjectTask: CustomDebugStringConvertible { } } #endif + +// FB7728351: watchOS doesn't define va_list +#if os(watchOS) +public typealias va_list = __darwin_va_list +#endif From e42f2ff78ead845f2af6c791f56d16f28c182ca4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 7 Jun 2020 18:44:45 +0200 Subject: [PATCH 14/77] Large class refactor, rename task to hook --- InterposeKit.xcodeproj/project.pbxproj | 30 ++- Sources/InterposeKit/ClassHook.swift | 42 ++++ Sources/InterposeKit/ClassTask.swift | 85 ------- Sources/InterposeKit/Hook.swift | 86 +++++++ Sources/InterposeKit/InterposeKit.swift | 212 ++---------------- .../InterposeKit/LinuxCompileSupport.swift | 23 ++ .../{ObjectTask.swift => ObjectHook.swift} | 139 +++++------- Sources/InterposeKit/Watcher.swift | 110 +++++++++ 8 files changed, 362 insertions(+), 365 deletions(-) create mode 100644 Sources/InterposeKit/ClassHook.swift delete mode 100644 Sources/InterposeKit/ClassTask.swift create mode 100644 Sources/InterposeKit/Hook.swift create mode 100644 Sources/InterposeKit/LinuxCompileSupport.swift rename Sources/InterposeKit/{ObjectTask.swift => ObjectHook.swift} (71%) create mode 100644 Sources/InterposeKit/Watcher.swift diff --git a/InterposeKit.xcodeproj/project.pbxproj b/InterposeKit.xcodeproj/project.pbxproj index 26adbb0..70fb4d1 100644 --- a/InterposeKit.xcodeproj/project.pbxproj +++ b/InterposeKit.xcodeproj/project.pbxproj @@ -7,14 +7,17 @@ objects = { /* Begin PBXBuildFile section */ + 7810959E248D43DC008A943C /* ClassHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7810959D248D43DC008A943C /* ClassHook.swift */; }; + 781095A0248D50C1008A943C /* Watcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7810959F248D50C1008A943C /* Watcher.swift */; }; 78C39D7C2482CC7D00B46395 /* InterposeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78863EC62464B2F900BA3762 /* InterposeKit.framework */; }; 78C39D8F2483164500B46395 /* InterposeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 78C39D8E2483164500B46395 /* InterposeKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 78C39D912483165600B46395 /* InterposeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C39D902483165600B46395 /* InterposeKit.swift */; }; 78C39D932483169300B46395 /* InterposeKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C39D922483169300B46395 /* InterposeKitTests.swift */; }; 78EDB8DA248BA9B300D2F6C1 /* TestClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */; }; 78EDB8DB248BA9BB00D2F6C1 /* ObjectInterposeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */; }; - 78EDB8DD248BAA5600D2F6C1 /* ClassTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8DC248BAA5600D2F6C1 /* ClassTask.swift */; }; - 78EDB8FF248D0A9900D2F6C1 /* ObjectTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8FE248D0A9900D2F6C1 /* ObjectTask.swift */; }; + 78EDB8DD248BAA5600D2F6C1 /* Hook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8DC248BAA5600D2F6C1 /* Hook.swift */; }; + 78EDB8FF248D0A9900D2F6C1 /* ObjectHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8FE248D0A9900D2F6C1 /* ObjectHook.swift */; }; + 78EDB903248D42CD00D2F6C1 /* LinuxCompileSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB902248D42CD00D2F6C1 /* LinuxCompileSupport.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -28,6 +31,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 7810959D248D43DC008A943C /* ClassHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ClassHook.swift; path = Sources/InterposeKit/ClassHook.swift; sourceTree = SOURCE_ROOT; }; + 7810959F248D50C1008A943C /* Watcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Watcher.swift; path = Sources/InterposeKit/Watcher.swift; sourceTree = SOURCE_ROOT; }; 78863EC62464B2F900BA3762 /* InterposeKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = InterposeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 78863ECA2464B2F900BA3762 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = Info.plist; path = InterposeKit.xcodeproj/Info.plist; sourceTree = ""; }; 78C39D772482CC7D00B46395 /* InterposeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InterposeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -41,8 +46,9 @@ 78C39DC1248317B400B46395 /* Defaults-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Defaults-Debug.xcconfig"; sourceTree = ""; }; 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TestClass.swift; path = Tests/InterposeKitTests/TestClass.swift; sourceTree = SOURCE_ROOT; }; 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ObjectInterposeTests.swift; path = Tests/InterposeKitTests/ObjectInterposeTests.swift; sourceTree = SOURCE_ROOT; }; - 78EDB8DC248BAA5600D2F6C1 /* ClassTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ClassTask.swift; path = Sources/InterposeKit/ClassTask.swift; sourceTree = SOURCE_ROOT; }; - 78EDB8FE248D0A9900D2F6C1 /* ObjectTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ObjectTask.swift; path = Sources/InterposeKit/ObjectTask.swift; sourceTree = SOURCE_ROOT; }; + 78EDB8DC248BAA5600D2F6C1 /* Hook.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Hook.swift; path = Sources/InterposeKit/Hook.swift; sourceTree = SOURCE_ROOT; }; + 78EDB8FE248D0A9900D2F6C1 /* ObjectHook.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ObjectHook.swift; path = Sources/InterposeKit/ObjectHook.swift; sourceTree = SOURCE_ROOT; }; + 78EDB902248D42CD00D2F6C1 /* LinuxCompileSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LinuxCompileSupport.swift; path = Sources/InterposeKit/LinuxCompileSupport.swift; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -88,9 +94,12 @@ children = ( 78863ECA2464B2F900BA3762 /* Info.plist */, 78C39D8E2483164500B46395 /* InterposeKit.h */, - 78EDB8DC248BAA5600D2F6C1 /* ClassTask.swift */, - 78EDB8FE248D0A9900D2F6C1 /* ObjectTask.swift */, + 78EDB8DC248BAA5600D2F6C1 /* Hook.swift */, + 7810959D248D43DC008A943C /* ClassHook.swift */, + 78EDB8FE248D0A9900D2F6C1 /* ObjectHook.swift */, 78C39D902483165600B46395 /* InterposeKit.swift */, + 78EDB902248D42CD00D2F6C1 /* LinuxCompileSupport.swift */, + 7810959F248D50C1008A943C /* Watcher.swift */, ); path = InterposeKit; sourceTree = ""; @@ -100,8 +109,8 @@ children = ( 78C39D922483169300B46395 /* InterposeKitTests.swift */, 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */, - 78C39D7B2482CC7D00B46395 /* Info-Tests.plist */, 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */, + 78C39D7B2482CC7D00B46395 /* Info-Tests.plist */, ); path = InterposeTests; sourceTree = ""; @@ -228,9 +237,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 781095A0248D50C1008A943C /* Watcher.swift in Sources */, + 7810959E248D43DC008A943C /* ClassHook.swift in Sources */, 78C39D912483165600B46395 /* InterposeKit.swift in Sources */, - 78EDB8FF248D0A9900D2F6C1 /* ObjectTask.swift in Sources */, - 78EDB8DD248BAA5600D2F6C1 /* ClassTask.swift in Sources */, + 78EDB8FF248D0A9900D2F6C1 /* ObjectHook.swift in Sources */, + 78EDB903248D42CD00D2F6C1 /* LinuxCompileSupport.swift in Sources */, + 78EDB8DD248BAA5600D2F6C1 /* Hook.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/InterposeKit/ClassHook.swift b/Sources/InterposeKit/ClassHook.swift new file mode 100644 index 0000000..647e576 --- /dev/null +++ b/Sources/InterposeKit/ClassHook.swift @@ -0,0 +1,42 @@ +import Foundation + +/// A hook to an instance method and stores both the original and new implementation. +final class ClassHook: InternalHookable { + public let `class`: AnyClass + public let selector: Selector + public internal(set) var origIMP: IMP? // fetched at apply time, changes late, thus class requirement + public private(set) var replacementIMP: IMP! // else we validate init order + public internal(set) var state = Interpose.State.prepared + + /// Initialize a new hook to interpose an instance method. + init(`class`: AnyClass, selector: Selector, implementation: (Hookable) -> Any) throws { + self.selector = selector + self.class = `class` + // Check if method exists + try validate() + replacementIMP = imp_implementationWithBlock(implementation(self)) + } + + func replaceImplementation() throws { + let method = try validate() + origIMP = class_replaceMethod(`class`, selector, replacementIMP, method_getTypeEncoding(method)) + guard origIMP != nil else { throw Interpose.Error.nonExistingImplementation } + Interpose.log("Swizzled -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") + } + + func resetImplementation() throws { + let method = try validate(expectedState: .interposed) + precondition(origIMP != nil) + let previousIMP = class_replaceMethod(`class`, selector, origIMP!, method_getTypeEncoding(method)) + guard previousIMP == replacementIMP else { throw Interpose.Error.unexpectedImplementation } + Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") + } +} + +#if DEBUG +extension ClassHook: CustomDebugStringConvertible { + public var debugDescription: String { + return "\(selector) -> \(String(describing: origIMP))" + } +} +#endif diff --git a/Sources/InterposeKit/ClassTask.swift b/Sources/InterposeKit/ClassTask.swift deleted file mode 100644 index 62145e7..0000000 --- a/Sources/InterposeKit/ClassTask.swift +++ /dev/null @@ -1,85 +0,0 @@ -import Foundation - -/// A task represents a hook to an instance method and stores both the original and new implementation. -final public class ClassTask: ValidatableTask { - public let `class`: AnyClass - public let selector: Selector - public private(set) var origIMP: IMP? // fetched at apply time, changes late, thus class requirement - public private(set) var replacementIMP: IMP! // else we validate init order - public private(set) var state = Interpose.State.prepared - - /// Initialize a new task to interpose an instance method. - public init(`class`: AnyClass, selector: Selector, implementation: (Task) -> Any) throws { - self.selector = selector - self.class = `class` - // Check if method exists - try validate() - replacementIMP = imp_implementationWithBlock(implementation(self)) - } - - /// Validate that the selector exists on the active class. - @discardableResult public func validate(expectedState: Interpose.State = .prepared) throws -> Method { - guard let method = class_getInstanceMethod(`class`, selector) else { throw Interpose.Error.methodNotFound } - guard state == expectedState else { throw Interpose.Error.invalidState } - return method - } - - public func apply() throws { - try execute(newState: .interposed) { try replaceImplementation() } - } - - public func revert() throws { - try execute(newState: .prepared) { try resetImplementation() } - } - - /// Release the hook block if possible. - public func cleanup() { - // TODO: remove subclass! - switch state { - case .prepared: - Interpose.log("Releasing -[\(`class`).\(selector)] IMP: \(replacementIMP!)") - imp_removeBlock(replacementIMP) - case .interposed: - Interpose.log("Keeping -[\(`class`).\(selector)] IMP: \(replacementIMP!)") - case let .error(error): - Interpose.log("Leaking -[\(`class`).\(selector)] IMP: \(replacementIMP!) due to error: \(error)") - } - } - - private func execute(newState: Interpose.State, task: () throws -> Void) throws { - do { - try task() - state = newState - } catch let error as Interpose.Error { - state = .error(error) - throw error - } - } - - private func replaceImplementation() throws { - let method = try validate() - origIMP = class_replaceMethod(`class`, selector, replacementIMP, method_getTypeEncoding(method)) - guard origIMP != nil else { throw Interpose.Error.nonExistingImplementation } - Interpose.log("Swizzled -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") - } - - private func resetImplementation() throws { - let method = try validate(expectedState: .interposed) - precondition(origIMP != nil) - let previousIMP = class_replaceMethod(`class`, selector, origIMP!, method_getTypeEncoding(method)) - guard previousIMP == replacementIMP else { throw Interpose.Error.unexpectedImplementation } - Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") - } - - public func callAsFunction(_ type: U.Type) -> U { - unsafeBitCast(origIMP, to: type) - } -} - -#if DEBUG -extension ClassTask: CustomDebugStringConvertible { - public var debugDescription: String { - return "\(selector) -> \(String(describing: origIMP))" - } -} -#endif diff --git a/Sources/InterposeKit/Hook.swift b/Sources/InterposeKit/Hook.swift new file mode 100644 index 0000000..e91190e --- /dev/null +++ b/Sources/InterposeKit/Hook.swift @@ -0,0 +1,86 @@ +import Foundation + +public protocol Hookable: class { + /// The class this hook operates on. + var `class`: AnyClass { get } + + /// If Interposing is object-based, this is set. + //var object: AnyObject? { get } + + /// The selector this hook operates on. + var selector: Selector { get } + + /// The original implementation is set once the swizzling is complete. + var origIMP: IMP? { get } + + /// The replacement implementation is created on initialization time. + var replacementIMP: IMP! { get } + + /// The state of the interpose operation. + var state: Interpose.State { get } + + /// Apply the interpose hook. + func apply() throws + + /// Revert the interpose hoook. + func revert() throws + + /// Convenience to call the original implementation. + func callAsFunction(_ type: U.Type) -> U +} + +extension Hookable { + public func callAsFunction(_ type: U.Type) -> U { + unsafeBitCast(origIMP, to: type) + } + + /// Validate that the selector exists on the active class. + @discardableResult func validate(expectedState: Interpose.State = .prepared) throws -> Method { + guard let method = class_getInstanceMethod(`class`, selector) else { throw Interpose.Error.methodNotFound } + guard state == expectedState else { throw Interpose.Error.invalidState } + return method + } +} + +public protocol InternalHookable: Hookable { + var state: Interpose.State { get set } + + func replaceImplementation() throws + + func resetImplementation() throws + + func cleanup() +} + +extension InternalHookable { + func apply() throws { + try execute(newState: .interposed) { try replaceImplementation() } + } + + func revert() throws { + try execute(newState: .prepared) { try resetImplementation() } + } + + private func execute(newState: Interpose.State, task: () throws -> Void) throws { + do { + try task() + state = newState + } catch let error as Interpose.Error { + state = .error(error) + throw error + } + } + + /// Release the hook block if possible. + public func cleanup() { + switch state { + case .prepared: + Interpose.log("Releasing -[\(`class`).\(selector)] IMP: \(replacementIMP!)") + imp_removeBlock(replacementIMP) + case .interposed: + Interpose.log("Keeping -[\(`class`).\(selector)] IMP: \(replacementIMP!)") + case let .error(error): + Interpose.log("Leaking -[\(`class`).\(selector)] IMP: \(replacementIMP!) due to error: \(error)") + } + } +} diff --git a/Sources/InterposeKit/InterposeKit.swift b/Sources/InterposeKit/InterposeKit.swift index a05e5b0..87e38e2 100644 --- a/Sources/InterposeKit/InterposeKit.swift +++ b/Sources/InterposeKit/InterposeKit.swift @@ -1,22 +1,11 @@ -// -// Interpose.swift -// InterposeKit -// -// Copyright © 2020 Peter Steinberger. All rights reserved. -// - import Foundation -#if !os(Linux) -import MachO.dyld -#endif - /// Helper to swizzle methods the right way, via replacing the IMP. final public class Interpose { - /// Stores swizzle tasks and executes them at once. + /// Stores swizzle hooks and executes them at once. public let `class`: AnyClass - /// Lists all tasks for the current interpose class object. - public private(set) var tasks: [Task] = [] + /// Lists all hooks for the current interpose class object. + public private(set) var hooks: [Hookable] = [] /// If Interposing is object-based, this is set. public let object: AnyObject? @@ -45,55 +34,55 @@ final public class Interpose { } deinit { - guard let validatableTasks = tasks as? [ValidatableTask] else { return } - validatableTasks.forEach({ $0.cleanup() }) + guard let internalHooks = hooks as? [InternalHookable] else { return } + internalHooks.forEach({ $0.cleanup() }) } /// Hook an `@objc dynamic` instance method via selector name on the current class. @discardableResult public func hook(_ selName: String, - _ implementation: (Task) -> Any) throws -> Task { + _ implementation: (Hookable) -> Any) throws -> Hookable { try hook(NSSelectorFromString(selName), implementation) } /// Hook an `@objc dynamic` instance method via selector on the current class. @discardableResult public func hook(_ selector: Selector, - _ implementation: (Task) -> Any) throws -> Task { + _ implementation: (Hookable) -> Any) throws -> Hookable { - var task: ValidatableTask + var hook: InternalHookable if let object = self.object { - task = try ObjectTask(object: object, selector: selector, implementation: implementation) + hook = try ObjectHook(object: object, selector: selector, implementation: implementation) } else { - task = try ClassTask(class: `class`, selector: selector, implementation: implementation) + hook = try ClassHook(class: `class`, selector: selector, implementation: implementation) } - tasks.append(task) - return task + hooks.append(hook) + return hook } /// Apply all stored hooks. - @discardableResult public func apply(_ task: ((Interpose) throws -> Void)? = nil) throws -> Interpose { - try execute(task) { try $0.apply() } + @discardableResult public func apply(_ hook: ((Interpose) throws -> Void)? = nil) throws -> Interpose { + try execute(hook) { try $0.apply() } } /// Revert all stored hooks. - @discardableResult public func revert(_ task: ((Interpose) throws -> Void)? = nil) throws -> Interpose { - try execute(task, expectedState: .interposed) { try $0.revert() } + @discardableResult public func revert(_ hook: ((Interpose) throws -> Void)? = nil) throws -> Interpose { + try execute(hook, expectedState: .interposed) { try $0.revert() } } private func execute(_ task: ((Interpose) throws -> Void)? = nil, expectedState: Interpose.State = .prepared, - executor: ((Task) throws -> Void)) throws -> Interpose { + executor: ((Hookable) throws -> Void)) throws -> Interpose { // Run pre-apply code first if let task = task { try task(self) } // Validate all tasks, stop if anything is not valid - guard let validatableTasks = tasks as? [ValidatableTask], validatableTasks.allSatisfy({ + guard let internalHooks = hooks as? [InternalHookable], internalHooks.allSatisfy({ (try? $0.validate(expectedState: expectedState)) != nil }) else { throw Error.invalidState } // Execute all tasks - try tasks.forEach(executor) + try hooks.forEach(executor) return self } @@ -128,42 +117,6 @@ final public class Interpose { } } -// MARK: Interpose Task - -public protocol Task { - /// The class this tasks operates on. - var `class`: AnyClass { get } - - /// If Interposing is object-based, this is set. - //var object: AnyObject? { get } - - /// The selector this tasks operates on. - var selector: Selector { get } - - /// The original implementation is set once the swizzling is complete. - var origIMP: IMP? { get } - - /// The replacement implementation is created on initialization time. - var replacementIMP: IMP! { get } - - /// The state of the interpose operation. - var state: Interpose.State { get } - - /// Apply the interpose hook. - func apply() throws - - /// Revert the interpose hoook. - func revert() throws - - /// Convenience to call the original implementation. - func callAsFunction(_ type: U.Type) -> U -} - -public protocol ValidatableTask: Task { - func validate(expectedState: Interpose.State) throws -> Method - func cleanup() -} - // MARK: Logging extension Interpose { @@ -177,130 +130,3 @@ extension Interpose { } } } - -// MARK: Interpose Class Load Watcher - -extension Interpose { - // Separate definitions to have more eleveant calling syntax when completion is not needed. - - /// Interpose a class once available. Class is passed via `classParts` string array. - @discardableResult public class func whenAvailable(_ classParts: [String], - builder: @escaping (Interpose) throws -> Void) throws -> Waiter { - try whenAvailable(classParts, builder: builder, completion: nil) - } - - /// Interpose a class once available. Class is passed via `classParts` string array, with completion handler. - @discardableResult public class func whenAvailable(_ classParts: [String], - builder: @escaping (Interpose) throws -> Void, - completion: (() -> Void)? = nil) throws -> Waiter { - try whenAvailable(classParts.joined(), builder: builder, completion: completion) - } - - /// Interpose a class once available. Class is passed via `className` string. - @discardableResult public class func whenAvailable(_ className: String, - builder: @escaping (Interpose) throws -> Void) throws -> Waiter { - try whenAvailable(className, builder: builder, completion: nil) - } - - /// Interpose a class once available. Class is passed via `className` string, with completion handler. - @discardableResult public class func whenAvailable(_ className: String, - builder: @escaping (Interpose) throws -> Void, - completion: (() -> Void)? = nil) throws -> Waiter { - try Waiter(className: className, builder: builder, completion: completion) - } - - /// Helper that stores tasks to a specific class and executes them once the class becomes available. - public struct Waiter { - fileprivate let className: String - private var builder: ((Interpose) throws -> Void)? - private var completion: (() -> Void)? - - /// Initialize waiter object. - @discardableResult init(className: String, - builder: @escaping (Interpose) throws -> Void, - completion: (() -> Void)? = nil) throws { - self.className = className - self.builder = builder - self.completion = completion - - // Immediately try to execute task. If not there, install waiter. - if try tryExecute() == false { - InterposeWatcher.append(waiter: self) - } - } - - func tryExecute() throws -> Bool { - guard let `class` = NSClassFromString(className), let builder = self.builder else { return false } - try Interpose(`class`).apply(builder) - if let completion = self.completion { - completion() - } - return true - } - } -} - -// dyld C function cannot capture class context so we pack it in a static struct. -private struct InterposeWatcher { - // Global list of waiters; can be multiple per class - private static var globalWatchers: [Interpose.Waiter] = { - // Register after Swift global registers to not deadlock - DispatchQueue.main.async { InterposeWatcher.installGlobalImageLoadWatcher() } - return [] - }() - - fileprivate static func append(waiter: Interpose.Waiter) { - InterposeWatcher.globalWatcherQueue.sync { - globalWatchers.append(waiter) - } - } - - // Register hook when dyld loads an image - private static let globalWatcherQueue = DispatchQueue(label: "com.steipete.global-image-watcher") - private static func installGlobalImageLoadWatcher() { - _dyld_register_func_for_add_image { _, _ in - InterposeWatcher.globalWatcherQueue.sync { - // this is called on the thread the image is loaded. - InterposeWatcher.globalWatchers = InterposeWatcher.globalWatchers.filter { waiter -> Bool in - do { - if try waiter.tryExecute() == false { - return true // only collect if this fails because class is not there yet - } else { - Interpose.log("\(waiter.className) was successful.") - } - } catch { - Interpose.log("Error while executing task: \(error).") - // We can't bubble up the throw into the C context. - #if DEBUG - // Instead of silently eating, it's better to crash in DEBUG. - fatalError("Error while executing task: \(error).") - #endif - } - return false - } - } - } - } -} - -// MARK: Debug Helper - -#if os(Linux) -// Linux is used to create Jazzy docs -/// :nodoc: Selector -public struct Selector {} -/// :nodoc: IMP -public struct IMP: Equatable {} -/// :nodoc: Method -public struct Method {} -func NSSelectorFromString(_ aSelectorName: String) -> Selector { Selector() } -func class_getInstanceMethod(_ cls: AnyClass?, _ name: Selector) -> Method? { return nil } -// swiftlint:disable:next line_length -func class_replaceMethod(_ cls: AnyClass?, _ name: Selector, _ imp: IMP, _ types: UnsafePointer?) -> IMP? { IMP() } -// swiftlint:disable:next identifier_name -func method_getTypeEncoding(_ m: Method) -> UnsafePointer? { return nil } -// swiftlint:disable:next identifier_name -func _dyld_register_func_for_add_image(_ func: (@convention(c) (UnsafePointer?, Int) -> Void)!) {} -func imp_implementationWithBlock(_ block: Any) -> IMP { IMP() } -func imp_removeBlock(_ anImp: IMP) -> Bool { false } -#endif diff --git a/Sources/InterposeKit/LinuxCompileSupport.swift b/Sources/InterposeKit/LinuxCompileSupport.swift new file mode 100644 index 0000000..2013fc7 --- /dev/null +++ b/Sources/InterposeKit/LinuxCompileSupport.swift @@ -0,0 +1,23 @@ +import Foundation + +// Linux is used to create Jazzy docs +#if os(Linux) + +/// :nodoc: Selector +public struct Selector {} +/// :nodoc: IMP +public struct IMP: Equatable {} +/// :nodoc: Method +public struct Method {} +func NSSelectorFromString(_ aSelectorName: String) -> Selector { Selector() } +func class_getInstanceMethod(_ cls: AnyClass?, _ name: Selector) -> Method? { return nil } +// swiftlint:disable:next line_length +func class_replaceMethod(_ cls: AnyClass?, _ name: Selector, _ imp: IMP, _ types: UnsafePointer?) -> IMP? { IMP() } +// swiftlint:disable:next identifier_name +func method_getTypeEncoding(_ m: Method) -> UnsafePointer? { return nil } +// swiftlint:disable:next identifier_name +func _dyld_register_func_for_add_image(_ func: (@convention(c) (UnsafePointer?, Int) -> Void)!) {} +func imp_implementationWithBlock(_ block: Any) -> IMP { IMP() } +func imp_removeBlock(_ anImp: IMP) -> Bool { false } + +#endif diff --git a/Sources/InterposeKit/ObjectTask.swift b/Sources/InterposeKit/ObjectHook.swift similarity index 71% rename from Sources/InterposeKit/ObjectTask.swift rename to Sources/InterposeKit/ObjectHook.swift index ab84129..1c6e336 100644 --- a/Sources/InterposeKit/ObjectTask.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -16,50 +16,37 @@ internal enum ObjCMethodEncoding { } } -/// A task represents a hook to an instance method of a single object and stores both the original and new implementation. -final public class ObjectTask: ValidatableTask { +/// A hook to an instance method of a single object, stores both the original and new implementation. +/// TODO: Multiple hooks for one object +final class ObjectHook: InternalHookable { public let `class`: AnyClass - public let object: AnyObject public let selector: Selector - public private(set) var origIMP: IMP? // fetched at apply time, changes late, thus class requirement + public internal(set) var origIMP: IMP? // fetched at apply time, changes late, thus class requirement public private(set) var replacementIMP: IMP! // else we validate init order - public private(set) var state = Interpose.State.prepared + public internal(set) var state = Interpose.State.prepared - // Subclass that we create on the fly + public let object: AnyObject + /// Subclass that we create on the fly var dynamicSubclass: AnyClass? - /// Initialize a new task to interpose an instance method. - public init(object: AnyObject, selector: Selector, implementation: (Task) -> Any) throws { - self.selector = selector + /// Initialize a new hook to interpose an instance method. + public init(object: AnyObject, selector: Selector, implementation: (Hookable) -> Any) throws { self.object = object + self.selector = selector self.class = type(of: object) // Check if method exists try validate() replacementIMP = imp_implementationWithBlock(implementation(self)) } - class KVOObserver: NSObject { - @objc var objectToObserve: AnyObject - var observation: NSKeyValueObservation? - - init(object: AnyObject) { - objectToObserve = object - super.init() - - // Can't use modern syntax cause https://bugs.swift.org/browse/SR-12944 - objectToObserve.addObserver(self, forKeyPath: "description", options: .new, context: nil) - } - } +// /// Release the hook block if possible. +// public override func cleanup() { +// // TODO: remove subclass! +// super.cleanup() +// } - // Before creating our subclass, we trigger KVO. - // KVO also creates a subclass at runtime. If we do this prior, then KVO fails. - // If KVO runs prior, and then we sub-subclass, everything works. - var kvoObserver: KVOObserver? - private func registerKVO() { - kvoObserver = KVOObserver(object: object) - } - - private func createSubclass() throws -> AnyClass { + /// Creates a unique dynamic subclass of the current object + private func createDynamicSubclass() throws -> AnyClass { let perceivedClass: AnyClass = `class` let className = NSStringFromClass(perceivedClass) // Right now we are wasteful. Might be able to optimize for shared IMP? @@ -130,47 +117,8 @@ final public class ObjectTask: ValidatableTask { } class_addMethod(subclass, self.selector, imp_implementationWithBlock(block), typeEncoding) } - - /// Validate that the selector exists on the active class. - @discardableResult public func validate(expectedState: Interpose.State = .prepared) throws -> Method { - // We need to validate on class, not the subclass - guard let method = class_getInstanceMethod(`class`, selector) else { throw Interpose.Error.methodNotFound } - guard state == expectedState else { throw Interpose.Error.invalidState } - return method - } - - public func apply() throws { - try execute(newState: .interposed) { try replaceImplementation() } - } - public func revert() throws { - try execute(newState: .prepared) { try resetImplementation() } - } - - /// Release the hook block if possible. - public func cleanup() { - switch state { - case .prepared: - Interpose.log("Releasing -[\(`class`).\(selector)] IMP: \(replacementIMP!)") - imp_removeBlock(replacementIMP) - case .interposed: - Interpose.log("Keeping -[\(`class`).\(selector)] IMP: \(replacementIMP!)") - case let .error(error): - Interpose.log("Leaking -[\(`class`).\(selector)] IMP: \(replacementIMP!) due to error: \(error)") - } - } - - private func execute(newState: Interpose.State, task: () throws -> Void) throws { - do { - try task() - state = newState - } catch let error as Interpose.Error { - state = .error(error) - throw error - } - } - - private func replaceImplementation() throws { + func replaceImplementation() throws { let method = try validate() // Register a KVO to work around any KVO issues with opposite order @@ -178,7 +126,7 @@ final public class ObjectTask: ValidatableTask { // Register subclass at runtime if we haven't already if dynamicSubclass == nil { - dynamicSubclass = try createSubclass() + dynamicSubclass = try createDynamicSubclass() } // Add empty trampoline that we then replace the IMP! addSuperTrampolineMethod(subclass: dynamicSubclass!, method: method) @@ -188,25 +136,60 @@ final public class ObjectTask: ValidatableTask { Interpose.log("Swizzled -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") } - private func resetImplementation() throws { + func resetImplementation() throws { let method = try validate(expectedState: .interposed) precondition(origIMP != nil) - precondition(dynamicSubclass != nil) + guard let dynamicSubclass = self.dynamicSubclass else { preconditionFailure("No dynamic subclass set") } - let previousIMP = class_replaceMethod(dynamicSubclass!, selector, origIMP!, method_getTypeEncoding(method)) + let previousIMP = class_replaceMethod(dynamicSubclass, selector, origIMP!, method_getTypeEncoding(method)) guard previousIMP == replacementIMP else { throw Interpose.Error.unexpectedImplementation } Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") + + // Restore the original class of the object + // TODO: Does this include the KVO'ed subclass? + object_setClass(object, `class`) + + // Dispose of the custom dynamic subclass + objc_disposeClassPair(dynamicSubclass) + self.dynamicSubclass = nil + + // Remove KVO after restoring class as last step. + deregisterKVO() + } + +// MARK: KVO Helper + + var kvoObserver: KVOObserver? + + class KVOObserver: NSObject { + @objc var objectToObserve: AnyObject + var observation: NSKeyValueObservation? + + init(object: AnyObject) { + objectToObserve = object + super.init() + + // Can't use modern syntax cause https://bugs.swift.org/browse/SR-12944 + objectToObserve.addObserver(self, forKeyPath: "description", options: .new, context: nil) + } + } + + // Before creating our subclass, we trigger KVO. + // KVO also creates a subclass at runtime. If we do this prior, then KVO fails. + // If KVO runs prior, and then we sub-subclass, everything works. + private func registerKVO() { + kvoObserver = KVOObserver(object: object) } - public func callAsFunction(_ type: U.Type) -> U { - unsafeBitCast(origIMP, to: type) + private func deregisterKVO() { + kvoObserver = nil } } #if DEBUG -extension ObjectTask: CustomDebugStringConvertible { +extension ObjectHook: CustomDebugStringConvertible { public var debugDescription: String { - return "\(selector) -> \(String(describing: origIMP))" + return "\(selector) of \(object) -> \(String(describing: origIMP))" } } #endif diff --git a/Sources/InterposeKit/Watcher.swift b/Sources/InterposeKit/Watcher.swift new file mode 100644 index 0000000..c985fd8 --- /dev/null +++ b/Sources/InterposeKit/Watcher.swift @@ -0,0 +1,110 @@ +import Foundation + +#if !os(Linux) +import MachO.dyld +#endif + +// MARK: Interpose Class Load Watcher + +extension Interpose { + // Separate definitions to have more eleveant calling syntax when completion is not needed. + + /// Interpose a class once available. Class is passed via `classParts` string array. + @discardableResult public class func whenAvailable(_ classParts: [String], + builder: @escaping (Interpose) throws -> Void) throws -> Waiter { + try whenAvailable(classParts, builder: builder, completion: nil) + } + + /// Interpose a class once available. Class is passed via `classParts` string array, with completion handler. + @discardableResult public class func whenAvailable(_ classParts: [String], + builder: @escaping (Interpose) throws -> Void, + completion: (() -> Void)? = nil) throws -> Waiter { + try whenAvailable(classParts.joined(), builder: builder, completion: completion) + } + + /// Interpose a class once available. Class is passed via `className` string. + @discardableResult public class func whenAvailable(_ className: String, + builder: @escaping (Interpose) throws -> Void) throws -> Waiter { + try whenAvailable(className, builder: builder, completion: nil) + } + + /// Interpose a class once available. Class is passed via `className` string, with completion handler. + @discardableResult public class func whenAvailable(_ className: String, + builder: @escaping (Interpose) throws -> Void, + completion: (() -> Void)? = nil) throws -> Waiter { + try Waiter(className: className, builder: builder, completion: completion) + } + + /// Helper that stores hooks to a specific class and executes them once the class becomes available. + public struct Waiter { + fileprivate let className: String + private var builder: ((Interpose) throws -> Void)? + private var completion: (() -> Void)? + + /// Initialize waiter object. + @discardableResult init(className: String, + builder: @escaping (Interpose) throws -> Void, + completion: (() -> Void)? = nil) throws { + self.className = className + self.builder = builder + self.completion = completion + + // Immediately try to execute task. If not there, install waiter. + if try tryExecute() == false { + InterposeWatcher.append(waiter: self) + } + } + + func tryExecute() throws -> Bool { + guard let `class` = NSClassFromString(className), let builder = self.builder else { return false } + try Interpose(`class`).apply(builder) + if let completion = self.completion { + completion() + } + return true + } + } +} + +// dyld C function cannot capture class context so we pack it in a static struct. +private struct InterposeWatcher { + // Global list of waiters; can be multiple per class + private static var globalWatchers: [Interpose.Waiter] = { + // Register after Swift global registers to not deadlock + DispatchQueue.main.async { InterposeWatcher.installGlobalImageLoadWatcher() } + return [] + }() + + fileprivate static func append(waiter: Interpose.Waiter) { + InterposeWatcher.globalWatcherQueue.sync { + globalWatchers.append(waiter) + } + } + + // Register hook when dyld loads an image + private static let globalWatcherQueue = DispatchQueue(label: "com.steipete.global-image-watcher") + private static func installGlobalImageLoadWatcher() { + _dyld_register_func_for_add_image { _, _ in + InterposeWatcher.globalWatcherQueue.sync { + // this is called on the thread the image is loaded. + InterposeWatcher.globalWatchers = InterposeWatcher.globalWatchers.filter { waiter -> Bool in + do { + if try waiter.tryExecute() == false { + return true // only collect if this fails because class is not there yet + } else { + Interpose.log("\(waiter.className) was successful.") + } + } catch { + Interpose.log("Error while executing task: \(error).") + // We can't bubble up the throw into the C context. + #if DEBUG + // Instead of silently eating, it's better to crash in DEBUG. + fatalError("Error while executing task: \(error).") + #endif + } + return false + } + } + } + } +} From 5f0fe6fc2fa8b42ff794750b31ebeb6052b54ec8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 7 Jun 2020 19:05:38 +0200 Subject: [PATCH 15/77] remove swiftlint rule for Foundation --- .swiftlint.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 882fb5c..61f3c51 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -2,7 +2,6 @@ included: - Sources - Tests analyzer_rules: - - unused_import - unused_declaration line_length: 120 identifier_name: From e05016be24b2cc53f207eb376e5e9dcd2a632109 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 7 Jun 2020 19:07:28 +0200 Subject: [PATCH 16/77] swiftlint --- Sources/InterposeKit/ObjectHook.swift | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index 1c6e336..3a0a8db 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -17,7 +17,7 @@ internal enum ObjCMethodEncoding { } /// A hook to an instance method of a single object, stores both the original and new implementation. -/// TODO: Multiple hooks for one object +/// Think about: Multiple hooks for one object final class ObjectHook: InternalHookable { public let `class`: AnyClass public let selector: Selector @@ -41,7 +41,7 @@ final class ObjectHook: InternalHookable { // /// Release the hook block if possible. // public override func cleanup() { -// // TODO: remove subclass! +// // remove subclass! // super.cleanup() // } @@ -86,7 +86,7 @@ final class ObjectHook: InternalHookable { } // https://bugs.swift.org/browse/SR-12945 - struct objcSuperFake { + struct ObjcSuperFake { public var receiver: Unmanaged public var superClass: AnyClass } @@ -94,7 +94,7 @@ final class ObjectHook: InternalHookable { // https://opensource.apple.com/source/objc4/objc4-493.9/runtime/objc-abi.h // objc_msgSendSuper2() takes the current search class, not its superclass. // OBJC_EXPORT id objc_msgSendSuper2(struct objc_super *super, SEL op, ...) - private lazy var msgSendSuper2 : UnsafeMutableRawPointer = { + private lazy var msgSendSuper2: UnsafeMutableRawPointer = { let handle = dlopen(nil, RTLD_LAZY) return dlsym(handle, "objc_msgSendSuper2") }() @@ -107,11 +107,12 @@ final class ObjectHook: InternalHookable { // let realSuperStruct = objc_super(receiver: raw, super_class: subclass) // https://bugs.swift.org/browse/SR-12945 let raw = Unmanaged.passUnretained(obj) - let superStruct = objcSuperFake(receiver: raw, superClass: subclass) + let superStruct = ObjcSuperFake(receiver: raw, superClass: subclass) let realSuperStruct = unsafeBitCast(superStruct, to: objc_super.self) // C: return ((id(*)(struct objc_super *, SEL, va_list))objc_msgSendSuper2)(&super, selector, argp); return withUnsafePointer(to: realSuperStruct) { realSuperStructPointer -> Unmanaged in - let msgSendSuper2 = unsafeBitCast(self.msgSendSuper2, to: (@convention(c) (UnsafePointer, Selector, va_list) -> Unmanaged).self) + let msgSendSuper2 = unsafeBitCast(self.msgSendSuper2, + to: (@convention(c) (UnsafePointer, Selector, va_list) -> Unmanaged).self) return msgSendSuper2(realSuperStructPointer, self.selector, vaList) } } @@ -146,7 +147,7 @@ final class ObjectHook: InternalHookable { Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") // Restore the original class of the object - // TODO: Does this include the KVO'ed subclass? + // Does this include the KVO'ed subclass? object_setClass(object, `class`) // Dispose of the custom dynamic subclass @@ -196,5 +197,6 @@ extension ObjectHook: CustomDebugStringConvertible { // FB7728351: watchOS doesn't define va_list #if os(watchOS) +// swiftlint:disable:nex type_name public typealias va_list = __darwin_va_list #endif From c5cb3448ada55460e1eae155c22919631ae5129c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 7 Jun 2020 23:05:38 +0200 Subject: [PATCH 17/77] Move types into method call --- .../project.pbxproj | 18 ++ .../xcschemes/InterposeExample.xcscheme | 10 + Example/InterposeExample/AppDelegate.swift | 20 +- Example/InterposeExampleTests/Info.plist | 22 +++ .../InterposeExampleTests.swift | 34 ++++ InterposeKit.xcodeproj/project.pbxproj | 185 +++++++++++++++++- InterposeTestHost/AppDelegate.swift | 37 ++++ .../AppIcon.appiconset/Contents.json | 98 ++++++++++ .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 +++ InterposeTestHost/Base.lproj/Main.storyboard | 24 +++ InterposeTestHost/Info.plist | 66 +++++++ .../InterposeTestHost.entitlements | 10 + InterposeTestHost/SceneDelegate.swift | 53 +++++ InterposeTestHost/ViewController.swift | 20 ++ Sources/InterposeKit/AnyHook.swift | 91 +++++++++ Sources/InterposeKit/ClassHook.swift | 25 +-- Sources/InterposeKit/Hook.swift | 86 -------- Sources/InterposeKit/InterposeKit.swift | 43 ++-- Sources/InterposeKit/ObjectHook.swift | 32 ++- .../InterposeKitTests/InterposeKitTests.swift | 81 ++++---- .../ObjectInterposeTests.swift | 59 +++--- 22 files changed, 818 insertions(+), 227 deletions(-) create mode 100644 Example/InterposeExampleTests/Info.plist create mode 100644 Example/InterposeExampleTests/InterposeExampleTests.swift create mode 100644 InterposeTestHost/AppDelegate.swift create mode 100644 InterposeTestHost/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 InterposeTestHost/Assets.xcassets/Contents.json create mode 100644 InterposeTestHost/Base.lproj/LaunchScreen.storyboard create mode 100644 InterposeTestHost/Base.lproj/Main.storyboard create mode 100644 InterposeTestHost/Info.plist create mode 100644 InterposeTestHost/InterposeTestHost.entitlements create mode 100644 InterposeTestHost/SceneDelegate.swift create mode 100644 InterposeTestHost/ViewController.swift create mode 100644 Sources/InterposeKit/AnyHook.swift delete mode 100644 Sources/InterposeKit/Hook.swift diff --git a/Example/InterposeExample.xcodeproj/project.pbxproj b/Example/InterposeExample.xcodeproj/project.pbxproj index ae90a14..ce78145 100644 --- a/Example/InterposeExample.xcodeproj/project.pbxproj +++ b/Example/InterposeExample.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 781095BF248D8AD7008A943C /* InterposeExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781095BD248D8AD7008A943C /* InterposeExampleTests.swift */; }; 7880B124248280B300AD2251 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7880B123248280B300AD2251 /* AppDelegate.swift */; }; 7880B126248280B300AD2251 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7880B125248280B300AD2251 /* SceneDelegate.swift */; }; 7880B128248280B300AD2251 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7880B127248280B300AD2251 /* ViewController.swift */; }; @@ -31,6 +32,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 781095BD248D8AD7008A943C /* InterposeExampleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InterposeExampleTests.swift; sourceTree = ""; }; + 781095BE248D8AD7008A943C /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7880B120248280B300AD2251 /* InterposeExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InterposeExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7880B123248280B300AD2251 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7880B125248280B300AD2251 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -67,11 +70,21 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 781095BC248D8AD7008A943C /* InterposeExampleTests */ = { + isa = PBXGroup; + children = ( + 781095BD248D8AD7008A943C /* InterposeExampleTests.swift */, + 781095BE248D8AD7008A943C /* Info.plist */, + ); + path = InterposeExampleTests; + sourceTree = ""; + }; 7880B117248280B300AD2251 = { isa = PBXGroup; children = ( 78C39DD8248335B100B46395 /* Interpose */, 7880B122248280B300AD2251 /* InterposeExample */, + 781095BC248D8AD7008A943C /* InterposeExampleTests */, 7880B121248280B300AD2251 /* Products */, 7880B14E248281D000AD2251 /* Frameworks */, ); @@ -242,6 +255,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 781095BF248D8AD7008A943C /* InterposeExampleTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -404,6 +418,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = InterposeExample/InterposeExample.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = Y5PE65HELJ; INFOPLIST_FILE = InterposeExample/Info.plist; @@ -423,6 +438,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = InterposeExample/InterposeExample.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = Y5PE65HELJ; INFOPLIST_FILE = InterposeExample/Info.plist; @@ -442,6 +458,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = Y5PE65HELJ; INFOPLIST_FILE = InterposeExampleTests/Info.plist; @@ -464,6 +481,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = Y5PE65HELJ; INFOPLIST_FILE = InterposeExampleTests/Info.plist; diff --git a/Example/InterposeExample.xcodeproj/xcshareddata/xcschemes/InterposeExample.xcscheme b/Example/InterposeExample.xcodeproj/xcshareddata/xcschemes/InterposeExample.xcscheme index 4bacafa..a7c33d9 100644 --- a/Example/InterposeExample.xcodeproj/xcshareddata/xcschemes/InterposeExample.xcscheme +++ b/Example/InterposeExample.xcodeproj/xcshareddata/xcschemes/InterposeExample.xcscheme @@ -39,6 +39,16 @@ ReferencedContainer = "container:.."> + + + + 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}) + + try $0.hook("documentState") { (store: TypedHook<@convention(c) (AnyObject, Selector) -> AnyObject, @convention(block) (AnyObject) -> AnyObject>) in { `self` in + lock.sync { store.original(`self`, store.selector) } + } + } + + try $0.hook("setDocumentState:") { (store: TypedHook<@convention(c) (AnyObject, Selector, AnyObject) -> Void, @convention(block) (AnyObject, AnyObject) -> Void>) in { `self`, newValue in + lock.sync { store.original(`self`, store.selector, newValue) } + } + } } } catch { print("Failed to fix input system: \(error).") diff --git a/Example/InterposeExampleTests/Info.plist b/Example/InterposeExampleTests/Info.plist new file mode 100644 index 0000000..64d65ca --- /dev/null +++ b/Example/InterposeExampleTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Example/InterposeExampleTests/InterposeExampleTests.swift b/Example/InterposeExampleTests/InterposeExampleTests.swift new file mode 100644 index 0000000..9b43cb7 --- /dev/null +++ b/Example/InterposeExampleTests/InterposeExampleTests.swift @@ -0,0 +1,34 @@ +// +// InterposeExampleTests.swift +// InterposeExampleTests +// +// Created by Peter Steinberger on 30.05.20. +// Copyright © 2020 PSPDFKit GmbH. All rights reserved. +// + +import XCTest +@testable import InterposeExample + +class InterposeExampleTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/InterposeKit.xcodeproj/project.pbxproj b/InterposeKit.xcodeproj/project.pbxproj index 70fb4d1..9e50ef8 100644 --- a/InterposeKit.xcodeproj/project.pbxproj +++ b/InterposeKit.xcodeproj/project.pbxproj @@ -9,18 +9,31 @@ /* Begin PBXBuildFile section */ 7810959E248D43DC008A943C /* ClassHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7810959D248D43DC008A943C /* ClassHook.swift */; }; 781095A0248D50C1008A943C /* Watcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7810959F248D50C1008A943C /* Watcher.swift */; }; + 781095A8248D6DFB008A943C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781095A7248D6DFB008A943C /* AppDelegate.swift */; }; + 781095AA248D6DFB008A943C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781095A9248D6DFB008A943C /* SceneDelegate.swift */; }; + 781095AC248D6DFB008A943C /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781095AB248D6DFB008A943C /* ViewController.swift */; }; + 781095AF248D6DFB008A943C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 781095AD248D6DFB008A943C /* Main.storyboard */; }; + 781095B1248D6DFD008A943C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 781095B0248D6DFD008A943C /* Assets.xcassets */; }; + 781095B4248D6DFD008A943C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 781095B2248D6DFD008A943C /* LaunchScreen.storyboard */; }; 78C39D7C2482CC7D00B46395 /* InterposeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78863EC62464B2F900BA3762 /* InterposeKit.framework */; }; 78C39D8F2483164500B46395 /* InterposeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 78C39D8E2483164500B46395 /* InterposeKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 78C39D912483165600B46395 /* InterposeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C39D902483165600B46395 /* InterposeKit.swift */; }; 78C39D932483169300B46395 /* InterposeKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C39D922483169300B46395 /* InterposeKitTests.swift */; }; 78EDB8DA248BA9B300D2F6C1 /* TestClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */; }; 78EDB8DB248BA9BB00D2F6C1 /* ObjectInterposeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */; }; - 78EDB8DD248BAA5600D2F6C1 /* Hook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8DC248BAA5600D2F6C1 /* Hook.swift */; }; + 78EDB8DD248BAA5600D2F6C1 /* AnyHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8DC248BAA5600D2F6C1 /* AnyHook.swift */; }; 78EDB8FF248D0A9900D2F6C1 /* ObjectHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8FE248D0A9900D2F6C1 /* ObjectHook.swift */; }; 78EDB903248D42CD00D2F6C1 /* LinuxCompileSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB902248D42CD00D2F6C1 /* LinuxCompileSupport.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 781095BA248D6E10008A943C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 78863EBD2464B2F900BA3762 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 781095A4248D6DFB008A943C; + remoteInfo = InterposeTestHost; + }; 78C39D7D2482CC7D00B46395 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 78863EBD2464B2F900BA3762 /* Project object */; @@ -33,6 +46,15 @@ /* Begin PBXFileReference section */ 7810959D248D43DC008A943C /* ClassHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ClassHook.swift; path = Sources/InterposeKit/ClassHook.swift; sourceTree = SOURCE_ROOT; }; 7810959F248D50C1008A943C /* Watcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Watcher.swift; path = Sources/InterposeKit/Watcher.swift; sourceTree = SOURCE_ROOT; }; + 781095A5248D6DFB008A943C /* InterposeTestHost.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InterposeTestHost.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 781095A7248D6DFB008A943C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 781095A9248D6DFB008A943C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 781095AB248D6DFB008A943C /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 781095AE248D6DFB008A943C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 781095B0248D6DFD008A943C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 781095B3248D6DFD008A943C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 781095B5248D6DFD008A943C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 781095B9248D6E0A008A943C /* InterposeTestHost.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = InterposeTestHost.entitlements; sourceTree = ""; }; 78863EC62464B2F900BA3762 /* InterposeKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = InterposeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 78863ECA2464B2F900BA3762 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = Info.plist; path = InterposeKit.xcodeproj/Info.plist; sourceTree = ""; }; 78C39D772482CC7D00B46395 /* InterposeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InterposeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -46,12 +68,19 @@ 78C39DC1248317B400B46395 /* Defaults-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Defaults-Debug.xcconfig"; sourceTree = ""; }; 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TestClass.swift; path = Tests/InterposeKitTests/TestClass.swift; sourceTree = SOURCE_ROOT; }; 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ObjectInterposeTests.swift; path = Tests/InterposeKitTests/ObjectInterposeTests.swift; sourceTree = SOURCE_ROOT; }; - 78EDB8DC248BAA5600D2F6C1 /* Hook.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Hook.swift; path = Sources/InterposeKit/Hook.swift; sourceTree = SOURCE_ROOT; }; + 78EDB8DC248BAA5600D2F6C1 /* AnyHook.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AnyHook.swift; path = Sources/InterposeKit/AnyHook.swift; sourceTree = SOURCE_ROOT; }; 78EDB8FE248D0A9900D2F6C1 /* ObjectHook.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ObjectHook.swift; path = Sources/InterposeKit/ObjectHook.swift; sourceTree = SOURCE_ROOT; }; 78EDB902248D42CD00D2F6C1 /* LinuxCompileSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LinuxCompileSupport.swift; path = Sources/InterposeKit/LinuxCompileSupport.swift; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 781095A2248D6DFB008A943C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 78863EC32464B2F900BA3762 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -70,12 +99,28 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 781095A6248D6DFB008A943C /* InterposeTestHost */ = { + isa = PBXGroup; + children = ( + 781095B9248D6E0A008A943C /* InterposeTestHost.entitlements */, + 781095A7248D6DFB008A943C /* AppDelegate.swift */, + 781095A9248D6DFB008A943C /* SceneDelegate.swift */, + 781095AB248D6DFB008A943C /* ViewController.swift */, + 781095AD248D6DFB008A943C /* Main.storyboard */, + 781095B0248D6DFD008A943C /* Assets.xcassets */, + 781095B2248D6DFD008A943C /* LaunchScreen.storyboard */, + 781095B5248D6DFD008A943C /* Info.plist */, + ); + path = InterposeTestHost; + sourceTree = ""; + }; 78863EBC2464B2F900BA3762 = { isa = PBXGroup; children = ( 78863EC82464B2F900BA3762 /* InterposeKit */, 78C39D782482CC7D00B46395 /* InterposeTests */, 78C39DBD248317B400B46395 /* Configuration */, + 781095A6248D6DFB008A943C /* InterposeTestHost */, 78863EC72464B2F900BA3762 /* Products */, ); sourceTree = ""; @@ -85,6 +130,7 @@ children = ( 78863EC62464B2F900BA3762 /* InterposeKit.framework */, 78C39D772482CC7D00B46395 /* InterposeTests.xctest */, + 781095A5248D6DFB008A943C /* InterposeTestHost.app */, ); name = Products; sourceTree = ""; @@ -94,7 +140,7 @@ children = ( 78863ECA2464B2F900BA3762 /* Info.plist */, 78C39D8E2483164500B46395 /* InterposeKit.h */, - 78EDB8DC248BAA5600D2F6C1 /* Hook.swift */, + 78EDB8DC248BAA5600D2F6C1 /* AnyHook.swift */, 7810959D248D43DC008A943C /* ClassHook.swift */, 78EDB8FE248D0A9900D2F6C1 /* ObjectHook.swift */, 78C39D902483165600B46395 /* InterposeKit.swift */, @@ -140,6 +186,23 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + 781095A4248D6DFB008A943C /* InterposeTestHost */ = { + isa = PBXNativeTarget; + buildConfigurationList = 781095B6248D6DFD008A943C /* Build configuration list for PBXNativeTarget "InterposeTestHost" */; + buildPhases = ( + 781095A1248D6DFB008A943C /* Sources */, + 781095A2248D6DFB008A943C /* Frameworks */, + 781095A3248D6DFB008A943C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = InterposeTestHost; + productName = InterposeTestHost; + productReference = 781095A5248D6DFB008A943C /* InterposeTestHost.app */; + productType = "com.apple.product-type.application"; + }; 78863EC52464B2F900BA3762 /* InterposeKit */ = { isa = PBXNativeTarget; buildConfigurationList = 78863ECE2464B2F900BA3762 /* Build configuration list for PBXNativeTarget "InterposeKit" */; @@ -170,6 +233,7 @@ ); dependencies = ( 78C39D7E2482CC7D00B46395 /* PBXTargetDependency */, + 781095BB248D6E10008A943C /* PBXTargetDependency */, ); name = InterposeTests; productName = InterposeTests; @@ -186,6 +250,9 @@ LastUpgradeCheck = 1150; ORGANIZATIONNAME = "PSPDFKit GmbH"; TargetAttributes = { + 781095A4248D6DFB008A943C = { + CreatedOnToolsVersion = 11.5; + }; 78863EC52464B2F900BA3762 = { CreatedOnToolsVersion = 11.5; LastSwiftMigration = 1150; @@ -211,11 +278,22 @@ targets = ( 78863EC52464B2F900BA3762 /* InterposeKit */, 78C39D762482CC7D00B46395 /* InterposeTests */, + 781095A4248D6DFB008A943C /* InterposeTestHost */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 781095A3248D6DFB008A943C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 781095B4248D6DFD008A943C /* LaunchScreen.storyboard in Resources */, + 781095B1248D6DFD008A943C /* Assets.xcassets in Resources */, + 781095AF248D6DFB008A943C /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 78863EC42464B2F900BA3762 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -233,6 +311,16 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 781095A1248D6DFB008A943C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 781095AC248D6DFB008A943C /* ViewController.swift in Sources */, + 781095A8248D6DFB008A943C /* AppDelegate.swift in Sources */, + 781095AA248D6DFB008A943C /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 78863EC22464B2F900BA3762 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -242,7 +330,7 @@ 78C39D912483165600B46395 /* InterposeKit.swift in Sources */, 78EDB8FF248D0A9900D2F6C1 /* ObjectHook.swift in Sources */, 78EDB903248D42CD00D2F6C1 /* LinuxCompileSupport.swift in Sources */, - 78EDB8DD248BAA5600D2F6C1 /* Hook.swift in Sources */, + 78EDB8DD248BAA5600D2F6C1 /* AnyHook.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -259,6 +347,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 781095BB248D6E10008A943C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 781095A4248D6DFB008A943C /* InterposeTestHost */; + targetProxy = 781095BA248D6E10008A943C /* PBXContainerItemProxy */; + }; 78C39D7E2482CC7D00B46395 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 78863EC52464B2F900BA3762 /* InterposeKit */; @@ -266,7 +359,82 @@ }; /* End PBXTargetDependency section */ +/* Begin PBXVariantGroup section */ + 781095AD248D6DFB008A943C /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 781095AE248D6DFB008A943C /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 781095B2248D6DFD008A943C /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 781095B3248D6DFD008A943C /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ + 781095B7248D6DFD008A943C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CODE_SIGN_ENTITLEMENTS = InterposeTestHost/InterposeTestHost.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = Y5PE65HELJ; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = InterposeTestHost/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.steipete.InterposeTestHost; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTS_MACCATALYST = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 781095B8248D6DFD008A943C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CODE_SIGN_ENTITLEMENTS = InterposeTestHost/InterposeTestHost.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = Y5PE65HELJ; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = InterposeTestHost/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.steipete.InterposeTestHost; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTS_MACCATALYST = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 78863ECC2464B2F900BA3762 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 78C39DC1248317B400B46395 /* Defaults-Debug.xcconfig */; @@ -461,6 +629,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 781095B6248D6DFD008A943C /* Build configuration list for PBXNativeTarget "InterposeTestHost" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 781095B7248D6DFD008A943C /* Debug */, + 781095B8248D6DFD008A943C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 78863EC02464B2F900BA3762 /* Build configuration list for PBXProject "InterposeKit" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/InterposeTestHost/AppDelegate.swift b/InterposeTestHost/AppDelegate.swift new file mode 100644 index 0000000..1a725f1 --- /dev/null +++ b/InterposeTestHost/AppDelegate.swift @@ -0,0 +1,37 @@ +// +// AppDelegate.swift +// InterposeTestHost +// +// Created by Peter Steinberger on 07.06.20. +// Copyright © 2020 PSPDFKit GmbH. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/InterposeTestHost/Assets.xcassets/AppIcon.appiconset/Contents.json b/InterposeTestHost/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/InterposeTestHost/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/InterposeTestHost/Assets.xcassets/Contents.json b/InterposeTestHost/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/InterposeTestHost/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/InterposeTestHost/Base.lproj/LaunchScreen.storyboard b/InterposeTestHost/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/InterposeTestHost/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/InterposeTestHost/Base.lproj/Main.storyboard b/InterposeTestHost/Base.lproj/Main.storyboard new file mode 100644 index 0000000..25a7638 --- /dev/null +++ b/InterposeTestHost/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/InterposeTestHost/Info.plist b/InterposeTestHost/Info.plist new file mode 100644 index 0000000..fc1c20f --- /dev/null +++ b/InterposeTestHost/Info.plist @@ -0,0 +1,66 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSApplicationCategoryType + public.app-category.developer-tools + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/InterposeTestHost/InterposeTestHost.entitlements b/InterposeTestHost/InterposeTestHost.entitlements new file mode 100644 index 0000000..ee95ab7 --- /dev/null +++ b/InterposeTestHost/InterposeTestHost.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/InterposeTestHost/SceneDelegate.swift b/InterposeTestHost/SceneDelegate.swift new file mode 100644 index 0000000..398728d --- /dev/null +++ b/InterposeTestHost/SceneDelegate.swift @@ -0,0 +1,53 @@ +// +// SceneDelegate.swift +// InterposeTestHost +// +// Created by Peter Steinberger on 07.06.20. +// Copyright © 2020 PSPDFKit GmbH. All rights reserved. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/InterposeTestHost/ViewController.swift b/InterposeTestHost/ViewController.swift new file mode 100644 index 0000000..9bfb8ea --- /dev/null +++ b/InterposeTestHost/ViewController.swift @@ -0,0 +1,20 @@ +// +// ViewController.swift +// InterposeTestHost +// +// Created by Peter Steinberger on 07.06.20. +// Copyright © 2020 PSPDFKit GmbH. All rights reserved. +// + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + } + + +} + diff --git a/Sources/InterposeKit/AnyHook.swift b/Sources/InterposeKit/AnyHook.swift new file mode 100644 index 0000000..1111ed5 --- /dev/null +++ b/Sources/InterposeKit/AnyHook.swift @@ -0,0 +1,91 @@ +import Foundation + + +public class AnyHook { + public let `class`: AnyClass + public let selector: Selector + public internal(set) var state = State.prepared + // fetched at apply time, changes late, thus class requirement + public internal(set) var origIMP: IMP? + // else we validate init order + public internal(set) var replacementIMP: IMP! + + /// The possible task states + public enum State: Equatable { + /// The task is prepared to be nterposed. + case prepared + + /// The method has been successfully interposed. + case interposed + + /// An error happened while interposing a method. + case error(Interpose.Error) + } + + init(`class`: AnyClass, selector: Selector) throws { + self.selector = selector + self.class = `class` + // Check if method exists + try validate() + // replacementIMP = imp_implementationWithBlock(implementation(self)) + } + + func replaceImplementation() throws { + preconditionFailure("Not implemented") + } + + func resetImplementation() throws { + preconditionFailure("Not implemented") + } + + + /// Apply the interpose hook. + public func apply() throws { + try execute(newState: .interposed) { try replaceImplementation() } + } + + /// Revert the interpose hoook. + public func revert() throws { + try execute(newState: .prepared) { try resetImplementation() } + } + + // public func callAsFunction(_ type: U.Type) -> U { + // unsafeBitCast(origIMP, to: type) + // } + + /// Validate that the selector exists on the active class. + @discardableResult func validate(expectedState: State = .prepared) throws -> Method { + guard let method = class_getInstanceMethod(`class`, selector) else { throw Interpose.Error.methodNotFound } + guard state == expectedState else { throw Interpose.Error.invalidState } + return method + } + + private func execute(newState: State, task: () throws -> Void) throws { + do { + try task() + state = newState + } catch let error as Interpose.Error { + state = .error(error) + throw error + } + } + + /// Release the hook block if possible. + public func cleanup() { + switch state { + case .prepared: + Interpose.log("Releasing -[\(`class`).\(selector)] IMP: \(replacementIMP!)") + imp_removeBlock(replacementIMP) + case .interposed: + Interpose.log("Keeping -[\(`class`).\(selector)] IMP: \(replacementIMP!)") + case let .error(error): + Interpose.log("Leaking -[\(`class`).\(selector)] IMP: \(replacementIMP!) due to error: \(error)") + } + } +} + +public class TypedHook: AnyHook { + public var original: MethodSignature { + unsafeBitCast(origIMP, to: MethodSignature.self) + } +} diff --git a/Sources/InterposeKit/ClassHook.swift b/Sources/InterposeKit/ClassHook.swift index 647e576..a5dca0b 100644 --- a/Sources/InterposeKit/ClassHook.swift +++ b/Sources/InterposeKit/ClassHook.swift @@ -1,30 +1,24 @@ import Foundation +extension Interpose { /// A hook to an instance method and stores both the original and new implementation. -final class ClassHook: InternalHookable { - public let `class`: AnyClass - public let selector: Selector - public internal(set) var origIMP: IMP? // fetched at apply time, changes late, thus class requirement - public private(set) var replacementIMP: IMP! // else we validate init order - public internal(set) var state = Interpose.State.prepared +final public class ClassHook: TypedHook { /// Initialize a new hook to interpose an instance method. - init(`class`: AnyClass, selector: Selector, implementation: (Hookable) -> Any) throws { - self.selector = selector - self.class = `class` - // Check if method exists - try validate() - replacementIMP = imp_implementationWithBlock(implementation(self)) + // TODO: report compiler crash + public init(`class`: AnyClass, selector: Selector, implementation:(ClassHook) -> HookSignature?) /* This must be optional or swift runtime will crash. Or swiftc may segfault. Compiler bug? */ throws { + try super.init(class: `class`, selector: selector) + replacementIMP = imp_implementationWithBlock(implementation(self) as Any) } - func replaceImplementation() throws { + override func replaceImplementation() throws { let method = try validate() origIMP = class_replaceMethod(`class`, selector, replacementIMP, method_getTypeEncoding(method)) guard origIMP != nil else { throw Interpose.Error.nonExistingImplementation } Interpose.log("Swizzled -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") } - func resetImplementation() throws { + override func resetImplementation() throws { let method = try validate(expectedState: .interposed) precondition(origIMP != nil) let previousIMP = class_replaceMethod(`class`, selector, origIMP!, method_getTypeEncoding(method)) @@ -32,9 +26,10 @@ final class ClassHook: InternalHookable { Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") } } +} #if DEBUG -extension ClassHook: CustomDebugStringConvertible { +extension Interpose.ClassHook: CustomDebugStringConvertible { public var debugDescription: String { return "\(selector) -> \(String(describing: origIMP))" } diff --git a/Sources/InterposeKit/Hook.swift b/Sources/InterposeKit/Hook.swift deleted file mode 100644 index e91190e..0000000 --- a/Sources/InterposeKit/Hook.swift +++ /dev/null @@ -1,86 +0,0 @@ -import Foundation - -public protocol Hookable: class { - /// The class this hook operates on. - var `class`: AnyClass { get } - - /// If Interposing is object-based, this is set. - //var object: AnyObject? { get } - - /// The selector this hook operates on. - var selector: Selector { get } - - /// The original implementation is set once the swizzling is complete. - var origIMP: IMP? { get } - - /// The replacement implementation is created on initialization time. - var replacementIMP: IMP! { get } - - /// The state of the interpose operation. - var state: Interpose.State { get } - - /// Apply the interpose hook. - func apply() throws - - /// Revert the interpose hoook. - func revert() throws - - /// Convenience to call the original implementation. - func callAsFunction(_ type: U.Type) -> U -} - -extension Hookable { - public func callAsFunction(_ type: U.Type) -> U { - unsafeBitCast(origIMP, to: type) - } - - /// Validate that the selector exists on the active class. - @discardableResult func validate(expectedState: Interpose.State = .prepared) throws -> Method { - guard let method = class_getInstanceMethod(`class`, selector) else { throw Interpose.Error.methodNotFound } - guard state == expectedState else { throw Interpose.Error.invalidState } - return method - } -} - -public protocol InternalHookable: Hookable { - var state: Interpose.State { get set } - - func replaceImplementation() throws - - func resetImplementation() throws - - func cleanup() -} - -extension InternalHookable { - func apply() throws { - try execute(newState: .interposed) { try replaceImplementation() } - } - - func revert() throws { - try execute(newState: .prepared) { try resetImplementation() } - } - - private func execute(newState: Interpose.State, task: () throws -> Void) throws { - do { - try task() - state = newState - } catch let error as Interpose.Error { - state = .error(error) - throw error - } - } - - /// Release the hook block if possible. - public func cleanup() { - switch state { - case .prepared: - Interpose.log("Releasing -[\(`class`).\(selector)] IMP: \(replacementIMP!)") - imp_removeBlock(replacementIMP) - case .interposed: - Interpose.log("Keeping -[\(`class`).\(selector)] IMP: \(replacementIMP!)") - case let .error(error): - Interpose.log("Leaking -[\(`class`).\(selector)] IMP: \(replacementIMP!) due to error: \(error)") - } - } -} diff --git a/Sources/InterposeKit/InterposeKit.swift b/Sources/InterposeKit/InterposeKit.swift index 87e38e2..2ef48dc 100644 --- a/Sources/InterposeKit/InterposeKit.swift +++ b/Sources/InterposeKit/InterposeKit.swift @@ -5,7 +5,7 @@ final public class Interpose { /// Stores swizzle hooks and executes them at once. public let `class`: AnyClass /// Lists all hooks for the current interpose class object. - public private(set) var hooks: [Hookable] = [] + public private(set) var hooks: [AnyHook] = [] /// If Interposing is object-based, this is set. public let object: AnyObject? @@ -34,21 +34,26 @@ final public class Interpose { } deinit { - guard let internalHooks = hooks as? [InternalHookable] else { return } - internalHooks.forEach({ $0.cleanup() }) + hooks.forEach({ $0.cleanup() }) } - + /// Hook an `@objc dynamic` instance method via selector name on the current class. - @discardableResult public func hook(_ selName: String, - _ implementation: (Hookable) -> Any) throws -> Hookable { - try hook(NSSelectorFromString(selName), implementation) + @discardableResult public func hook( + _ selName: String, + methodSignature: MethodSignature.Type = MethodSignature.self, + hookSignature: HookSignature.Type = HookSignature.self, + _ implementation:(TypedHook) -> HookSignature?) throws -> TypedHook { + try hook(NSSelectorFromString(selName), methodSignature: methodSignature, hookSignature: hookSignature, implementation) } /// Hook an `@objc dynamic` instance method via selector on the current class. - @discardableResult public func hook(_ selector: Selector, - _ implementation: (Hookable) -> Any) throws -> Hookable { + @discardableResult public func hook ( + _ selector: Selector, + methodSignature: MethodSignature.Type = MethodSignature.self, + hookSignature: HookSignature.Type = HookSignature.self, + _ implementation:(TypedHook) -> HookSignature?) throws -> TypedHook { - var hook: InternalHookable + var hook: TypedHook if let object = self.object { hook = try ObjectHook(object: object, selector: selector, implementation: implementation) } else { @@ -69,14 +74,14 @@ final public class Interpose { } private func execute(_ task: ((Interpose) throws -> Void)? = nil, - expectedState: Interpose.State = .prepared, - executor: ((Hookable) throws -> Void)) throws -> Interpose { + expectedState: AnyHook.State = .prepared, + executor: ((AnyHook) throws -> Void)) throws -> Interpose { // Run pre-apply code first if let task = task { try task(self) } // Validate all tasks, stop if anything is not valid - guard let internalHooks = hooks as? [InternalHookable], internalHooks.allSatisfy({ + guard hooks.allSatisfy({ (try? $0.validate(expectedState: expectedState)) != nil }) else { throw Error.invalidState @@ -103,18 +108,6 @@ final public class Interpose { /// Can't revert or apply if already done so. case invalidState } - - /// The possible task states. - public enum State: Equatable { - /// The task is prepared to be interposed. - case prepared - - /// The method has been successfully interposed. - case interposed - - /// An error happened while interposing a method. - case error(Interpose.Error) - } } // MARK: Logging diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index 3a0a8db..f42e26b 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -1,5 +1,7 @@ import Foundation +extension Interpose { + private enum Constants { static let subclassSuffix = "InterposeKit_" } @@ -18,25 +20,17 @@ internal enum ObjCMethodEncoding { /// A hook to an instance method of a single object, stores both the original and new implementation. /// Think about: Multiple hooks for one object -final class ObjectHook: InternalHookable { - public let `class`: AnyClass - public let selector: Selector - public internal(set) var origIMP: IMP? // fetched at apply time, changes late, thus class requirement - public private(set) var replacementIMP: IMP! // else we validate init order - public internal(set) var state = Interpose.State.prepared +final class ObjectHook: TypedHook { public let object: AnyObject /// Subclass that we create on the fly var dynamicSubclass: AnyClass? /// Initialize a new hook to interpose an instance method. - public init(object: AnyObject, selector: Selector, implementation: (Hookable) -> Any) throws { + public init(object: AnyObject, selector: Selector, implementation:(TypedHook) -> HookSignature?) throws { self.object = object - self.selector = selector - self.class = type(of: object) - // Check if method exists - try validate() - replacementIMP = imp_implementationWithBlock(implementation(self)) + try super.init(class: type(of: object), selector: selector) + replacementIMP = imp_implementationWithBlock(implementation(self) as Any) } // /// Release the hook block if possible. @@ -119,7 +113,7 @@ final class ObjectHook: InternalHookable { class_addMethod(subclass, self.selector, imp_implementationWithBlock(block), typeEncoding) } - func replaceImplementation() throws { + override func replaceImplementation() throws { let method = try validate() // Register a KVO to work around any KVO issues with opposite order @@ -137,7 +131,7 @@ final class ObjectHook: InternalHookable { Interpose.log("Swizzled -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") } - func resetImplementation() throws { + override func resetImplementation() throws { let method = try validate(expectedState: .interposed) precondition(origIMP != nil) guard let dynamicSubclass = self.dynamicSubclass else { preconditionFailure("No dynamic subclass set") } @@ -158,6 +152,7 @@ final class ObjectHook: InternalHookable { deregisterKVO() } + // MARK: KVO Helper var kvoObserver: KVOObserver? @@ -186,17 +181,12 @@ final class ObjectHook: InternalHookable { kvoObserver = nil } } +} #if DEBUG -extension ObjectHook: CustomDebugStringConvertible { +extension Interpose.ObjectHook: CustomDebugStringConvertible { public var debugDescription: String { return "\(selector) of \(object) -> \(String(describing: origIMP))" } } #endif - -// FB7728351: watchOS doesn't define va_list -#if os(watchOS) -// swiftlint:disable:nex type_name -public typealias va_list = __darwin_va_list -#endif diff --git a/Tests/InterposeKitTests/InterposeKitTests.swift b/Tests/InterposeKitTests/InterposeKitTests.swift index 3f1c8be..d553202 100644 --- a/Tests/InterposeKitTests/InterposeKitTests.swift +++ b/Tests/InterposeKitTests/InterposeKitTests.swift @@ -13,21 +13,20 @@ final class InterposeKitTests: XCTestCase { // Functions need to be `@objc dynamic` to be hookable. let interposer = try Interpose(TestClass.self) { - try $0.hook(#selector(TestClass.sayHi), { store in { `self` in - - print("Before Interposing \(`self`)") - - // Calling convention and passing selector is important! - // You're free to skip calling the original implementation. - let origCall = store((@convention(c) (AnyObject, Selector) -> String).self) - let string = origCall(`self`, store.selector) - - print("After Interposing \(`self`)") - - return string + testSwizzleAddition - - // Similar signature cast as above, but without selector. - } as @convention(block) (AnyObject) -> String}) + try $0.hook( + #selector(TestClass.sayHi), + methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, + hookSignature: (@convention(block) (AnyObject) -> String).self) { + store in { `self` in + + // You're free to skip calling the original implementation. + print("Before Interposing \(`self`)") + let string = store.original(`self`, store.selector) + print("After Interposing \(`self`)") + + return string + testSwizzleAddition + } + } } print(TestClass().sayHi()) @@ -53,10 +52,14 @@ final class InterposeKitTests: XCTestCase { // Swizzle test class let interposed = try Interpose(TestClass.self) { - try $0.hook(#selector(TestClass.sayHi), { store in { `self` in - let origCall = store((@convention(c) (AnyObject, Selector) -> String).self) - return origCall(`self`, store.selector) + testSwizzleAddition - } as @convention(block) (AnyObject) -> String}) + try $0.hook( + #selector(TestClass.sayHi), + methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, + hookSignature: (@convention(block) (AnyObject) -> String).self) { + store in { `self` in + return store.original(`self`, store.selector) + testSwizzleAddition + } + } } XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition + testSubclass) @@ -67,10 +70,14 @@ final class InterposeKitTests: XCTestCase { // Swizzle subclass, automatically applys let interposedSubclass = try Interpose(TestSubclass.self) { - try $0.hook(#selector(TestSubclass.sayHi), { store in { blockSelf in - let origCall = store((@convention(c) (AnyObject, Selector) -> String).self) - return origCall(blockSelf, store.selector) + testSwizzleAddition - } as @convention(block) (AnyObject) -> String}) + try $0.hook( + #selector(TestSubclass.sayHi), + methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, + hookSignature: (@convention(block) (AnyObject) -> String).self) { + store in { `self` in + return store.original(`self`, store.selector) + testSwizzleAddition + } + } } XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition + testSubclass + testSwizzleAddition) @@ -90,11 +97,15 @@ final class InterposeKitTests: XCTestCase { // Swizzle test class let interposer = try Interpose(TestClass.self) { - try $0.hook(#selector(TestClass.doNothing), { store in { `self` in - tracker.keep() - let origCall = store((@convention(c) (AnyObject, Selector) -> Void).self) - return origCall(`self`, store.selector) - } as @convention(block) (AnyObject) -> Void }) + try $0.hook( + #selector(TestClass.doNothing), + methodSignature: (@convention(c) (AnyObject, Selector) -> Void).self, + hookSignature: (@convention(block) (AnyObject) -> Void).self) { + store in { `self` in + tracker.keep() + return store.original(`self`, store.selector) + } + } } // Dealloc interposer without removing hooks @@ -115,11 +126,15 @@ final class InterposeKitTests: XCTestCase { // Swizzle test class let interposer = try Interpose(TestClass.self) { - try $0.hook(#selector(TestClass.doNothing), { store in { `self` in - tracker.keep() - let origCall = store((@convention(c) (AnyObject, Selector) -> Void).self) - return origCall(`self`, store.selector) - } as @convention(block) (AnyObject) -> Void }) + try $0.hook( + #selector(TestClass.doNothing), + methodSignature: (@convention(c) (AnyObject, Selector) -> Void).self, + hookSignature: (@convention(block) (AnyObject) -> Void).self) { + store in { `self` in + tracker.keep() + return store.original(`self`, store.selector) + } + } } try interposer.revert() diff --git a/Tests/InterposeKitTests/ObjectInterposeTests.swift b/Tests/InterposeKitTests/ObjectInterposeTests.swift index 8019778..912f49a 100644 --- a/Tests/InterposeKitTests/ObjectInterposeTests.swift +++ b/Tests/InterposeKitTests/ObjectInterposeTests.swift @@ -17,22 +17,24 @@ final class ObjectInterposeTests: XCTestCase { // Functions need to be `@objc dynamic` to be hookable. let interposer = try Interpose(testObj) { - try $0.hook(#selector(TestClass.sayHi), { store in { `self` in + try $0.hook( + #selector(TestClass.sayHi), + methodSignature: (@convention(c) (AnyObject, Selector) -> String).self) { store in { `self` in - print("Before Interposing \(`self`)") + print("Before Interposing \(`self`)") - // Calling convention and passing selector is important! - // You're free to skip calling the original implementation. - let origCall = store((@convention(c) (AnyObject, Selector) -> String).self) - let string = origCall(`self`, store.selector) + // Calling convention and passing selector is important! + // You're free to skip calling the original implementation. + let string = store.original(`self`, store.selector) - print("After Interposing \(`self`)") + print("After Interposing \(`self`)") - return string + testSwizzleAddition + return string + testSwizzleAddition - // Similar signature cast as above, but without selector. - } as @convention(block) (AnyObject) -> String}) + // Similar signature cast as above, but without selector. + } as @convention(block) (AnyObject) -> String } } + XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition) XCTAssertEqual(testObj2.sayHi(), testClassHi) try interposer.revert() @@ -51,15 +53,13 @@ final class ObjectInterposeTests: XCTestCase { // Functions need to be `@objc dynamic` to be hookable. let interposer = try Interpose(testObj) { - try $0.hook(#selector(TestClass.returnInt), { store in { `self` in - // Calling convention and passing selector is important! + try $0.hook(#selector(TestClass.returnInt)) { (store: TypedHook<@convention(c) (AnyObject, Selector) -> Int, @convention(block) (AnyObject) -> Int>) in { + // You're free to skip calling the original implementation. - let origCall = store((@convention(c) (AnyObject, Selector) -> Int).self) - let int = origCall(`self`, store.selector) + let int = store.original($0, store.selector) return int + returnIntOverrideOffset - - // Similar signature cast as above, but without selector. - } as @convention(block) (AnyObject) -> Int}) + } + } } XCTAssertEqual(testObj.returnInt(), returnIntDefault + returnIntOverrideOffset) try interposer.revert() @@ -82,29 +82,20 @@ final class ObjectInterposeTests: XCTestCase { // Functions need to be `@objc dynamic` to be hookable. let interposer = try Interpose(testObj) { - try $0.hook(#selector(TestClass.returnInt), { store in { `self` in - // Calling convention and passing selector is important! + try $0.hook(#selector(TestClass.returnInt)) { (store: TypedHook<@convention(c) (AnyObject, Selector) -> Int, @convention(block) (AnyObject) -> Int>) in { // You're free to skip calling the original implementation. - let origCall = store((@convention(c) (AnyObject, Selector) -> Int).self) - let int = origCall(`self`, store.selector) - return int + returnIntOverrideOffset - - // Similar signature cast as above, but without selector. - } as @convention(block) (AnyObject) -> Int}) + store.original($0, store.selector) + returnIntOverrideOffset + } + } } XCTAssertEqual(testObj.returnInt(), returnIntDefault + returnIntOverrideOffset) // Interpose on TestClass itself! let classInterposer = try Interpose(TestClass.self) { - try $0.hook(#selector(TestClass.returnInt), { store in { `self` in - // Calling convention and passing selector is important! - // You're free to skip calling the original implementation. - let origCall = store((@convention(c) (AnyObject, Selector) -> Int).self) - let int = origCall(`self`, store.selector) - return int * returnIntClassMultiplier - - // Similar signature cast as above, but without selector. - } as @convention(block) (AnyObject) -> Int}) + try $0.hook(#selector(TestClass.returnInt)) { (store: TypedHook<@convention(c) (AnyObject, Selector) -> Int, @convention(block) (AnyObject) -> Int>) in { + store.original($0, store.selector) * returnIntClassMultiplier + } + } } XCTAssertEqual(testObj.returnInt(), (returnIntDefault * returnIntClassMultiplier) + returnIntOverrideOffset) From eadb0734a479f8eec6e0fca087eede84cf827658 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 8 Jun 2020 16:35:01 +0200 Subject: [PATCH 18/77] Add ITKAddSuperMethod and new generics --- InterposeKit.xcodeproj/project.pbxproj | 97 ++++++++++++-- .../xcschemes/InterposeKit.xcscheme | 11 +- .../xcschemes/InterposeTests.xcscheme | 8 +- InterposeTestHost/AppDelegate.swift | 30 +---- InterposeTestHost/Base.lproj/Main.storyboard | 38 ++++-- InterposeTestHost/Info.plist | 19 --- InterposeTestHost/SceneDelegate.swift | 53 -------- Package.swift | 17 +-- Sources/ITKAddSuperMethod/ITKAddSuperMethod.h | 20 +++ Sources/ITKAddSuperMethod/ITKAddSuperMethod.m | 124 ++++++++++++++++++ Sources/InterposeKit/ObjectHook.swift | 38 ++---- Tests/InterposeKitTests/TestClass.swift | 4 + 12 files changed, 296 insertions(+), 163 deletions(-) delete mode 100644 InterposeTestHost/SceneDelegate.swift create mode 100644 Sources/ITKAddSuperMethod/ITKAddSuperMethod.h create mode 100644 Sources/ITKAddSuperMethod/ITKAddSuperMethod.m diff --git a/InterposeKit.xcodeproj/project.pbxproj b/InterposeKit.xcodeproj/project.pbxproj index 9e50ef8..22b3392 100644 --- a/InterposeKit.xcodeproj/project.pbxproj +++ b/InterposeKit.xcodeproj/project.pbxproj @@ -10,11 +10,14 @@ 7810959E248D43DC008A943C /* ClassHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7810959D248D43DC008A943C /* ClassHook.swift */; }; 781095A0248D50C1008A943C /* Watcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7810959F248D50C1008A943C /* Watcher.swift */; }; 781095A8248D6DFB008A943C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781095A7248D6DFB008A943C /* AppDelegate.swift */; }; - 781095AA248D6DFB008A943C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781095A9248D6DFB008A943C /* SceneDelegate.swift */; }; 781095AC248D6DFB008A943C /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781095AB248D6DFB008A943C /* ViewController.swift */; }; 781095AF248D6DFB008A943C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 781095AD248D6DFB008A943C /* Main.storyboard */; }; 781095B1248D6DFD008A943C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 781095B0248D6DFD008A943C /* Assets.xcassets */; }; 781095B4248D6DFD008A943C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 781095B2248D6DFD008A943C /* LaunchScreen.storyboard */; }; + 781095F5248E7C91008A943C /* InterposeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78863EC62464B2F900BA3762 /* InterposeKit.framework */; }; + 781095F6248E7C91008A943C /* InterposeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 78863EC62464B2F900BA3762 /* InterposeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 781095FF248E83D7008A943C /* ITKAddSuperMethod.h in Headers */ = {isa = PBXBuildFile; fileRef = 781095FD248E83D7008A943C /* ITKAddSuperMethod.h */; }; + 78109600248E83D7008A943C /* ITKAddSuperMethod.m in Sources */ = {isa = PBXBuildFile; fileRef = 781095FE248E83D7008A943C /* ITKAddSuperMethod.m */; }; 78C39D7C2482CC7D00B46395 /* InterposeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78863EC62464B2F900BA3762 /* InterposeKit.framework */; }; 78C39D8F2483164500B46395 /* InterposeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 78C39D8E2483164500B46395 /* InterposeKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 78C39D912483165600B46395 /* InterposeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C39D902483165600B46395 /* InterposeKit.swift */; }; @@ -34,6 +37,13 @@ remoteGlobalIDString = 781095A4248D6DFB008A943C; remoteInfo = InterposeTestHost; }; + 781095F1248E7C72008A943C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 78863EBD2464B2F900BA3762 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 78863EC52464B2F900BA3762; + remoteInfo = InterposeKit; + }; 78C39D7D2482CC7D00B46395 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 78863EBD2464B2F900BA3762 /* Project object */; @@ -43,21 +53,36 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 781095F7248E7C91008A943C /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 781095F6248E7C91008A943C /* InterposeKit.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 7810959D248D43DC008A943C /* ClassHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ClassHook.swift; path = Sources/InterposeKit/ClassHook.swift; sourceTree = SOURCE_ROOT; }; 7810959F248D50C1008A943C /* Watcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Watcher.swift; path = Sources/InterposeKit/Watcher.swift; sourceTree = SOURCE_ROOT; }; 781095A5248D6DFB008A943C /* InterposeTestHost.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InterposeTestHost.app; sourceTree = BUILT_PRODUCTS_DIR; }; 781095A7248D6DFB008A943C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 781095A9248D6DFB008A943C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 781095AB248D6DFB008A943C /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 781095AE248D6DFB008A943C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 781095B0248D6DFD008A943C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 781095B3248D6DFD008A943C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 781095B5248D6DFD008A943C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 781095B9248D6E0A008A943C /* InterposeTestHost.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = InterposeTestHost.entitlements; sourceTree = ""; }; + 781095FD248E83D7008A943C /* ITKAddSuperMethod.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ITKAddSuperMethod.h; sourceTree = ""; }; + 781095FE248E83D7008A943C /* ITKAddSuperMethod.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ITKAddSuperMethod.m; sourceTree = ""; }; 78863EC62464B2F900BA3762 /* InterposeKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = InterposeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 78863ECA2464B2F900BA3762 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = Info.plist; path = InterposeKit.xcodeproj/Info.plist; sourceTree = ""; }; - 78C39D772482CC7D00B46395 /* InterposeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InterposeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 78C39D772482CC7D00B46395 /* InterposeKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InterposeKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 78C39D7B2482CC7D00B46395 /* Info-Tests.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = "Info-Tests.plist"; path = "InterposeKit.xcodeproj/Info-Tests.plist"; sourceTree = ""; }; 78C39D8E2483164500B46395 /* InterposeKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = InterposeKit.h; path = Sources/InterposeKit/InterposeKit.h; sourceTree = SOURCE_ROOT; }; 78C39D902483165600B46395 /* InterposeKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InterposeKit.swift; path = Sources/InterposeKit/InterposeKit.swift; sourceTree = SOURCE_ROOT; }; @@ -78,6 +103,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 781095F5248E7C91008A943C /* InterposeKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -104,7 +130,6 @@ children = ( 781095B9248D6E0A008A943C /* InterposeTestHost.entitlements */, 781095A7248D6DFB008A943C /* AppDelegate.swift */, - 781095A9248D6DFB008A943C /* SceneDelegate.swift */, 781095AB248D6DFB008A943C /* ViewController.swift */, 781095AD248D6DFB008A943C /* Main.storyboard */, 781095B0248D6DFD008A943C /* Assets.xcassets */, @@ -114,14 +139,33 @@ path = InterposeTestHost; sourceTree = ""; }; + 781095F3248E7C76008A943C /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 781095FC248E83D7008A943C /* ITKAddSuperMethod */ = { + isa = PBXGroup; + children = ( + 781095FD248E83D7008A943C /* ITKAddSuperMethod.h */, + 781095FE248E83D7008A943C /* ITKAddSuperMethod.m */, + ); + name = ITKAddSuperMethod; + path = Sources/ITKAddSuperMethod; + sourceTree = ""; + }; 78863EBC2464B2F900BA3762 = { isa = PBXGroup; children = ( + 781095FC248E83D7008A943C /* ITKAddSuperMethod */, 78863EC82464B2F900BA3762 /* InterposeKit */, 78C39D782482CC7D00B46395 /* InterposeTests */, 78C39DBD248317B400B46395 /* Configuration */, 781095A6248D6DFB008A943C /* InterposeTestHost */, 78863EC72464B2F900BA3762 /* Products */, + 781095F3248E7C76008A943C /* Frameworks */, ); sourceTree = ""; }; @@ -129,7 +173,7 @@ isa = PBXGroup; children = ( 78863EC62464B2F900BA3762 /* InterposeKit.framework */, - 78C39D772482CC7D00B46395 /* InterposeTests.xctest */, + 78C39D772482CC7D00B46395 /* InterposeKitTests.xctest */, 781095A5248D6DFB008A943C /* InterposeTestHost.app */, ); name = Products; @@ -179,6 +223,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 781095FF248E83D7008A943C /* ITKAddSuperMethod.h in Headers */, 78C39D8F2483164500B46395 /* InterposeKit.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -193,10 +238,12 @@ 781095A1248D6DFB008A943C /* Sources */, 781095A2248D6DFB008A943C /* Frameworks */, 781095A3248D6DFB008A943C /* Resources */, + 781095F7248E7C91008A943C /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( + 781095F2248E7C72008A943C /* PBXTargetDependency */, ); name = InterposeTestHost; productName = InterposeTestHost; @@ -221,9 +268,9 @@ productReference = 78863EC62464B2F900BA3762 /* InterposeKit.framework */; productType = "com.apple.product-type.framework"; }; - 78C39D762482CC7D00B46395 /* InterposeTests */ = { + 78C39D762482CC7D00B46395 /* InterposeKitTests */ = { isa = PBXNativeTarget; - buildConfigurationList = 78C39D7F2482CC7D00B46395 /* Build configuration list for PBXNativeTarget "InterposeTests" */; + buildConfigurationList = 78C39D7F2482CC7D00B46395 /* Build configuration list for PBXNativeTarget "InterposeKitTests" */; buildPhases = ( 78C39D732482CC7D00B46395 /* Sources */, 78C39D742482CC7D00B46395 /* Frameworks */, @@ -235,9 +282,9 @@ 78C39D7E2482CC7D00B46395 /* PBXTargetDependency */, 781095BB248D6E10008A943C /* PBXTargetDependency */, ); - name = InterposeTests; + name = InterposeKitTests; productName = InterposeTests; - productReference = 78C39D772482CC7D00B46395 /* InterposeTests.xctest */; + productReference = 78C39D772482CC7D00B46395 /* InterposeKitTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ @@ -260,6 +307,7 @@ 78C39D762482CC7D00B46395 = { CreatedOnToolsVersion = 11.5; LastSwiftMigration = 1150; + TestTargetID = 781095A4248D6DFB008A943C; }; }; }; @@ -277,7 +325,7 @@ projectRoot = ""; targets = ( 78863EC52464B2F900BA3762 /* InterposeKit */, - 78C39D762482CC7D00B46395 /* InterposeTests */, + 78C39D762482CC7D00B46395 /* InterposeKitTests */, 781095A4248D6DFB008A943C /* InterposeTestHost */, ); }; @@ -317,7 +365,6 @@ files = ( 781095AC248D6DFB008A943C /* ViewController.swift in Sources */, 781095A8248D6DFB008A943C /* AppDelegate.swift in Sources */, - 781095AA248D6DFB008A943C /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -325,6 +372,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 78109600248E83D7008A943C /* ITKAddSuperMethod.m in Sources */, 781095A0248D50C1008A943C /* Watcher.swift in Sources */, 7810959E248D43DC008A943C /* ClassHook.swift in Sources */, 78C39D912483165600B46395 /* InterposeKit.swift in Sources */, @@ -352,6 +400,11 @@ target = 781095A4248D6DFB008A943C /* InterposeTestHost */; targetProxy = 781095BA248D6E10008A943C /* PBXContainerItemProxy */; }; + 781095F2248E7C72008A943C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 78863EC52464B2F900BA3762 /* InterposeKit */; + targetProxy = 781095F1248E7C72008A943C /* PBXContainerItemProxy */; + }; 78C39D7E2482CC7D00B46395 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 78863EC52464B2F900BA3762 /* InterposeKit */; @@ -386,6 +439,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CODE_SIGN_ENTITLEMENTS = InterposeTestHost/InterposeTestHost.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = dwarf; @@ -401,6 +455,8 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.steipete.InterposeTestHost; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SUPPORTS_MACCATALYST = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -414,6 +470,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CODE_SIGN_ENTITLEMENTS = InterposeTestHost/InterposeTestHost.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; @@ -429,6 +486,8 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.steipete.InterposeTestHost; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SUPPORTS_MACCATALYST = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -466,6 +525,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "Apple Development"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -523,6 +583,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "Apple Development"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; ENABLE_NS_ASSERTIONS = NO; @@ -593,7 +654,10 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = Y5PE65HELJ; INFOPLIST_FILE = "$(SRCROOT)/InterposeKit.xcodeproj/Info-Tests.plist"; @@ -604,7 +668,10 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.steipete.InterposeTests; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/InterposeTestHost.app/InterposeTestHost"; }; name = Debug; }; @@ -612,7 +679,10 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = Y5PE65HELJ; INFOPLIST_FILE = "$(SRCROOT)/InterposeKit.xcodeproj/Info-Tests.plist"; @@ -623,6 +693,9 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.steipete.InterposeTests; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/InterposeTestHost.app/InterposeTestHost"; }; name = Release; }; @@ -656,7 +729,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 78C39D7F2482CC7D00B46395 /* Build configuration list for PBXNativeTarget "InterposeTests" */ = { + 78C39D7F2482CC7D00B46395 /* Build configuration list for PBXNativeTarget "InterposeKitTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 78C39D802482CC7D00B46395 /* Debug */, diff --git a/InterposeKit.xcodeproj/xcshareddata/xcschemes/InterposeKit.xcscheme b/InterposeKit.xcodeproj/xcshareddata/xcschemes/InterposeKit.xcscheme index 5b12cf9..90f49c9 100644 --- a/InterposeKit.xcodeproj/xcshareddata/xcschemes/InterposeKit.xcscheme +++ b/InterposeKit.xcodeproj/xcshareddata/xcschemes/InterposeKit.xcscheme @@ -28,14 +28,21 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" codeCoverageEnabled = "YES"> + + + + diff --git a/InterposeKit.xcodeproj/xcshareddata/xcschemes/InterposeTests.xcscheme b/InterposeKit.xcodeproj/xcshareddata/xcschemes/InterposeTests.xcscheme index 2627530..722b662 100644 --- a/InterposeKit.xcodeproj/xcshareddata/xcschemes/InterposeTests.xcscheme +++ b/InterposeKit.xcodeproj/xcshareddata/xcschemes/InterposeTests.xcscheme @@ -15,8 +15,8 @@ @@ -52,8 +52,8 @@ diff --git a/InterposeTestHost/AppDelegate.swift b/InterposeTestHost/AppDelegate.swift index 1a725f1..6dbf796 100644 --- a/InterposeTestHost/AppDelegate.swift +++ b/InterposeTestHost/AppDelegate.swift @@ -1,37 +1,17 @@ -// -// AppDelegate.swift -// InterposeTestHost -// -// Created by Peter Steinberger on 07.06.20. -// Copyright © 2020 PSPDFKit GmbH. All rights reserved. -// import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - - + var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. + window = UIWindow(frame: UIScreen.main.bounds) + window!.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController()! + (window!.rootViewController as? UINavigationController)?.topViewController?.title = "Test Host" + window!.makeKeyAndVisible() return true } - // MARK: UISceneSession Lifecycle - - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - } - - } diff --git a/InterposeTestHost/Base.lproj/Main.storyboard b/InterposeTestHost/Base.lproj/Main.storyboard index 25a7638..0a52cef 100644 --- a/InterposeTestHost/Base.lproj/Main.storyboard +++ b/InterposeTestHost/Base.lproj/Main.storyboard @@ -1,24 +1,44 @@ - + + - + + + + + + + + + + + + + + + + + + - + - - - + + + - - + + + - + + diff --git a/InterposeTestHost/Info.plist b/InterposeTestHost/Info.plist index fc1c20f..1aeceb5 100644 --- a/InterposeTestHost/Info.plist +++ b/InterposeTestHost/Info.plist @@ -22,25 +22,6 @@ public.app-category.developer-tools LSRequiresIPhoneOS - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main - - - - UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/InterposeTestHost/SceneDelegate.swift b/InterposeTestHost/SceneDelegate.swift deleted file mode 100644 index 398728d..0000000 --- a/InterposeTestHost/SceneDelegate.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// SceneDelegate.swift -// InterposeTestHost -// -// Created by Peter Steinberger on 07.06.20. -// Copyright © 2020 PSPDFKit GmbH. All rights reserved. -// - -import UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - - var window: UIWindow? - - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - } - - -} - diff --git a/Package.swift b/Package.swift index 7479323..81dae69 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.2 +// swift-tools-version:5.0 import PackageDescription @@ -11,18 +11,11 @@ let package = Package( .watchOS(.v5) ], products: [ - // Products define the executables and libraries produced by a package, and make them visible to other packages. - .library( - name: "InterposeKit", - targets: ["InterposeKit"]), + .library(name: "InterposeKit",targets: ["InterposeKit"]), ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages which this package depends on. - .target( - name: "InterposeKit"), - .testTarget( - name: "InterposeKitTests", - dependencies: ["InterposeKit"]), + .target(name: "ITKAddSuperMethod"), + .target(name: "InterposeKit", dependencies: ["ITKAddSuperMethod"]), + .testTarget(name: "InterposeKitTests", dependencies: ["InterposeKit"]), ] ) diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h new file mode 100644 index 0000000..e8a427a --- /dev/null +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h @@ -0,0 +1,20 @@ +// +// ITKAddSuperMethod.h +// InterposeKit +// +// Created by Peter Steinberger on 08.06.20. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Adds an empty super implementation instance method to klass. + If a method already exists, this will return NO. + + @note This uses inline assembly to forward the parameters to objc_msgSendSuper. +*/ +BOOL IKTAddSuperImplementationToClass(Class klass, SEL selector); + +NS_ASSUME_NONNULL_END diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m new file mode 100644 index 0000000..9b4541f --- /dev/null +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m @@ -0,0 +1,124 @@ +// +// ITKAddSuperMethod.m +// InterposeKit +// +// Created by Peter Steinberger on 08.06.20. +// Copyright © 2020 PSPDFKit GmbH. All rights reserved. +// + +#import "ITKAddSuperMethod.h" + +@import ObjectiveC.message; +@import ObjectiveC.runtime; + +NS_ASSUME_NONNULL_BEGIN + +void msgSendSuperTrampoline(void); + +#if defined(__arm64__) + +__attribute__((__naked__)) +void msgSendSuperTrampoline(void) { + asm volatile ( + " sub sp, sp, #48 ; =48 \n\t" + " stp x29, x30, [sp, #32] ; 16-byte Folded Spill \n\t" + " add x29, sp, #32 ; =32 \n\t" + " stur x0, [x29, #-8] \n\t" + " str x1, [sp, #16] \n\t" + " ldur x8, [x29, #-8] \n\t" + " str x8, [sp] \n\t" + " ldur x0, [x29, #-8] \n\t" + " bl _objc_opt_class \n\t" + " str x0, [sp, #8] \n\t" + " ldr x1, [sp, #16] \n\t" + " mov x0, sp \n\t" + " bl _objc_msgSendSuper \n\t" + " mov x29, x29 ; marker for objc_retainAutoreleaseReturnValue \n\t" + " ldp x29, x30, [sp, #32] ; 16-byte Folded Reload \n\t" + " add sp, sp, #48 ; =48 \n\t" + " ret \n\t" + : : : "x0", "x1"); +} + +#elif defined(__x86_64__) + +__attribute__((__naked__)) +void msgSendSuperTrampoline(void) { + asm volatile ( + "pushq %%rbp # push frame pointer \n\t" + "movq %%rsp, %%rbp # set stack to frame pointer \n\t" + "subq $32, %%rsp # reserve 32 byte on the stack (need 16 byte alignment) \n\t" + "movq %%rdi, -8(%%rbp) # copy self to stack[1] \n\t" + "movq %%rsi, -16(%%rbp) # copy _cmd to stack[2] \n\t" + "movq -8(%%rbp), %%rax # load self to rax \n\t" + "movq %%rax, -32(%%rbp) # store self to stack[4] \n\t" + "movq -8(%%rbp), %%rdi # load self to rdi-first parameter \n\t" + "callq _objc_opt_class # call objc_opt_class(self) \n\t" + "#movq %%rax, %%rdi # move result to rdi-first parameter \n\t" + "#callq _class_getSuperclass # call class_getSuperclass(self) \n\t" + "movq %%rax, -24(%%rbp) # move result to stack[3] \n\t" + "movq -16(%%rbp), %%rsi # copy _cmd to #rsi \n\t" + "xorl %%ecx, %%ecx # nill out rcx? \n\t" + "leaq -32(%%rbp), %%rdi # load address of objc_super struct to rdi-first param \n\t" + "movb %%cl, %%al # nill ot rax/rcx? \n\t" + "callq _objc_msgSendSuper # regular call \n\t" + "movq %%rax, %%rdi \n\t" + "addq $32, %%rsp # remove 32 byte from stack \n\t" + "popq %%rbp # pop frame pointer \n\t" + "retq\n\r" + : : : "rsi", "rdi"); +} + +#endif + +typedef NS_ENUM(NSInteger, DispatchMode) { + DispatchMode_Normal, + DispatchMode_Stret, +}; + +static DispatchMode IKTGetDispatchMode(const char * typeEncoding) { + DispatchMode dispatchMode = DispatchMode_Normal; +#if defined (__arm64__) + // ARM64 doesn't use stret dispatch +#elif defined (__x86_64__) + // On x86-64, stret dispatch is used whenever return type doesn't fit into two registers + NSUInteger returnTypeActualSize = 0; + NSGetSizeAndAlignment(typeEncoding, &returnTypeActualSize, NULL); + dispatchMode = returnTypeActualSize > (sizeof(void *) * 2) ? DispatchMode_Stret : DispatchMode_Normal; +#else +#error - Unknown architecture +#endif + return dispatchMode; +} + +BOOL IKTAddSuperImplementationToClass(Class klass, SEL selector) { + Class originalClass = klass; + + Class superClass = class_getSuperclass(originalClass); + if (superClass == nil) { + return NO; + } + Method method = class_getInstanceMethod(superClass, selector); + if (method == NULL) { + [NSException raise:NSInternalInconsistencyException + format:@"No dynamically dispatched method with selector %@ is available on any of the superclasses of %@", + NSStringFromSelector(selector), NSStringFromClass(originalClass)]; + return NO; + } + const char *typeEncoding = method_getTypeEncoding(method); + // Need to write asm for x64 + __unused DispatchMode dispatchMode = IKTGetDispatchMode(typeEncoding); + BOOL methodAdded = class_addMethod(klass, + selector, + msgSendSuperTrampoline, + typeEncoding); + if (!methodAdded) { + NSLog(@"Failed to add method for selector %@ to class %@", + NSStringFromSelector(selector), + NSStringFromClass(klass)); + } + + return methodAdded; +} + +NS_ASSUME_NONNULL_END diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index f42e26b..0ec1fad 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -20,14 +20,13 @@ internal enum ObjCMethodEncoding { /// A hook to an instance method of a single object, stores both the original and new implementation. /// Think about: Multiple hooks for one object -final class ObjectHook: TypedHook { - +final public class ObjectHook: TypedHook { public let object: AnyObject /// Subclass that we create on the fly var dynamicSubclass: AnyClass? /// Initialize a new hook to interpose an instance method. - public init(object: AnyObject, selector: Selector, implementation:(TypedHook) -> HookSignature?) throws { + public init(object: AnyObject, selector: Selector, implementation:(ObjectHook) -> HookSignature?) throws { self.object = object try super.init(class: type(of: object), selector: selector) replacementIMP = imp_implementationWithBlock(implementation(self) as Any) @@ -80,37 +79,21 @@ final class ObjectHook: TypedHook public var superClass: AnyClass } - // https://opensource.apple.com/source/objc4/objc4-493.9/runtime/objc-abi.h - // objc_msgSendSuper2() takes the current search class, not its superclass. - // OBJC_EXPORT id objc_msgSendSuper2(struct objc_super *super, SEL op, ...) - private lazy var msgSendSuper2: UnsafeMutableRawPointer = { + private lazy var addSuperImpl: @convention(c) (AnyClass, Selector) -> Bool = { let handle = dlopen(nil, RTLD_LAZY) - return dlsym(handle, "objc_msgSendSuper2") + let imp = dlsym(handle, "IKTAddSuperImplementationToClass") + return unsafeBitCast(imp, to: (@convention(c) (AnyClass, Selector) -> Bool).self) }() - private func addSuperTrampolineMethod(subclass: AnyClass, method: Method) { - let typeEncoding = method_getTypeEncoding(method) - - let block: @convention(block) (AnyObject, va_list) -> Unmanaged = { obj, vaList in - // This is an extremely cursed workaround for following crashing the compiler: - // let realSuperStruct = objc_super(receiver: raw, super_class: subclass) - // https://bugs.swift.org/browse/SR-12945 - let raw = Unmanaged.passUnretained(obj) - let superStruct = ObjcSuperFake(receiver: raw, superClass: subclass) - let realSuperStruct = unsafeBitCast(superStruct, to: objc_super.self) - // C: return ((id(*)(struct objc_super *, SEL, va_list))objc_msgSendSuper2)(&super, selector, argp); - return withUnsafePointer(to: realSuperStruct) { realSuperStructPointer -> Unmanaged in - let msgSendSuper2 = unsafeBitCast(self.msgSendSuper2, - to: (@convention(c) (UnsafePointer, Selector, va_list) -> Unmanaged).self) - return msgSendSuper2(realSuperStructPointer, self.selector, vaList) - } + private func addSuperTrampolineMethod(subclass: AnyClass) { + if addSuperImpl(subclass, self.selector) == false { + Interpose.log("Failed to add super implementation to -[\(`class`).\(selector)]") } - class_addMethod(subclass, self.selector, imp_implementationWithBlock(block), typeEncoding) } override func replaceImplementation() throws { @@ -123,8 +106,9 @@ final class ObjectHook: TypedHook String { return super.sayHi() + testSubclass } + + override func doNothing() { + super.doNothing() + } } From 0c5624c1ece0a27c6e5272738c2cc833b3d13d8b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 8 Jun 2020 21:23:54 +0200 Subject: [PATCH 19/77] Add more tests --- .../ObjectInterposeTests.swift | 35 +++++++++++++++++++ Tests/InterposeKitTests/TestClass.swift | 8 +++++ 2 files changed, 43 insertions(+) diff --git a/Tests/InterposeKitTests/ObjectInterposeTests.swift b/Tests/InterposeKitTests/ObjectInterposeTests.swift index 912f49a..595d198 100644 --- a/Tests/InterposeKitTests/ObjectInterposeTests.swift +++ b/Tests/InterposeKitTests/ObjectInterposeTests.swift @@ -109,4 +109,39 @@ final class ObjectInterposeTests: XCTestCase { try classInterposer.revert() XCTAssertEqual(testObj.returnInt(), returnIntDefault) } + + func test3IntParameters() throws { + let testObj = TestClass() + XCTAssertEqual(testObj.calculate(var1: 1, var2: 2, var3: 3), 1 + 2 + 3) + + // Functions need to be `@objc dynamic` to be hookable. + let interposer = try Interpose(testObj) { + try $0.hook(#selector(TestClass.calculate)) { (store: TypedHook<@convention(c) (AnyObject, Selector, Int, Int, Int) -> Int, @convention(block) (AnyObject, Int, Int, Int) -> Int>) in { + // You're free to skip calling the original implementation. + let orig = store.original($0, store.selector, $1, $2, $3) + return orig + 1 + } + } + } + XCTAssertEqual(testObj.calculate(var1: 1, var2: 2, var3: 3), 1 + 2 + 3 + 1) + try interposer.revert() + } + + func test6IntParameters() throws { + let testObj = TestClass() + + XCTAssertEqual(testObj.calculate2(var1: 1, var2: 2, var3: 3, var4: 4, var5: 5, var6: 6), 1 + 2 + 3 + 4 + 5 + 6) + + // Functions need to be `@objc dynamic` to be hookable. + let interposer = try Interpose(testObj) { + try $0.hook(#selector(TestClass.calculate2)) { (store: TypedHook<@convention(c) (AnyObject, Selector, Int, Int, Int, Int, Int, Int) -> Int, @convention(block) (AnyObject, Int, Int, Int, Int, Int, Int) -> Int>) in { + // You're free to skip calling the original implementation. + let orig = store.original($0, store.selector, $1, $2, $3, $4, $5, $6) + return orig + 1 + } + } + } + XCTAssertEqual(testObj.calculate2(var1: 1, var2: 2, var3: 3, var4: 4, var5: 5, var6: 6), 1 + 2 + 3 + 4 + 5 + 6 + 1) + try interposer.revert() + } } diff --git a/Tests/InterposeKitTests/TestClass.swift b/Tests/InterposeKitTests/TestClass.swift index ea3f86e..c0253d4 100644 --- a/Tests/InterposeKitTests/TestClass.swift +++ b/Tests/InterposeKitTests/TestClass.swift @@ -15,6 +15,14 @@ class TestClass: NSObject { @objc dynamic func returnInt() -> Int { 7 } + + @objc dynamic func calculate(var1: Int, var2: Int, var3: Int) -> Int { + var1 + var2 + var3 + } + + @objc dynamic func calculate2(var1: Int, var2: Int, var3: Int, var4: Int, var5: Int, var6: Int) -> Int { + var1 + var2 + var3 + var4 + var5 + var6 + } } class TestSubclass: TestClass { From 40e8f6634085de70aab23c81ca3ade9c04280402 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 8 Jun 2020 22:57:02 +0200 Subject: [PATCH 20/77] x64 works! --- Sources/ITKAddSuperMethod/ITKAddSuperMethod.m | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m index 9b4541f..250b4f5 100644 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m @@ -42,30 +42,63 @@ asm volatile ( #elif defined(__x86_64__) +// Arguments passed: rdi, rsi, rdx, rcx, r8, r9 __attribute__((__naked__)) void msgSendSuperTrampoline(void) { asm volatile ( + // stack: ret(rbp) | self 8 | _cmd 16 | super_class 24 | self?? 32 + // 40: rdx 48: rcx 56 r8 "pushq %%rbp # push frame pointer \n\t" + "movq %%rsp, %%rbp # set stack to frame pointer \n\t" - "subq $32, %%rsp # reserve 32 byte on the stack (need 16 byte alignment) \n\t" + "subq $64, %%rsp # reserve 64 byte on the stack (need 16 byte alignment) \n\t" + + "movq %%rdx, -40(%%rbp) \n\t" + "movq %%rcx, -48(%%rbp) \n\t" + "movq %%r8, -56(%%rbp) \n\t" + "movq %%rdi, -8(%%rbp) # copy self to stack[1] \n\t" "movq %%rsi, -16(%%rbp) # copy _cmd to stack[2] \n\t" "movq -8(%%rbp), %%rax # load self to rax \n\t" "movq %%rax, -32(%%rbp) # store self to stack[4] \n\t" "movq -8(%%rbp), %%rdi # load self to rdi-first parameter \n\t" "callq _objc_opt_class # call objc_opt_class(self) \n\t" - "#movq %%rax, %%rdi # move result to rdi-first parameter \n\t" - "#callq _class_getSuperclass # call class_getSuperclass(self) \n\t" "movq %%rax, -24(%%rbp) # move result to stack[3] \n\t" + + // alloc memory for the super struct + "movl $16, %%edi \n\t" + "callq _malloc \n\t" + + // save the malloc memory in r11 + "movq %%rax, %%r11 \n\t" + + // copy objc_super: self (rbx), later: super_class (rsp) + "movq -8(%%rbp), %%rax \n\t" + "movq %%rax, (%%r11) \n\t" + "movq -24(%%rbp), %%rax \n\t" + "movq %%rax, 8(%%r11) # set super_class \n\t" + "movq -16(%%rbp), %%rsi # copy _cmd to #rsi \n\t" - "xorl %%ecx, %%ecx # nill out rcx? \n\t" - "leaq -32(%%rbp), %%rdi # load address of objc_super struct to rdi-first param \n\t" - "movb %%cl, %%al # nill ot rax/rcx? \n\t" - "callq _objc_msgSendSuper # regular call \n\t" - "movq %%rax, %%rdi \n\t" - "addq $32, %%rsp # remove 32 byte from stack \n\t" + + "#xorl %%ecx, %%ecx # nil out rcx? \n\t" + "#leaq -32(%%rbp), %%rdi # load address of objc_super struct to rdi-first param \n\t" + "#movb %%cl, %%al # nill ot rax/rcx? \n\t" + + // rdi needs to point to the address of the struct + "leaq (%%r11), %%rdi \n\t" + + "movq -40(%%rbp), %%rdx \n\t" + "movq -48(%%rbp), %%rcx \n\t" + "movq -56(%%rbp), %%r8 \n\t" + + "addq $64, %%rsp # remove 64 byte from stack \n\t" + "popq %%rbp # pop frame pointer \n\t" - "retq\n\r" + + "jmp _objc_msgSendSuper # regular call \n\t" + "#movq %%rax, %%rdi \n\t" + + "#retq\n\r" : : : "rsi", "rdi"); } From c9fc2dfbcca6448b381105b7c42aef04bc4f08e2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 8 Jun 2020 23:30:13 +0200 Subject: [PATCH 21/77] Add additional tests --- .../ObjectInterposeTests.swift | 17 +++++++++++++++++ Tests/InterposeKitTests/TestClass.swift | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/Tests/InterposeKitTests/ObjectInterposeTests.swift b/Tests/InterposeKitTests/ObjectInterposeTests.swift index 595d198..1479836 100644 --- a/Tests/InterposeKitTests/ObjectInterposeTests.swift +++ b/Tests/InterposeKitTests/ObjectInterposeTests.swift @@ -144,4 +144,21 @@ final class ObjectInterposeTests: XCTestCase { XCTAssertEqual(testObj.calculate2(var1: 1, var2: 2, var3: 3, var4: 4, var5: 5, var6: 6), 1 + 2 + 3 + 4 + 5 + 6 + 1) try interposer.revert() } + + func testObjectCallReturn() throws { + let testObj = TestClass() + let str = "foo" + XCTAssertEqual(testObj.doubleString(string: str), str + str) + + // Functions need to be `@objc dynamic` to be hookable. + let interposer = try Interpose(testObj) { + try $0.hook(#selector(TestClass.doubleString)) { (store: TypedHook<@convention(c) (AnyObject, Selector, String) -> String, @convention(block) (AnyObject, String) -> String>) in { + store.original($0, store.selector, $1) + str + } + } + } + XCTAssertEqual(testObj.doubleString(string: str), str + str + str) + try interposer.revert() + XCTAssertEqual(testObj.doubleString(string: str), str + str) + } } diff --git a/Tests/InterposeKitTests/TestClass.swift b/Tests/InterposeKitTests/TestClass.swift index c0253d4..d5d2e99 100644 --- a/Tests/InterposeKitTests/TestClass.swift +++ b/Tests/InterposeKitTests/TestClass.swift @@ -12,6 +12,10 @@ class TestClass: NSObject { @objc dynamic func doNothing() { } + @objc dynamic func doubleString(string: String) -> String { + string + string + } + @objc dynamic func returnInt() -> Int { 7 } From 7888297c339a98b5faa08f77d90d6b3f2c3fb979 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 8 Jun 2020 23:30:46 +0200 Subject: [PATCH 22/77] Fix memory leak --- Sources/ITKAddSuperMethod/ITKAddSuperMethod.h | 2 +- Sources/ITKAddSuperMethod/ITKAddSuperMethod.m | 36 +++++++++++++------ Sources/InterposeKit/ObjectHook.swift | 6 ++-- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h index e8a427a..e65bb97 100644 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h @@ -15,6 +15,6 @@ NS_ASSUME_NONNULL_BEGIN @note This uses inline assembly to forward the parameters to objc_msgSendSuper. */ -BOOL IKTAddSuperImplementationToClass(Class klass, SEL selector); +BOOL IKTAddSuperImplementationToClass(id self, Class klass, SEL selector); NS_ASSUME_NONNULL_END diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m index 250b4f5..b77a248 100644 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m @@ -15,6 +15,20 @@ void msgSendSuperTrampoline(void); +struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj, SEL cmd); +struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj, SEL cmd) { + struct objc_super *_super = malloc(sizeof(_super)); + _super->receiver = obj; + _super->super_class = [obj class]; + + // TODO: Inefficient + dispatch_async(dispatch_get_main_queue(), ^{ + free(_super); + }); + + return _super; +} + #if defined(__arm64__) __attribute__((__naked__)) @@ -56,6 +70,7 @@ asm volatile ( "movq %%rdx, -40(%%rbp) \n\t" "movq %%rcx, -48(%%rbp) \n\t" "movq %%r8, -56(%%rbp) \n\t" + "movq %%r9, -64(%%rbp) \n\t" "movq %%rdi, -8(%%rbp) # copy self to stack[1] \n\t" "movq %%rsi, -16(%%rbp) # copy _cmd to stack[2] \n\t" @@ -66,8 +81,12 @@ asm volatile ( "movq %%rax, -24(%%rbp) # move result to stack[3] \n\t" // alloc memory for the super struct - "movl $16, %%edi \n\t" - "callq _malloc \n\t" +// //"movl $16, %%edi \n\t" + //"callq _malloc \n\t" + + "movq -8(%%rbp), %%rdi \n\t" + "movq -16(%%rbp), %%rsi \n\t" + "callq _ITKReturnThreadSuper \n\t" // save the malloc memory in r11 "movq %%rax, %%r11 \n\t" @@ -80,25 +99,20 @@ asm volatile ( "movq -16(%%rbp), %%rsi # copy _cmd to #rsi \n\t" - "#xorl %%ecx, %%ecx # nil out rcx? \n\t" - "#leaq -32(%%rbp), %%rdi # load address of objc_super struct to rdi-first param \n\t" - "#movb %%cl, %%al # nill ot rax/rcx? \n\t" - // rdi needs to point to the address of the struct "leaq (%%r11), %%rdi \n\t" + //"leaq -24(%%rbp), %%rdi \n\t" "movq -40(%%rbp), %%rdx \n\t" "movq -48(%%rbp), %%rcx \n\t" "movq -56(%%rbp), %%r8 \n\t" + "movq -64(%%rbp), %%r9 \n\t" "addq $64, %%rsp # remove 64 byte from stack \n\t" "popq %%rbp # pop frame pointer \n\t" - "jmp _objc_msgSendSuper # regular call \n\t" - "#movq %%rax, %%rdi \n\t" - - "#retq\n\r" + "jmp _objc_msgSendSuper # tail call \n\t" : : : "rsi", "rdi"); } @@ -124,7 +138,7 @@ static DispatchMode IKTGetDispatchMode(const char * typeEncoding) { return dispatchMode; } -BOOL IKTAddSuperImplementationToClass(Class klass, SEL selector) { +BOOL IKTAddSuperImplementationToClass(id self, Class klass, SEL selector) { Class originalClass = klass; Class superClass = class_getSuperclass(originalClass); diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index 0ec1fad..e3ce9f3 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -84,14 +84,14 @@ final public class ObjectHook: TypedHook Bool = { + private lazy var addSuperImpl: @convention(c) (AnyObject, AnyClass, Selector) -> Bool = { let handle = dlopen(nil, RTLD_LAZY) let imp = dlsym(handle, "IKTAddSuperImplementationToClass") - return unsafeBitCast(imp, to: (@convention(c) (AnyClass, Selector) -> Bool).self) + return unsafeBitCast(imp, to: (@convention(c) (AnyObject, AnyClass, Selector) -> Bool).self) }() private func addSuperTrampolineMethod(subclass: AnyClass) { - if addSuperImpl(subclass, self.selector) == false { + if addSuperImpl(self.object, subclass, self.selector) == false { Interpose.log("Failed to add super implementation to -[\(`class`).\(selector)]") } } From 17475fdcda955d8e767a8bdb467006790488f84f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 8 Jun 2020 23:39:12 +0200 Subject: [PATCH 23/77] cleanup --- Sources/ITKAddSuperMethod/ITKAddSuperMethod.m | 58 ++++++------------- 1 file changed, 17 insertions(+), 41 deletions(-) diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m index b77a248..e585012 100644 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m @@ -60,56 +60,32 @@ asm volatile ( __attribute__((__naked__)) void msgSendSuperTrampoline(void) { asm volatile ( - // stack: ret(rbp) | self 8 | _cmd 16 | super_class 24 | self?? 32 - // 40: rdx 48: rcx 56 r8 "pushq %%rbp # push frame pointer \n\t" - "movq %%rsp, %%rbp # set stack to frame pointer \n\t" - "subq $64, %%rsp # reserve 64 byte on the stack (need 16 byte alignment) \n\t" - - "movq %%rdx, -40(%%rbp) \n\t" - "movq %%rcx, -48(%%rbp) \n\t" - "movq %%r8, -56(%%rbp) \n\t" - "movq %%r9, -64(%%rbp) \n\t" + "subq $48, %%rsp # reserve 48 byte on the stack (need 16 byte alignment) \n\t" + // Save call params "movq %%rdi, -8(%%rbp) # copy self to stack[1] \n\t" "movq %%rsi, -16(%%rbp) # copy _cmd to stack[2] \n\t" - "movq -8(%%rbp), %%rax # load self to rax \n\t" - "movq %%rax, -32(%%rbp) # store self to stack[4] \n\t" - "movq -8(%%rbp), %%rdi # load self to rdi-first parameter \n\t" - "callq _objc_opt_class # call objc_opt_class(self) \n\t" - "movq %%rax, -24(%%rbp) # move result to stack[3] \n\t" - - // alloc memory for the super struct -// //"movl $16, %%edi \n\t" - //"callq _malloc \n\t" + "movq %%rdx, -24(%%rbp) \n\t" + "movq %%rcx, -32(%%rbp) \n\t" + "movq %%r8, -40(%%rbp) \n\t" + "movq %%r9, -48(%%rbp) \n\t" - "movq -8(%%rbp), %%rdi \n\t" - "movq -16(%%rbp), %%rsi \n\t" + // fetch filled struct objc_super, call with self + _cmd "callq _ITKReturnThreadSuper \n\t" + // first param is now struct objc_super + "movq %%rax, %%rdi \n\t" - // save the malloc memory in r11 - "movq %%rax, %%r11 \n\t" - - // copy objc_super: self (rbx), later: super_class (rsp) - "movq -8(%%rbp), %%rax \n\t" - "movq %%rax, (%%r11) \n\t" - "movq -24(%%rbp), %%rax \n\t" - "movq %%rax, 8(%%r11) # set super_class \n\t" - - "movq -16(%%rbp), %%rsi # copy _cmd to #rsi \n\t" - - // rdi needs to point to the address of the struct - "leaq (%%r11), %%rdi \n\t" - //"leaq -24(%%rbp), %%rdi \n\t" - - "movq -40(%%rbp), %%rdx \n\t" - "movq -48(%%rbp), %%rcx \n\t" - "movq -56(%%rbp), %%r8 \n\t" - "movq -64(%%rbp), %%r9 \n\t" - - "addq $64, %%rsp # remove 64 byte from stack \n\t" + // Restore call params + "movq -16(%%rbp), %%rsi \n\t" + "movq -24(%%rbp), %%rdx \n\t" + "movq -32(%%rbp), %%rcx \n\t" + "movq -40(%%rbp), %%r8 \n\t" + "movq -48(%%rbp), %%r9 \n\t" + // remove everything to prepare tail call + "addq $48, %%rsp # remove 64 byte from stack \n\t" "popq %%rbp # pop frame pointer \n\t" "jmp _objc_msgSendSuper # tail call \n\t" From 1357b8d36aa9f123d87ce8a89b5233dcff11069f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 9 Jun 2020 10:46:17 +0200 Subject: [PATCH 24/77] write arm64 --- Sources/ITKAddSuperMethod/ITKAddSuperMethod.m | 52 +++++++++++-------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m index e585012..7a85456 100644 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m @@ -34,29 +34,37 @@ __attribute__((__naked__)) void msgSendSuperTrampoline(void) { asm volatile ( - " sub sp, sp, #48 ; =48 \n\t" - " stp x29, x30, [sp, #32] ; 16-byte Folded Spill \n\t" - " add x29, sp, #32 ; =32 \n\t" - " stur x0, [x29, #-8] \n\t" - " str x1, [sp, #16] \n\t" - " ldur x8, [x29, #-8] \n\t" - " str x8, [sp] \n\t" - " ldur x0, [x29, #-8] \n\t" - " bl _objc_opt_class \n\t" - " str x0, [sp, #8] \n\t" - " ldr x1, [sp, #16] \n\t" - " mov x0, sp \n\t" - " bl _objc_msgSendSuper \n\t" - " mov x29, x29 ; marker for objc_retainAutoreleaseReturnValue \n\t" - " ldp x29, x30, [sp, #32] ; 16-byte Folded Reload \n\t" - " add sp, sp, #48 ; =48 \n\t" - " ret \n\t" + // push {x0-x8, lr} (call params are: x0-x7) + // stp: store pair of registers: from, from, to, via indexed write + "stp x8, lr, [sp, #-16]!\n" // push lr (link register == x30), then x8 + "stp x6, x7, [sp, #-16]!\n" + "stp x4, x5, [sp, #-16]!\n" + "stp x2, x3, [sp, #-16]!\n" // push x3, then x2 + "stp x0, x1, [sp, #-16]!\n" // push x1, then x0 + + // fetch filled struct objc_super, call with self + _cmd + "bl _ITKReturnThreadSuper \n" + + // first param is now struct objc_super (x0) + // protect returned new value when we restore the pairs + "mov x9, x0\n" + + // pop {x0-x8, lr} + "ldp x0, x1, [sp], #16\n" + "ldp x2, x3, [sp], #16\n" + "ldp x4, x5, [sp], #16\n" + "ldp x6, x7, [sp], #16\n" + "ldp x8, lr, [sp], #16\n" + + // get new return (adr of the objc_super class) + "mov x0, x9\n" + // tail call + "b _objc_msgSendSuper \n" : : : "x0", "x1"); } #elif defined(__x86_64__) -// Arguments passed: rdi, rsi, rdx, rcx, r8, r9 __attribute__((__naked__)) void msgSendSuperTrampoline(void) { asm volatile ( @@ -64,9 +72,10 @@ asm volatile ( "movq %%rsp, %%rbp # set stack to frame pointer \n\t" "subq $48, %%rsp # reserve 48 byte on the stack (need 16 byte alignment) \n\t" - // Save call params - "movq %%rdi, -8(%%rbp) # copy self to stack[1] \n\t" - "movq %%rsi, -16(%%rbp) # copy _cmd to stack[2] \n\t" + // TODO: save rax for va_arg? + // Save call params: rdi, rsi, rdx, rcx, r8, r9 + "movq %%rdi, -8(%%rbp) # copy self to stack[1] \n\t" // po *(id *) + "movq %%rsi, -16(%%rbp) # copy _cmd to stack[2] \n\t" // p (SEL)$rsi "movq %%rdx, -24(%%rbp) \n\t" "movq %%rcx, -32(%%rbp) \n\t" "movq %%r8, -40(%%rbp) \n\t" @@ -85,6 +94,7 @@ asm volatile ( "movq -48(%%rbp), %%r9 \n\t" // remove everything to prepare tail call + // debug stack via print *(int *) ($rsp+8) "addq $48, %%rsp # remove 64 byte from stack \n\t" "popq %%rbp # pop frame pointer \n\t" From 7a562e86a94633c90348765c10c92e7b6467da76 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 9 Jun 2020 11:12:15 +0200 Subject: [PATCH 25/77] Use thread local storage --- Sources/ITKAddSuperMethod/ITKAddSuperMethod.m | 52 +++++++++---------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m index 7a85456..cd566b4 100644 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m @@ -15,17 +15,13 @@ void msgSendSuperTrampoline(void); -struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj, SEL cmd); -struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj, SEL cmd) { - struct objc_super *_super = malloc(sizeof(_super)); +_Thread_local struct objc_super _threadSuperStorage; + +struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj); +struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj) { + struct objc_super *_super = &_threadSuperStorage; _super->receiver = obj; _super->super_class = [obj class]; - - // TODO: Inefficient - dispatch_async(dispatch_get_main_queue(), ^{ - free(_super); - }); - return _super; } @@ -68,37 +64,37 @@ asm volatile ( __attribute__((__naked__)) void msgSendSuperTrampoline(void) { asm volatile ( - "pushq %%rbp # push frame pointer \n\t" - "movq %%rsp, %%rbp # set stack to frame pointer \n\t" - "subq $48, %%rsp # reserve 48 byte on the stack (need 16 byte alignment) \n\t" + "pushq %%rbp # push frame pointer \n" + "movq %%rsp, %%rbp # set stack to frame pointer \n" + "subq $48, %%rsp # reserve 48 byte on the stack (need 16 byte alignment) \n" // TODO: save rax for va_arg? // Save call params: rdi, rsi, rdx, rcx, r8, r9 - "movq %%rdi, -8(%%rbp) # copy self to stack[1] \n\t" // po *(id *) - "movq %%rsi, -16(%%rbp) # copy _cmd to stack[2] \n\t" // p (SEL)$rsi - "movq %%rdx, -24(%%rbp) \n\t" - "movq %%rcx, -32(%%rbp) \n\t" - "movq %%r8, -40(%%rbp) \n\t" - "movq %%r9, -48(%%rbp) \n\t" + "movq %%rdi, -8(%%rbp) # copy self to stack[1] \n" // po *(id *) + "movq %%rsi, -16(%%rbp) # copy _cmd to stack[2] \n" // p (SEL)$rsi + "movq %%rdx, -24(%%rbp) \n" + "movq %%rcx, -32(%%rbp) \n" + "movq %%r8, -40(%%rbp) \n" + "movq %%r9, -48(%%rbp) \n" // fetch filled struct objc_super, call with self + _cmd - "callq _ITKReturnThreadSuper \n\t" + "callq _ITKReturnThreadSuper \n" // first param is now struct objc_super - "movq %%rax, %%rdi \n\t" + "movq %%rax, %%rdi \n" // Restore call params - "movq -16(%%rbp), %%rsi \n\t" - "movq -24(%%rbp), %%rdx \n\t" - "movq -32(%%rbp), %%rcx \n\t" - "movq -40(%%rbp), %%r8 \n\t" - "movq -48(%%rbp), %%r9 \n\t" + "movq -16(%%rbp), %%rsi \n" + "movq -24(%%rbp), %%rdx \n" + "movq -32(%%rbp), %%rcx \n" + "movq -40(%%rbp), %%r8 \n" + "movq -48(%%rbp), %%r9 \n" // remove everything to prepare tail call // debug stack via print *(int *) ($rsp+8) - "addq $48, %%rsp # remove 64 byte from stack \n\t" - "popq %%rbp # pop frame pointer \n\t" + "addq $48, %%rsp # remove 64 byte from stack \n" + "popq %%rbp # pop frame pointer \n" - "jmp _objc_msgSendSuper # tail call \n\t" + "jmp _objc_msgSendSuper # tail call \n" : : : "rsi", "rdi"); } From 5c6b75d1fa109cfe03eb4db185c3691ab6987532 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 9 Jun 2020 14:05:07 +0200 Subject: [PATCH 26/77] cleanup --- Sources/ITKAddSuperMethod/ITKAddSuperMethod.m | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m index cd566b4..6cb4848 100644 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m @@ -68,8 +68,7 @@ asm volatile ( "movq %%rsp, %%rbp # set stack to frame pointer \n" "subq $48, %%rsp # reserve 48 byte on the stack (need 16 byte alignment) \n" - // TODO: save rax for va_arg? - // Save call params: rdi, rsi, rdx, rcx, r8, r9 + // Save call params: rax(for va_arg) rdi, rsi, rdx, rcx, r8, r9 "movq %%rdi, -8(%%rbp) # copy self to stack[1] \n" // po *(id *) "movq %%rsi, -16(%%rbp) # copy _cmd to stack[2] \n" // p (SEL)$rsi "movq %%rdx, -24(%%rbp) \n" From 9993a61e1b0c16350fe6dfeea51d8b5d5ff6df6c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 9 Jun 2020 14:29:22 +0200 Subject: [PATCH 27/77] add links to assembly --- Sources/ITKAddSuperMethod/ITKAddSuperMethod.m | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m index 6cb4848..2de46f3 100644 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m @@ -25,6 +25,22 @@ return _super; } +/** + Assembly is hard, here are some useful resources: + https://azeria-labs.com/functions-and-the-stack-part-7/ + https://github.com/DavidGoldman/InspectiveC/blob/master/InspectiveCarm64.mm + https://blog.nelhage.com/2010/10/amd64-and-va_arg/ + https://developer.apple.com/library/ios/documentation/Xcode/Conceptual/iPhoneOSABIReference/Articles/ARM64FunctionCallingConventions.html + https://c9x.me/compile/bib/abi-arm64.pdf + http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0801a/BABBDBAD.html + https://community.arm.com/developer/ip-products/processors/b/processors-ip-blog/posts/using-the-stack-in-aarch64-implementing-push-and-pop + https://www.cs.yale.edu/flint/cs421/papers/x86-asm/asm.html + https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64 + https://en.wikipedia.org/wiki/Calling_convention#x86_(32-bit) + https://bob.cs.sonoma.edu/IntroCompOrg-RPi/sec-varstack.html + https://azeria-labs.com/functions-and-the-stack-part-7/ + */ + #if defined(__arm64__) __attribute__((__naked__)) From 9f345e37488e3ae345d29d2a63e9dfbf514dcb5b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 9 Jun 2020 18:03:34 +0200 Subject: [PATCH 28/77] Add test and stret handling --- Sources/ITKAddSuperMethod/ITKAddSuperMethod.h | 4 +- Sources/ITKAddSuperMethod/ITKAddSuperMethod.m | 74 +++++++++++++++---- .../ObjectInterposeTests.swift | 22 ++++++ Tests/InterposeKitTests/TestClass.swift | 23 ++++++ 4 files changed, 108 insertions(+), 15 deletions(-) diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h index e65bb97..a74eb89 100644 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h @@ -14,7 +14,9 @@ NS_ASSUME_NONNULL_BEGIN If a method already exists, this will return NO. @note This uses inline assembly to forward the parameters to objc_msgSendSuper. -*/ + Currently implemented architectures are x86_64 and arm64. + (arm7 was dropped in OS 11 and i386 with macOS Catalina.) + */ BOOL IKTAddSuperImplementationToClass(id self, Class klass, SEL selector); NS_ASSUME_NONNULL_END diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m index 2de46f3..bc936a1 100644 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m @@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN void msgSendSuperTrampoline(void); +void msgSendSuperStretTrampoline(void); _Thread_local struct objc_super _threadSuperStorage; @@ -75,6 +76,9 @@ asm volatile ( : : : "x0", "x1"); } +// arm64 doesn't use _stret variants. +void msgSendSuperStretTrampoline(void) {} + #elif defined(__x86_64__) __attribute__((__naked__)) @@ -84,7 +88,7 @@ asm volatile ( "movq %%rsp, %%rbp # set stack to frame pointer \n" "subq $48, %%rsp # reserve 48 byte on the stack (need 16 byte alignment) \n" - // Save call params: rax(for va_arg) rdi, rsi, rdx, rcx, r8, r9 + // Save call params: rdi, rsi, rdx, rcx, r8, r9 "movq %%rdi, -8(%%rbp) # copy self to stack[1] \n" // po *(id *) "movq %%rsi, -16(%%rbp) # copy _cmd to stack[2] \n" // p (SEL)$rsi "movq %%rdx, -24(%%rbp) \n" @@ -113,22 +117,67 @@ asm volatile ( : : : "rsi", "rdi"); } + +__attribute__((__naked__)) +void msgSendSuperStretTrampoline(void) { + asm volatile ( + "pushq %%rbp # push frame pointer \n" + "movq %%rsp, %%rbp # set stack to frame pointer \n" + "subq $48, %%rsp # reserve 48 byte on the stack (need 16 byte alignment) \n" + + // Save call params: rax(for va_arg) rdi, rsi, rdx, rcx, r8, r9 + "movq %%rdi, -8(%%rbp) \n" // struct return + "movq %%rsi, -16(%%rbp) \n" // self + "movq %%rdx, -24(%%rbp) \n" // _cmd + "movq %%rcx, -32(%%rbp) \n" // param 1 + "movq %%r8, -40(%%rbp) \n" // param 2 + "movq %%r9, -48(%%rbp) \n" // param 3 + + // fetch filled struct objc_super, call with self + _cmd + // Since stret offsets, we move back by one + "movq -16(%%rbp), %%rdi \n" + "movq -24(%%rbp), %%rdx \n" + "callq _ITKReturnThreadSuper \n" + // second param is now struct objc_super + "movq %%rax, %%rsi \n" + // First is our struct return + + // Restore call params + "movq -8(%%rbp), %%rdi \n" + //"movq -16(%%rbp), %%rsi \n" + "movq -24(%%rbp), %%rdx \n" + "movq -32(%%rbp), %%rcx \n" + "movq -40(%%rbp), %%r8 \n" + "movq -48(%%rbp), %%r9 \n" + + // remove everything to prepare tail call + // debug stack via print *(int *) ($rsp+8) + "addq $48, %%rsp # remove 64 byte from stack \n" + "popq %%rbp # pop frame pointer \n" + + "jmp _objc_msgSendSuper_stret # tail call \n" + : : : "rsi", "rdi"); +} + #endif typedef NS_ENUM(NSInteger, DispatchMode) { - DispatchMode_Normal, - DispatchMode_Stret, + DispatchModeNormal, + DispatchModeStret, }; -static DispatchMode IKTGetDispatchMode(const char * typeEncoding) { - DispatchMode dispatchMode = DispatchMode_Normal; +static DispatchMode IKTGetDispatchMode(const char *typeEncoding) { + DispatchMode dispatchMode = DispatchModeNormal; #if defined (__arm64__) - // ARM64 doesn't use stret dispatch + // ARM64 doesn't use stret dispatch. Yay! #elif defined (__x86_64__) - // On x86-64, stret dispatch is used whenever return type doesn't fit into two registers + // On x86-64, stret dispatch is ~used whenever return type doesn't fit into two registers + // + // http://www.sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html + // x86_64 is more complicated, including rules for returning floating-point struct fields in FPU registers, and ppc64's rules and exceptions will make your head spin. The gory details are documented in the Mac OS X ABI Guide, though as usual if the documentation and the compiler disagree then the documentation is wrong. NSUInteger returnTypeActualSize = 0; NSGetSizeAndAlignment(typeEncoding, &returnTypeActualSize, NULL); - dispatchMode = returnTypeActualSize > (sizeof(void *) * 2) ? DispatchMode_Stret : DispatchMode_Normal; + dispatchMode = returnTypeActualSize > (sizeof(void *) * 2) ? DispatchModeStret : DispatchModeNormal; #else #error - Unknown architecture #endif @@ -150,12 +199,9 @@ BOOL IKTAddSuperImplementationToClass(id self, Class klass, SEL selector) { return NO; } const char *typeEncoding = method_getTypeEncoding(method); - // Need to write asm for x64 - __unused DispatchMode dispatchMode = IKTGetDispatchMode(typeEncoding); - BOOL methodAdded = class_addMethod(klass, - selector, - msgSendSuperTrampoline, - typeEncoding); + const BOOL isNormalDispatch = IKTGetDispatchMode(typeEncoding) == DispatchModeNormal; + IMP trampoline = isNormalDispatch ? msgSendSuperTrampoline : msgSendSuperStretTrampoline; + BOOL methodAdded = class_addMethod(klass, selector, trampoline, typeEncoding); if (!methodAdded) { NSLog(@"Failed to add method for selector %@ to class %@", NSStringFromSelector(selector), diff --git a/Tests/InterposeKitTests/ObjectInterposeTests.swift b/Tests/InterposeKitTests/ObjectInterposeTests.swift index 1479836..2ca2c6d 100644 --- a/Tests/InterposeKitTests/ObjectInterposeTests.swift +++ b/Tests/InterposeKitTests/ObjectInterposeTests.swift @@ -161,4 +161,26 @@ final class ObjectInterposeTests: XCTestCase { try interposer.revert() XCTAssertEqual(testObj.doubleString(string: str), str + str) } + + func testLargeStructReturn() throws { + let testObj = TestClass() + let transform = CATransform3D() + XCTAssertEqual(testObj.invert3DTransform(transform), transform.inverted) + + func transformMatrix(_ matrix: CATransform3D) -> CATransform3D { + matrix.translated(x: 10, y: 5, z: 2) + } + + // Functions need to be `@objc dynamic` to be hookable. + let interposer = try Interpose(testObj) { + try $0.hook(#selector(TestClass.invert3DTransform)) { (store: TypedHook<@convention(c) (AnyObject, Selector, CATransform3D) -> CATransform3D, @convention(block) (AnyObject, CATransform3D) -> CATransform3D>) in { + let matrix = store.original($0, store.selector, $1) + return transformMatrix(matrix) + } + } + } + XCTAssertEqual(testObj.invert3DTransform(transform), transformMatrix(transform.inverted)) + try interposer.revert() + XCTAssertEqual(testObj.invert3DTransform(transform), transform.inverted) + } } diff --git a/Tests/InterposeKitTests/TestClass.swift b/Tests/InterposeKitTests/TestClass.swift index d5d2e99..62cacc1 100644 --- a/Tests/InterposeKitTests/TestClass.swift +++ b/Tests/InterposeKitTests/TestClass.swift @@ -1,9 +1,27 @@ import Foundation +import QuartzCore let testClassHi = "Hi from TestClass!" let testSwizzleAddition = " and Interpose" let testSubclass = "Subclass is here!" +public func ==(lhs: CATransform3D, rhs: CATransform3D) -> Bool { + return CATransform3DEqualToTransform(lhs, rhs) +} + +extension CATransform3D: Equatable { } + +public extension CATransform3D { + + func translated(x: CGFloat = 0, y: CGFloat = 0, z: CGFloat = 0) -> CATransform3D { + return CATransform3DTranslate(self, x, y, z) + } + + var inverted: CATransform3D { + return CATransform3DInvert(self) + } +} + class TestClass: NSObject { @objc dynamic func sayHi() -> String { print(testClassHi) @@ -27,6 +45,11 @@ class TestClass: NSObject { @objc dynamic func calculate2(var1: Int, var2: Int, var3: Int, var4: Int, var5: Int, var6: Int) -> Int { var1 + var2 + var3 + var4 + var5 + var6 } + + // This requires _objc_msgSendSuper_stret on x64, returns a large struct + @objc dynamic func invert3DTransform(_ input: CATransform3D) -> CATransform3D { + input.inverted + } } class TestSubclass: TestClass { From 10c4aa44d6d5f44d23c057f6d7e4cbf128df5a70 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 9 Jun 2020 18:38:33 +0200 Subject: [PATCH 29/77] Document! --- Sources/ITKAddSuperMethod/ITKAddSuperMethod.h | 56 ++++- Sources/ITKAddSuperMethod/ITKAddSuperMethod.m | 217 ++++++++++-------- Sources/InterposeKit/ObjectHook.swift | 6 +- 3 files changed, 177 insertions(+), 102 deletions(-) diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h index a74eb89..f58a256 100644 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h @@ -9,14 +9,58 @@ NS_ASSUME_NONNULL_BEGIN +NSString *const SuperBuilderErrorDomain; + +typedef NS_ERROR_ENUM(SuperBuilderErrorDomain, SuperBuilderErrorCode) { + SuperBuilderErrorCodeNoSuperClass, + SuperBuilderErrorCodeNoDynamicallyDispatchedMethodAvailable, + SuperBuilderErrorCodeFailedToAddMethod +}; + +@interface SuperBuilder : NSObject + /** - Adds an empty super implementation instance method to klass. - If a method already exists, this will return NO. + Adds an empty super implementation instance method to originalClass. + If a method already exists, this will return NO and a descriptive error message. + + Example: You have an empty UIViewController subclass and call this with viewDidLoad as selector. + The result will be code that looks similar to this: + + override func viewDidLoad() { + super.viewDidLoad() + } + + What the compiler creates in following code: + + - (void)viewDidLoad { + struct objc_super _super = { + .receiver = self, + .super_class = object_getClass(obj); + }; + objc_msgSendSuper2(&_super, _cmd); + } - @note This uses inline assembly to forward the parameters to objc_msgSendSuper. - Currently implemented architectures are x86_64 and arm64. - (arm7 was dropped in OS 11 and i386 with macOS Catalina.) + There are a few important details: + + 1) We use objc_msgSendSuper2, not objc_msgSendSuper. + The difference is minor, but important. + objc_msgSendSuper starts looking at the current class, which would cause an endless loop + objc_msgSendSuper2 looks for the superclass. + + 2) This uses a completely dynamic lookup. + While slightly slower, this is resilient even if you change superclasses later on. + + 3) The resolution method calls out to C, so it could be customized to jump over specific implementations. + (Such API is not currently exposed) + + 4) This uses inline assembly to forward the parameters to objc_msgSendSuper2 and objc_msgSendSuper2_stret. + This is currently implemented architectures are x86_64 and arm64. + armv7 was dropped in OS 11 and i386 with macOS Catalina. */ -BOOL IKTAddSuperImplementationToClass(id self, Class klass, SEL selector); ++ (BOOL)addSuperInstanceMethodToClass:(Class)originalClass selector:(SEL)selector error:(NSError **)error; + +@end + + NS_ASSUME_NONNULL_END diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m index bc936a1..878022e 100644 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m @@ -13,21 +13,89 @@ NS_ASSUME_NONNULL_BEGIN +NSString *const PSPDFErrorDomain = @"com.steipete.superbuilder"; + void msgSendSuperTrampoline(void); void msgSendSuperStretTrampoline(void); +typedef NS_ENUM(NSInteger, DispatchMode) { + DispatchModeNormal, + DispatchModeStret, +}; + +#define let const __auto_type +#define var __auto_type + +static DispatchMode IKTGetDispatchMode(const char *typeEncoding) { + DispatchMode dispatchMode = DispatchModeNormal; +#if defined (__arm64__) + // ARM64 doesn't use stret dispatch. Yay! +#elif defined (__x86_64__) + // On x86-64, stret dispatch is ~used whenever return type doesn't fit into two registers + // + // http://www.sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html + // x86_64 is more complicated, including rules for returning floating-point struct fields in FPU registers, and ppc64's rules and exceptions will make your head spin. The gory details are documented in the Mac OS X ABI Guide, though as usual if the documentation and the compiler disagree then the documentation is wrong. + NSUInteger returnTypeActualSize = 0; + NSGetSizeAndAlignment(typeEncoding, &returnTypeActualSize, NULL); + dispatchMode = returnTypeActualSize > (sizeof(void *) * 2) ? DispatchModeStret : DispatchModeNormal; +#else +#error - Unknown architecture +#endif + return dispatchMode; +} + +// Helper for binding with Swift +BOOL IKTAddSuperImplementationToClass(Class originalClass, SEL selector, NSError **error); +BOOL IKTAddSuperImplementationToClass(Class originalClass, SEL selector, NSError **error) { + return [SuperBuilder addSuperInstanceMethodToClass:originalClass selector:selector error:error]; +} + +#define ERROR_AND_RETURN(CODE, STRING)\ +if (error) { *error = [NSError errorWithDomain:SuperBuilderErrorDomain code:CODE userInfo:@{NSLocalizedDescriptionKey: STRING}];} return NO; + +@implementation SuperBuilder + ++ (BOOL)addSuperInstanceMethodToClass:(Class)originalClass selector:(SEL)selector error:(NSError **)error { + let superClass = class_getSuperclass(originalClass); + if (superClass == nil) { + let msg = [NSString stringWithFormat:@"Unable to find superclass for %@", NSStringFromClass(originalClass)]; + ERROR_AND_RETURN(SuperBuilderErrorCodeNoSuperClass, msg) + } + let method = class_getInstanceMethod(superClass, selector); + if (method == NULL) { + let msg = [NSString stringWithFormat:@"No dynamically dispatched method with selector %@ is available on any of the superclasses of %@", NSStringFromSelector(selector), NSStringFromClass(originalClass)]; + ERROR_AND_RETURN(SuperBuilderErrorCodeNoDynamicallyDispatchedMethodAvailable, msg) + } + const char *typeEncoding = method_getTypeEncoding(method); + let isNormalDispatch = IKTGetDispatchMode(typeEncoding) == DispatchModeNormal; + IMP trampoline = isNormalDispatch ? msgSendSuperTrampoline : msgSendSuperStretTrampoline; + let methodAdded = class_addMethod(originalClass, selector, trampoline, typeEncoding); + if (!methodAdded) { + let msg = [NSString stringWithFormat:@"Failed to add method for selector %@ to class %@", NSStringFromSelector(selector), NSStringFromClass(originalClass)]; + ERROR_AND_RETURN(SuperBuilderErrorCodeFailedToAddMethod, msg) + } + return methodAdded; +} + +@end + +// One thread local per thread should be enough _Thread_local struct objc_super _threadSuperStorage; struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj); struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj) { struct objc_super *_super = &_threadSuperStorage; _super->receiver = obj; - _super->super_class = [obj class]; + _super->super_class = object_getClass(obj); return _super; } /** + Inline assembly is used to perfectly forward all parameters to objc_msgSendSuper, + while also looking up the target on-the-fly. + Assembly is hard, here are some useful resources: + https://azeria-labs.com/functions-and-the-stack-part-7/ https://github.com/DavidGoldman/InspectiveC/blob/master/InspectiveCarm64.mm https://blog.nelhage.com/2010/10/amd64-and-va_arg/ @@ -72,7 +140,7 @@ asm volatile ( // get new return (adr of the objc_super class) "mov x0, x9\n" // tail call - "b _objc_msgSendSuper \n" + "b _objc_msgSendSuper2 \n" : : : "x0", "x1"); } @@ -84,36 +152,42 @@ void msgSendSuperStretTrampoline(void) {} __attribute__((__naked__)) void msgSendSuperTrampoline(void) { asm volatile ( - "pushq %%rbp # push frame pointer \n" - "movq %%rsp, %%rbp # set stack to frame pointer \n" - "subq $48, %%rsp # reserve 48 byte on the stack (need 16 byte alignment) \n" + // push frame pointer + "pushq %%rbp \n" + // set stack to frame pointer + "movq %%rsp, %%rbp \n" + // reserve 48 byte on the stack (need 16 byte alignment) + "subq $48, %%rsp \n" // Save call params: rdi, rsi, rdx, rcx, r8, r9 - "movq %%rdi, -8(%%rbp) # copy self to stack[1] \n" // po *(id *) - "movq %%rsi, -16(%%rbp) # copy _cmd to stack[2] \n" // p (SEL)$rsi - "movq %%rdx, -24(%%rbp) \n" - "movq %%rcx, -32(%%rbp) \n" - "movq %%r8, -40(%%rbp) \n" - "movq %%r9, -48(%%rbp) \n" + "movq %%rdi, -8(%%rbp) \n" // self po *(id *) + "movq %%rsi, -16(%%rbp) \n" // _cmd p (SEL)$rsi + "movq %%rdx, -24(%%rbp) \n" // param 1 + "movq %%rcx, -32(%%rbp) \n" // param 2 + "movq %%r8, -40(%%rbp) \n" // param 3 + "movq %%r9, -48(%%rbp) \n" // param 4 (rest goes on stack) // fetch filled struct objc_super, call with self + _cmd - "callq _ITKReturnThreadSuper \n" + "callq _ITKReturnThreadSuper \n" // first param is now struct objc_super "movq %%rax, %%rdi \n" // Restore call params - "movq -16(%%rbp), %%rsi \n" - "movq -24(%%rbp), %%rdx \n" - "movq -32(%%rbp), %%rcx \n" - "movq -40(%%rbp), %%r8 \n" - "movq -48(%%rbp), %%r9 \n" + // do not restore first parameter: super class + "movq -16(%%rbp), %%rsi \n" + "movq -24(%%rbp), %%rdx \n" + "movq -32(%%rbp), %%rcx \n" + "movq -40(%%rbp), %%r8 \n" + "movq -48(%%rbp), %%r9 \n" - // remove everything to prepare tail call // debug stack via print *(int *) ($rsp+8) - "addq $48, %%rsp # remove 64 byte from stack \n" - "popq %%rbp # pop frame pointer \n" + // remove 64 byte from stack + "addq $48, %%rsp \n" + // pop frame pointer + "popq %%rbp \n" - "jmp _objc_msgSendSuper # tail call \n" + // tail call time! + "jmp _objc_msgSendSuper2 \n" : : : "rsi", "rdi"); } @@ -121,94 +195,51 @@ asm volatile ( __attribute__((__naked__)) void msgSendSuperStretTrampoline(void) { asm volatile ( - "pushq %%rbp # push frame pointer \n" - "movq %%rsp, %%rbp # set stack to frame pointer \n" - "subq $48, %%rsp # reserve 48 byte on the stack (need 16 byte alignment) \n" + // push frame pointer + "pushq %%rbp \n" + // set stack to frame pointer + "movq %%rsp, %%rbp \n" + // reserve 48 byte on the stack (need 16 byte alignment) + "subq $48, %%rsp \n" // Save call params: rax(for va_arg) rdi, rsi, rdx, rcx, r8, r9 - "movq %%rdi, -8(%%rbp) \n" // struct return - "movq %%rsi, -16(%%rbp) \n" // self - "movq %%rdx, -24(%%rbp) \n" // _cmd - "movq %%rcx, -32(%%rbp) \n" // param 1 - "movq %%r8, -40(%%rbp) \n" // param 2 - "movq %%r9, -48(%%rbp) \n" // param 3 + "movq %%rdi, -8(%%rbp) \n" // struct return + "movq %%rsi, -16(%%rbp) \n" // self + "movq %%rdx, -24(%%rbp) \n" // _cmd + "movq %%rcx, -32(%%rbp) \n" // param 1 + "movq %%r8, -40(%%rbp) \n" // param 2 + "movq %%r9, -48(%%rbp) \n" // param 3 (rest goes on stack) // fetch filled struct objc_super, call with self + _cmd // Since stret offsets, we move back by one - "movq -16(%%rbp), %%rdi \n" - "movq -24(%%rbp), %%rdx \n" - "callq _ITKReturnThreadSuper \n" + "movq -16(%%rbp), %%rdi \n" + "movq -24(%%rbp), %%rdx \n" + "callq _ITKReturnThreadSuper \n" // second param is now struct objc_super "movq %%rax, %%rsi \n" // First is our struct return // Restore call params - "movq -8(%%rbp), %%rdi \n" - //"movq -16(%%rbp), %%rsi \n" - "movq -24(%%rbp), %%rdx \n" - "movq -32(%%rbp), %%rcx \n" - "movq -40(%%rbp), %%r8 \n" - "movq -48(%%rbp), %%r9 \n" - - // remove everything to prepare tail call + "movq -8(%%rbp), %%rdi \n" + // do not restore second parameter: super class + "movq -24(%%rbp), %%rdx \n" + "movq -32(%%rbp), %%rcx \n" + "movq -40(%%rbp), %%r8 \n" + "movq -48(%%rbp), %%r9 \n" + // debug stack via print *(int *) ($rsp+8) - "addq $48, %%rsp # remove 64 byte from stack \n" - "popq %%rbp # pop frame pointer \n" + // remove 64 byte from stack + "addq $48, %%rsp \n" + // pop frame pointer + "popq %%rbp \n" - "jmp _objc_msgSendSuper_stret # tail call \n" + // tail call time! + "jmp _objc_msgSendSuper2_stret \n" : : : "rsi", "rdi"); } -#endif - -typedef NS_ENUM(NSInteger, DispatchMode) { - DispatchModeNormal, - DispatchModeStret, -}; - -static DispatchMode IKTGetDispatchMode(const char *typeEncoding) { - DispatchMode dispatchMode = DispatchModeNormal; -#if defined (__arm64__) - // ARM64 doesn't use stret dispatch. Yay! -#elif defined (__x86_64__) - // On x86-64, stret dispatch is ~used whenever return type doesn't fit into two registers - // - // http://www.sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html - // x86_64 is more complicated, including rules for returning floating-point struct fields in FPU registers, and ppc64's rules and exceptions will make your head spin. The gory details are documented in the Mac OS X ABI Guide, though as usual if the documentation and the compiler disagree then the documentation is wrong. - NSUInteger returnTypeActualSize = 0; - NSGetSizeAndAlignment(typeEncoding, &returnTypeActualSize, NULL); - dispatchMode = returnTypeActualSize > (sizeof(void *) * 2) ? DispatchModeStret : DispatchModeNormal; #else -#error - Unknown architecture +#error - Unknown architecture - time to write some assembly :) #endif - return dispatchMode; -} - -BOOL IKTAddSuperImplementationToClass(id self, Class klass, SEL selector) { - Class originalClass = klass; - - Class superClass = class_getSuperclass(originalClass); - if (superClass == nil) { - return NO; - } - Method method = class_getInstanceMethod(superClass, selector); - if (method == NULL) { - [NSException raise:NSInternalInconsistencyException - format:@"No dynamically dispatched method with selector %@ is available on any of the superclasses of %@", - NSStringFromSelector(selector), NSStringFromClass(originalClass)]; - return NO; - } - const char *typeEncoding = method_getTypeEncoding(method); - const BOOL isNormalDispatch = IKTGetDispatchMode(typeEncoding) == DispatchModeNormal; - IMP trampoline = isNormalDispatch ? msgSendSuperTrampoline : msgSendSuperStretTrampoline; - BOOL methodAdded = class_addMethod(klass, selector, trampoline, typeEncoding); - if (!methodAdded) { - NSLog(@"Failed to add method for selector %@ to class %@", - NSStringFromSelector(selector), - NSStringFromClass(klass)); - } - - return methodAdded; -} NS_ASSUME_NONNULL_END diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index e3ce9f3..0ec1fad 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -84,14 +84,14 @@ final public class ObjectHook: TypedHook Bool = { + private lazy var addSuperImpl: @convention(c) (AnyClass, Selector) -> Bool = { let handle = dlopen(nil, RTLD_LAZY) let imp = dlsym(handle, "IKTAddSuperImplementationToClass") - return unsafeBitCast(imp, to: (@convention(c) (AnyObject, AnyClass, Selector) -> Bool).self) + return unsafeBitCast(imp, to: (@convention(c) (AnyClass, Selector) -> Bool).self) }() private func addSuperTrampolineMethod(subclass: AnyClass) { - if addSuperImpl(self.object, subclass, self.selector) == false { + if addSuperImpl(subclass, self.selector) == false { Interpose.log("Failed to add super implementation to -[\(`class`).\(selector)]") } } From ef4431fbc2e3ebeca680920f5b931f70fef6232b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 9 Jun 2020 22:29:56 +0200 Subject: [PATCH 30/77] Remove assembly based object hook --- Sources/InterposeKit/AnyHook.swift | 13 +- Sources/InterposeKit/ClassHook.swift | 50 ++-- Sources/InterposeKit/InterposeKit.swift | 59 +++-- Sources/InterposeKit/ObjectHook.swift | 299 ++++++++++++++---------- 4 files changed, 254 insertions(+), 167 deletions(-) diff --git a/Sources/InterposeKit/AnyHook.swift b/Sources/InterposeKit/AnyHook.swift index 1111ed5..fee7224 100644 --- a/Sources/InterposeKit/AnyHook.swift +++ b/Sources/InterposeKit/AnyHook.swift @@ -5,8 +5,6 @@ public class AnyHook { public let `class`: AnyClass public let selector: Selector public internal(set) var state = State.prepared - // fetched at apply time, changes late, thus class requirement - public internal(set) var origIMP: IMP? // else we validate init order public internal(set) var replacementIMP: IMP! @@ -19,7 +17,7 @@ public class AnyHook { case interposed /// An error happened while interposing a method. - case error(Interpose.Error) + indirect case error(InterposeError) } init(`class`: AnyClass, selector: Selector) throws { @@ -38,7 +36,6 @@ public class AnyHook { preconditionFailure("Not implemented") } - /// Apply the interpose hook. public func apply() throws { try execute(newState: .interposed) { try replaceImplementation() } @@ -55,8 +52,8 @@ public class AnyHook { /// Validate that the selector exists on the active class. @discardableResult func validate(expectedState: State = .prepared) throws -> Method { - guard let method = class_getInstanceMethod(`class`, selector) else { throw Interpose.Error.methodNotFound } - guard state == expectedState else { throw Interpose.Error.invalidState } + guard let method = class_getInstanceMethod(`class`, selector) else { throw InterposeError.methodNotFound(`class`, selector)} + guard state == expectedState else { throw InterposeError.invalidState(expectedState: expectedState) } return method } @@ -64,7 +61,7 @@ public class AnyHook { do { try task() state = newState - } catch let error as Interpose.Error { + } catch let error as InterposeError { state = .error(error) throw error } @@ -86,6 +83,6 @@ public class AnyHook { public class TypedHook: AnyHook { public var original: MethodSignature { - unsafeBitCast(origIMP, to: MethodSignature.self) + preconditionFailure("Always override") } } diff --git a/Sources/InterposeKit/ClassHook.swift b/Sources/InterposeKit/ClassHook.swift index a5dca0b..b956336 100644 --- a/Sources/InterposeKit/ClassHook.swift +++ b/Sources/InterposeKit/ClassHook.swift @@ -1,32 +1,40 @@ import Foundation extension Interpose { -/// A hook to an instance method and stores both the original and new implementation. -final public class ClassHook: TypedHook { + /// A hook to an instance method and stores both the original and new implementation. + final public class ClassHook: TypedHook { - /// Initialize a new hook to interpose an instance method. - // TODO: report compiler crash - public init(`class`: AnyClass, selector: Selector, implementation:(ClassHook) -> HookSignature?) /* This must be optional or swift runtime will crash. Or swiftc may segfault. Compiler bug? */ throws { - try super.init(class: `class`, selector: selector) - replacementIMP = imp_implementationWithBlock(implementation(self) as Any) - } + // fetched at apply time, changes late, thus class requirement + public internal(set) var origIMP: IMP? - override func replaceImplementation() throws { - let method = try validate() - origIMP = class_replaceMethod(`class`, selector, replacementIMP, method_getTypeEncoding(method)) - guard origIMP != nil else { throw Interpose.Error.nonExistingImplementation } - Interpose.log("Swizzled -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") - } + /// Initialize a new hook to interpose an instance method. + // TODO: report compiler crash + public init(`class`: AnyClass, selector: Selector, implementation:(ClassHook) -> HookSignature?) /* This must be optional or swift runtime will crash. Or swiftc may segfault. Compiler bug? */ throws { + try super.init(class: `class`, selector: selector) + replacementIMP = imp_implementationWithBlock(implementation(self) as Any) + } + + override func replaceImplementation() throws { + let method = try validate() + origIMP = class_replaceMethod(`class`, selector, replacementIMP, method_getTypeEncoding(method)) + guard origIMP != nil else { throw InterposeError.nonExistingImplementation(`class`, selector) } + Interpose.log("Swizzled -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") + } - override func resetImplementation() throws { - let method = try validate(expectedState: .interposed) - precondition(origIMP != nil) - let previousIMP = class_replaceMethod(`class`, selector, origIMP!, method_getTypeEncoding(method)) - guard previousIMP == replacementIMP else { throw Interpose.Error.unexpectedImplementation } - Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") + override func resetImplementation() throws { + let method = try validate(expectedState: .interposed) + precondition(origIMP != nil) + let previousIMP = class_replaceMethod(`class`, selector, origIMP!, method_getTypeEncoding(method)) + guard previousIMP == replacementIMP else { throw InterposeError.unexpectedImplementation(`class`, selector, previousIMP) } + Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") + } + + /// The original implementation is cached at hook time. + public override var original: MethodSignature { + unsafeBitCast(origIMP, to: MethodSignature.self) + } } } -} #if DEBUG extension Interpose.ClassHook: CustomDebugStringConvertible { diff --git a/Sources/InterposeKit/InterposeKit.swift b/Sources/InterposeKit/InterposeKit.swift index 2ef48dc..1e3bb91 100644 --- a/Sources/InterposeKit/InterposeKit.swift +++ b/Sources/InterposeKit/InterposeKit.swift @@ -84,29 +84,62 @@ final public class Interpose { guard hooks.allSatisfy({ (try? $0.validate(expectedState: expectedState)) != nil }) else { - throw Error.invalidState + throw InterposeError.invalidState(expectedState: expectedState) } // Execute all tasks try hooks.forEach(executor) return self } +} + +/// The list of errors while hooking a method. +public enum InterposeError: LocalizedError { + /// The method couldn't be found. Usually happens for when you use stringified selectors that do not exist. + case methodNotFound(AnyClass, Selector) + + /// The implementation could not be found. Class must be in a weird state for this to happen. + case nonExistingImplementation(AnyClass, Selector) + + /// Someone else changed the implementation; reverting removed this implementation. + /// This is bad, likely someone else also hooked this method. If you are in such a codebase, do not use revert. + case unexpectedImplementation(AnyClass, Selector, IMP?) + + /// Unable to register subclass for object-based interposing. + case failedToAllocateClassPair(class: AnyClass, subclassName: String) - /// The list of errors while hooking a method. - public enum Error: Swift.Error { - /// The method couldn't be found. Usually happens for when you use stringified selectors that do not exist. - case methodNotFound + /// Unable to add method for object-based interposing. + case unableToAddMethod(AnyClass, Selector) - /// The implementation could not be found. Class must be in a weird state for this to happen. - case nonExistingImplementation + /// Can't revert or apply if already done so. + case invalidState(expectedState: AnyHook.State) +} - /// Someone else changed the implementation; reverting removed this implementation. - /// This is bad, likely someone else also hooked this method. If you are in such a codebase, do not use revert. - case unexpectedImplementation +extension InterposeError: Equatable { + // Lazy equating via string compare + public static func == (lhs: InterposeError, rhs: InterposeError) -> Bool { + return lhs.errorDescription == rhs.errorDescription + } - case failedToAllocateClassPair + public var errorDescription: String? { + switch self { + case .methodNotFound(let klass, let selector): + return "Method not found: -[\(klass) \(selector)]" + case .nonExistingImplementation(let klass, let selector): + return "Implementation not found: -[\(klass) \(selector)]" + case .unexpectedImplementation(let klass, let selector, let IMP): + return "Unexpected Implementation in -[\(klass) \(selector)]: \(String(describing: IMP))" + case .failedToAllocateClassPair(let klass, let subclassName): + return "Failed to allocate class pair: \(klass), \(subclassName)" + case .unableToAddMethod(let klass, let selector): + return "Unable to add method: -[\(klass) \(selector)]" + case .invalidState(let expectedState): + return "Invalid State. Expected: \(expectedState)" + } + } - /// Can't revert or apply if already done so. - case invalidState + @discardableResult func log() -> InterposeError { + Interpose.log(self.errorDescription!) + return self } } diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index 0ec1fad..9313d07 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -2,175 +2,224 @@ import Foundation extension Interpose { -private enum Constants { - static let subclassSuffix = "InterposeKit_" -} + private enum Constants { + static let subclassSuffix = "InterposeKit_" + } -internal enum ObjCSelector { - static let getClass = Selector((("class"))) -} + internal enum ObjCSelector { + static let getClass = Selector((("class"))) + } -internal enum ObjCMethodEncoding { - static let getClass = extract("#@:") + internal enum ObjCMethodEncoding { + static let getClass = extract("#@:") - private static func extract(_ string: StaticString) -> UnsafePointer { - return UnsafeRawPointer(string.utf8Start).assumingMemoryBound(to: CChar.self) + private static func extract(_ string: StaticString) -> UnsafePointer { + return UnsafeRawPointer(string.utf8Start).assumingMemoryBound(to: CChar.self) + } } -} -/// A hook to an instance method of a single object, stores both the original and new implementation. -/// Think about: Multiple hooks for one object -final public class ObjectHook: TypedHook { - public let object: AnyObject - /// Subclass that we create on the fly - var dynamicSubclass: AnyClass? - - /// Initialize a new hook to interpose an instance method. - public init(object: AnyObject, selector: Selector, implementation:(ObjectHook) -> HookSignature?) throws { - self.object = object - try super.init(class: type(of: object), selector: selector) - replacementIMP = imp_implementationWithBlock(implementation(self) as Any) - } + /// A hook to an instance method of a single object, stores both the original and new implementation. + /// Think about: Multiple hooks for one object + final public class ObjectHook: TypedHook { + public let object: AnyObject + /// Subclass that we create on the fly + var dynamicSubclass: AnyClass? + + /// Initialize a new hook to interpose an instance method. + public init(object: AnyObject, selector: Selector, implementation:(ObjectHook) -> HookSignature?) throws { + self.object = object + try super.init(class: type(of: object), selector: selector) + replacementIMP = imp_implementationWithBlock(implementation(self) as Any) + } -// /// Release the hook block if possible. -// public override func cleanup() { -// // remove subclass! -// super.cleanup() -// } - - /// Creates a unique dynamic subclass of the current object - private func createDynamicSubclass() throws -> AnyClass { - let perceivedClass: AnyClass = `class` - let className = NSStringFromClass(perceivedClass) - // Right now we are wasteful. Might be able to optimize for shared IMP? - let uuid = UUID().uuidString.replacingOccurrences(of: "-", with: "") - let subclassName = Constants.subclassSuffix + className + uuid - - let subclass: AnyClass? = subclassName.withCString { cString in - // swiftlint:disable:next force_cast - if let existingClass = objc_getClass(cString) as! AnyClass? { - return existingClass - } else { - if let subclass: AnyClass = objc_allocateClassPair(perceivedClass, cString, 0) { - replaceGetClass(in: subclass, decoy: perceivedClass) - objc_registerClassPair(subclass) - return subclass + // /// Release the hook block if possible. + // public override func cleanup() { + // // remove subclass! + // super.cleanup() + // } + + /// Creates a unique dynamic subclass of the current object + private func createDynamicSubclass() throws -> AnyClass { + let perceivedClass: AnyClass = `class` + let className = NSStringFromClass(perceivedClass) + // Right now we are wasteful. Might be able to optimize for shared IMP? + let uuid = UUID().uuidString.replacingOccurrences(of: "-", with: "") + let subclassName = Constants.subclassSuffix + className + uuid + + let subclass: AnyClass? = subclassName.withCString { cString in + // swiftlint:disable:next force_cast + if let existingClass = objc_getClass(cString) as! AnyClass? { + return existingClass } else { - return nil + if let subclass: AnyClass = objc_allocateClassPair(perceivedClass, cString, 0) { + replaceGetClass(in: subclass, decoy: perceivedClass) + objc_registerClassPair(subclass) + return subclass + } else { + return nil + } } } - } - guard let nonnullSubclass = subclass else { - throw Interpose.Error.failedToAllocateClassPair + guard let nonnullSubclass = subclass else { + throw InterposeError.failedToAllocateClassPair(class: perceivedClass, subclassName: subclassName) + } + + object_setClass(object, nonnullSubclass) + return nonnullSubclass } - object_setClass(object, nonnullSubclass) - return nonnullSubclass - } + private func replaceGetClass(in class: AnyClass, decoy perceivedClass: AnyClass) { + let getClass: @convention(block) (UnsafeRawPointer?) -> AnyClass = { _ in + perceivedClass + } + let impl = imp_implementationWithBlock(getClass as Any) + _ = class_replaceMethod(`class`, ObjCSelector.getClass, impl, ObjCMethodEncoding.getClass) + _ = class_replaceMethod(object_getClass(`class`), ObjCSelector.getClass, impl, ObjCMethodEncoding.getClass) + } - private func replaceGetClass(in class: AnyClass, decoy perceivedClass: AnyClass) { - let getClass: @convention(block) (UnsafeRawPointer?) -> AnyClass = { _ in - perceivedClass + // https://bugs.swift.org/browse/SR-12945 + public struct ObjcSuperFake { + public var receiver: Unmanaged + public var superClass: AnyClass } - let impl = imp_implementationWithBlock(getClass as Any) - _ = class_replaceMethod(`class`, ObjCSelector.getClass, impl, ObjCMethodEncoding.getClass) - _ = class_replaceMethod(object_getClass(`class`), ObjCSelector.getClass, impl, ObjCMethodEncoding.getClass) - } - // https://bugs.swift.org/browse/SR-12945 - public struct ObjcSuperFake { - public var receiver: Unmanaged - public var superClass: AnyClass - } + private lazy var addSuperImpl: @convention(c) (AnyClass, Selector) -> Bool = { + let handle = dlopen(nil, RTLD_LAZY) + let imp = dlsym(handle, "IKTAddSuperImplementationToClass") + return unsafeBitCast(imp, to: (@convention(c) (AnyClass, Selector) -> Bool).self) + }() - private lazy var addSuperImpl: @convention(c) (AnyClass, Selector) -> Bool = { - let handle = dlopen(nil, RTLD_LAZY) - let imp = dlsym(handle, "IKTAddSuperImplementationToClass") - return unsafeBitCast(imp, to: (@convention(c) (AnyClass, Selector) -> Bool).self) - }() + private func addSuperTrampolineMethod(subclass: AnyClass) { + if addSuperImpl(subclass, self.selector) == false { + Interpose.log("Failed to add super implementation to -[\(`class`).\(selector)]") + } + } - private func addSuperTrampolineMethod(subclass: AnyClass) { - if addSuperImpl(subclass, self.selector) == false { - Interpose.log("Failed to add super implementation to -[\(`class`).\(selector)]") + /// The original implementation is looked up at runtime . + public override var original: MethodSignature { + guard let origIMP = lookupOrigIMP else { InterposeError.nonExistingImplementation(`class`, selector).log() + preconditionFailure("IMP must be found for call") + } + return origIMP } - } - override func replaceImplementation() throws { - let method = try validate() + /// We look for the parent IMP dynamically, so later modifications to the class are no problem. + private var lookupOrigIMP: MethodSignature? { + var currentClass: AnyClass? = self.class + repeat { + if let currentClass = currentClass, + let method = class_getInstanceMethod(currentClass, self.selector) { + let origIMP = method_getImplementation(method) + return unsafeBitCast(origIMP, to: MethodSignature.self) + } + currentClass = class_getSuperclass(currentClass) + } while currentClass != nil + return nil + } + + override func replaceImplementation() throws { + let method = try validate() + + // Register a KVO to work around any KVO issues with opposite order + registerKVO() - // Register a KVO to work around any KVO issues with opposite order - registerKVO() + // Register subclass at runtime if we haven't already + if dynamicSubclass == nil { + dynamicSubclass = try createDynamicSubclass() + } + + /* + // Add empty trampoline that we then replace the IMP! + addSuperTrampolineMethod(subclass: dynamicSubclass!) + origIMP = class_replaceMethod(dynamicSubclass!, selector, replacementIMP, method_getTypeEncoding(method)) + guard origIMP != nil else { throw InterposeError.nonExistingImplementation } + */ - // Register subclass at runtime if we haven't already - if dynamicSubclass == nil { - dynamicSubclass = try createDynamicSubclass() + guard lookupOrigIMP != nil else { + throw InterposeError.nonExistingImplementation(`class`, selector).log() + } + + // Since we are creating a dynamic subclass, there cannot be an existing method + let encoding = method_getTypeEncoding(method) + let didAddMethod = class_addMethod(dynamicSubclass!, selector, replacementIMP, encoding) + if didAddMethod { + Interpose.log("Added -[\(`class`).\(selector)] IMP: \(replacementIMP!)") + } else { + Interpose.log("Unable to add: -[\(`class`).\(selector)] IMP: \(replacementIMP!) - method already set?") + throw InterposeError.unableToAddMethod(`class`, selector) + } } - // Add empty trampoline that we then replace the IMP! - addSuperTrampolineMethod(subclass: dynamicSubclass!) + override func resetImplementation() throws { + _ = try validate(expectedState: .interposed) - origIMP = class_replaceMethod(dynamicSubclass!, selector, replacementIMP, method_getTypeEncoding(method)) - guard origIMP != nil else { throw Interpose.Error.nonExistingImplementation } - Interpose.log("Swizzled -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") - } + guard let dynamicSubclass = self.dynamicSubclass else { preconditionFailure("No dynamic subclass set") } - override func resetImplementation() throws { - let method = try validate(expectedState: .interposed) - precondition(origIMP != nil) - guard let dynamicSubclass = self.dynamicSubclass else { preconditionFailure("No dynamic subclass set") } + // Removing methods at runtime is not supported. + // https://stackoverflow.com/questions/1315169/how-do-i-remove-instance-methods-at-runtime-in-objective-c-2-0 - let previousIMP = class_replaceMethod(dynamicSubclass, selector, origIMP!, method_getTypeEncoding(method)) - guard previousIMP == replacementIMP else { throw Interpose.Error.unexpectedImplementation } - Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") + // Instead, we have to recreate the whole subclass + // Temporary, to remove objc override + _ = try createDynamicSubclass() - // Restore the original class of the object - // Does this include the KVO'ed subclass? - object_setClass(object, `class`) + // Dispose of the custom dynamic subclass + objc_disposeClassPair(dynamicSubclass) + self.dynamicSubclass = nil - // Dispose of the custom dynamic subclass - objc_disposeClassPair(dynamicSubclass) - self.dynamicSubclass = nil - // Remove KVO after restoring class as last step. - deregisterKVO() - } + // TODO: recreate subclass completely + /* + let previousIMP = class_replaceMethod(dynamicSubclass, selector, _objc_msgForward, method_getTypeEncoding(method)) + guard previousIMP == replacementIMP else { throw InterposeError.unexpectedImplementation(`class`, selector, previousIMP) } + Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") -// MARK: KVO Helper + // Restore the original class of the object + // Does this include the KVO'ed subclass? + object_setClass(object, `class`) + */ - var kvoObserver: KVOObserver? +// +// // Remove KVO after restoring class as last step. +// deregisterKVO() + } + + + // MARK: KVO Helper - class KVOObserver: NSObject { - @objc var objectToObserve: AnyObject - var observation: NSKeyValueObservation? + var kvoObserver: KVOObserver? - init(object: AnyObject) { - objectToObserve = object - super.init() + class KVOObserver: NSObject { + @objc var objectToObserve: AnyObject + var observation: NSKeyValueObservation? - // Can't use modern syntax cause https://bugs.swift.org/browse/SR-12944 - objectToObserve.addObserver(self, forKeyPath: "description", options: .new, context: nil) + init(object: AnyObject) { + objectToObserve = object + super.init() + + // Can't use modern syntax cause https://bugs.swift.org/browse/SR-12944 + objectToObserve.addObserver(self, forKeyPath: "description", options: .new, context: nil) + } } - } - // Before creating our subclass, we trigger KVO. - // KVO also creates a subclass at runtime. If we do this prior, then KVO fails. - // If KVO runs prior, and then we sub-subclass, everything works. - private func registerKVO() { - kvoObserver = KVOObserver(object: object) - } + // Before creating our subclass, we trigger KVO. + // KVO also creates a subclass at runtime. If we do this prior, then KVO fails. + // If KVO runs prior, and then we sub-subclass, everything works. + private func registerKVO() { + kvoObserver = KVOObserver(object: object) + } - private func deregisterKVO() { - kvoObserver = nil + private func deregisterKVO() { + kvoObserver = nil + } } } -} #if DEBUG extension Interpose.ObjectHook: CustomDebugStringConvertible { public var debugDescription: String { - return "\(selector) of \(object) -> \(String(describing: origIMP))" + return "\(selector) of \(object) -> \(String(describing: original))" } } #endif From db7b24d46acc9f23c7c6eeaa8ee833581f4bfe73 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 10 Jun 2020 18:07:27 +0200 Subject: [PATCH 31/77] add link to article --- Sources/ITKAddSuperMethod/ITKAddSuperMethod.h | 2 + Sources/InterposeKit/ObjectHook.swift | 44 +++++++++++++------ 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h index f58a256..5504a99 100644 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h @@ -56,6 +56,8 @@ typedef NS_ERROR_ENUM(SuperBuilderErrorDomain, SuperBuilderErrorCode) { 4) This uses inline assembly to forward the parameters to objc_msgSendSuper2 and objc_msgSendSuper2_stret. This is currently implemented architectures are x86_64 and arm64. armv7 was dropped in OS 11 and i386 with macOS Catalina. + + @see https://steipete.com/posts/calling-super-at-runtime/ */ + (BOOL)addSuperInstanceMethodToClass:(Class)originalClass selector:(SEL)selector error:(NSError **)error; diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index 9313d07..aec3296 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -25,6 +25,12 @@ extension Interpose { /// Subclass that we create on the fly var dynamicSubclass: AnyClass? + // fetched at apply time, changes late, thus class requirement + public internal(set) var origIMP: IMP? + + // Logic switch to use super builder + let generatesSuperIMP = true + /// Initialize a new hook to interpose an instance method. public init(object: AnyObject, selector: Selector, implementation:(ObjectHook) -> HookSignature?) throws { self.object = object @@ -92,12 +98,20 @@ extension Interpose { private func addSuperTrampolineMethod(subclass: AnyClass) { if addSuperImpl(subclass, self.selector) == false { + // TODO: use error log! Interpose.log("Failed to add super implementation to -[\(`class`).\(selector)]") + } else { + Interpose.log("Added super for -[\(`class`).\(selector)]") } } /// The original implementation is looked up at runtime . public override var original: MethodSignature { + // If we switched implementations, return stored. + if let savedOrigIMP = origIMP { + return unsafeBitCast(savedOrigIMP, to: MethodSignature.self) + } + // Else, perform a dynamic lookup guard let origIMP = lookupOrigIMP else { InterposeError.nonExistingImplementation(`class`, selector).log() preconditionFailure("IMP must be found for call") } @@ -129,25 +143,29 @@ extension Interpose { dynamicSubclass = try createDynamicSubclass() } - /* - // Add empty trampoline that we then replace the IMP! - addSuperTrampolineMethod(subclass: dynamicSubclass!) - origIMP = class_replaceMethod(dynamicSubclass!, selector, replacementIMP, method_getTypeEncoding(method)) - guard origIMP != nil else { throw InterposeError.nonExistingImplementation } - */ - guard lookupOrigIMP != nil else { throw InterposeError.nonExistingImplementation(`class`, selector).log() } - // Since we are creating a dynamic subclass, there cannot be an existing method let encoding = method_getTypeEncoding(method) - let didAddMethod = class_addMethod(dynamicSubclass!, selector, replacementIMP, encoding) - if didAddMethod { - Interpose.log("Added -[\(`class`).\(selector)] IMP: \(replacementIMP!)") + if self.generatesSuperIMP { + // Add empty trampoline that we then replace the IMP! + addSuperTrampolineMethod(subclass: dynamicSubclass!) + + origIMP = class_replaceMethod(dynamicSubclass!, selector, replacementIMP, encoding) + guard origIMP != nil else { throw InterposeError.nonExistingImplementation(dynamicSubclass!, selector) } + + Interpose.log("Added -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") } else { - Interpose.log("Unable to add: -[\(`class`).\(selector)] IMP: \(replacementIMP!) - method already set?") - throw InterposeError.unableToAddMethod(`class`, selector) + // Since we are creating a dynamic subclass, there cannot be an existing method + // TODO: think about hooking twice!! + let didAddMethod = class_addMethod(dynamicSubclass!, selector, replacementIMP, encoding) + if didAddMethod { + Interpose.log("Added -[\(`class`).\(selector)] IMP: \(replacementIMP!)") + } else { + Interpose.log("Unable to add: -[\(`class`).\(selector)] IMP: \(replacementIMP!) - method already set?") + throw InterposeError.unableToAddMethod(`class`, selector) + } } } From 5619117305a5d1f1d5cdbc10b89b8cbc1a7e957a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 11 Jun 2020 11:51:21 +0200 Subject: [PATCH 32/77] header cleanup --- Sources/ITKAddSuperMethod/ITKAddSuperMethod.m | 44 +++++++------------ 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m index 878022e..917df2c 100644 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m @@ -1,11 +1,3 @@ -// -// ITKAddSuperMethod.m -// InterposeKit -// -// Created by Peter Steinberger on 08.06.20. -// Copyright © 2020 PSPDFKit GmbH. All rights reserved. -// - #import "ITKAddSuperMethod.h" @import ObjectiveC.message; @@ -18,30 +10,26 @@ void msgSendSuperTrampoline(void); void msgSendSuperStretTrampoline(void); -typedef NS_ENUM(NSInteger, DispatchMode) { - DispatchModeNormal, - DispatchModeStret, -}; - #define let const __auto_type #define var __auto_type -static DispatchMode IKTGetDispatchMode(const char *typeEncoding) { - DispatchMode dispatchMode = DispatchModeNormal; -#if defined (__arm64__) +static IMP ITKGetTrampolineForTypeEncoding(__unused const char *typeEncoding) { + BOOL requiresStructDispatch = NO; + #if defined (__arm64__) // ARM64 doesn't use stret dispatch. Yay! -#elif defined (__x86_64__) - // On x86-64, stret dispatch is ~used whenever return type doesn't fit into two registers - // - // http://www.sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html - // x86_64 is more complicated, including rules for returning floating-point struct fields in FPU registers, and ppc64's rules and exceptions will make your head spin. The gory details are documented in the Mac OS X ABI Guide, though as usual if the documentation and the compiler disagree then the documentation is wrong. - NSUInteger returnTypeActualSize = 0; - NSGetSizeAndAlignment(typeEncoding, &returnTypeActualSize, NULL); - dispatchMode = returnTypeActualSize > (sizeof(void *) * 2) ? DispatchModeStret : DispatchModeNormal; -#else -#error - Unknown architecture -#endif - return dispatchMode; + #elif defined (__x86_64__) + // On x86-64, stret dispatch is ~used whenever return type doesn't fit into two registers + // + // http://www.sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html + // x86_64 is more complicated, including rules for returning floating-point struct fields in FPU registers, and ppc64's rules and exceptions will make your head spin. The gory details are documented in the Mac OS X ABI Guide, though as usual if the documentation and the compiler disagree then the documentation is wrong. + NSUInteger returnTypeActualSize = 0; + NSGetSizeAndAlignment(typeEncoding, &returnTypeActualSize, NULL); + requiresStructDispatch = returnTypeActualSize > (sizeof(void *) * 2); + #else + #error - Unknown architecture + #endif + + return requiresStructDispatch ? msgSendSuperStretTrampoline : msgSendSuperTrampoline; } // Helper for binding with Swift From 84a1923cdcf1ce28b73f233eac49f544abfc9c4d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 11 Jun 2020 11:51:52 +0200 Subject: [PATCH 33/77] Add floating point register protection --- Sources/ITKAddSuperMethod/ITKAddSuperMethod.m | 51 +++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m index 917df2c..8856cf0 100644 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m @@ -54,9 +54,8 @@ + (BOOL)addSuperInstanceMethodToClass:(Class)originalClass selector:(SEL)selecto let msg = [NSString stringWithFormat:@"No dynamically dispatched method with selector %@ is available on any of the superclasses of %@", NSStringFromSelector(selector), NSStringFromClass(originalClass)]; ERROR_AND_RETURN(SuperBuilderErrorCodeNoDynamicallyDispatchedMethodAvailable, msg) } - const char *typeEncoding = method_getTypeEncoding(method); - let isNormalDispatch = IKTGetDispatchMode(typeEncoding) == DispatchModeNormal; - IMP trampoline = isNormalDispatch ? msgSendSuperTrampoline : msgSendSuperStretTrampoline; + let typeEncoding = method_getTypeEncoding(method); + let trampoline = ITKGetTrampolineForTypeEncoding(typeEncoding); let methodAdded = class_addMethod(originalClass, selector, trampoline, typeEncoding); if (!methodAdded) { let msg = [NSString stringWithFormat:@"Failed to add method for selector %@ to class %@", NSStringFromSelector(selector), NSStringFromClass(originalClass)]; @@ -67,6 +66,11 @@ + (BOOL)addSuperInstanceMethodToClass:(Class)originalClass selector:(SEL)selecto @end +// Control if the trampoline should also push/pop the floating point registers. +// This is slightly slower and not needed for our simple implementation +// However, even if you just use memcpy, you will want to enable this. +#define PROTECT_FLOATING_POINT_REGISTERS 0 + // One thread local per thread should be enough _Thread_local struct objc_super _threadSuperStorage; @@ -103,6 +107,15 @@ + (BOOL)addSuperInstanceMethodToClass:(Class)originalClass selector:(SEL)selecto __attribute__((__naked__)) void msgSendSuperTrampoline(void) { asm volatile ( + +#if PROTECT_FLOATING_POINT_REGISTERS + // push {q0-q7} floating point registers + "stp q6, q7, [sp, #-32]!\n" + "stp q4, q5, [sp, #-32]!\n" + "stp q2, q3, [sp, #-32]!\n" + "stp q0, q1, [sp, #-32]!\n" +#endif + // push {x0-x8, lr} (call params are: x0-x7) // stp: store pair of registers: from, from, to, via indexed write "stp x8, lr, [sp, #-16]!\n" // push lr (link register == x30), then x8 @@ -125,6 +138,14 @@ asm volatile ( "ldp x6, x7, [sp], #16\n" "ldp x8, lr, [sp], #16\n" +#if PROTECT_FLOATING_POINT_REGISTERS + // pop {q0-q7} + "ldp q6, q7, [sp], #32\n" + "ldp q4, q5, [sp], #32\n" + "ldp q2, q3, [sp], #32\n" + "ldp q0, q1, [sp], #32\n" +#endif + // get new return (adr of the objc_super class) "mov x0, x9\n" // tail call @@ -144,8 +165,19 @@ asm volatile ( "pushq %%rbp \n" // set stack to frame pointer "movq %%rsp, %%rbp \n" + +#if PROTECT_FLOATING_POINT_REGISTERS + // reserve 48+4*16 = 112 byte on the stack (need 16 byte alignment) + "subq $112, %%rsp \n" + + "movdqu %%xmm0, -64(%%rbp) \n" + "movdqu %%xmm1, -80(%%rbp) \n" + "movdqu %%xmm2, -96(%%rbp) \n" + "movdqu %%xmm3, -112(%%rbp) \n" +#else // reserve 48 byte on the stack (need 16 byte alignment) "subq $48, %%rsp \n" +#endif // Save call params: rdi, rsi, rdx, rcx, r8, r9 "movq %%rdi, -8(%%rbp) \n" // self po *(id *) @@ -160,6 +192,13 @@ asm volatile ( // first param is now struct objc_super "movq %%rax, %%rdi \n" +#if PROTECT_FLOATING_POINT_REGISTERS + "movdqu -64(%%rbp), %%xmm0 \n" + "movdqu -80(%%rbp), %%xmm1 \n" + "movdqu -96(%%rbp), %%xmm2 \n" + "movdqu -112(%%rbp), %%xmm3 \n" +#endif + // Restore call params // do not restore first parameter: super class "movq -16(%%rbp), %%rsi \n" @@ -169,8 +208,12 @@ asm volatile ( "movq -48(%%rbp), %%r9 \n" // debug stack via print *(int *) ($rsp+8) - // remove 64 byte from stack + // remove 112/48 byte from stack +#if PROTECT_FLOATING_POINT_REGISTERS + "addq $112, %%rsp \n" +#else "addq $48, %%rsp \n" +#endif // pop frame pointer "popq %%rbp \n" From aa86ace4548197e94665e8e923f6a0a5d83ba259 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 12 Jun 2020 00:40:08 +0200 Subject: [PATCH 34/77] tweak assembly --- Sources/ITKAddSuperMethod/ITKAddSuperMethod.m | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m index 8856cf0..e926415 100644 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m @@ -180,7 +180,10 @@ asm volatile ( #endif // Save call params: rdi, rsi, rdx, rcx, r8, r9 - "movq %%rdi, -8(%%rbp) \n" // self po *(id *) + // + // First parameter can be avoided, + // but we need to keep the stack 16-byte algined. + //"movq %%rdi, -8(%%rbp) \n" // self po *(id *) "movq %%rsi, -16(%%rbp) \n" // _cmd p (SEL)$rsi "movq %%rdx, -24(%%rbp) \n" // param 1 "movq %%rcx, -32(%%rbp) \n" // param 2 @@ -233,7 +236,7 @@ asm volatile ( // reserve 48 byte on the stack (need 16 byte alignment) "subq $48, %%rsp \n" - // Save call params: rax(for va_arg) rdi, rsi, rdx, rcx, r8, r9 + // Save call params: rdi, rsi, rdx, rcx, r8, r9 "movq %%rdi, -8(%%rbp) \n" // struct return "movq %%rsi, -16(%%rbp) \n" // self "movq %%rdx, -24(%%rbp) \n" // _cmd From 430ea3704e0d55232035f2741f41eb277101b598 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 13 Jun 2020 14:30:16 +0200 Subject: [PATCH 35/77] Prepare new test --- InterposeKit.xcodeproj/project.pbxproj | 4 ++ .../MultipleInterposing.swift | 48 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 Tests/InterposeKitTests/MultipleInterposing.swift diff --git a/InterposeKit.xcodeproj/project.pbxproj b/InterposeKit.xcodeproj/project.pbxproj index 22b3392..f77f4c2 100644 --- a/InterposeKit.xcodeproj/project.pbxproj +++ b/InterposeKit.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 78C39D8F2483164500B46395 /* InterposeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 78C39D8E2483164500B46395 /* InterposeKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 78C39D912483165600B46395 /* InterposeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C39D902483165600B46395 /* InterposeKit.swift */; }; 78C39D932483169300B46395 /* InterposeKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C39D922483169300B46395 /* InterposeKitTests.swift */; }; + 78C5A4A82494D75100EE9756 /* MultipleInterposing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C5A4A72494D75100EE9756 /* MultipleInterposing.swift */; }; 78EDB8DA248BA9B300D2F6C1 /* TestClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */; }; 78EDB8DB248BA9BB00D2F6C1 /* ObjectInterposeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */; }; 78EDB8DD248BAA5600D2F6C1 /* AnyHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8DC248BAA5600D2F6C1 /* AnyHook.swift */; }; @@ -91,6 +92,7 @@ 78C39DBF248317B400B46395 /* Defaults-Testing.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Defaults-Testing.xcconfig"; sourceTree = ""; }; 78C39DC0248317B400B46395 /* Defaults.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Defaults.xcconfig; sourceTree = ""; }; 78C39DC1248317B400B46395 /* Defaults-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Defaults-Debug.xcconfig"; sourceTree = ""; }; + 78C5A4A72494D75100EE9756 /* MultipleInterposing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MultipleInterposing.swift; path = Tests/InterposeKitTests/MultipleInterposing.swift; sourceTree = SOURCE_ROOT; }; 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TestClass.swift; path = Tests/InterposeKitTests/TestClass.swift; sourceTree = SOURCE_ROOT; }; 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ObjectInterposeTests.swift; path = Tests/InterposeKitTests/ObjectInterposeTests.swift; sourceTree = SOURCE_ROOT; }; 78EDB8DC248BAA5600D2F6C1 /* AnyHook.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AnyHook.swift; path = Sources/InterposeKit/AnyHook.swift; sourceTree = SOURCE_ROOT; }; @@ -201,6 +203,7 @@ 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */, 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */, 78C39D7B2482CC7D00B46395 /* Info-Tests.plist */, + 78C5A4A72494D75100EE9756 /* MultipleInterposing.swift */, ); path = InterposeTests; sourceTree = ""; @@ -387,6 +390,7 @@ buildActionMask = 2147483647; files = ( 78C39D932483169300B46395 /* InterposeKitTests.swift in Sources */, + 78C5A4A82494D75100EE9756 /* MultipleInterposing.swift in Sources */, 78EDB8DA248BA9B300D2F6C1 /* TestClass.swift in Sources */, 78EDB8DB248BA9BB00D2F6C1 /* ObjectInterposeTests.swift in Sources */, ); diff --git a/Tests/InterposeKitTests/MultipleInterposing.swift b/Tests/InterposeKitTests/MultipleInterposing.swift new file mode 100644 index 0000000..7938353 --- /dev/null +++ b/Tests/InterposeKitTests/MultipleInterposing.swift @@ -0,0 +1,48 @@ +import Foundation +import XCTest +@testable import InterposeKit + +func testInterposeSingleObjectMultipleTimes() throws { + let testObj = TestClass() + let testObj2 = TestClass() + + XCTAssertEqual(testObj.sayHi(), testClassHi) + XCTAssertEqual(testObj2.sayHi(), testClassHi) + + // Functions need to be `@objc dynamic` to be hookable. + let interposer = try Interpose(testObj) { + try $0.hook( + #selector(TestClass.sayHi), + methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, + hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { `self` in + return store.original(`self`, store.selector) + testSwizzleAddition + } + } + } + + XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition) + XCTAssertEqual(testObj2.sayHi(), testClassHi) + + let interposer2 = try Interpose(testObj) { + try $0.hook( + #selector(TestClass.sayHi), + methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, + hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { `self` in + return store.original(`self`, store.selector) + testSwizzleAddition + } + } + } + + // TODO: detect existing hook? + + XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition) + + + try interposer.revert() + try interposer2.revert() +// XCTAssertEqual(testObj.sayHi(), testClassHi) +// XCTAssertEqual(testObj2.sayHi(), testClassHi) +// try interposer.apply() +// XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition) +// XCTAssertEqual(testObj2.sayHi(), testClassHi) +} From c0779aac36a06489f38f0e888586c69c02f47ce6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 14 Jun 2020 15:28:40 +0200 Subject: [PATCH 36/77] Add tests, remove KVO support --- InterposeKit.xcodeproj/project.pbxproj | 12 +- Sources/ITKAddSuperMethod/ITKAddSuperMethod.m | 41 ++++- Sources/InterposeKit/AnyHook.swift | 22 ++- Sources/InterposeKit/ClassHook.swift | 9 +- Sources/InterposeKit/InterposeKit.swift | 64 +++++++- Sources/InterposeKit/ObjectHook.swift | 152 ++++++++---------- .../InterposeKitTestCase.swift | 36 +++++ .../InterposeKitTests/InterposeKitTests.swift | 20 +-- Tests/InterposeKitTests/KVOTests.swift | 67 ++++++++ .../MultipleInterposing.swift | 86 ++++++---- .../ObjectInterposeTests.swift | 12 +- Tests/InterposeKitTests/TestClass.swift | 7 +- 12 files changed, 377 insertions(+), 151 deletions(-) create mode 100644 Tests/InterposeKitTests/InterposeKitTestCase.swift create mode 100644 Tests/InterposeKitTests/KVOTests.swift diff --git a/InterposeKit.xcodeproj/project.pbxproj b/InterposeKit.xcodeproj/project.pbxproj index f77f4c2..825b794 100644 --- a/InterposeKit.xcodeproj/project.pbxproj +++ b/InterposeKit.xcodeproj/project.pbxproj @@ -18,6 +18,8 @@ 781095F6248E7C91008A943C /* InterposeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 78863EC62464B2F900BA3762 /* InterposeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 781095FF248E83D7008A943C /* ITKAddSuperMethod.h in Headers */ = {isa = PBXBuildFile; fileRef = 781095FD248E83D7008A943C /* ITKAddSuperMethod.h */; }; 78109600248E83D7008A943C /* ITKAddSuperMethod.m in Sources */ = {isa = PBXBuildFile; fileRef = 781095FE248E83D7008A943C /* ITKAddSuperMethod.m */; }; + 78A2F265249635B100F5AC5F /* KVOTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F264249635B100F5AC5F /* KVOTests.swift */; }; + 78A2F26724964AF200F5AC5F /* InterposeKitTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F26624964AF200F5AC5F /* InterposeKitTestCase.swift */; }; 78C39D7C2482CC7D00B46395 /* InterposeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78863EC62464B2F900BA3762 /* InterposeKit.framework */; }; 78C39D8F2483164500B46395 /* InterposeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 78C39D8E2483164500B46395 /* InterposeKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 78C39D912483165600B46395 /* InterposeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C39D902483165600B46395 /* InterposeKit.swift */; }; @@ -83,6 +85,8 @@ 781095FE248E83D7008A943C /* ITKAddSuperMethod.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ITKAddSuperMethod.m; sourceTree = ""; }; 78863EC62464B2F900BA3762 /* InterposeKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = InterposeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 78863ECA2464B2F900BA3762 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = Info.plist; path = InterposeKit.xcodeproj/Info.plist; sourceTree = ""; }; + 78A2F264249635B100F5AC5F /* KVOTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = KVOTests.swift; path = Tests/InterposeKitTests/KVOTests.swift; sourceTree = SOURCE_ROOT; }; + 78A2F26624964AF200F5AC5F /* InterposeKitTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InterposeKitTestCase.swift; path = Tests/InterposeKitTests/InterposeKitTestCase.swift; sourceTree = SOURCE_ROOT; }; 78C39D772482CC7D00B46395 /* InterposeKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InterposeKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 78C39D7B2482CC7D00B46395 /* Info-Tests.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = "Info-Tests.plist"; path = "InterposeKit.xcodeproj/Info-Tests.plist"; sourceTree = ""; }; 78C39D8E2483164500B46395 /* InterposeKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = InterposeKit.h; path = Sources/InterposeKit/InterposeKit.h; sourceTree = SOURCE_ROOT; }; @@ -199,11 +203,13 @@ 78C39D782482CC7D00B46395 /* InterposeTests */ = { isa = PBXGroup; children = ( - 78C39D922483169300B46395 /* InterposeKitTests.swift */, 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */, + 78C39D922483169300B46395 /* InterposeKitTests.swift */, 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */, - 78C39D7B2482CC7D00B46395 /* Info-Tests.plist */, 78C5A4A72494D75100EE9756 /* MultipleInterposing.swift */, + 78C39D7B2482CC7D00B46395 /* Info-Tests.plist */, + 78A2F264249635B100F5AC5F /* KVOTests.swift */, + 78A2F26624964AF200F5AC5F /* InterposeKitTestCase.swift */, ); path = InterposeTests; sourceTree = ""; @@ -393,6 +399,8 @@ 78C5A4A82494D75100EE9756 /* MultipleInterposing.swift in Sources */, 78EDB8DA248BA9B300D2F6C1 /* TestClass.swift in Sources */, 78EDB8DB248BA9BB00D2F6C1 /* ObjectInterposeTests.swift in Sources */, + 78A2F26724964AF200F5AC5F /* InterposeKitTestCase.swift in Sources */, + 78A2F265249635B100F5AC5F /* KVOTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m index e926415..ed7f806 100644 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m @@ -5,7 +5,7 @@ NS_ASSUME_NONNULL_BEGIN -NSString *const PSPDFErrorDomain = @"com.steipete.superbuilder"; +NSString *const SuperBuilderErrorDomain = @"com.steipete.superbuilder"; void msgSendSuperTrampoline(void); void msgSendSuperStretTrampoline(void); @@ -74,11 +74,42 @@ + (BOOL)addSuperInstanceMethodToClass:(Class)originalClass selector:(SEL)selecto // One thread local per thread should be enough _Thread_local struct objc_super _threadSuperStorage; -struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj); -struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj) { +static BOOL ITKMethodIsSuperTrampoline(Method method) { + let methodIMP = method_getImplementation(method); + return methodIMP == msgSendSuperTrampoline || methodIMP == msgSendSuperStretTrampoline; +} + +struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj, SEL _cmd); +struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj, SEL _cmd) { + /** + Assume you have a class hierarchy made of four classes `Level1` <- `Level2` <- `Level3` <- `Level4`, + with `Level1` implementing a method called `-sayHello`, not implemented elsewhere in descendants classes. + + If you use: `[SuperBuilder addSuperInstanceMethodToClass:Level2.class selector:@selector(sayHello) error:NULL];` + to inject a _dummy_ implementation at `Level2`, the following will happen: + + - Calling `-[Level2 sayHello]` works. The trampoline is called, the `super_class ` is found to be `Level1`, and the `-sayHello` parent implementation is called. + - Calling `-[LevelN sayHello]` for any N > 2 ends in an infinite recursion. Since the `obj` passed to the trampoline is a descendant of `Level2`, `objc_msgSendSuper2` will of course call the injected implementation on `Level2`, which in turn will call itself with the same arguments, again and again. + + This is fixed by walking up the hierarchy until we find the class implementing the method. + + Looking at the method implementation we can also skip subsequent super calls. + */ + Class clazz = object_getClass(obj); + Class superclazz = class_getSuperclass(clazz); + do { + let superclassMethod = class_getInstanceMethod(superclazz, _cmd); + let sameMethods = class_getInstanceMethod(clazz, _cmd) == superclassMethod; + if (!sameMethods && !ITKMethodIsSuperTrampoline(superclassMethod)) { + break; + } + clazz = superclazz; + superclazz = class_getSuperclass(clazz); + }while (1); + struct objc_super *_super = &_threadSuperStorage; _super->receiver = obj; - _super->super_class = object_getClass(obj); + _super->super_class = clazz; return _super; } @@ -247,7 +278,7 @@ asm volatile ( // fetch filled struct objc_super, call with self + _cmd // Since stret offsets, we move back by one "movq -16(%%rbp), %%rdi \n" - "movq -24(%%rbp), %%rdx \n" + "movq -24(%%rbp), %%rsi \n" "callq _ITKReturnThreadSuper \n" // second param is now struct objc_super "movq %%rax, %%rsi \n" diff --git a/Sources/InterposeKit/AnyHook.swift b/Sources/InterposeKit/AnyHook.swift index fee7224..7810c11 100644 --- a/Sources/InterposeKit/AnyHook.swift +++ b/Sources/InterposeKit/AnyHook.swift @@ -7,6 +7,9 @@ public class AnyHook { public internal(set) var state = State.prepared // else we validate init order public internal(set) var replacementIMP: IMP! + + // fetched at apply time, changes late, thus class requirement + public internal(set) var origIMP: IMP? /// The possible task states public enum State: Equatable { @@ -23,9 +26,9 @@ public class AnyHook { init(`class`: AnyClass, selector: Selector) throws { self.selector = selector self.class = `class` + // Check if method exists try validate() - // replacementIMP = imp_implementationWithBlock(implementation(self)) } func replaceImplementation() throws { @@ -45,10 +48,10 @@ public class AnyHook { public func revert() throws { try execute(newState: .prepared) { try resetImplementation() } } - - // public func callAsFunction(_ type: U.Type) -> U { - // unsafeBitCast(origIMP, to: type) - // } + + public func callAsFunction(_ type: U.Type) -> U { + unsafeBitCast(origIMP, to: type) + } /// Validate that the selector exists on the active class. @discardableResult func validate(expectedState: State = .prepared) throws -> Method { @@ -79,6 +82,15 @@ public class AnyHook { Interpose.log("Leaking -[\(`class`).\(selector)] IMP: \(replacementIMP!) due to error: \(error)") } } + + /// Internal: Restores the previous implementation if one is set. + func restorePreviousIMP(exactClass: AnyClass) throws { + let method = try validate(expectedState: .interposed) + precondition(origIMP != nil) + let previousIMP = class_replaceMethod(exactClass, selector, origIMP!, method_getTypeEncoding(method)) + guard previousIMP == replacementIMP else { throw InterposeError.unexpectedImplementation(exactClass, selector, previousIMP) } + Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") + } } public class TypedHook: AnyHook { diff --git a/Sources/InterposeKit/ClassHook.swift b/Sources/InterposeKit/ClassHook.swift index b956336..6384fed 100644 --- a/Sources/InterposeKit/ClassHook.swift +++ b/Sources/InterposeKit/ClassHook.swift @@ -4,9 +4,6 @@ extension Interpose { /// A hook to an instance method and stores both the original and new implementation. final public class ClassHook: TypedHook { - // fetched at apply time, changes late, thus class requirement - public internal(set) var origIMP: IMP? - /// Initialize a new hook to interpose an instance method. // TODO: report compiler crash public init(`class`: AnyClass, selector: Selector, implementation:(ClassHook) -> HookSignature?) /* This must be optional or swift runtime will crash. Or swiftc may segfault. Compiler bug? */ throws { @@ -22,11 +19,7 @@ extension Interpose { } override func resetImplementation() throws { - let method = try validate(expectedState: .interposed) - precondition(origIMP != nil) - let previousIMP = class_replaceMethod(`class`, selector, origIMP!, method_getTypeEncoding(method)) - guard previousIMP == replacementIMP else { throw InterposeError.unexpectedImplementation(`class`, selector, previousIMP) } - Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") + try restorePreviousIMP(exactClass: `class`) } /// The original implementation is cached at hook time. diff --git a/Sources/InterposeKit/InterposeKit.swift b/Sources/InterposeKit/InterposeKit.swift index 1e3bb91..fd7d5cb 100644 --- a/Sources/InterposeKit/InterposeKit.swift +++ b/Sources/InterposeKit/InterposeKit.swift @@ -1,6 +1,23 @@ import Foundation -/// Helper to swizzle methods the right way, via replacing the IMP. +private var interposeKey: Character = "_" + +struct AssociatedKeys { + static var interposeObject: UInt8 = 0 +} + +extension NSObject { + /// Access an existing Interpose container, if available. + var interpose: Interpose? { + get { objc_getAssociatedObject(self, &AssociatedKeys.interposeObject) as? Interpose } + set { objc_setAssociatedObject(self, &AssociatedKeys.interposeObject, newValue, .OBJC_ASSOCIATION_RETAIN) } + } +} + +/// Interpose is a modern library to swizzle elegantly in Swift. +/// +/// Methods are hooked via replacing the implementation, instead of the usual exchange. +/// Supports both swizzling classes and individual objects. final public class Interpose { /// Stores swizzle hooks and executes them at once. public let `class`: AnyClass @@ -10,6 +27,22 @@ final public class Interpose { /// If Interposing is object-based, this is set. public let object: AnyObject? + // Checks if a object is posing as a different class + // via implementing 'class' and returning something else. + private func checkObjectPosingAsDifferentClass(_ object: AnyObject) -> AnyClass? { + let perceivedClass: AnyClass = type(of: object) + let actualClass: AnyClass = object_getClass(object)! + if actualClass != perceivedClass { + return actualClass + } + return nil + } + + // This is based on observation, there is no documented way + private func isKVORuntimeGeneratedClass(_ klass: AnyClass) -> Bool { + NSStringFromClass(klass).hasPrefix("NSKVO") + } + /// Initializes an instance of Interpose for a specific class. /// If `builder` is present, `apply()` is automatically called. public init(_ `class`: AnyClass, builder: ((Interpose) throws -> Void)? = nil) throws { @@ -23,14 +56,25 @@ final public class Interpose { } /// Initialize with a single object to interpose. - public init(_ object: AnyObject, builder: ((Interpose) throws -> Void)? = nil) throws { + public init(_ object: NSObject, builder: ((Interpose) throws -> Void)? = nil) throws { self.object = object self.class = type(of: object) + if let actualClass = checkObjectPosingAsDifferentClass(object) { + if isKVORuntimeGeneratedClass(actualClass) { + throw InterposeError.keyValueObservationDetected(object) + } else { + throw InterposeError.objectPosingAsDifferentClass(object, actualClass: actualClass) + } + } + // Only apply if a builder is present if let builder = builder { try apply(builder) } + + // Store interpose on object + object.interpose = self } deinit { @@ -110,6 +154,18 @@ public enum InterposeError: LocalizedError { /// Unable to add method for object-based interposing. case unableToAddMethod(AnyClass, Selector) + /// Object-based hooking does not work if an object is using KVO. + /// The KVO mechanism also uses subclasses created at runtime but doesn't check for additional overrides. + /// Adding a hook eventually crashes the KVO management code so we reject hooking altogether in this case. + case keyValueObservationDetected(AnyObject) + + /// Object is lying about it's actual class metadata. + /// This usually happens when other swizzling libraries (like Aspects) also interfere with a class. + /// While this might just work, it's not worth risking a crash, so similar to KVO this case is rejected. + /// + /// @note Printing classes in Swift uses the class posing mechanism. Use `NSClassFromString` to get the correct name. + case objectPosingAsDifferentClass(AnyObject, actualClass: AnyClass) + /// Can't revert or apply if already done so. case invalidState(expectedState: AnyHook.State) } @@ -132,6 +188,10 @@ extension InterposeError: Equatable { return "Failed to allocate class pair: \(klass), \(subclassName)" case .unableToAddMethod(let klass, let selector): return "Unable to add method: -[\(klass) \(selector)]" + case .keyValueObservationDetected(let obj): + return "Unable to hook object that uses Key Value Observing: \(obj)" + case .objectPosingAsDifferentClass(let obj, let actualClass): + return "Unable to hook object posing as different class. Expected: \(type(of: obj)) Is: \(NSStringFromClass(actualClass))/" case .invalidState(let expectedState): return "Invalid State. Expected: \(expectedState)" } diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index aec3296..313a5d9 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -25,9 +25,6 @@ extension Interpose { /// Subclass that we create on the fly var dynamicSubclass: AnyClass? - // fetched at apply time, changes late, thus class requirement - public internal(set) var origIMP: IMP? - // Logic switch to use super builder let generatesSuperIMP = true @@ -46,7 +43,16 @@ extension Interpose { /// Creates a unique dynamic subclass of the current object private func createDynamicSubclass() throws -> AnyClass { + + // If the class has been altered (e.g. via NSKVONotifying_ KVO logic) + // then perceived and actual class don't match. + // + // Making KVO and Object-based hooking work at the same time is difficult. + // If we make a dynamic subclass over KVO, invalidating the token crashes in cache_getImp. + let perceivedClass: AnyClass = `class` + let actualClass: AnyClass = object_getClass(object)! + let className = NSStringFromClass(perceivedClass) // Right now we are wasteful. Might be able to optimize for shared IMP? let uuid = UUID().uuidString.replacingOccurrences(of: "-", with: "") @@ -57,7 +63,7 @@ extension Interpose { if let existingClass = objc_getClass(cString) as! AnyClass? { return existingClass } else { - if let subclass: AnyClass = objc_allocateClassPair(perceivedClass, cString, 0) { + if let subclass: AnyClass = objc_allocateClassPair(actualClass, cString, 0) { replaceGetClass(in: subclass, decoy: perceivedClass) objc_registerClassPair(subclass) return subclass @@ -72,6 +78,7 @@ extension Interpose { } object_setClass(object, nonnullSubclass) + Interpose.log("Generated \(NSStringFromClass(nonnullSubclass)) for object (was: \(NSStringFromClass(class_getSuperclass(object_getClass(object)!)!)))") return nonnullSubclass } @@ -84,12 +91,6 @@ extension Interpose { _ = class_replaceMethod(object_getClass(`class`), ObjCSelector.getClass, impl, ObjCMethodEncoding.getClass) } - // https://bugs.swift.org/browse/SR-12945 - public struct ObjcSuperFake { - public var receiver: Unmanaged - public var superClass: AnyClass - } - private lazy var addSuperImpl: @convention(c) (AnyClass, Selector) -> Bool = { let handle = dlopen(nil, RTLD_LAZY) let imp = dlsym(handle, "IKTAddSuperImplementationToClass") @@ -101,7 +102,8 @@ extension Interpose { // TODO: use error log! Interpose.log("Failed to add super implementation to -[\(`class`).\(selector)]") } else { - Interpose.log("Added super for -[\(`class`).\(selector)]") + let imp = class_getMethodImplementation(subclass, self.selector)! + Interpose.log("Added super for -[\(`class`).\(selector)]: \(imp)") } } @@ -132,12 +134,23 @@ extension Interpose { return nil } + /// Looks for an instance method in the exact class, without looking up the hierarchy. + func exactClassImplementsSelector(_ klass: AnyClass, _ selector: Selector) -> Bool { + var methodCount : CUnsignedInt = 0 + guard let methodsInAClass = class_copyMethodList(klass, &methodCount) else { return false } + defer { free(methodsInAClass) } + for i in 0 ..< Int(methodCount) { + let method = methodsInAClass[i] + if method_getName(method) == selector { + return true + } + } + return false + } + override func replaceImplementation() throws { let method = try validate() - // Register a KVO to work around any KVO issues with opposite order - registerKVO() - // Register subclass at runtime if we haven't already if dynamicSubclass == nil { dynamicSubclass = try createDynamicSubclass() @@ -148,23 +161,40 @@ extension Interpose { } let encoding = method_getTypeEncoding(method) + // This function searches superclasses for implementations + let hasExistingMethod = exactClassImplementsSelector(dynamicSubclass!, selector) + if self.generatesSuperIMP { - // Add empty trampoline that we then replace the IMP! - addSuperTrampolineMethod(subclass: dynamicSubclass!) - origIMP = class_replaceMethod(dynamicSubclass!, selector, replacementIMP, encoding) - guard origIMP != nil else { throw InterposeError.nonExistingImplementation(dynamicSubclass!, selector) } + // If the subclass is empty, we create a super trampoline first. + // If a hook already exists, we must skip this. + if !hasExistingMethod { + addSuperTrampolineMethod(subclass: dynamicSubclass!) + } + // Replace IMP (by now we guarantee that it exists) + origIMP = class_replaceMethod(dynamicSubclass!, selector, replacementIMP, encoding) + guard origIMP != nil else { + throw InterposeError.nonExistingImplementation(dynamicSubclass!, selector) + } Interpose.log("Added -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") } else { - // Since we are creating a dynamic subclass, there cannot be an existing method - // TODO: think about hooking twice!! - let didAddMethod = class_addMethod(dynamicSubclass!, selector, replacementIMP, encoding) - if didAddMethod { - Interpose.log("Added -[\(`class`).\(selector)] IMP: \(replacementIMP!)") + if hasExistingMethod { + origIMP = class_replaceMethod(dynamicSubclass!, selector, replacementIMP, encoding) + if origIMP != nil { + Interpose.log("Added -[\(`class`).\(selector)] IMP: \(replacementIMP!) via replacement") + } else { + Interpose.log("Unable to replace: -[\(`class`).\(selector)] IMP: \(replacementIMP!)") + throw InterposeError.unableToAddMethod(`class`, selector) + } } else { - Interpose.log("Unable to add: -[\(`class`).\(selector)] IMP: \(replacementIMP!) - method already set?") - throw InterposeError.unableToAddMethod(`class`, selector) + let didAddMethod = class_addMethod(dynamicSubclass!, selector, replacementIMP, encoding) + if didAddMethod { + Interpose.log("Added -[\(`class`).\(selector)] IMP: \(replacementIMP!)") + } else { + Interpose.log("Unable to add: -[\(`class`).\(selector)] IMP: \(replacementIMP!)") + throw InterposeError.unableToAddMethod(`class`, selector) + } } } } @@ -172,64 +202,24 @@ extension Interpose { override func resetImplementation() throws { _ = try validate(expectedState: .interposed) - guard let dynamicSubclass = self.dynamicSubclass else { preconditionFailure("No dynamic subclass set") } - - // Removing methods at runtime is not supported. - // https://stackoverflow.com/questions/1315169/how-do-i-remove-instance-methods-at-runtime-in-objective-c-2-0 - - // Instead, we have to recreate the whole subclass - // Temporary, to remove objc override - _ = try createDynamicSubclass() - - // Dispose of the custom dynamic subclass - objc_disposeClassPair(dynamicSubclass) - self.dynamicSubclass = nil - - - - // TODO: recreate subclass completely - /* - let previousIMP = class_replaceMethod(dynamicSubclass, selector, _objc_msgForward, method_getTypeEncoding(method)) - guard previousIMP == replacementIMP else { throw InterposeError.unexpectedImplementation(`class`, selector, previousIMP) } - Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") - - // Restore the original class of the object - // Does this include the KVO'ed subclass? - object_setClass(object, `class`) - */ - -// -// // Remove KVO after restoring class as last step. -// deregisterKVO() - } - - - // MARK: KVO Helper - - var kvoObserver: KVOObserver? - - class KVOObserver: NSObject { - @objc var objectToObserve: AnyObject - var observation: NSKeyValueObservation? - - init(object: AnyObject) { - objectToObserve = object - super.init() - - // Can't use modern syntax cause https://bugs.swift.org/browse/SR-12944 - objectToObserve.addObserver(self, forKeyPath: "description", options: .new, context: nil) + if super.origIMP != nil { + try restorePreviousIMP(exactClass: dynamicSubclass!) + } else { + // Removing methods at runtime is not supported. + // https://stackoverflow.com/questions/1315169/how-do-i-remove-instance-methods-at-runtime-in-objective-c-2-0 + // + // This codepath will be hit if the super helper is missing. + // We could recreate the whole class at runtime and rebuild all hooks, + // but that seesm excessive when we have a trampoline at our disposal. + Interpose.log("Reset of -[\(`class`).\(selector)] not supported. No Original IMP") } - } - - // Before creating our subclass, we trigger KVO. - // KVO also creates a subclass at runtime. If we do this prior, then KVO fails. - // If KVO runs prior, and then we sub-subclass, everything works. - private func registerKVO() { - kvoObserver = KVOObserver(object: object) - } - private func deregisterKVO() { - kvoObserver = nil + // TODO: remove class pair! + // This might fail if we get KVO observed. + // objc_disposeClassPair does not return a bool but logs if it fails. + // + // objc_disposeClassPair(dynamicSubclass) + // self.dynamicSubclass = nil } } } diff --git a/Tests/InterposeKitTests/InterposeKitTestCase.swift b/Tests/InterposeKitTests/InterposeKitTestCase.swift new file mode 100644 index 0000000..0b8c064 --- /dev/null +++ b/Tests/InterposeKitTests/InterposeKitTestCase.swift @@ -0,0 +1,36 @@ +import XCTest +@testable import InterposeKit + +class InterposeKitTestCase: XCTestCase { + override func setUpWithError() throws { + Interpose.isLoggingEnabled = true + } +} + +extension InterposeKitTestCase { + // https://www.swiftbysundell.com/articles/testing-error-code-paths-in-swift/ + func assert( + _ expression: @autoclosure () throws -> T, + throws error: E, + in file: StaticString = #file, + line: UInt = #line + ) { + var thrownError: Error? + + XCTAssertThrowsError(try expression(), + file: file, line: line) { + thrownError = $0 + } + + XCTAssertTrue( + thrownError is E, + "Unexpected error type: \(type(of: thrownError))", + file: file, line: line + ) + + XCTAssertEqual( + thrownError as? E, error, + file: file, line: line + ) + } +} diff --git a/Tests/InterposeKitTests/InterposeKitTests.swift b/Tests/InterposeKitTests/InterposeKitTests.swift index d553202..2e7a1a9 100644 --- a/Tests/InterposeKitTests/InterposeKitTests.swift +++ b/Tests/InterposeKitTests/InterposeKitTests.swift @@ -1,7 +1,7 @@ import XCTest @testable import InterposeKit -final class InterposeKitTests: XCTestCase { +final class InterposeKitTests: InterposeKitTestCase { override func setUpWithError() throws { Interpose.isLoggingEnabled = true @@ -24,7 +24,7 @@ final class InterposeKitTests: XCTestCase { let string = store.original(`self`, store.selector) print("After Interposing \(`self`)") - return string + testSwizzleAddition + return string + testString } } } @@ -32,11 +32,11 @@ final class InterposeKitTests: XCTestCase { print(TestClass().sayHi()) // Test various apply/revert's - XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition) + XCTAssertEqual(testObj.sayHi(), testClassHi + testString) try interposer.revert() XCTAssertEqual(testObj.sayHi(), testClassHi) try interposer.apply() - XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition) + XCTAssertEqual(testObj.sayHi(), testClassHi + testString) XCTAssertThrowsError(try interposer.apply()) XCTAssertThrowsError(try interposer.apply()) try interposer.revert() @@ -57,16 +57,16 @@ final class InterposeKitTests: XCTestCase { methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { `self` in - return store.original(`self`, store.selector) + testSwizzleAddition + return store.original(`self`, store.selector) + testString } } } - XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition + testSubclass) + XCTAssertEqual(testObj.sayHi(), testClassHi + testString + testSubclass) try interposed.revert() XCTAssertEqual(testObj.sayHi(), testClassHi + testSubclass) try interposed.apply() - XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition + testSubclass) + XCTAssertEqual(testObj.sayHi(), testClassHi + testString + testSubclass) // Swizzle subclass, automatically applys let interposedSubclass = try Interpose(TestSubclass.self) { @@ -75,14 +75,14 @@ final class InterposeKitTests: XCTestCase { methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { `self` in - return store.original(`self`, store.selector) + testSwizzleAddition + return store.original(`self`, store.selector) + testString } } } - XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition + testSubclass + testSwizzleAddition) + XCTAssertEqual(testObj.sayHi(), testClassHi + testString + testSubclass + testString) try interposed.revert() - XCTAssertEqual(testObj.sayHi(), testClassHi + testSubclass + testSwizzleAddition) + XCTAssertEqual(testObj.sayHi(), testClassHi + testSubclass + testString) try interposedSubclass.revert() XCTAssertEqual(testObj.sayHi(), testClassHi + testSubclass) } diff --git a/Tests/InterposeKitTests/KVOTests.swift b/Tests/InterposeKitTests/KVOTests.swift new file mode 100644 index 0000000..d7704b5 --- /dev/null +++ b/Tests/InterposeKitTests/KVOTests.swift @@ -0,0 +1,67 @@ +import Foundation +import XCTest +@testable import InterposeKit + +final class KVOTests: InterposeKitTestCase { + + // Helper observer that wraps a token and removes it on deinit. + class TestClassObserver { + var kvoToken: NSKeyValueObservation? + var didCallObserver = false + + func observe(obj: TestClass) { + kvoToken = obj.observe(\.age, options: .new) { [weak self] obj, change in + guard let age = change.newValue else { return } + print("New age is: \(age)") + self?.didCallObserver = true + } + } + + deinit { + kvoToken?.invalidate() + } + } + + + func testBasicKVO() throws { + let testObj = TestClass() + + // KVO before hooking works, but hooking will fail + try withExtendedLifetime(TestClassObserver()) { observer in + observer.observe(obj: testObj) + XCTAssertEqual(testObj.age, 1) + testObj.age = 2 + XCTAssertEqual(testObj.age, 2) + // Hooking is expected to fail + assert(try Interpose(testObj), throws: InterposeError.keyValueObservationDetected(testObj)) + XCTAssertEqual(testObj.age, 2) + } + + // Hook without KVO! + let interpose = try Interpose(testObj) { + try $0.hook(#selector(getter: TestClass.age), + methodSignature: (@convention(c) (AnyObject, Selector) -> Int).self, + hookSignature: (@convention(block) (AnyObject) -> Int).self) { + store in { `self` in + return 3 + } + } + } + XCTAssertEqual(testObj.age, 3) + try interpose.revert() + XCTAssertEqual(testObj.age, 2) + try interpose.apply() + XCTAssertEqual(testObj.age, 3) + + // Now we KVO after hooking! + withExtendedLifetime(TestClassObserver()) { observer in + observer.observe(obj: testObj) + XCTAssertEqual(testObj.age, 3) + // Setter is fine but won't change outcome + XCTAssertFalse(observer.didCallObserver) + testObj.age = 4 + XCTAssertTrue(observer.didCallObserver) + XCTAssertEqual(testObj.age, 3) + } + } +} diff --git a/Tests/InterposeKitTests/MultipleInterposing.swift b/Tests/InterposeKitTests/MultipleInterposing.swift index 7938353..6505bbd 100644 --- a/Tests/InterposeKitTests/MultipleInterposing.swift +++ b/Tests/InterposeKitTests/MultipleInterposing.swift @@ -2,47 +2,75 @@ import Foundation import XCTest @testable import InterposeKit -func testInterposeSingleObjectMultipleTimes() throws { - let testObj = TestClass() - let testObj2 = TestClass() +final class MultipleInterposingTests: InterposeKitTestCase { - XCTAssertEqual(testObj.sayHi(), testClassHi) - XCTAssertEqual(testObj2.sayHi(), testClassHi) + func testInterposeSingleObjectMultipleTimes() throws { + let testObj = TestClass() + let testObj2 = TestClass() - // Functions need to be `@objc dynamic` to be hookable. - let interposer = try Interpose(testObj) { - try $0.hook( - #selector(TestClass.sayHi), - methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, - hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { `self` in - return store.original(`self`, store.selector) + testSwizzleAddition - } + XCTAssertEqual(testObj.sayHi(), testClassHi) + XCTAssertEqual(testObj2.sayHi(), testClassHi) + + // Functions need to be `@objc dynamic` to be hookable. + let interposer = try Interpose(testObj) { + try $0.hook( + #selector(TestClass.sayHi), + methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, + hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { `self` in + return store.original(`self`, store.selector) + testString + } + } } - } - XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition) - XCTAssertEqual(testObj2.sayHi(), testClassHi) + XCTAssertEqual(testObj.sayHi(), testClassHi + testString) + XCTAssertEqual(testObj2.sayHi(), testClassHi) - let interposer2 = try Interpose(testObj) { - try $0.hook( + let interposer2 = try testObj.interpose?.hook( #selector(TestClass.sayHi), methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { `self` in - return store.original(`self`, store.selector) + testSwizzleAddition + return store.original(`self`, store.selector) + testString2 } } - } - // TODO: detect existing hook? + // TODO: detect existing hook? - XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition) + XCTAssertEqual(testObj.sayHi(), testClassHi + testString) - try interposer.revert() - try interposer2.revert() -// XCTAssertEqual(testObj.sayHi(), testClassHi) -// XCTAssertEqual(testObj2.sayHi(), testClassHi) -// try interposer.apply() -// XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition) -// XCTAssertEqual(testObj2.sayHi(), testClassHi) + // try interposer.revert() + //try interposer2.revert() + // XCTAssertEqual(testObj.sayHi(), testClassHi) + // XCTAssertEqual(testObj2.sayHi(), testClassHi) + // try interposer.apply() + // XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition) + // XCTAssertEqual(testObj2.sayHi(), testClassHi) + } + + func testInterposeAgeAndRevert() throws { + let testObj = TestClass() + XCTAssertEqual(testObj.age, 1) + + let interpose = try Interpose(testObj) { + try $0.hook(#selector(getter: TestClass.age), + methodSignature: (@convention(c) (AnyObject, Selector) -> Int).self, + hookSignature: (@convention(block) (AnyObject) -> Int).self) { + store in { `self` in + return 3 + } + } + } + XCTAssertEqual(testObj.age, 3) + + try interpose.hook(#selector(getter: TestClass.age), + methodSignature: (@convention(c) (AnyObject, Selector) -> Int).self, + hookSignature: (@convention(block) (AnyObject) -> Int).self) { + store in { `self` in + return 5 + } + }.apply() + XCTAssertEqual(testObj.age, 5) + try interpose.revert() + XCTAssertEqual(testObj.age, 1) + } } diff --git a/Tests/InterposeKitTests/ObjectInterposeTests.swift b/Tests/InterposeKitTests/ObjectInterposeTests.swift index 2ca2c6d..5d7b0a7 100644 --- a/Tests/InterposeKitTests/ObjectInterposeTests.swift +++ b/Tests/InterposeKitTests/ObjectInterposeTests.swift @@ -2,11 +2,7 @@ import Foundation import XCTest @testable import InterposeKit -final class ObjectInterposeTests: XCTestCase { - - override func setUpWithError() throws { - Interpose.isLoggingEnabled = true - } +final class ObjectInterposeTests: InterposeKitTestCase { func testInterposeSingleObject() throws { let testObj = TestClass() @@ -29,19 +25,19 @@ final class ObjectInterposeTests: XCTestCase { print("After Interposing \(`self`)") - return string + testSwizzleAddition + return string + testString // Similar signature cast as above, but without selector. } as @convention(block) (AnyObject) -> String } } - XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition) + XCTAssertEqual(testObj.sayHi(), testClassHi + testString) XCTAssertEqual(testObj2.sayHi(), testClassHi) try interposer.revert() XCTAssertEqual(testObj.sayHi(), testClassHi) XCTAssertEqual(testObj2.sayHi(), testClassHi) try interposer.apply() - XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition) + XCTAssertEqual(testObj.sayHi(), testClassHi + testString) XCTAssertEqual(testObj2.sayHi(), testClassHi) } diff --git a/Tests/InterposeKitTests/TestClass.swift b/Tests/InterposeKitTests/TestClass.swift index 62cacc1..9c403ea 100644 --- a/Tests/InterposeKitTests/TestClass.swift +++ b/Tests/InterposeKitTests/TestClass.swift @@ -2,7 +2,8 @@ import Foundation import QuartzCore let testClassHi = "Hi from TestClass!" -let testSwizzleAddition = " and Interpose" +let testString = " and Interpose" +let testString2 = " testString2" let testSubclass = "Subclass is here!" public func ==(lhs: CATransform3D, rhs: CATransform3D) -> Bool { @@ -23,6 +24,10 @@ public extension CATransform3D { } class TestClass: NSObject { + + @objc dynamic var age: Int = 1 + @objc dynamic var name: String = "Tim Apple" + @objc dynamic func sayHi() -> String { print(testClassHi) return testClassHi From d014741cda54f606ff32667b36a085fd135ec9f2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 14 Jun 2020 15:30:45 +0200 Subject: [PATCH 37/77] lipstick --- Sources/InterposeKit/AnyHook.swift | 2 +- Sources/InterposeKit/ClassHook.swift | 2 -- Sources/InterposeKit/ObjectHook.swift | 4 ++-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Sources/InterposeKit/AnyHook.swift b/Sources/InterposeKit/AnyHook.swift index 7810c11..7a4761b 100644 --- a/Sources/InterposeKit/AnyHook.swift +++ b/Sources/InterposeKit/AnyHook.swift @@ -1,10 +1,10 @@ import Foundation - public class AnyHook { public let `class`: AnyClass public let selector: Selector public internal(set) var state = State.prepared + // else we validate init order public internal(set) var replacementIMP: IMP! diff --git a/Sources/InterposeKit/ClassHook.swift b/Sources/InterposeKit/ClassHook.swift index 6384fed..4a15b36 100644 --- a/Sources/InterposeKit/ClassHook.swift +++ b/Sources/InterposeKit/ClassHook.swift @@ -3,9 +3,7 @@ import Foundation extension Interpose { /// A hook to an instance method and stores both the original and new implementation. final public class ClassHook: TypedHook { - /// Initialize a new hook to interpose an instance method. - // TODO: report compiler crash public init(`class`: AnyClass, selector: Selector, implementation:(ClassHook) -> HookSignature?) /* This must be optional or swift runtime will crash. Or swiftc may segfault. Compiler bug? */ throws { try super.init(class: `class`, selector: selector) replacementIMP = imp_implementationWithBlock(implementation(self) as Any) diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index 313a5d9..7b3d899 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -6,11 +6,11 @@ extension Interpose { static let subclassSuffix = "InterposeKit_" } - internal enum ObjCSelector { + enum ObjCSelector { static let getClass = Selector((("class"))) } - internal enum ObjCMethodEncoding { + enum ObjCMethodEncoding { static let getClass = extract("#@:") private static func extract(_ string: StaticString) -> UnsafePointer { From 53c9371d90e8462600fe94f37cf13be292425504 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 14 Jun 2020 15:36:05 +0200 Subject: [PATCH 38/77] remove test host for CI --- InterposeKit.xcodeproj/project.pbxproj | 7 +------ Tests/InterposeKitTests/InterposeKitTestCase.swift | 3 ++- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/InterposeKit.xcodeproj/project.pbxproj b/InterposeKit.xcodeproj/project.pbxproj index 825b794..19421ba 100644 --- a/InterposeKit.xcodeproj/project.pbxproj +++ b/InterposeKit.xcodeproj/project.pbxproj @@ -207,9 +207,9 @@ 78C39D922483169300B46395 /* InterposeKitTests.swift */, 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */, 78C5A4A72494D75100EE9756 /* MultipleInterposing.swift */, - 78C39D7B2482CC7D00B46395 /* Info-Tests.plist */, 78A2F264249635B100F5AC5F /* KVOTests.swift */, 78A2F26624964AF200F5AC5F /* InterposeKitTestCase.swift */, + 78C39D7B2482CC7D00B46395 /* Info-Tests.plist */, ); path = InterposeTests; sourceTree = ""; @@ -316,7 +316,6 @@ 78C39D762482CC7D00B46395 = { CreatedOnToolsVersion = 11.5; LastSwiftMigration = 1150; - TestTargetID = 781095A4248D6DFB008A943C; }; }; }; @@ -666,7 +665,6 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; @@ -683,7 +681,6 @@ PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/InterposeTestHost.app/InterposeTestHost"; }; name = Debug; }; @@ -691,7 +688,6 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; @@ -707,7 +703,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/InterposeTestHost.app/InterposeTestHost"; }; name = Release; }; diff --git a/Tests/InterposeKitTests/InterposeKitTestCase.swift b/Tests/InterposeKitTests/InterposeKitTestCase.swift index 0b8c064..674fc7f 100644 --- a/Tests/InterposeKitTests/InterposeKitTestCase.swift +++ b/Tests/InterposeKitTests/InterposeKitTestCase.swift @@ -8,13 +8,14 @@ class InterposeKitTestCase: XCTestCase { } extension InterposeKitTestCase { - // https://www.swiftbysundell.com/articles/testing-error-code-paths-in-swift/ + /// Assert that a specific error is thrown. func assert( _ expression: @autoclosure () throws -> T, throws error: E, in file: StaticString = #file, line: UInt = #line ) { + // https://www.swiftbysundell.com/articles/testing-error-code-paths-in-swift/ var thrownError: Error? XCTAssertThrowsError(try expression(), From 3c7e36f5f5f62e1fc64b226ab1ca347a9f107368 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 14 Jun 2020 15:39:02 +0200 Subject: [PATCH 39/77] really remove test host --- InterposeKit.xcodeproj/project.pbxproj | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/InterposeKit.xcodeproj/project.pbxproj b/InterposeKit.xcodeproj/project.pbxproj index 19421ba..34c76ab 100644 --- a/InterposeKit.xcodeproj/project.pbxproj +++ b/InterposeKit.xcodeproj/project.pbxproj @@ -33,13 +33,6 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 781095BA248D6E10008A943C /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 78863EBD2464B2F900BA3762 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 781095A4248D6DFB008A943C; - remoteInfo = InterposeTestHost; - }; 781095F1248E7C72008A943C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 78863EBD2464B2F900BA3762 /* Project object */; @@ -289,7 +282,6 @@ ); dependencies = ( 78C39D7E2482CC7D00B46395 /* PBXTargetDependency */, - 781095BB248D6E10008A943C /* PBXTargetDependency */, ); name = InterposeKitTests; productName = InterposeTests; @@ -406,11 +398,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 781095BB248D6E10008A943C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 781095A4248D6DFB008A943C /* InterposeTestHost */; - targetProxy = 781095BA248D6E10008A943C /* PBXContainerItemProxy */; - }; 781095F2248E7C72008A943C /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 78863EC52464B2F900BA3762 /* InterposeKit */; From 5faede9f84087604670ab82a37a76453aafd0e9c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 14 Jun 2020 15:47:18 +0200 Subject: [PATCH 40/77] use error log --- .../InterposeExampleTests.swift | 1 - InterposeTestHost/ViewController.swift | 3 --- Sources/InterposeKit/ObjectHook.swift | 14 +++++++------- Tests/InterposeKitTests/TestClass.swift | 1 + 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/Example/InterposeExampleTests/InterposeExampleTests.swift b/Example/InterposeExampleTests/InterposeExampleTests.swift index 9b43cb7..4422873 100644 --- a/Example/InterposeExampleTests/InterposeExampleTests.swift +++ b/Example/InterposeExampleTests/InterposeExampleTests.swift @@ -3,7 +3,6 @@ // InterposeExampleTests // // Created by Peter Steinberger on 30.05.20. -// Copyright © 2020 PSPDFKit GmbH. All rights reserved. // import XCTest diff --git a/InterposeTestHost/ViewController.swift b/InterposeTestHost/ViewController.swift index 9bfb8ea..580d46e 100644 --- a/InterposeTestHost/ViewController.swift +++ b/InterposeTestHost/ViewController.swift @@ -3,7 +3,6 @@ // InterposeTestHost // // Created by Peter Steinberger on 07.06.20. -// Copyright © 2020 PSPDFKit GmbH. All rights reserved. // import UIKit @@ -12,9 +11,7 @@ class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - // Do any additional setup after loading the view. } - } diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index 7b3d899..0b23b8a 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -91,16 +91,16 @@ extension Interpose { _ = class_replaceMethod(object_getClass(`class`), ObjCSelector.getClass, impl, ObjCMethodEncoding.getClass) } - private lazy var addSuperImpl: @convention(c) (AnyClass, Selector) -> Bool = { + private lazy var addSuperImpl: @convention(c) (AnyClass, Selector, NSErrorPointer) -> Bool = { let handle = dlopen(nil, RTLD_LAZY) let imp = dlsym(handle, "IKTAddSuperImplementationToClass") - return unsafeBitCast(imp, to: (@convention(c) (AnyClass, Selector) -> Bool).self) + return unsafeBitCast(imp, to: (@convention(c) (AnyClass, Selector, NSErrorPointer) -> Bool).self) }() private func addSuperTrampolineMethod(subclass: AnyClass) { - if addSuperImpl(subclass, self.selector) == false { - // TODO: use error log! - Interpose.log("Failed to add super implementation to -[\(`class`).\(selector)]") + var error: NSError? + if addSuperImpl(subclass, self.selector, &error) == false { + Interpose.log("Failed to add super implementation to -[\(`class`).\(selector)]: \(error!)") } else { let imp = class_getMethodImplementation(subclass, self.selector)! Interpose.log("Added super for -[\(`class`).\(selector)]: \(imp)") @@ -139,8 +139,8 @@ extension Interpose { var methodCount : CUnsignedInt = 0 guard let methodsInAClass = class_copyMethodList(klass, &methodCount) else { return false } defer { free(methodsInAClass) } - for i in 0 ..< Int(methodCount) { - let method = methodsInAClass[i] + for index in 0 ..< Int(methodCount) { + let method = methodsInAClass[index] if method_getName(method) == selector { return true } diff --git a/Tests/InterposeKitTests/TestClass.swift b/Tests/InterposeKitTests/TestClass.swift index 9c403ea..6130efc 100644 --- a/Tests/InterposeKitTests/TestClass.swift +++ b/Tests/InterposeKitTests/TestClass.swift @@ -14,6 +14,7 @@ extension CATransform3D: Equatable { } public extension CATransform3D { + // swiftlint:disable:next identifier_name func translated(x: CGFloat = 0, y: CGFloat = 0, z: CGFloat = 0) -> CATransform3D { return CATransform3DTranslate(self, x, y, z) } From 1695fc7751d27adf8933b95b0e11f37927d70086 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 14 Jun 2020 15:51:29 +0200 Subject: [PATCH 41/77] Disable code signing --- InterposeKit.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/InterposeKit.xcodeproj/project.pbxproj b/InterposeKit.xcodeproj/project.pbxproj index 34c76ab..e67f772 100644 --- a/InterposeKit.xcodeproj/project.pbxproj +++ b/InterposeKit.xcodeproj/project.pbxproj @@ -654,9 +654,9 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = Y5PE65HELJ; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/InterposeKit.xcodeproj/Info-Tests.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -677,9 +677,9 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = Y5PE65HELJ; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/InterposeKit.xcodeproj/Info-Tests.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", From 9c9d0d0bac094cd2f11381eafe566e68c977eb61 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 14 Jun 2020 15:51:33 +0200 Subject: [PATCH 42/77] todo --- Sources/InterposeKit/ObjectHook.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index 0b23b8a..83588e2 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -214,7 +214,7 @@ extension Interpose { Interpose.log("Reset of -[\(`class`).\(selector)] not supported. No Original IMP") } - // TODO: remove class pair! + // FUTURE: remove class pair! // This might fail if we get KVO observed. // objc_disposeClassPair does not return a bool but logs if it fails. // From ef94e2f070e3f5e74779a7d60afd90ca233a5f17 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 14 Jun 2020 17:32:27 +0200 Subject: [PATCH 43/77] Warn instead of error for unknown architectures --- Sources/ITKAddSuperMethod/ITKAddSuperMethod.h | 14 +++++- Sources/ITKAddSuperMethod/ITKAddSuperMethod.m | 45 ++++++++++++++++--- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h index 5504a99..a1abf46 100644 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h @@ -1,6 +1,6 @@ // // ITKAddSuperMethod.h -// InterposeKit +// SuperBuilder // // Created by Peter Steinberger on 08.06.20. // @@ -12,6 +12,7 @@ NS_ASSUME_NONNULL_BEGIN NSString *const SuperBuilderErrorDomain; typedef NS_ERROR_ENUM(SuperBuilderErrorDomain, SuperBuilderErrorCode) { + SuperBuilderErrorCodeArchitectureNotSupported, SuperBuilderErrorCodeNoSuperClass, SuperBuilderErrorCodeNoDynamicallyDispatchedMethodAvailable, SuperBuilderErrorCodeFailedToAddMethod @@ -61,6 +62,17 @@ typedef NS_ERROR_ENUM(SuperBuilderErrorDomain, SuperBuilderErrorCode) { */ + (BOOL)addSuperInstanceMethodToClass:(Class)originalClass selector:(SEL)selector error:(NSError **)error; +/// Check if the instance method in `originalClass` is a super trampoline. ++ (BOOL)isSuperTrampolineForClass:(Class)originalClass selector:(SEL)selector; + +/// x86-64 and ARM64 are currently supported. +@property(class, readonly) BOOL isSupportedArchitecure; + +#if defined (__arm64__) || defined (__x86_64__) +/// Helper that does not exist if architecture is not supported. ++ (BOOL)isCompileTimeSupportedArchitecure; +#endif + @end diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m index ed7f806..dce0494 100644 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m @@ -26,7 +26,9 @@ static IMP ITKGetTrampolineForTypeEncoding(__unused const char *typeEncoding) { NSGetSizeAndAlignment(typeEncoding, &returnTypeActualSize, NULL); requiresStructDispatch = returnTypeActualSize > (sizeof(void *) * 2); #else - #error - Unknown architecture + // Unknown architecture + // https://devblogs.microsoft.com/xamarin/apple-new-processor-architecture/ + // watchOS uses arm64_32 since series 4, before armv7k. watch Simulator uses i386. #endif return requiresStructDispatch ? msgSendSuperStretTrampoline : msgSendSuperTrampoline; @@ -43,17 +45,47 @@ BOOL IKTAddSuperImplementationToClass(Class originalClass, SEL selector, NSError @implementation SuperBuilder ++ (BOOL)isSupportedArchitecure { +#if defined (__arm64__) || defined (__x86_64__) + return YES; +#else + return NO; +#endif +} + +#if defined (__arm64__) || defined (__x86_64__) ++ (BOOL)isCompileTimeSupportedArchitecure { + return [self isSupportedArchitecure]; +} +#endif + ++ (BOOL)isSuperTrampolineForClass:(Class)originalClass selector:(SEL)selector { + // No architecture check needed - will just be NO. + let method = class_getInstanceMethod(originalClass, selector); + return ITKMethodIsSuperTrampoline(method); +} + + (BOOL)addSuperInstanceMethodToClass:(Class)originalClass selector:(SEL)selector error:(NSError **)error { + if (!self.isSupportedArchitecure) { + let msg = @"Unsupported Architecture. (Support includes ARM64 and x86-64 )"; + ERROR_AND_RETURN(SuperBuilderErrorCodeArchitectureNotSupported, msg) + } + + // Check that class has a superclass let superClass = class_getSuperclass(originalClass); if (superClass == nil) { let msg = [NSString stringWithFormat:@"Unable to find superclass for %@", NSStringFromClass(originalClass)]; ERROR_AND_RETURN(SuperBuilderErrorCodeNoSuperClass, msg) } + + // Fetch method called with super let method = class_getInstanceMethod(superClass, selector); if (method == NULL) { let msg = [NSString stringWithFormat:@"No dynamically dispatched method with selector %@ is available on any of the superclasses of %@", NSStringFromSelector(selector), NSStringFromClass(originalClass)]; ERROR_AND_RETURN(SuperBuilderErrorCodeNoDynamicallyDispatchedMethodAvailable, msg) } + + // Add trampoline let typeEncoding = method_getTypeEncoding(method); let trampoline = ITKGetTrampolineForTypeEncoding(typeEncoding); let methodAdded = class_addMethod(originalClass, selector, trampoline, typeEncoding); @@ -64,12 +96,11 @@ + (BOOL)addSuperInstanceMethodToClass:(Class)originalClass selector:(SEL)selecto return methodAdded; } -@end - // Control if the trampoline should also push/pop the floating point registers. // This is slightly slower and not needed for our simple implementation // However, even if you just use memcpy, you will want to enable this. -#define PROTECT_FLOATING_POINT_REGISTERS 0 +// We keep this enabled to be doubly safe. +#define PROTECT_FLOATING_POINT_REGISTERS 1 // One thread local per thread should be enough _Thread_local struct objc_super _threadSuperStorage; @@ -113,6 +144,8 @@ static BOOL ITKMethodIsSuperTrampoline(Method method) { return _super; } +@end + /** Inline assembly is used to perfectly forward all parameters to objc_msgSendSuper, while also looking up the target on-the-fly. @@ -304,7 +337,9 @@ asm volatile ( } #else -#error - Unknown architecture - time to write some assembly :) +// Unknown architecture - time to write some assembly :) +void msgSendSuperTrampoline(void) {} +void msgSendSuperStretTrampoline(void) {} #endif NS_ASSUME_NONNULL_END From ab78db4b32185f253443a65573ac182c283c6d1f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 14 Jun 2020 17:32:41 +0200 Subject: [PATCH 44/77] Touched by Xcode 11.6 --- Example/InterposeExample.xcodeproj/project.pbxproj | 10 +--------- .../xcshareddata/xcschemes/InterposeExample.xcscheme | 2 +- .../xcshareddata/xcschemes/InterposeKit.xcscheme | 2 +- .../xcshareddata/xcschemes/InterposeTests.xcscheme | 2 +- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/Example/InterposeExample.xcodeproj/project.pbxproj b/Example/InterposeExample.xcodeproj/project.pbxproj index ce78145..b4c355e 100644 --- a/Example/InterposeExample.xcodeproj/project.pbxproj +++ b/Example/InterposeExample.xcodeproj/project.pbxproj @@ -15,10 +15,6 @@ 7880B12D248280B500AD2251 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7880B12C248280B500AD2251 /* Assets.xcassets */; }; 7880B130248280B500AD2251 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7880B12E248280B500AD2251 /* LaunchScreen.storyboard */; }; 78C39DDC2483363300B46395 /* InterposeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 78C39DDB2483363300B46395 /* InterposeKit */; }; - 78C39DE22483366B00B46395 /* Defaults-Release.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 78C39DDE2483366B00B46395 /* Defaults-Release.xcconfig */; }; - 78C39DE32483366B00B46395 /* Defaults-Testing.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 78C39DDF2483366B00B46395 /* Defaults-Testing.xcconfig */; }; - 78C39DE42483366B00B46395 /* Defaults.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 78C39DE02483366B00B46395 /* Defaults.xcconfig */; }; - 78C39DE52483366B00B46395 /* Defaults-Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 78C39DE12483366B00B46395 /* Defaults-Debug.xcconfig */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -183,7 +179,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1150; - LastUpgradeCheck = 1150; + LastUpgradeCheck = 1160; ORGANIZATIONNAME = "PSPDFKit GmbH"; TargetAttributes = { 7880B11F248280B300AD2251 = { @@ -223,10 +219,6 @@ files = ( 7880B130248280B500AD2251 /* LaunchScreen.storyboard in Resources */, 7880B12D248280B500AD2251 /* Assets.xcassets in Resources */, - 78C39DE22483366B00B46395 /* Defaults-Release.xcconfig in Resources */, - 78C39DE52483366B00B46395 /* Defaults-Debug.xcconfig in Resources */, - 78C39DE32483366B00B46395 /* Defaults-Testing.xcconfig in Resources */, - 78C39DE42483366B00B46395 /* Defaults.xcconfig in Resources */, 7880B12B248280B300AD2251 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Example/InterposeExample.xcodeproj/xcshareddata/xcschemes/InterposeExample.xcscheme b/Example/InterposeExample.xcodeproj/xcshareddata/xcschemes/InterposeExample.xcscheme index a7c33d9..c579df2 100644 --- a/Example/InterposeExample.xcodeproj/xcshareddata/xcschemes/InterposeExample.xcscheme +++ b/Example/InterposeExample.xcodeproj/xcshareddata/xcschemes/InterposeExample.xcscheme @@ -1,6 +1,6 @@ Date: Sun, 14 Jun 2020 17:38:08 +0200 Subject: [PATCH 45/77] Addd ILP32 link --- Sources/ITKAddSuperMethod/ITKAddSuperMethod.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m index dce0494..2966d76 100644 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m +++ b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m @@ -29,6 +29,7 @@ static IMP ITKGetTrampolineForTypeEncoding(__unused const char *typeEncoding) { // Unknown architecture // https://devblogs.microsoft.com/xamarin/apple-new-processor-architecture/ // watchOS uses arm64_32 since series 4, before armv7k. watch Simulator uses i386. + // See ILP32: http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dai0490a/ar01s01.html #endif return requiresStructDispatch ? msgSendSuperStretTrampoline : msgSendSuperTrampoline; From 0d20b61a8719bcea20a6951be9139a1b5823bd05 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 14 Jun 2020 17:38:12 +0200 Subject: [PATCH 46/77] disable code signing --- InterposeKit.xcodeproj/project.pbxproj | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/InterposeKit.xcodeproj/project.pbxproj b/InterposeKit.xcodeproj/project.pbxproj index e67f772..736c49a 100644 --- a/InterposeKit.xcodeproj/project.pbxproj +++ b/InterposeKit.xcodeproj/project.pbxproj @@ -606,9 +606,10 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = Y5PE65HELJ; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -629,9 +630,10 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = Y5PE65HELJ; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; From f9600720edd43f6b5fc749bf249104a7fbb76e70 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 14 Jun 2020 19:14:33 +0200 Subject: [PATCH 47/77] Support removal of multiple hooks --- InterposeKit.xcodeproj/project.pbxproj | 36 +++---- Package.swift | 7 +- README.md | 40 ++++---- Sources/ITKAddSuperMethod/ITKAddSuperMethod.h | 80 --------------- Sources/InterposeKit/AnyHook.swift | 9 -- Sources/InterposeKit/ClassHook.swift | 6 +- Sources/InterposeKit/InterposeKit.swift | 12 ++- Sources/InterposeKit/ObjectHook.swift | 98 ++++++++++++++++--- Sources/SuperBuilder/ITKSuperBuilder.h | 73 ++++++++++++++ .../ITKSuperBuilder.m} | 2 +- .../MultipleInterposing.swift | 19 +--- 11 files changed, 220 insertions(+), 162 deletions(-) delete mode 100644 Sources/ITKAddSuperMethod/ITKAddSuperMethod.h create mode 100644 Sources/SuperBuilder/ITKSuperBuilder.h rename Sources/{ITKAddSuperMethod/ITKAddSuperMethod.m => SuperBuilder/ITKSuperBuilder.m} (99%) diff --git a/InterposeKit.xcodeproj/project.pbxproj b/InterposeKit.xcodeproj/project.pbxproj index 736c49a..c3cefa5 100644 --- a/InterposeKit.xcodeproj/project.pbxproj +++ b/InterposeKit.xcodeproj/project.pbxproj @@ -16,10 +16,10 @@ 781095B4248D6DFD008A943C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 781095B2248D6DFD008A943C /* LaunchScreen.storyboard */; }; 781095F5248E7C91008A943C /* InterposeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78863EC62464B2F900BA3762 /* InterposeKit.framework */; }; 781095F6248E7C91008A943C /* InterposeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 78863EC62464B2F900BA3762 /* InterposeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 781095FF248E83D7008A943C /* ITKAddSuperMethod.h in Headers */ = {isa = PBXBuildFile; fileRef = 781095FD248E83D7008A943C /* ITKAddSuperMethod.h */; }; - 78109600248E83D7008A943C /* ITKAddSuperMethod.m in Sources */ = {isa = PBXBuildFile; fileRef = 781095FE248E83D7008A943C /* ITKAddSuperMethod.m */; }; 78A2F265249635B100F5AC5F /* KVOTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F264249635B100F5AC5F /* KVOTests.swift */; }; 78A2F26724964AF200F5AC5F /* InterposeKitTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F26624964AF200F5AC5F /* InterposeKitTestCase.swift */; }; + 78A2F26B24967DB500F5AC5F /* ITKSuperBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F26924967DB500F5AC5F /* ITKSuperBuilder.m */; }; + 78A2F26C24967DB500F5AC5F /* ITKSuperBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 78A2F26A24967DB500F5AC5F /* ITKSuperBuilder.h */; }; 78C39D7C2482CC7D00B46395 /* InterposeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78863EC62464B2F900BA3762 /* InterposeKit.framework */; }; 78C39D8F2483164500B46395 /* InterposeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 78C39D8E2483164500B46395 /* InterposeKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 78C39D912483165600B46395 /* InterposeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C39D902483165600B46395 /* InterposeKit.swift */; }; @@ -74,12 +74,12 @@ 781095B3248D6DFD008A943C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 781095B5248D6DFD008A943C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 781095B9248D6E0A008A943C /* InterposeTestHost.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = InterposeTestHost.entitlements; sourceTree = ""; }; - 781095FD248E83D7008A943C /* ITKAddSuperMethod.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ITKAddSuperMethod.h; sourceTree = ""; }; - 781095FE248E83D7008A943C /* ITKAddSuperMethod.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ITKAddSuperMethod.m; sourceTree = ""; }; 78863EC62464B2F900BA3762 /* InterposeKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = InterposeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 78863ECA2464B2F900BA3762 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = Info.plist; path = InterposeKit.xcodeproj/Info.plist; sourceTree = ""; }; 78A2F264249635B100F5AC5F /* KVOTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = KVOTests.swift; path = Tests/InterposeKitTests/KVOTests.swift; sourceTree = SOURCE_ROOT; }; 78A2F26624964AF200F5AC5F /* InterposeKitTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InterposeKitTestCase.swift; path = Tests/InterposeKitTests/InterposeKitTestCase.swift; sourceTree = SOURCE_ROOT; }; + 78A2F26924967DB500F5AC5F /* ITKSuperBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ITKSuperBuilder.m; sourceTree = ""; }; + 78A2F26A24967DB500F5AC5F /* ITKSuperBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ITKSuperBuilder.h; sourceTree = ""; }; 78C39D772482CC7D00B46395 /* InterposeKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InterposeKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 78C39D7B2482CC7D00B46395 /* Info-Tests.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = "Info-Tests.plist"; path = "InterposeKit.xcodeproj/Info-Tests.plist"; sourceTree = ""; }; 78C39D8E2483164500B46395 /* InterposeKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = InterposeKit.h; path = Sources/InterposeKit/InterposeKit.h; sourceTree = SOURCE_ROOT; }; @@ -145,20 +145,10 @@ name = Frameworks; sourceTree = ""; }; - 781095FC248E83D7008A943C /* ITKAddSuperMethod */ = { - isa = PBXGroup; - children = ( - 781095FD248E83D7008A943C /* ITKAddSuperMethod.h */, - 781095FE248E83D7008A943C /* ITKAddSuperMethod.m */, - ); - name = ITKAddSuperMethod; - path = Sources/ITKAddSuperMethod; - sourceTree = ""; - }; 78863EBC2464B2F900BA3762 = { isa = PBXGroup; children = ( - 781095FC248E83D7008A943C /* ITKAddSuperMethod */, + 78A2F26824967DB500F5AC5F /* SuperBuilder */, 78863EC82464B2F900BA3762 /* InterposeKit */, 78C39D782482CC7D00B46395 /* InterposeTests */, 78C39DBD248317B400B46395 /* Configuration */, @@ -183,16 +173,26 @@ children = ( 78863ECA2464B2F900BA3762 /* Info.plist */, 78C39D8E2483164500B46395 /* InterposeKit.h */, + 78C39D902483165600B46395 /* InterposeKit.swift */, 78EDB8DC248BAA5600D2F6C1 /* AnyHook.swift */, 7810959D248D43DC008A943C /* ClassHook.swift */, 78EDB8FE248D0A9900D2F6C1 /* ObjectHook.swift */, - 78C39D902483165600B46395 /* InterposeKit.swift */, 78EDB902248D42CD00D2F6C1 /* LinuxCompileSupport.swift */, 7810959F248D50C1008A943C /* Watcher.swift */, ); path = InterposeKit; sourceTree = ""; }; + 78A2F26824967DB500F5AC5F /* SuperBuilder */ = { + isa = PBXGroup; + children = ( + 78A2F26924967DB500F5AC5F /* ITKSuperBuilder.m */, + 78A2F26A24967DB500F5AC5F /* ITKSuperBuilder.h */, + ); + name = SuperBuilder; + path = Sources/SuperBuilder; + sourceTree = ""; + }; 78C39D782482CC7D00B46395 /* InterposeTests */ = { isa = PBXGroup; children = ( @@ -225,8 +225,8 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 781095FF248E83D7008A943C /* ITKAddSuperMethod.h in Headers */, 78C39D8F2483164500B46395 /* InterposeKit.h in Headers */, + 78A2F26C24967DB500F5AC5F /* ITKSuperBuilder.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -372,8 +372,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 78109600248E83D7008A943C /* ITKAddSuperMethod.m in Sources */, 781095A0248D50C1008A943C /* Watcher.swift in Sources */, + 78A2F26B24967DB500F5AC5F /* ITKSuperBuilder.m in Sources */, 7810959E248D43DC008A943C /* ClassHook.swift in Sources */, 78C39D912483165600B46395 /* InterposeKit.swift in Sources */, 78EDB8FF248D0A9900D2F6C1 /* ObjectHook.swift in Sources */, diff --git a/Package.swift b/Package.swift index 81dae69..5508959 100644 --- a/Package.swift +++ b/Package.swift @@ -1,5 +1,4 @@ // swift-tools-version:5.0 - import PackageDescription let package = Package( @@ -11,11 +10,11 @@ let package = Package( .watchOS(.v5) ], products: [ - .library(name: "InterposeKit",targets: ["InterposeKit"]), + .library(name: "InterposeKit", targets: ["InterposeKit"]), ], targets: [ - .target(name: "ITKAddSuperMethod"), - .target(name: "InterposeKit", dependencies: ["ITKAddSuperMethod"]), + .target(name: "SuperBuilder"), + .target(name: "InterposeKit", dependencies: ["SuperBuilder"]), .testTarget(name: "InterposeKitTests", dependencies: ["InterposeKit"]), ] ) diff --git a/README.md b/README.md index ed89b32..b9b4781 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ -InterposeKit is a modern library to swizzle elegantly in Swift. It is [well-documented](http://interposekit.com/), [tested](https://github.com/jsteipete/InterposeKit/actions?query=workflow%3ASwiftPM), written in "pure" Swift 5.2 and works on `@objc dynamic` Swift functions or Objective-C instance methods. The Inspiration for InterposeKit was [a race condition in Mac Catalyst](https://steipete.com/posts/mac-catalyst-crash-hunt/), which required tricky swizzling to fix, I also wrote up [implementation thoughts on my blog](https://steipete.com/posts/interposekit/). +InterposeKit is a modern library to swizzle elegantly in Swift, supporting hooks on classes and individual objects. It is [well-documented](http://interposekit.com/), [tested](https://github.com/jsteipete/InterposeKit/actions?query=workflow%3ASwiftPM), written in "pure" Swift 5.2 and works on `@objc dynamic` Swift functions or Objective-C instance methods. The Inspiration for InterposeKit was [a race condition in Mac Catalyst](https://steipete.com/posts/mac-catalyst-crash-hunt/), which required tricky swizzling to fix, I also wrote up [implementation thoughts on my blog](https://steipete.com/posts/interposekit/). 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/). @@ -32,21 +32,20 @@ class TestClass: NSObject { } let interposer = try Interpose(TestClass.self) { - try $0.hook(#selector(TestClass.sayHi), { store in { `self` in - - print("Before Interposing \(`self`)") - - // Calling convention and passing selector is important! - // You're free to skip calling the original implementation. - let origCall = store((@convention(c) (AnyObject, Selector) -> String).self) - let string = origCall(`self`, store.selector) - - print("After Interposing \(`self`)") - - return string + " and Interpose" - - // Similar signature cast as above, but without selector. - } as @convention(block) (AnyObject) -> String}) + try $0.hook( + #selector(TestClass.sayHi), + methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, + hookSignature: (@convention(block) (AnyObject) -> String).self) { + store in { `self` in + + // You're free to skip calling the original implementation. + print("Before Interposing \(`self`)") + let string = store.original(`self`, store.selector) + print("After Interposing \(`self`)") + + return string + "and Interpose" + } + } } // Don't need the hook anymore? Undo is built-in! @@ -62,13 +61,14 @@ After Interposing Hi there 👋 and Interpose ``` -## Key Facts +## Key Features -- Interpose directly modifies the implementation of a `Method`, which is [better than selector-based swizzling]((https://pspdfkit.com/blog/2019/swizzling-in-swift/)). +- Interpose directly modifies the implementation of a `Method`, which is [safer than selector-based swizzling]((https://pspdfkit.com/blog/2019/swizzling-in-swift/)). +- Interpose works on classes and individual objects. - Hooks can easily be undone via calling `revert()`. This also checks and errors if someone else changed stuff in between. -- Pure Swift, no `NSInvocation`, which requires boxing and can be slow. +- Mostly 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. +- Yes, you have to type the resulting type twice This is a tradeoff, else we need `NSInvocation`. - Delayed Interposing helps when a class is loaded at runtime. This is useful for [Mac Catalyst](https://steipete.com/posts/mac-catalyst-crash-hunt/). ## Delayed Hooking diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h b/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h deleted file mode 100644 index a1abf46..0000000 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.h +++ /dev/null @@ -1,80 +0,0 @@ -// -// ITKAddSuperMethod.h -// SuperBuilder -// -// Created by Peter Steinberger on 08.06.20. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -NSString *const SuperBuilderErrorDomain; - -typedef NS_ERROR_ENUM(SuperBuilderErrorDomain, SuperBuilderErrorCode) { - SuperBuilderErrorCodeArchitectureNotSupported, - SuperBuilderErrorCodeNoSuperClass, - SuperBuilderErrorCodeNoDynamicallyDispatchedMethodAvailable, - SuperBuilderErrorCodeFailedToAddMethod -}; - -@interface SuperBuilder : NSObject - -/** - Adds an empty super implementation instance method to originalClass. - If a method already exists, this will return NO and a descriptive error message. - - Example: You have an empty UIViewController subclass and call this with viewDidLoad as selector. - The result will be code that looks similar to this: - - override func viewDidLoad() { - super.viewDidLoad() - } - - What the compiler creates in following code: - - - (void)viewDidLoad { - struct objc_super _super = { - .receiver = self, - .super_class = object_getClass(obj); - }; - objc_msgSendSuper2(&_super, _cmd); - } - - There are a few important details: - - 1) We use objc_msgSendSuper2, not objc_msgSendSuper. - The difference is minor, but important. - objc_msgSendSuper starts looking at the current class, which would cause an endless loop - objc_msgSendSuper2 looks for the superclass. - - 2) This uses a completely dynamic lookup. - While slightly slower, this is resilient even if you change superclasses later on. - - 3) The resolution method calls out to C, so it could be customized to jump over specific implementations. - (Such API is not currently exposed) - - 4) This uses inline assembly to forward the parameters to objc_msgSendSuper2 and objc_msgSendSuper2_stret. - This is currently implemented architectures are x86_64 and arm64. - armv7 was dropped in OS 11 and i386 with macOS Catalina. - - @see https://steipete.com/posts/calling-super-at-runtime/ - */ -+ (BOOL)addSuperInstanceMethodToClass:(Class)originalClass selector:(SEL)selector error:(NSError **)error; - -/// Check if the instance method in `originalClass` is a super trampoline. -+ (BOOL)isSuperTrampolineForClass:(Class)originalClass selector:(SEL)selector; - -/// x86-64 and ARM64 are currently supported. -@property(class, readonly) BOOL isSupportedArchitecure; - -#if defined (__arm64__) || defined (__x86_64__) -/// Helper that does not exist if architecture is not supported. -+ (BOOL)isCompileTimeSupportedArchitecure; -#endif - -@end - - - -NS_ASSUME_NONNULL_END diff --git a/Sources/InterposeKit/AnyHook.swift b/Sources/InterposeKit/AnyHook.swift index 7a4761b..0ad7b2d 100644 --- a/Sources/InterposeKit/AnyHook.swift +++ b/Sources/InterposeKit/AnyHook.swift @@ -82,15 +82,6 @@ public class AnyHook { Interpose.log("Leaking -[\(`class`).\(selector)] IMP: \(replacementIMP!) due to error: \(error)") } } - - /// Internal: Restores the previous implementation if one is set. - func restorePreviousIMP(exactClass: AnyClass) throws { - let method = try validate(expectedState: .interposed) - precondition(origIMP != nil) - let previousIMP = class_replaceMethod(exactClass, selector, origIMP!, method_getTypeEncoding(method)) - guard previousIMP == replacementIMP else { throw InterposeError.unexpectedImplementation(exactClass, selector, previousIMP) } - Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") - } } public class TypedHook: AnyHook { diff --git a/Sources/InterposeKit/ClassHook.swift b/Sources/InterposeKit/ClassHook.swift index 4a15b36..53cde26 100644 --- a/Sources/InterposeKit/ClassHook.swift +++ b/Sources/InterposeKit/ClassHook.swift @@ -17,7 +17,11 @@ extension Interpose { } override func resetImplementation() throws { - try restorePreviousIMP(exactClass: `class`) + let method = try validate(expectedState: .interposed) + precondition(origIMP != nil) + let previousIMP = class_replaceMethod(`class`, selector, origIMP!, method_getTypeEncoding(method)) + guard previousIMP == replacementIMP else { throw InterposeError.unexpectedImplementation(`class`, selector, previousIMP) } + Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") } /// The original implementation is cached at hook time. diff --git a/Sources/InterposeKit/InterposeKit.swift b/Sources/InterposeKit/InterposeKit.swift index fd7d5cb..2a22ea1 100644 --- a/Sources/InterposeKit/InterposeKit.swift +++ b/Sources/InterposeKit/InterposeKit.swift @@ -1,7 +1,5 @@ import Foundation -private var interposeKey: Character = "_" - struct AssociatedKeys { static var interposeObject: UInt8 = 0 } @@ -168,6 +166,12 @@ public enum InterposeError: LocalizedError { /// Can't revert or apply if already done so. case invalidState(expectedState: AnyHook.State) + + /// Unable to remove hook. + case resetUnsupported(_ reason: String) + + /// Generic failure + case unknownError(_ reason: String) } extension InterposeError: Equatable { @@ -194,6 +198,10 @@ extension InterposeError: Equatable { return "Unable to hook object posing as different class. Expected: \(type(of: obj)) Is: \(NSStringFromClass(actualClass))/" case .invalidState(let expectedState): return "Invalid State. Expected: \(expectedState)" + case .resetUnsupported(let reason): + return "Reset Unsupported: \(reason)" + case .unknownError(let reason): + return reason } } diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index 83588e2..bb3a35d 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -18,6 +18,21 @@ extension Interpose { } } + struct AssociatedKeys { + static var hookForBlock: UInt8 = 0 + } + + public class WeakObjectContainer: NSObject { + private weak var _object: T? + + public var object: T? { + return _object + } + public init(with object: T?) { + _object = object + } + } + /// A hook to an instance method of a single object, stores both the original and new implementation. /// Think about: Multiple hooks for one object final public class ObjectHook: TypedHook { @@ -26,13 +41,28 @@ extension Interpose { var dynamicSubclass: AnyClass? // Logic switch to use super builder - let generatesSuperIMP = true + let generatesSuperIMP = NSClassFromString("SuperBuilder")?.value(forKey: "isSupportedArchitecure") as? Bool ?? false /// Initialize a new hook to interpose an instance method. public init(object: AnyObject, selector: Selector, implementation:(ObjectHook) -> HookSignature?) throws { self.object = object try super.init(class: type(of: object), selector: selector) - replacementIMP = imp_implementationWithBlock(implementation(self) as Any) + let block = implementation(self) as AnyObject + replacementIMP = imp_implementationWithBlock(block) + guard replacementIMP != nil else { + throw InterposeError.unknownError("imp_implementationWithBlock failed for \(block) - slots exceeded?") + } + + // Weakly store reference to hook inside the block of the IMP. + objc_setAssociatedObject(block, &AssociatedKeys.hookForBlock, WeakObjectContainer(with: self), .OBJC_ASSOCIATION_RETAIN) + } + + // Finds the hook to a given implementation. + private func hookForIMP(_ imp: IMP) -> ObjectHook? { + // Get the block that backs our IMP replacement + guard let block = imp_getBlock(imp) else { return nil } + let container = objc_getAssociatedObject(block, &AssociatedKeys.hookForBlock) as? WeakObjectContainer> + return container?.object } // /// Release the hook block if possible. @@ -41,8 +71,17 @@ extension Interpose { // super.cleanup() // } + /// We need to reuse a dynamic subclass if the object already has one. + private func getExistingSubclass() -> AnyClass? { + let actualClass: AnyClass = object_getClass(object)! + if NSStringFromClass(actualClass).hasPrefix(Constants.subclassSuffix) { + return actualClass + } + return nil + } + /// Creates a unique dynamic subclass of the current object - private func createDynamicSubclass() throws -> AnyClass { + private func createSubclass() throws -> AnyClass { // If the class has been altered (e.g. via NSKVONotifying_ KVO logic) // then perceived and actual class don't match. @@ -151,21 +190,20 @@ extension Interpose { override func replaceImplementation() throws { let method = try validate() - // Register subclass at runtime if we haven't already - if dynamicSubclass == nil { - dynamicSubclass = try createDynamicSubclass() - } + // Check if there's an existing subclass we can reuse. + // Create one at runtime if there is none. + dynamicSubclass = try getExistingSubclass() ?? createSubclass() + // The implementation of the call that is hooked must exist. guard lookupOrigIMP != nil else { throw InterposeError.nonExistingImplementation(`class`, selector).log() } - let encoding = method_getTypeEncoding(method) // This function searches superclasses for implementations let hasExistingMethod = exactClassImplementsSelector(dynamicSubclass!, selector) + let encoding = method_getTypeEncoding(method) if self.generatesSuperIMP { - // If the subclass is empty, we create a super trampoline first. // If a hook already exists, we must skip this. if !hasExistingMethod { @@ -179,6 +217,7 @@ extension Interpose { } Interpose.log("Added -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") } else { + // Could potentially be unified in the code paths if hasExistingMethod { origIMP = class_replaceMethod(dynamicSubclass!, selector, replacementIMP, encoding) if origIMP != nil { @@ -199,12 +238,29 @@ extension Interpose { } } + // Find the hook above us (not necessarily topmost) + private func findNextHook(_ topmostIMP: IMP) -> ObjectHook? { + // We are not topmost hook, so find the hook above us! + var impl: IMP? = topmostIMP + var currentHook: ObjectHook? + repeat { + // get topmost hook + let hook = hookForIMP(impl!) + if hook === self { + // return parent + return currentHook + } + // crawl down the chain until we find ourselves + currentHook = hook + impl = hook?.origIMP + } while impl != nil + return nil + } + override func resetImplementation() throws { - _ = try validate(expectedState: .interposed) + let method = try validate(expectedState: .interposed) - if super.origIMP != nil { - try restorePreviousIMP(exactClass: dynamicSubclass!) - } else { + guard super.origIMP != nil else { // Removing methods at runtime is not supported. // https://stackoverflow.com/questions/1315169/how-do-i-remove-instance-methods-at-runtime-in-objective-c-2-0 // @@ -212,6 +268,22 @@ extension Interpose { // We could recreate the whole class at runtime and rebuild all hooks, // but that seesm excessive when we have a trampoline at our disposal. Interpose.log("Reset of -[\(`class`).\(selector)] not supported. No Original IMP") + throw InterposeError.resetUnsupported("No Original IMP found. SuperBuilder missing?") + } + + guard let currentIMP = class_getMethodImplementation(dynamicSubclass!, selector) else { + throw InterposeError.unknownError("No Implementation found") + } + + // We are the topmost hook, replace method. + if currentIMP == replacementIMP { + let previousIMP = class_replaceMethod(dynamicSubclass!, selector, origIMP!, method_getTypeEncoding(method)) + guard previousIMP == replacementIMP else { throw InterposeError.unexpectedImplementation(dynamicSubclass!, selector, previousIMP) } + Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") + } else { + let nextHook = findNextHook(currentIMP) + // Replace next's original IMP + nextHook?.origIMP = self.origIMP } // FUTURE: remove class pair! diff --git a/Sources/SuperBuilder/ITKSuperBuilder.h b/Sources/SuperBuilder/ITKSuperBuilder.h new file mode 100644 index 0000000..9580b39 --- /dev/null +++ b/Sources/SuperBuilder/ITKSuperBuilder.h @@ -0,0 +1,73 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +/** +Adds an empty super implementation instance method to originalClass. +If a method already exists, this will return NO and a descriptive error message. + +Example: You have an empty UIViewController subclass and call this with viewDidLoad as selector. +The result will be code that looks similar to this: + +override func viewDidLoad() { + super.viewDidLoad() +} + +What the compiler creates in following code: + +- (void)viewDidLoad { + struct objc_super _super = { + .receiver = self, + .super_class = object_getClass(obj); + }; + objc_msgSendSuper2(&_super, _cmd); +} + +There are a few important details: + +1) We use objc_msgSendSuper2, not objc_msgSendSuper. + The difference is minor, but important. + objc_msgSendSuper starts looking at the current class, which would cause an endless loop + objc_msgSendSuper2 looks for the superclass. + +2) This uses a completely dynamic lookup. + While slightly slower, this is resilient even if you change superclasses later on. + +3) The resolution method calls out to C, so it could be customized to jump over specific implementations. + (Such API is not currently exposed) + +4) This uses inline assembly to forward the parameters to objc_msgSendSuper2 and objc_msgSendSuper2_stret. + This is currently implemented architectures are x86_64 and arm64. + armv7 was dropped in OS 11 and i386 with macOS Catalina. + +@see https://steipete.com/posts/calling-super-at-runtime/ +*/ +@interface SuperBuilder : NSObject + +/// Adds an empty super implementation instance method to originalClass. +/// If a method already exists, this will return NO and a descriptive error message. ++ (BOOL)addSuperInstanceMethodToClass:(Class)originalClass selector:(SEL)selector error:(NSError **)error; + +/// Check if the instance method in `originalClass` is a super trampoline. ++ (BOOL)isSuperTrampolineForClass:(Class)originalClass selector:(SEL)selector; + +/// x86-64 and ARM64 are currently supported. +@property(class, readonly) BOOL isSupportedArchitecure; + +#if defined (__arm64__) || defined (__x86_64__) +/// Helper that does not exist if architecture is not supported. ++ (BOOL)isCompileTimeSupportedArchitecure; +#endif + +@end + +NSString *const SuperBuilderErrorDomain; + +typedef NS_ERROR_ENUM(SuperBuilderErrorDomain, SuperBuilderErrorCode) { + SuperBuilderErrorCodeArchitectureNotSupported, + SuperBuilderErrorCodeNoSuperClass, + SuperBuilderErrorCodeNoDynamicallyDispatchedMethodAvailable, + SuperBuilderErrorCodeFailedToAddMethod +}; + +NS_ASSUME_NONNULL_END diff --git a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m b/Sources/SuperBuilder/ITKSuperBuilder.m similarity index 99% rename from Sources/ITKAddSuperMethod/ITKAddSuperMethod.m rename to Sources/SuperBuilder/ITKSuperBuilder.m index 2966d76..4caee24 100644 --- a/Sources/ITKAddSuperMethod/ITKAddSuperMethod.m +++ b/Sources/SuperBuilder/ITKSuperBuilder.m @@ -1,4 +1,4 @@ -#import "ITKAddSuperMethod.h" +#import "ITKSuperBuilder.h" @import ObjectiveC.message; @import ObjectiveC.runtime; diff --git a/Tests/InterposeKitTests/MultipleInterposing.swift b/Tests/InterposeKitTests/MultipleInterposing.swift index 6505bbd..cb7500c 100644 --- a/Tests/InterposeKitTests/MultipleInterposing.swift +++ b/Tests/InterposeKitTests/MultipleInterposing.swift @@ -25,26 +25,17 @@ final class MultipleInterposingTests: InterposeKitTestCase { XCTAssertEqual(testObj.sayHi(), testClassHi + testString) XCTAssertEqual(testObj2.sayHi(), testClassHi) - let interposer2 = try testObj.interpose?.hook( + try testObj.interpose!.hook( #selector(TestClass.sayHi), methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { `self` in return store.original(`self`, store.selector) + testString2 } - } - - // TODO: detect existing hook? - - XCTAssertEqual(testObj.sayHi(), testClassHi + testString) - + }.apply() - // try interposer.revert() - //try interposer2.revert() - // XCTAssertEqual(testObj.sayHi(), testClassHi) - // XCTAssertEqual(testObj2.sayHi(), testClassHi) - // try interposer.apply() - // XCTAssertEqual(testObj.sayHi(), testClassHi + testSwizzleAddition) - // XCTAssertEqual(testObj2.sayHi(), testClassHi) + XCTAssertEqual(testObj.sayHi(), testClassHi + testString + testString2) + try interposer.revert() + XCTAssertEqual(testObj.sayHi(), testClassHi) } func testInterposeAgeAndRevert() throws { From df9ce3364dd11c35092c58a0841eef02c916a67a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 14 Jun 2020 21:33:06 +0200 Subject: [PATCH 48/77] fix jazzy --- Sources/SuperBuilder/ITKSuperBuilder.h | 2 ++ Sources/SuperBuilder/ITKSuperBuilder.m | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Sources/SuperBuilder/ITKSuperBuilder.h b/Sources/SuperBuilder/ITKSuperBuilder.h index 9580b39..d947083 100644 --- a/Sources/SuperBuilder/ITKSuperBuilder.h +++ b/Sources/SuperBuilder/ITKSuperBuilder.h @@ -1,3 +1,4 @@ +#if __APPLE__ #import NS_ASSUME_NONNULL_BEGIN @@ -71,3 +72,4 @@ typedef NS_ERROR_ENUM(SuperBuilderErrorDomain, SuperBuilderErrorCode) { }; NS_ASSUME_NONNULL_END +#endif diff --git a/Sources/SuperBuilder/ITKSuperBuilder.m b/Sources/SuperBuilder/ITKSuperBuilder.m index 4caee24..7d479c4 100644 --- a/Sources/SuperBuilder/ITKSuperBuilder.m +++ b/Sources/SuperBuilder/ITKSuperBuilder.m @@ -1,3 +1,4 @@ +#if __APPLE__ #import "ITKSuperBuilder.h" @import ObjectiveC.message; @@ -344,3 +345,4 @@ void msgSendSuperStretTrampoline(void) {} #endif NS_ASSUME_NONNULL_END +#endif From 562885f961ff2bb422e78a8eb7d1d51893e8b316 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 14 Jun 2020 21:48:36 +0200 Subject: [PATCH 49/77] Move error to own file --- InterposeKit.xcodeproj/project.pbxproj | 4 + Sources/InterposeKit/InterposeKit.swift | 77 ------------------- Tests/InterposeKitTests/InterposeError.swift | 78 ++++++++++++++++++++ 3 files changed, 82 insertions(+), 77 deletions(-) create mode 100644 Tests/InterposeKitTests/InterposeError.swift diff --git a/InterposeKit.xcodeproj/project.pbxproj b/InterposeKit.xcodeproj/project.pbxproj index c3cefa5..59755c5 100644 --- a/InterposeKit.xcodeproj/project.pbxproj +++ b/InterposeKit.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 78A2F26724964AF200F5AC5F /* InterposeKitTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F26624964AF200F5AC5F /* InterposeKitTestCase.swift */; }; 78A2F26B24967DB500F5AC5F /* ITKSuperBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F26924967DB500F5AC5F /* ITKSuperBuilder.m */; }; 78A2F26C24967DB500F5AC5F /* ITKSuperBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 78A2F26A24967DB500F5AC5F /* ITKSuperBuilder.h */; }; + 78A2F26E2496B54B00F5AC5F /* InterposeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F26D2496B54B00F5AC5F /* InterposeError.swift */; }; 78C39D7C2482CC7D00B46395 /* InterposeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78863EC62464B2F900BA3762 /* InterposeKit.framework */; }; 78C39D8F2483164500B46395 /* InterposeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 78C39D8E2483164500B46395 /* InterposeKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 78C39D912483165600B46395 /* InterposeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C39D902483165600B46395 /* InterposeKit.swift */; }; @@ -80,6 +81,7 @@ 78A2F26624964AF200F5AC5F /* InterposeKitTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InterposeKitTestCase.swift; path = Tests/InterposeKitTests/InterposeKitTestCase.swift; sourceTree = SOURCE_ROOT; }; 78A2F26924967DB500F5AC5F /* ITKSuperBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ITKSuperBuilder.m; sourceTree = ""; }; 78A2F26A24967DB500F5AC5F /* ITKSuperBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ITKSuperBuilder.h; sourceTree = ""; }; + 78A2F26D2496B54B00F5AC5F /* InterposeError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InterposeError.swift; path = Tests/InterposeKitTests/InterposeError.swift; sourceTree = SOURCE_ROOT; }; 78C39D772482CC7D00B46395 /* InterposeKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InterposeKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 78C39D7B2482CC7D00B46395 /* Info-Tests.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = "Info-Tests.plist"; path = "InterposeKit.xcodeproj/Info-Tests.plist"; sourceTree = ""; }; 78C39D8E2483164500B46395 /* InterposeKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = InterposeKit.h; path = Sources/InterposeKit/InterposeKit.h; sourceTree = SOURCE_ROOT; }; @@ -174,6 +176,7 @@ 78863ECA2464B2F900BA3762 /* Info.plist */, 78C39D8E2483164500B46395 /* InterposeKit.h */, 78C39D902483165600B46395 /* InterposeKit.swift */, + 78A2F26D2496B54B00F5AC5F /* InterposeError.swift */, 78EDB8DC248BAA5600D2F6C1 /* AnyHook.swift */, 7810959D248D43DC008A943C /* ClassHook.swift */, 78EDB8FE248D0A9900D2F6C1 /* ObjectHook.swift */, @@ -374,6 +377,7 @@ files = ( 781095A0248D50C1008A943C /* Watcher.swift in Sources */, 78A2F26B24967DB500F5AC5F /* ITKSuperBuilder.m in Sources */, + 78A2F26E2496B54B00F5AC5F /* InterposeError.swift in Sources */, 7810959E248D43DC008A943C /* ClassHook.swift in Sources */, 78C39D912483165600B46395 /* InterposeKit.swift in Sources */, 78EDB8FF248D0A9900D2F6C1 /* ObjectHook.swift in Sources */, diff --git a/Sources/InterposeKit/InterposeKit.swift b/Sources/InterposeKit/InterposeKit.swift index 2a22ea1..54ce56a 100644 --- a/Sources/InterposeKit/InterposeKit.swift +++ b/Sources/InterposeKit/InterposeKit.swift @@ -134,83 +134,6 @@ final public class Interpose { } } -/// The list of errors while hooking a method. -public enum InterposeError: LocalizedError { - /// The method couldn't be found. Usually happens for when you use stringified selectors that do not exist. - case methodNotFound(AnyClass, Selector) - - /// The implementation could not be found. Class must be in a weird state for this to happen. - case nonExistingImplementation(AnyClass, Selector) - - /// Someone else changed the implementation; reverting removed this implementation. - /// This is bad, likely someone else also hooked this method. If you are in such a codebase, do not use revert. - case unexpectedImplementation(AnyClass, Selector, IMP?) - - /// Unable to register subclass for object-based interposing. - case failedToAllocateClassPair(class: AnyClass, subclassName: String) - - /// Unable to add method for object-based interposing. - case unableToAddMethod(AnyClass, Selector) - - /// Object-based hooking does not work if an object is using KVO. - /// The KVO mechanism also uses subclasses created at runtime but doesn't check for additional overrides. - /// Adding a hook eventually crashes the KVO management code so we reject hooking altogether in this case. - case keyValueObservationDetected(AnyObject) - - /// Object is lying about it's actual class metadata. - /// This usually happens when other swizzling libraries (like Aspects) also interfere with a class. - /// While this might just work, it's not worth risking a crash, so similar to KVO this case is rejected. - /// - /// @note Printing classes in Swift uses the class posing mechanism. Use `NSClassFromString` to get the correct name. - case objectPosingAsDifferentClass(AnyObject, actualClass: AnyClass) - - /// Can't revert or apply if already done so. - case invalidState(expectedState: AnyHook.State) - - /// Unable to remove hook. - case resetUnsupported(_ reason: String) - - /// Generic failure - case unknownError(_ reason: String) -} - -extension InterposeError: Equatable { - // Lazy equating via string compare - public static func == (lhs: InterposeError, rhs: InterposeError) -> Bool { - return lhs.errorDescription == rhs.errorDescription - } - - public var errorDescription: String? { - switch self { - case .methodNotFound(let klass, let selector): - return "Method not found: -[\(klass) \(selector)]" - case .nonExistingImplementation(let klass, let selector): - return "Implementation not found: -[\(klass) \(selector)]" - case .unexpectedImplementation(let klass, let selector, let IMP): - return "Unexpected Implementation in -[\(klass) \(selector)]: \(String(describing: IMP))" - case .failedToAllocateClassPair(let klass, let subclassName): - return "Failed to allocate class pair: \(klass), \(subclassName)" - case .unableToAddMethod(let klass, let selector): - return "Unable to add method: -[\(klass) \(selector)]" - case .keyValueObservationDetected(let obj): - return "Unable to hook object that uses Key Value Observing: \(obj)" - case .objectPosingAsDifferentClass(let obj, let actualClass): - return "Unable to hook object posing as different class. Expected: \(type(of: obj)) Is: \(NSStringFromClass(actualClass))/" - case .invalidState(let expectedState): - return "Invalid State. Expected: \(expectedState)" - case .resetUnsupported(let reason): - return "Reset Unsupported: \(reason)" - case .unknownError(let reason): - return reason - } - } - - @discardableResult func log() -> InterposeError { - Interpose.log(self.errorDescription!) - return self - } -} - // MARK: Logging extension Interpose { diff --git a/Tests/InterposeKitTests/InterposeError.swift b/Tests/InterposeKitTests/InterposeError.swift new file mode 100644 index 0000000..3357f3d --- /dev/null +++ b/Tests/InterposeKitTests/InterposeError.swift @@ -0,0 +1,78 @@ +import Foundation + +/// The list of errors while hooking a method. +public enum InterposeError: LocalizedError { + /// The method couldn't be found. Usually happens for when you use stringified selectors that do not exist. + case methodNotFound(AnyClass, Selector) + + /// The implementation could not be found. Class must be in a weird state for this to happen. + case nonExistingImplementation(AnyClass, Selector) + + /// Someone else changed the implementation; reverting removed this implementation. + /// This is bad, likely someone else also hooked this method. If you are in such a codebase, do not use revert. + case unexpectedImplementation(AnyClass, Selector, IMP?) + + /// Unable to register subclass for object-based interposing. + case failedToAllocateClassPair(class: AnyClass, subclassName: String) + + /// Unable to add method for object-based interposing. + case unableToAddMethod(AnyClass, Selector) + + /// Object-based hooking does not work if an object is using KVO. + /// The KVO mechanism also uses subclasses created at runtime but doesn't check for additional overrides. + /// Adding a hook eventually crashes the KVO management code so we reject hooking altogether in this case. + case keyValueObservationDetected(AnyObject) + + /// Object is lying about it's actual class metadata. + /// This usually happens when other swizzling libraries (like Aspects) also interfere with a class. + /// While this might just work, it's not worth risking a crash, so similar to KVO this case is rejected. + /// + /// @note Printing classes in Swift uses the class posing mechanism. Use `NSClassFromString` to get the correct name. + case objectPosingAsDifferentClass(AnyObject, actualClass: AnyClass) + + /// Can't revert or apply if already done so. + case invalidState(expectedState: AnyHook.State) + + /// Unable to remove hook. + case resetUnsupported(_ reason: String) + + /// Generic failure + case unknownError(_ reason: String) +} + +extension InterposeError: Equatable { + // Lazy equating via string compare + public static func == (lhs: InterposeError, rhs: InterposeError) -> Bool { + return lhs.errorDescription == rhs.errorDescription + } + + public var errorDescription: String? { + switch self { + case .methodNotFound(let klass, let selector): + return "Method not found: -[\(klass) \(selector)]" + case .nonExistingImplementation(let klass, let selector): + return "Implementation not found: -[\(klass) \(selector)]" + case .unexpectedImplementation(let klass, let selector, let IMP): + return "Unexpected Implementation in -[\(klass) \(selector)]: \(String(describing: IMP))" + case .failedToAllocateClassPair(let klass, let subclassName): + return "Failed to allocate class pair: \(klass), \(subclassName)" + case .unableToAddMethod(let klass, let selector): + return "Unable to add method: -[\(klass) \(selector)]" + case .keyValueObservationDetected(let obj): + return "Unable to hook object that uses Key Value Observing: \(obj)" + case .objectPosingAsDifferentClass(let obj, let actualClass): + return "Unable to hook object posing as different class. Expected: \(type(of: obj)) Is: \(NSStringFromClass(actualClass))/" + case .invalidState(let expectedState): + return "Invalid State. Expected: \(expectedState)" + case .resetUnsupported(let reason): + return "Reset Unsupported: \(reason)" + case .unknownError(let reason): + return reason + } + } + + @discardableResult func log() -> InterposeError { + Interpose.log(self.errorDescription!) + return self + } +} From 505578f6179e742ca270ce0e8a5ab2e7e5b91a10 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 14 Jun 2020 21:48:47 +0200 Subject: [PATCH 50/77] Return self on apply --- Sources/InterposeKit/AnyHook.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/InterposeKit/AnyHook.swift b/Sources/InterposeKit/AnyHook.swift index 0ad7b2d..ebdc227 100644 --- a/Sources/InterposeKit/AnyHook.swift +++ b/Sources/InterposeKit/AnyHook.swift @@ -40,13 +40,15 @@ public class AnyHook { } /// Apply the interpose hook. - public func apply() throws { + @discardableResult public func apply() throws -> AnyHook { try execute(newState: .interposed) { try replaceImplementation() } + return self } /// Revert the interpose hoook. - public func revert() throws { + @discardableResult public func revert() throws -> AnyHook { try execute(newState: .prepared) { try resetImplementation() } + return self } public func callAsFunction(_ type: U.Type) -> U { From 524bf93d5f1abd203bb97d291afd4cef75449a8b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 14 Jun 2020 21:49:04 +0200 Subject: [PATCH 51/77] Support shorter syntax as NSObject category --- Sources/InterposeKit/InterposeKit.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Sources/InterposeKit/InterposeKit.swift b/Sources/InterposeKit/InterposeKit.swift index 54ce56a..f2247e7 100644 --- a/Sources/InterposeKit/InterposeKit.swift +++ b/Sources/InterposeKit/InterposeKit.swift @@ -10,6 +10,20 @@ extension NSObject { get { objc_getAssociatedObject(self, &AssociatedKeys.interposeObject) as? Interpose } set { objc_setAssociatedObject(self, &AssociatedKeys.interposeObject, newValue, .OBJC_ASSOCIATION_RETAIN) } } + + /// Hook an `@objc dynamic` instance method via selector on the current object or class.. + @discardableResult public func hook ( + _ selector: Selector, + methodSignature: MethodSignature.Type = MethodSignature.self, + hookSignature: HookSignature.Type = HookSignature.self, + _ implementation:(TypedHook) -> HookSignature?) throws -> AnyHook { + + if let klass = self as? AnyClass { + return try Interpose.ClassHook(class: klass, selector: selector, implementation: implementation).apply() + } else { + return try Interpose.ObjectHook(object: self, selector: selector, implementation: implementation).apply() + } + } } /// Interpose is a modern library to swizzle elegantly in Swift. From e3a954d59bf31aa0594d267178780ce0d3f8e59e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 14 Jun 2020 21:49:11 +0200 Subject: [PATCH 52/77] Use shorter syntax --- .../ObjectInterposeTests.swift | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Tests/InterposeKitTests/ObjectInterposeTests.swift b/Tests/InterposeKitTests/ObjectInterposeTests.swift index 5d7b0a7..e22d9cf 100644 --- a/Tests/InterposeKitTests/ObjectInterposeTests.swift +++ b/Tests/InterposeKitTests/ObjectInterposeTests.swift @@ -48,24 +48,23 @@ final class ObjectInterposeTests: InterposeKitTestCase { XCTAssertEqual(testObj.returnInt(), returnIntDefault) // Functions need to be `@objc dynamic` to be hookable. - let interposer = try Interpose(testObj) { - try $0.hook(#selector(TestClass.returnInt)) { (store: TypedHook<@convention(c) (AnyObject, Selector) -> Int, @convention(block) (AnyObject) -> Int>) in { + let hook = try testObj.hook(#selector(TestClass.returnInt)) { (store: TypedHook<@convention(c) (AnyObject, Selector) -> Int, @convention(block) (AnyObject) -> Int>) in { - // You're free to skip calling the original implementation. - let int = store.original($0, store.selector) - return int + returnIntOverrideOffset - } + // You're free to skip calling the original implementation. + let int = store.original($0, store.selector) + return int + returnIntOverrideOffset } } + XCTAssertEqual(testObj.returnInt(), returnIntDefault + returnIntOverrideOffset) - try interposer.revert() + try hook.revert() XCTAssertEqual(testObj.returnInt(), returnIntDefault) - try interposer.apply() + try hook.apply() // ensure we really don't leak into another object let testObj2 = TestClass() XCTAssertEqual(testObj2.returnInt(), returnIntDefault) XCTAssertEqual(testObj.returnInt(), returnIntDefault + returnIntOverrideOffset) - try interposer.revert() + try hook.revert() XCTAssertEqual(testObj.returnInt(), returnIntDefault) } From 26721b6316456d5cf9eeea7172d659a2bb131b3e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 14 Jun 2020 21:49:19 +0200 Subject: [PATCH 53/77] Update readme --- README.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/README.md b/README.md index b9b4781..b9ffe82 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,42 @@ Hi there 👋 and Interpose - Yes, you have to type the resulting type twice This is a tradeoff, else we need `NSInvocation`. - Delayed Interposing helps when a class is loaded at runtime. This is useful for [Mac Catalyst](https://steipete.com/posts/mac-catalyst-crash-hunt/). +## Object Hooking + +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. + +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. + +## Various ways to define the signature + +Next to using `methodSignature` and `hookSignature`, following variants to define the signature are also possible: + +### methodSignature + casted block +``` +let interposer = try Interpose(testObj) { + try $0.hook( + #selector(TestClass.sayHi), + methodSignature: (@convention(c) (AnyObject, Selector) -> String).self) { store in { `self` in + let string = store.original(`self`, store.selector) + return string + testString + } as @convention(block) (AnyObject) -> String } +} +``` + +### Define type via store object +``` +// Functions need to be `@objc dynamic` to be hookable. +let interposer = try Interpose(testObj) { + try $0.hook(#selector(TestClass.returnInt)) { (store: TypedHook<@convention(c) (AnyObject, Selector) -> Int, @convention(block) (AnyObject) -> Int>) in { + + // You're free to skip calling the original implementation. + let int = store.original($0, store.selector) + return int + returnIntOverrideOffset + } + } +} +``` + ## 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. @@ -90,6 +126,7 @@ try Interpose.whenAvailable(["RTIInput", "SystemSession"]) { } ``` + ## FAQ ### Why didn't you call it Interpose? "Kit" feels so old-school. From 304f8ad082b200286cea1b605d845cce9ab0454d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 14 Jun 2020 21:55:21 +0200 Subject: [PATCH 54/77] Fix Linux --- Sources/SuperBuilder/ITKSuperBuilder.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SuperBuilder/ITKSuperBuilder.h b/Sources/SuperBuilder/ITKSuperBuilder.h index d947083..8a988bb 100644 --- a/Sources/SuperBuilder/ITKSuperBuilder.h +++ b/Sources/SuperBuilder/ITKSuperBuilder.h @@ -1,5 +1,6 @@ #if __APPLE__ #import +#endif NS_ASSUME_NONNULL_BEGIN @@ -55,7 +56,7 @@ There are a few important details: /// x86-64 and ARM64 are currently supported. @property(class, readonly) BOOL isSupportedArchitecure; -#if defined (__arm64__) || defined (__x86_64__) +#if (defined (__arm64__) || defined (__x86_64__)) && __APPLE__ /// Helper that does not exist if architecture is not supported. + (BOOL)isCompileTimeSupportedArchitecure; #endif @@ -72,4 +73,3 @@ typedef NS_ERROR_ENUM(SuperBuilderErrorDomain, SuperBuilderErrorCode) { }; NS_ASSUME_NONNULL_END -#endif From 33f077763ce9b14a4e0f8366204d4e31eb52acb9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 14:39:55 +0200 Subject: [PATCH 55/77] move file to source --- InterposeKit.xcodeproj/project.pbxproj | 2 +- .../InterposeKit}/InterposeError.swift | 0 Sources/SuperBuilder/ITKSuperBuilder.m | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename {Tests/InterposeKitTests => Sources/InterposeKit}/InterposeError.swift (100%) diff --git a/InterposeKit.xcodeproj/project.pbxproj b/InterposeKit.xcodeproj/project.pbxproj index 59755c5..080a46b 100644 --- a/InterposeKit.xcodeproj/project.pbxproj +++ b/InterposeKit.xcodeproj/project.pbxproj @@ -81,7 +81,7 @@ 78A2F26624964AF200F5AC5F /* InterposeKitTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InterposeKitTestCase.swift; path = Tests/InterposeKitTests/InterposeKitTestCase.swift; sourceTree = SOURCE_ROOT; }; 78A2F26924967DB500F5AC5F /* ITKSuperBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ITKSuperBuilder.m; sourceTree = ""; }; 78A2F26A24967DB500F5AC5F /* ITKSuperBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ITKSuperBuilder.h; sourceTree = ""; }; - 78A2F26D2496B54B00F5AC5F /* InterposeError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InterposeError.swift; path = Tests/InterposeKitTests/InterposeError.swift; sourceTree = SOURCE_ROOT; }; + 78A2F26D2496B54B00F5AC5F /* InterposeError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InterposeError.swift; path = Sources/InterposeKit/InterposeError.swift; sourceTree = SOURCE_ROOT; }; 78C39D772482CC7D00B46395 /* InterposeKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InterposeKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 78C39D7B2482CC7D00B46395 /* Info-Tests.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = "Info-Tests.plist"; path = "InterposeKit.xcodeproj/Info-Tests.plist"; sourceTree = ""; }; 78C39D8E2483164500B46395 /* InterposeKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = InterposeKit.h; path = Sources/InterposeKit/InterposeKit.h; sourceTree = SOURCE_ROOT; }; diff --git a/Tests/InterposeKitTests/InterposeError.swift b/Sources/InterposeKit/InterposeError.swift similarity index 100% rename from Tests/InterposeKitTests/InterposeError.swift rename to Sources/InterposeKit/InterposeError.swift diff --git a/Sources/SuperBuilder/ITKSuperBuilder.m b/Sources/SuperBuilder/ITKSuperBuilder.m index 7d479c4..9101052 100644 --- a/Sources/SuperBuilder/ITKSuperBuilder.m +++ b/Sources/SuperBuilder/ITKSuperBuilder.m @@ -33,7 +33,7 @@ static IMP ITKGetTrampolineForTypeEncoding(__unused const char *typeEncoding) { // See ILP32: http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dai0490a/ar01s01.html #endif - return requiresStructDispatch ? msgSendSuperStretTrampoline : msgSendSuperTrampoline; + return requiresStructDispatch ? (IMP)msgSendSuperStretTrampoline : (IMP)msgSendSuperTrampoline; } // Helper for binding with Swift @@ -109,7 +109,7 @@ + (BOOL)addSuperInstanceMethodToClass:(Class)originalClass selector:(SEL)selecto static BOOL ITKMethodIsSuperTrampoline(Method method) { let methodIMP = method_getImplementation(method); - return methodIMP == msgSendSuperTrampoline || methodIMP == msgSendSuperStretTrampoline; + return methodIMP == (IMP)msgSendSuperTrampoline || methodIMP == (IMP)msgSendSuperStretTrampoline; } struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj, SEL _cmd); From 8e024eb874174cc3cee940a2d2b902cdb7c807f3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 14:51:19 +0200 Subject: [PATCH 56/77] Remove interpose NSObject helper, add class-based version --- Sources/InterposeKit/InterposeKit.swift | 22 ++++++++----------- .../MultipleInterposing.swift | 6 ++--- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/Sources/InterposeKit/InterposeKit.swift b/Sources/InterposeKit/InterposeKit.swift index f2247e7..f57305d 100644 --- a/Sources/InterposeKit/InterposeKit.swift +++ b/Sources/InterposeKit/InterposeKit.swift @@ -1,16 +1,6 @@ import Foundation -struct AssociatedKeys { - static var interposeObject: UInt8 = 0 -} - extension NSObject { - /// Access an existing Interpose container, if available. - var interpose: Interpose? { - get { objc_getAssociatedObject(self, &AssociatedKeys.interposeObject) as? Interpose } - set { objc_setAssociatedObject(self, &AssociatedKeys.interposeObject, newValue, .OBJC_ASSOCIATION_RETAIN) } - } - /// Hook an `@objc dynamic` instance method via selector on the current object or class.. @discardableResult public func hook ( _ selector: Selector, @@ -24,6 +14,15 @@ extension NSObject { return try Interpose.ObjectHook(object: self, selector: selector, implementation: implementation).apply() } } + + /// Hook an `@objc dynamic` instance method via selector on the current object or class.. + @discardableResult public class func hook ( + _ selector: Selector, + methodSignature: MethodSignature.Type = MethodSignature.self, + hookSignature: HookSignature.Type = HookSignature.self, + _ implementation:(TypedHook) -> HookSignature?) throws -> AnyHook { + return try Interpose.ClassHook(class: self as AnyClass, selector: selector, implementation: implementation).apply() + } } /// Interpose is a modern library to swizzle elegantly in Swift. @@ -84,9 +83,6 @@ final public class Interpose { if let builder = builder { try apply(builder) } - - // Store interpose on object - object.interpose = self } deinit { diff --git a/Tests/InterposeKitTests/MultipleInterposing.swift b/Tests/InterposeKitTests/MultipleInterposing.swift index cb7500c..d4ed3c1 100644 --- a/Tests/InterposeKitTests/MultipleInterposing.swift +++ b/Tests/InterposeKitTests/MultipleInterposing.swift @@ -25,17 +25,17 @@ final class MultipleInterposingTests: InterposeKitTestCase { XCTAssertEqual(testObj.sayHi(), testClassHi + testString) XCTAssertEqual(testObj2.sayHi(), testClassHi) - try testObj.interpose!.hook( + try testObj.hook( #selector(TestClass.sayHi), methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { `self` in return store.original(`self`, store.selector) + testString2 } - }.apply() + } XCTAssertEqual(testObj.sayHi(), testClassHi + testString + testString2) try interposer.revert() - XCTAssertEqual(testObj.sayHi(), testClassHi) + XCTAssertEqual(testObj.sayHi(), testClassHi + testString2) } func testInterposeAgeAndRevert() throws { From 54f868619a674e350c8f185668549ad309683e12 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 14:56:26 +0200 Subject: [PATCH 57/77] refer to @_dynamicReplacement --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b9ffe82..2998572 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ try Interpose.whenAvailable(["RTIInput", "SystemSession"]) { 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. ### 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. +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.) ### Can I ship this? 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)". @@ -162,7 +162,10 @@ Add `github "steipete/InterposeKit"` to your `Cartfile`. - Write proposal to allow to [convert the calling convention of existing types](https://twitter.com/steipete/status/1266799174563041282?s=21). - 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. -- Add object-based hooking with dynamic subclassing (Aspects again) +- Add a way to get all current hooks from an object/class. +- Add a way to revert hooks without super helper. +- Add a way to apply multiple hooks to classes +- Enable hooking of class methods. - Add [dyld_dynamic_interpose](https://twitter.com/steipete/status/1258482647933870080) to hook pure C functions - Combine Promise-API for `Interpose.whenAvailable` for better error bubbling. - Experiment with [Swift function hooking](https://github.com/rodionovd/SWRoute/wiki/Function-hooking-in-Swift)? ⚡️ From a66718a756799e02f456bdfaa25320c341d5f2ee Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 15:05:11 +0200 Subject: [PATCH 58/77] add improvement idea --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index fe9a535..1debcda 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,7 @@ Add `github "steipete/InterposeKit"` to your `Cartfile`. - Combine Promise-API for `Interpose.whenAvailable` for better error bubbling. - Experiment with [Swift function hooking](https://github.com/rodionovd/SWRoute/wiki/Function-hooking-in-Swift)? ⚡️ - Test against Swift Nightly as Cron Job +- 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). - I'm sure there's more - Pull Requests or [comments](https://twitter.com/steipete) very welcome! Make this happen: From 66b22246164c4205877a552efbbf0bfd66a3db1e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 15:11:54 +0200 Subject: [PATCH 59/77] use shorter hook syntax --- Tests/InterposeKitTests/KVOTests.swift | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/Tests/InterposeKitTests/KVOTests.swift b/Tests/InterposeKitTests/KVOTests.swift index d7704b5..7de46bf 100644 --- a/Tests/InterposeKitTests/KVOTests.swift +++ b/Tests/InterposeKitTests/KVOTests.swift @@ -22,7 +22,6 @@ final class KVOTests: InterposeKitTestCase { } } - func testBasicKVO() throws { let testObj = TestClass() @@ -38,19 +37,18 @@ final class KVOTests: InterposeKitTestCase { } // Hook without KVO! - let interpose = try Interpose(testObj) { - try $0.hook(#selector(getter: TestClass.age), - methodSignature: (@convention(c) (AnyObject, Selector) -> Int).self, - hookSignature: (@convention(block) (AnyObject) -> Int).self) { - store in { `self` in - return 3 - } - } + let hook = try testObj.hook( + #selector(getter: TestClass.age), + methodSignature: (@convention(c) (AnyObject, Selector) -> Int).self, + hookSignature: (@convention(block) (AnyObject) -> Int).self) { + store in { `self` in + return 3 + } } XCTAssertEqual(testObj.age, 3) - try interpose.revert() + try hook.revert() XCTAssertEqual(testObj.age, 2) - try interpose.apply() + try hook.apply() XCTAssertEqual(testObj.age, 3) // Now we KVO after hooking! From 600c58bc1cb71b591edbd8016894e9af3a1da96e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 15:12:32 +0200 Subject: [PATCH 60/77] Don't swiftlint analyze tests --- .github/workflows/swiftlint_analyze.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/swiftlint_analyze.yml b/.github/workflows/swiftlint_analyze.yml index 9f47030..f8be8a9 100644 --- a/.github/workflows/swiftlint_analyze.yml +++ b/.github/workflows/swiftlint_analyze.yml @@ -8,16 +8,12 @@ on: - 'InterposeKit.xcodeproj/**' - 'Sources/**/*.[ch]' - 'Sources/**/*.swift' - - 'Tests/**/*.swift' - - '!Tests/LinuxMain.swift' pull_request: paths: - '.github/workflows/swiftlint_analyze.yml' - 'InterposeKit.xcodeproj/**' - 'Sources/**/*.[ch]' - 'Sources/**/*.swift' - - 'Tests/**/*.swift' - - '!Tests/LinuxMain.swift' jobs: Analyze: From 471adce00f73704cc8b345269c00826e621164b9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 15:47:24 +0200 Subject: [PATCH 61/77] Move SuperBuilder to SwiftPM folder structure --- InterposeKit.xcodeproj/project.pbxproj | 32 ++++++++++++++----- .../{ => include}/ITKSuperBuilder.h | 0 .../SuperBuilder/{ => src}/ITKSuperBuilder.m | 0 3 files changed, 24 insertions(+), 8 deletions(-) rename Sources/SuperBuilder/{ => include}/ITKSuperBuilder.h (100%) rename Sources/SuperBuilder/{ => src}/ITKSuperBuilder.m (100%) diff --git a/InterposeKit.xcodeproj/project.pbxproj b/InterposeKit.xcodeproj/project.pbxproj index 080a46b..bd0926d 100644 --- a/InterposeKit.xcodeproj/project.pbxproj +++ b/InterposeKit.xcodeproj/project.pbxproj @@ -18,14 +18,14 @@ 781095F6248E7C91008A943C /* InterposeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 78863EC62464B2F900BA3762 /* InterposeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 78A2F265249635B100F5AC5F /* KVOTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F264249635B100F5AC5F /* KVOTests.swift */; }; 78A2F26724964AF200F5AC5F /* InterposeKitTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F26624964AF200F5AC5F /* InterposeKitTestCase.swift */; }; - 78A2F26B24967DB500F5AC5F /* ITKSuperBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F26924967DB500F5AC5F /* ITKSuperBuilder.m */; }; - 78A2F26C24967DB500F5AC5F /* ITKSuperBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 78A2F26A24967DB500F5AC5F /* ITKSuperBuilder.h */; }; 78A2F26E2496B54B00F5AC5F /* InterposeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F26D2496B54B00F5AC5F /* InterposeError.swift */; }; 78C39D7C2482CC7D00B46395 /* InterposeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78863EC62464B2F900BA3762 /* InterposeKit.framework */; }; 78C39D8F2483164500B46395 /* InterposeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 78C39D8E2483164500B46395 /* InterposeKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 78C39D912483165600B46395 /* InterposeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C39D902483165600B46395 /* InterposeKit.swift */; }; 78C39D932483169300B46395 /* InterposeKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C39D922483169300B46395 /* InterposeKitTests.swift */; }; 78C5A4A82494D75100EE9756 /* MultipleInterposing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C5A4A72494D75100EE9756 /* MultipleInterposing.swift */; }; + 78E20D952497B3480021552C /* ITKSuperBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 78E20D922497B3470021552C /* ITKSuperBuilder.h */; }; + 78E20D962497B3480021552C /* ITKSuperBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 78E20D942497B3470021552C /* ITKSuperBuilder.m */; }; 78EDB8DA248BA9B300D2F6C1 /* TestClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */; }; 78EDB8DB248BA9BB00D2F6C1 /* ObjectInterposeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */; }; 78EDB8DD248BAA5600D2F6C1 /* AnyHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EDB8DC248BAA5600D2F6C1 /* AnyHook.swift */; }; @@ -79,8 +79,6 @@ 78863ECA2464B2F900BA3762 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = Info.plist; path = InterposeKit.xcodeproj/Info.plist; sourceTree = ""; }; 78A2F264249635B100F5AC5F /* KVOTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = KVOTests.swift; path = Tests/InterposeKitTests/KVOTests.swift; sourceTree = SOURCE_ROOT; }; 78A2F26624964AF200F5AC5F /* InterposeKitTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InterposeKitTestCase.swift; path = Tests/InterposeKitTests/InterposeKitTestCase.swift; sourceTree = SOURCE_ROOT; }; - 78A2F26924967DB500F5AC5F /* ITKSuperBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ITKSuperBuilder.m; sourceTree = ""; }; - 78A2F26A24967DB500F5AC5F /* ITKSuperBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ITKSuperBuilder.h; sourceTree = ""; }; 78A2F26D2496B54B00F5AC5F /* InterposeError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InterposeError.swift; path = Sources/InterposeKit/InterposeError.swift; sourceTree = SOURCE_ROOT; }; 78C39D772482CC7D00B46395 /* InterposeKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InterposeKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 78C39D7B2482CC7D00B46395 /* Info-Tests.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = "Info-Tests.plist"; path = "InterposeKit.xcodeproj/Info-Tests.plist"; sourceTree = ""; }; @@ -92,6 +90,8 @@ 78C39DC0248317B400B46395 /* Defaults.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Defaults.xcconfig; sourceTree = ""; }; 78C39DC1248317B400B46395 /* Defaults-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Defaults-Debug.xcconfig"; sourceTree = ""; }; 78C5A4A72494D75100EE9756 /* MultipleInterposing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MultipleInterposing.swift; path = Tests/InterposeKitTests/MultipleInterposing.swift; sourceTree = SOURCE_ROOT; }; + 78E20D922497B3470021552C /* ITKSuperBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ITKSuperBuilder.h; sourceTree = ""; }; + 78E20D942497B3470021552C /* ITKSuperBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ITKSuperBuilder.m; sourceTree = ""; }; 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TestClass.swift; path = Tests/InterposeKitTests/TestClass.swift; sourceTree = SOURCE_ROOT; }; 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ObjectInterposeTests.swift; path = Tests/InterposeKitTests/ObjectInterposeTests.swift; sourceTree = SOURCE_ROOT; }; 78EDB8DC248BAA5600D2F6C1 /* AnyHook.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AnyHook.swift; path = Sources/InterposeKit/AnyHook.swift; sourceTree = SOURCE_ROOT; }; @@ -189,8 +189,8 @@ 78A2F26824967DB500F5AC5F /* SuperBuilder */ = { isa = PBXGroup; children = ( - 78A2F26924967DB500F5AC5F /* ITKSuperBuilder.m */, - 78A2F26A24967DB500F5AC5F /* ITKSuperBuilder.h */, + 78E20D912497B3470021552C /* include */, + 78E20D932497B3470021552C /* src */, ); name = SuperBuilder; path = Sources/SuperBuilder; @@ -221,6 +221,22 @@ path = Configuration; sourceTree = ""; }; + 78E20D912497B3470021552C /* include */ = { + isa = PBXGroup; + children = ( + 78E20D922497B3470021552C /* ITKSuperBuilder.h */, + ); + path = include; + sourceTree = ""; + }; + 78E20D932497B3470021552C /* src */ = { + isa = PBXGroup; + children = ( + 78E20D942497B3470021552C /* ITKSuperBuilder.m */, + ); + path = src; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -229,7 +245,7 @@ buildActionMask = 2147483647; files = ( 78C39D8F2483164500B46395 /* InterposeKit.h in Headers */, - 78A2F26C24967DB500F5AC5F /* ITKSuperBuilder.h in Headers */, + 78E20D952497B3480021552C /* ITKSuperBuilder.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -376,8 +392,8 @@ buildActionMask = 2147483647; files = ( 781095A0248D50C1008A943C /* Watcher.swift in Sources */, - 78A2F26B24967DB500F5AC5F /* ITKSuperBuilder.m in Sources */, 78A2F26E2496B54B00F5AC5F /* InterposeError.swift in Sources */, + 78E20D962497B3480021552C /* ITKSuperBuilder.m in Sources */, 7810959E248D43DC008A943C /* ClassHook.swift in Sources */, 78C39D912483165600B46395 /* InterposeKit.swift in Sources */, 78EDB8FF248D0A9900D2F6C1 /* ObjectHook.swift in Sources */, diff --git a/Sources/SuperBuilder/ITKSuperBuilder.h b/Sources/SuperBuilder/include/ITKSuperBuilder.h similarity index 100% rename from Sources/SuperBuilder/ITKSuperBuilder.h rename to Sources/SuperBuilder/include/ITKSuperBuilder.h diff --git a/Sources/SuperBuilder/ITKSuperBuilder.m b/Sources/SuperBuilder/src/ITKSuperBuilder.m similarity index 100% rename from Sources/SuperBuilder/ITKSuperBuilder.m rename to Sources/SuperBuilder/src/ITKSuperBuilder.m From c73f3c8948791e9d4bf3c795ba0d28cc81cbe1d5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 15:52:32 +0200 Subject: [PATCH 62/77] Update swiftlint_analyze.yml --- .github/workflows/swiftlint_analyze.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/swiftlint_analyze.yml b/.github/workflows/swiftlint_analyze.yml index f8be8a9..5c3c583 100644 --- a/.github/workflows/swiftlint_analyze.yml +++ b/.github/workflows/swiftlint_analyze.yml @@ -8,12 +8,16 @@ on: - 'InterposeKit.xcodeproj/**' - 'Sources/**/*.[ch]' - 'Sources/**/*.swift' + - '!Tests/**/*.swift' + - '!Tests/LinuxMain.swift' pull_request: paths: - '.github/workflows/swiftlint_analyze.yml' - 'InterposeKit.xcodeproj/**' - 'Sources/**/*.[ch]' - 'Sources/**/*.swift' + - '!Tests/**/*.swift' + - '!Tests/LinuxMain.swift' jobs: Analyze: From ff9644c30b8bd4de5f6522a5f117f2ff8142f2a0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 15:54:58 +0200 Subject: [PATCH 63/77] Improve Linux Support --- Sources/InterposeKit/LinuxCompileSupport.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/InterposeKit/LinuxCompileSupport.swift b/Sources/InterposeKit/LinuxCompileSupport.swift index 2013fc7..7b9f516 100644 --- a/Sources/InterposeKit/LinuxCompileSupport.swift +++ b/Sources/InterposeKit/LinuxCompileSupport.swift @@ -4,7 +4,9 @@ import Foundation #if os(Linux) /// :nodoc: Selector -public struct Selector {} +public struct Selector { + init(_ name: String) {} +} /// :nodoc: IMP public struct IMP: Equatable {} /// :nodoc: Method @@ -18,6 +20,8 @@ func method_getTypeEncoding(_ m: Method) -> UnsafePointer? { return nil } // swiftlint:disable:next identifier_name func _dyld_register_func_for_add_image(_ func: (@convention(c) (UnsafePointer?, Int) -> Void)!) {} func imp_implementationWithBlock(_ block: Any) -> IMP { IMP() } +func imp_getBlock(_ anImp: IMP) -> Any? { return nil } func imp_removeBlock(_ anImp: IMP) -> Bool { false } - +class NSError : NSObject {} +public typealias NSErrorPointer = AutoreleasingUnsafeMutablePointer? #endif From 9207555edef18a6ad197327fa7b6203990fdd69b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 15:56:56 +0200 Subject: [PATCH 64/77] Linux Support --- Sources/InterposeKit/LinuxCompileSupport.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Sources/InterposeKit/LinuxCompileSupport.swift b/Sources/InterposeKit/LinuxCompileSupport.swift index 7b9f516..9d59da5 100644 --- a/Sources/InterposeKit/LinuxCompileSupport.swift +++ b/Sources/InterposeKit/LinuxCompileSupport.swift @@ -24,4 +24,17 @@ func imp_getBlock(_ anImp: IMP) -> Any? { return nil } func imp_removeBlock(_ anImp: IMP) -> Bool { false } class NSError : NSObject {} public typealias NSErrorPointer = AutoreleasingUnsafeMutablePointer? +extension NSObject { +open func value(forKey key: String) -> Any? +} +/// :nodoc: objc_AssociationPolicy +public enum objc_AssociationPolicy : UInt { + case OBJC_ASSOCIATION_ASSIGN = 0 + case OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1 + case OBJC_ASSOCIATION_COPY_NONATOMIC = 3 + case OBJC_ASSOCIATION_RETAIN = 769 + case OBJC_ASSOCIATION_COPY = 771 +} +public func objc_setAssociatedObject(_ object: Any, _ key: UnsafeRawPointer, _ value: Any?, _ policy: objc_AssociationPolicy) {} +public func objc_getAssociatedObject(_ object: Any, _ key: UnsafeRawPointer) -> Any? { return nil } #endif From cb3e6b3efa3c1c3c26f00fc1c6517436c382d130 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 16:00:45 +0200 Subject: [PATCH 65/77] Swiftlint --- Sources/InterposeKit/InterposeError.swift | 5 +++-- Sources/InterposeKit/LinuxCompileSupport.swift | 8 +++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Sources/InterposeKit/InterposeError.swift b/Sources/InterposeKit/InterposeError.swift index 3357f3d..e19a372 100644 --- a/Sources/InterposeKit/InterposeError.swift +++ b/Sources/InterposeKit/InterposeError.swift @@ -27,7 +27,8 @@ public enum InterposeError: LocalizedError { /// This usually happens when other swizzling libraries (like Aspects) also interfere with a class. /// While this might just work, it's not worth risking a crash, so similar to KVO this case is rejected. /// - /// @note Printing classes in Swift uses the class posing mechanism. Use `NSClassFromString` to get the correct name. + /// @note Printing classes in Swift uses the class posing mechanism. + /// Use `NSClassFromString` to get the correct name. case objectPosingAsDifferentClass(AnyObject, actualClass: AnyClass) /// Can't revert or apply if already done so. @@ -61,7 +62,7 @@ extension InterposeError: Equatable { case .keyValueObservationDetected(let obj): return "Unable to hook object that uses Key Value Observing: \(obj)" case .objectPosingAsDifferentClass(let obj, let actualClass): - return "Unable to hook object posing as different class. Expected: \(type(of: obj)) Is: \(NSStringFromClass(actualClass))/" + return "Unable to hook \(type(of: obj)) posing as \(NSStringFromClass(actualClass))/" case .invalidState(let expectedState): return "Invalid State. Expected: \(expectedState)" case .resetUnsupported(let reason): diff --git a/Sources/InterposeKit/LinuxCompileSupport.swift b/Sources/InterposeKit/LinuxCompileSupport.swift index 9d59da5..80da334 100644 --- a/Sources/InterposeKit/LinuxCompileSupport.swift +++ b/Sources/InterposeKit/LinuxCompileSupport.swift @@ -25,14 +25,20 @@ func imp_removeBlock(_ anImp: IMP) -> Bool { false } class NSError : NSObject {} public typealias NSErrorPointer = AutoreleasingUnsafeMutablePointer? extension NSObject { -open func value(forKey key: String) -> Any? + open func value(forKey key: String) -> Any? } /// :nodoc: objc_AssociationPolicy +// swiftlint:disable:next type_name public enum objc_AssociationPolicy : UInt { + // swiftlint:disable:next identifier_name case OBJC_ASSOCIATION_ASSIGN = 0 + // swiftlint:disable:next identifier_name case OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1 + // swiftlint:disable:next identifier_name case OBJC_ASSOCIATION_COPY_NONATOMIC = 3 + // swiftlint:disable:next identifier_name case OBJC_ASSOCIATION_RETAIN = 769 + // swiftlint:disable:next identifier_name case OBJC_ASSOCIATION_COPY = 771 } public func objc_setAssociatedObject(_ object: Any, _ key: UnsafeRawPointer, _ value: Any?, _ policy: objc_AssociationPolicy) {} From e55364a04c8e7b9dcd17a4174e2037caa1a79219 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 16:04:18 +0200 Subject: [PATCH 66/77] remove unused decl rule --- .swiftlint.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 61f3c51..4ff71f0 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -2,7 +2,6 @@ included: - Sources - Tests analyzer_rules: - - unused_declaration line_length: 120 identifier_name: excluded: From dcb01ab52fa4073cd487fe0f9181146c04740fbc Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 16:09:50 +0200 Subject: [PATCH 67/77] add object example --- README.md | 9 ++ .../ObjectInterposeTests.swift | 86 +++++++------------ 2 files changed, 41 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 1debcda..b8217a4 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,15 @@ let interposer = try Interpose(TestClass.self) { // Don't need the hook anymore? Undo is built-in! interposer.revert() + +// Want to hook just a single instance? No problem! +let hook = try testObj.hook( + #selector(TestClass.sayHi), + methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, + hookSignature:(@convention(block) (AnyObject) -> String).self) { store in { `self` in + return store.original(`self`, store.selector) + "just this instance" + } +} ``` Here's what we get when calling `print(TestClass().sayHi())` diff --git a/Tests/InterposeKitTests/ObjectInterposeTests.swift b/Tests/InterposeKitTests/ObjectInterposeTests.swift index e22d9cf..44c7c87 100644 --- a/Tests/InterposeKitTests/ObjectInterposeTests.swift +++ b/Tests/InterposeKitTests/ObjectInterposeTests.swift @@ -11,32 +11,23 @@ final class ObjectInterposeTests: InterposeKitTestCase { XCTAssertEqual(testObj.sayHi(), testClassHi) XCTAssertEqual(testObj2.sayHi(), testClassHi) - // Functions need to be `@objc dynamic` to be hookable. - let interposer = try Interpose(testObj) { - try $0.hook( - #selector(TestClass.sayHi), - methodSignature: (@convention(c) (AnyObject, Selector) -> String).self) { store in { `self` in - - print("Before Interposing \(`self`)") - - // Calling convention and passing selector is important! - // You're free to skip calling the original implementation. - let string = store.original(`self`, store.selector) - - print("After Interposing \(`self`)") - - return string + testString - - // Similar signature cast as above, but without selector. - } as @convention(block) (AnyObject) -> String } + let hook = try testObj.hook( + #selector(TestClass.sayHi), + methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, + hookSignature:(@convention(block) (AnyObject) -> String).self) { store in { `self` in + print("Before Interposing \(`self`)") + let string = store.original(`self`, store.selector) + print("After Interposing \(`self`)") + return string + testString + } } XCTAssertEqual(testObj.sayHi(), testClassHi + testString) XCTAssertEqual(testObj2.sayHi(), testClassHi) - try interposer.revert() + try hook.revert() XCTAssertEqual(testObj.sayHi(), testClassHi) XCTAssertEqual(testObj2.sayHi(), testClassHi) - try interposer.apply() + try hook.apply() XCTAssertEqual(testObj.sayHi(), testClassHi + testString) XCTAssertEqual(testObj2.sayHi(), testClassHi) } @@ -47,10 +38,7 @@ final class ObjectInterposeTests: InterposeKitTestCase { let returnIntOverrideOffset = 2 XCTAssertEqual(testObj.returnInt(), returnIntDefault) - // Functions need to be `@objc dynamic` to be hookable. let hook = try testObj.hook(#selector(TestClass.returnInt)) { (store: TypedHook<@convention(c) (AnyObject, Selector) -> Int, @convention(block) (AnyObject) -> Int>) in { - - // You're free to skip calling the original implementation. let int = store.original($0, store.selector) return int + returnIntOverrideOffset } @@ -76,11 +64,9 @@ final class ObjectInterposeTests: InterposeKitTestCase { XCTAssertEqual(testObj.returnInt(), returnIntDefault) // Functions need to be `@objc dynamic` to be hookable. - let interposer = try Interpose(testObj) { - try $0.hook(#selector(TestClass.returnInt)) { (store: TypedHook<@convention(c) (AnyObject, Selector) -> Int, @convention(block) (AnyObject) -> Int>) in { - // You're free to skip calling the original implementation. - store.original($0, store.selector) + returnIntOverrideOffset - } + let hook = try testObj.hook(#selector(TestClass.returnInt)) { (store: TypedHook<@convention(c) (AnyObject, Selector) -> Int, @convention(block) (AnyObject) -> Int>) in { + // You're free to skip calling the original implementation. + store.original($0, store.selector) + returnIntOverrideOffset } } XCTAssertEqual(testObj.returnInt(), returnIntDefault + returnIntOverrideOffset) @@ -99,7 +85,7 @@ final class ObjectInterposeTests: InterposeKitTestCase { let testObj2 = TestClass() XCTAssertEqual(testObj2.returnInt(), returnIntDefault * returnIntClassMultiplier) - try interposer.revert() + try hook.revert() XCTAssertEqual(testObj.returnInt(), returnIntDefault * returnIntClassMultiplier) try classInterposer.revert() XCTAssertEqual(testObj.returnInt(), returnIntDefault) @@ -110,16 +96,14 @@ final class ObjectInterposeTests: InterposeKitTestCase { XCTAssertEqual(testObj.calculate(var1: 1, var2: 2, var3: 3), 1 + 2 + 3) // Functions need to be `@objc dynamic` to be hookable. - let interposer = try Interpose(testObj) { - try $0.hook(#selector(TestClass.calculate)) { (store: TypedHook<@convention(c) (AnyObject, Selector, Int, Int, Int) -> Int, @convention(block) (AnyObject, Int, Int, Int) -> Int>) in { - // You're free to skip calling the original implementation. - let orig = store.original($0, store.selector, $1, $2, $3) - return orig + 1 - } + let hook = try testObj.hook(#selector(TestClass.calculate)) { (store: TypedHook<@convention(c) (AnyObject, Selector, Int, Int, Int) -> Int, @convention(block) (AnyObject, Int, Int, Int) -> Int>) in { + // You're free to skip calling the original implementation. + let orig = store.original($0, store.selector, $1, $2, $3) + return orig + 1 } } XCTAssertEqual(testObj.calculate(var1: 1, var2: 2, var3: 3), 1 + 2 + 3 + 1) - try interposer.revert() + try hook.revert() } func test6IntParameters() throws { @@ -128,16 +112,14 @@ final class ObjectInterposeTests: InterposeKitTestCase { XCTAssertEqual(testObj.calculate2(var1: 1, var2: 2, var3: 3, var4: 4, var5: 5, var6: 6), 1 + 2 + 3 + 4 + 5 + 6) // Functions need to be `@objc dynamic` to be hookable. - let interposer = try Interpose(testObj) { - try $0.hook(#selector(TestClass.calculate2)) { (store: TypedHook<@convention(c) (AnyObject, Selector, Int, Int, Int, Int, Int, Int) -> Int, @convention(block) (AnyObject, Int, Int, Int, Int, Int, Int) -> Int>) in { - // You're free to skip calling the original implementation. - let orig = store.original($0, store.selector, $1, $2, $3, $4, $5, $6) - return orig + 1 - } + let hook = try testObj.hook(#selector(TestClass.calculate2)) { (store: TypedHook<@convention(c) (AnyObject, Selector, Int, Int, Int, Int, Int, Int) -> Int, @convention(block) (AnyObject, Int, Int, Int, Int, Int, Int) -> Int>) in { + // You're free to skip calling the original implementation. + let orig = store.original($0, store.selector, $1, $2, $3, $4, $5, $6) + return orig + 1 } } XCTAssertEqual(testObj.calculate2(var1: 1, var2: 2, var3: 3, var4: 4, var5: 5, var6: 6), 1 + 2 + 3 + 4 + 5 + 6 + 1) - try interposer.revert() + try hook.revert() } func testObjectCallReturn() throws { @@ -146,14 +128,12 @@ final class ObjectInterposeTests: InterposeKitTestCase { XCTAssertEqual(testObj.doubleString(string: str), str + str) // Functions need to be `@objc dynamic` to be hookable. - let interposer = try Interpose(testObj) { - try $0.hook(#selector(TestClass.doubleString)) { (store: TypedHook<@convention(c) (AnyObject, Selector, String) -> String, @convention(block) (AnyObject, String) -> String>) in { - store.original($0, store.selector, $1) + str - } + let hook = try testObj.hook(#selector(TestClass.doubleString)) { (store: TypedHook<@convention(c) (AnyObject, Selector, String) -> String, @convention(block) (AnyObject, String) -> String>) in { + store.original($0, store.selector, $1) + str } } XCTAssertEqual(testObj.doubleString(string: str), str + str + str) - try interposer.revert() + try hook.revert() XCTAssertEqual(testObj.doubleString(string: str), str + str) } @@ -167,15 +147,13 @@ final class ObjectInterposeTests: InterposeKitTestCase { } // Functions need to be `@objc dynamic` to be hookable. - let interposer = try Interpose(testObj) { - try $0.hook(#selector(TestClass.invert3DTransform)) { (store: TypedHook<@convention(c) (AnyObject, Selector, CATransform3D) -> CATransform3D, @convention(block) (AnyObject, CATransform3D) -> CATransform3D>) in { - let matrix = store.original($0, store.selector, $1) - return transformMatrix(matrix) - } + let hook = try testObj.hook(#selector(TestClass.invert3DTransform)) { (store: TypedHook<@convention(c) (AnyObject, Selector, CATransform3D) -> CATransform3D, @convention(block) (AnyObject, CATransform3D) -> CATransform3D>) in { + let matrix = store.original($0, store.selector, $1) + return transformMatrix(matrix) } } XCTAssertEqual(testObj.invert3DTransform(transform), transformMatrix(transform.inverted)) - try interposer.revert() + try hook.revert() XCTAssertEqual(testObj.invert3DTransform(transform), transform.inverted) } } From 748a5447c046750c3e700817da740f8500fb716c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 16:20:57 +0200 Subject: [PATCH 68/77] Linux --- Sources/InterposeKit/LinuxCompileSupport.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Sources/InterposeKit/LinuxCompileSupport.swift b/Sources/InterposeKit/LinuxCompileSupport.swift index 80da334..fcd0454 100644 --- a/Sources/InterposeKit/LinuxCompileSupport.swift +++ b/Sources/InterposeKit/LinuxCompileSupport.swift @@ -11,10 +11,15 @@ public struct Selector { public struct IMP: Equatable {} /// :nodoc: Method public struct Method {} -func NSSelectorFromString(_ aSelectorName: String) -> Selector { Selector() } +func NSSelectorFromString(_ aSelectorName: String) -> Selector { Selector("") } func class_getInstanceMethod(_ cls: AnyClass?, _ name: Selector) -> Method? { return nil } +func class_getMethodImplementation(_ cls: AnyClass?, _ name: Selector) -> IMP? { return nil } // swiftlint:disable:next line_length func class_replaceMethod(_ cls: AnyClass?, _ name: Selector, _ imp: IMP, _ types: UnsafePointer?) -> IMP? { IMP() } +func class_addMethod(_ cls: AnyClass?, _ name: Selector, _ imp: IMP, _ types: UnsafePointer?) -> Bool { return false } +func class_copyMethodList(_ cls: AnyClass?, _ outCount: UnsafeMutablePointer?) -> UnsafeMutablePointer? { return nil } +func object_getClass(_ obj: Any?) -> AnyClass? { return nil } +func class_getSuperclass(_ cls: AnyClass?) -> AnyClass? { return nil } // swiftlint:disable:next identifier_name func method_getTypeEncoding(_ m: Method) -> UnsafePointer? { return nil } // swiftlint:disable:next identifier_name @@ -23,13 +28,13 @@ func imp_implementationWithBlock(_ block: Any) -> IMP { IMP() } func imp_getBlock(_ anImp: IMP) -> Any? { return nil } func imp_removeBlock(_ anImp: IMP) -> Bool { false } class NSError : NSObject {} -public typealias NSErrorPointer = AutoreleasingUnsafeMutablePointer? +typealias NSErrorPointer = AutoreleasingUnsafeMutablePointer? extension NSObject { open func value(forKey key: String) -> Any? } /// :nodoc: objc_AssociationPolicy // swiftlint:disable:next type_name -public enum objc_AssociationPolicy : UInt { +enum objc_AssociationPolicy : UInt { // swiftlint:disable:next identifier_name case OBJC_ASSOCIATION_ASSIGN = 0 // swiftlint:disable:next identifier_name @@ -43,4 +48,5 @@ public enum objc_AssociationPolicy : UInt { } public func objc_setAssociatedObject(_ object: Any, _ key: UnsafeRawPointer, _ value: Any?, _ policy: objc_AssociationPolicy) {} public func objc_getAssociatedObject(_ object: Any, _ key: UnsafeRawPointer) -> Any? { return nil } +struct AutoreleasingUnsafeMutablePointer {} #endif From e994a633f203b9eb03691791bf783a80f2b3645f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 16:23:57 +0200 Subject: [PATCH 69/77] Linux --- Sources/InterposeKit/LinuxCompileSupport.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/InterposeKit/LinuxCompileSupport.swift b/Sources/InterposeKit/LinuxCompileSupport.swift index fcd0454..119f5c2 100644 --- a/Sources/InterposeKit/LinuxCompileSupport.swift +++ b/Sources/InterposeKit/LinuxCompileSupport.swift @@ -24,11 +24,14 @@ func class_getSuperclass(_ cls: AnyClass?) -> AnyClass? { return nil } func method_getTypeEncoding(_ m: Method) -> UnsafePointer? { return nil } // swiftlint:disable:next identifier_name func _dyld_register_func_for_add_image(_ func: (@convention(c) (UnsafePointer?, Int) -> Void)!) {} +func objc_allocateClassPair(_ superclass: AnyClass?, _ name: UnsafePointer, _ extraBytes: Int) -> AnyClass? { return nil } +func objc_registerClassPair(_ cls: AnyClass) {} func imp_implementationWithBlock(_ block: Any) -> IMP { IMP() } func imp_getBlock(_ anImp: IMP) -> Any? { return nil } func imp_removeBlock(_ anImp: IMP) -> Bool { false } class NSError : NSObject {} -typealias NSErrorPointer = AutoreleasingUnsafeMutablePointer? +// AutoreleasingUnsafeMutablePointer is not available on Linux. +typealias NSErrorPointer = UnsafeMutablePointer? extension NSObject { open func value(forKey key: String) -> Any? } @@ -48,5 +51,4 @@ enum objc_AssociationPolicy : UInt { } public func objc_setAssociatedObject(_ object: Any, _ key: UnsafeRawPointer, _ value: Any?, _ policy: objc_AssociationPolicy) {} public func objc_getAssociatedObject(_ object: Any, _ key: UnsafeRawPointer) -> Any? { return nil } -struct AutoreleasingUnsafeMutablePointer {} #endif From 122d4dfea2aeb972c7b55caeeb87b1ce500e01e9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 16:31:39 +0200 Subject: [PATCH 70/77] Linux --- Sources/InterposeKit/LinuxCompileSupport.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Sources/InterposeKit/LinuxCompileSupport.swift b/Sources/InterposeKit/LinuxCompileSupport.swift index 119f5c2..e452d94 100644 --- a/Sources/InterposeKit/LinuxCompileSupport.swift +++ b/Sources/InterposeKit/LinuxCompileSupport.swift @@ -19,21 +19,25 @@ func class_replaceMethod(_ cls: AnyClass?, _ name: Selector, _ imp: IMP, _ types func class_addMethod(_ cls: AnyClass?, _ name: Selector, _ imp: IMP, _ types: UnsafePointer?) -> Bool { return false } func class_copyMethodList(_ cls: AnyClass?, _ outCount: UnsafeMutablePointer?) -> UnsafeMutablePointer? { return nil } func object_getClass(_ obj: Any?) -> AnyClass? { return nil } +func object_setClass(_ obj: Any?, _ cls: AnyClass) -> AnyClass? { return nil } +func method_getName(_ m: Method) -> Selector { Selector("") } func class_getSuperclass(_ cls: AnyClass?) -> AnyClass? { return nil } // swiftlint:disable:next identifier_name func method_getTypeEncoding(_ m: Method) -> UnsafePointer? { return nil } +func method_getImplementation(_ m: Method) -> IMP { IMP() } // swiftlint:disable:next identifier_name func _dyld_register_func_for_add_image(_ func: (@convention(c) (UnsafePointer?, Int) -> Void)!) {} func objc_allocateClassPair(_ superclass: AnyClass?, _ name: UnsafePointer, _ extraBytes: Int) -> AnyClass? { return nil } func objc_registerClassPair(_ cls: AnyClass) {} +func objc_getClass(_: UnsafePointer!) -> Any! { return nil } func imp_implementationWithBlock(_ block: Any) -> IMP { IMP() } func imp_getBlock(_ anImp: IMP) -> Any? { return nil } -func imp_removeBlock(_ anImp: IMP) -> Bool { false } +@discardableResult func imp_removeBlock(_ anImp: IMP) -> Bool { false } class NSError : NSObject {} // AutoreleasingUnsafeMutablePointer is not available on Linux. typealias NSErrorPointer = UnsafeMutablePointer? extension NSObject { - open func value(forKey key: String) -> Any? + open func value(forKey key: String) -> Any? { return nil } } /// :nodoc: objc_AssociationPolicy // swiftlint:disable:next type_name @@ -49,6 +53,6 @@ enum objc_AssociationPolicy : UInt { // swiftlint:disable:next identifier_name case OBJC_ASSOCIATION_COPY = 771 } -public func objc_setAssociatedObject(_ object: Any, _ key: UnsafeRawPointer, _ value: Any?, _ policy: objc_AssociationPolicy) {} -public func objc_getAssociatedObject(_ object: Any, _ key: UnsafeRawPointer) -> Any? { return nil } +func objc_setAssociatedObject(_ object: Any, _ key: UnsafeRawPointer, _ value: Any?, _ policy: objc_AssociationPolicy) {} +func objc_getAssociatedObject(_ object: Any, _ key: UnsafeRawPointer) -> Any? { return nil } #endif From 363429bb28ac64d52604065d8b296b10fa2bbd8e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 16:35:38 +0200 Subject: [PATCH 71/77] Linux --- .../InterposeKit/LinuxCompileSupport.swift | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/Sources/InterposeKit/LinuxCompileSupport.swift b/Sources/InterposeKit/LinuxCompileSupport.swift index e452d94..2699dab 100644 --- a/Sources/InterposeKit/LinuxCompileSupport.swift +++ b/Sources/InterposeKit/LinuxCompileSupport.swift @@ -14,26 +14,30 @@ public struct Method {} func NSSelectorFromString(_ aSelectorName: String) -> Selector { Selector("") } func class_getInstanceMethod(_ cls: AnyClass?, _ name: Selector) -> Method? { return nil } func class_getMethodImplementation(_ cls: AnyClass?, _ name: Selector) -> IMP? { return nil } -// swiftlint:disable:next line_length -func class_replaceMethod(_ cls: AnyClass?, _ name: Selector, _ imp: IMP, _ types: UnsafePointer?) -> IMP? { IMP() } -func class_addMethod(_ cls: AnyClass?, _ name: Selector, _ imp: IMP, _ types: UnsafePointer?) -> Bool { return false } +func class_replaceMethod(_ cls: AnyClass?, _ name: Selector, + _ imp: IMP, _ types: UnsafePointer?) -> IMP? { IMP() } +func class_addMethod(_ cls: AnyClass?, _ name: Selector, + _ imp: IMP, _ types: UnsafePointer?) -> Bool { return false } func class_copyMethodList(_ cls: AnyClass?, _ outCount: UnsafeMutablePointer?) -> UnsafeMutablePointer? { return nil } func object_getClass(_ obj: Any?) -> AnyClass? { return nil } func object_setClass(_ obj: Any?, _ cls: AnyClass) -> AnyClass? { return nil } -func method_getName(_ m: Method) -> Selector { Selector("") } +func method_getName(_ method: Method) -> Selector { Selector("") } func class_getSuperclass(_ cls: AnyClass?) -> AnyClass? { return nil } // swiftlint:disable:next identifier_name -func method_getTypeEncoding(_ m: Method) -> UnsafePointer? { return nil } -func method_getImplementation(_ m: Method) -> IMP { IMP() } +func method_getTypeEncoding(_ method: Method) -> UnsafePointer? { return nil } +func method_getImplementation(_ method: Method) -> IMP { IMP() } // swiftlint:disable:next identifier_name -func _dyld_register_func_for_add_image(_ func: (@convention(c) (UnsafePointer?, Int) -> Void)!) {} -func objc_allocateClassPair(_ superclass: AnyClass?, _ name: UnsafePointer, _ extraBytes: Int) -> AnyClass? { return nil } +func _dyld_register_func_for_add_image(_ func: + (@convention(c) (UnsafePointer?, Int) -> Void)!) {} +func objc_allocateClassPair(_ superclass: AnyClass?, + _ name: UnsafePointer, + _ extraBytes: Int) -> AnyClass? { return nil } func objc_registerClassPair(_ cls: AnyClass) {} func objc_getClass(_: UnsafePointer!) -> Any! { return nil } func imp_implementationWithBlock(_ block: Any) -> IMP { IMP() } func imp_getBlock(_ anImp: IMP) -> Any? { return nil } @discardableResult func imp_removeBlock(_ anImp: IMP) -> Bool { false } -class NSError : NSObject {} +class NSError: NSObject {} // AutoreleasingUnsafeMutablePointer is not available on Linux. typealias NSErrorPointer = UnsafeMutablePointer? extension NSObject { @@ -41,7 +45,7 @@ extension NSObject { } /// :nodoc: objc_AssociationPolicy // swiftlint:disable:next type_name -enum objc_AssociationPolicy : UInt { +enum objc_AssociationPolicy: UInt { // swiftlint:disable:next identifier_name case OBJC_ASSOCIATION_ASSIGN = 0 // swiftlint:disable:next identifier_name @@ -53,6 +57,8 @@ enum objc_AssociationPolicy : UInt { // swiftlint:disable:next identifier_name case OBJC_ASSOCIATION_COPY = 771 } -func objc_setAssociatedObject(_ object: Any, _ key: UnsafeRawPointer, _ value: Any?, _ policy: objc_AssociationPolicy) {} -func objc_getAssociatedObject(_ object: Any, _ key: UnsafeRawPointer) -> Any? { return nil } +func objc_setAssociatedObject(_ object: Any, _ key: UnsafeRawPointer, + _ value: Any?, _ policy: objc_AssociationPolicy) {} +func objc_getAssociatedObject(_ object: Any, _ key: UnsafeRawPointer) -> Any? +{ return nil } #endif From b040f7fa1f7ed646c8ec038ffdc98bd6654a5cbd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 17:48:45 +0200 Subject: [PATCH 72/77] equatable --- Sources/InterposeKit/LinuxCompileSupport.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/InterposeKit/LinuxCompileSupport.swift b/Sources/InterposeKit/LinuxCompileSupport.swift index 2699dab..9290f10 100644 --- a/Sources/InterposeKit/LinuxCompileSupport.swift +++ b/Sources/InterposeKit/LinuxCompileSupport.swift @@ -2,10 +2,10 @@ import Foundation // Linux is used to create Jazzy docs #if os(Linux) - /// :nodoc: Selector -public struct Selector { - init(_ name: String) {} +public struct Selector: Equatable { + var name: String? + init(_ name: String) { self.name = name } } /// :nodoc: IMP public struct IMP: Equatable {} @@ -20,7 +20,7 @@ func class_addMethod(_ cls: AnyClass?, _ name: Selector, _ imp: IMP, _ types: UnsafePointer?) -> Bool { return false } func class_copyMethodList(_ cls: AnyClass?, _ outCount: UnsafeMutablePointer?) -> UnsafeMutablePointer? { return nil } func object_getClass(_ obj: Any?) -> AnyClass? { return nil } -func object_setClass(_ obj: Any?, _ cls: AnyClass) -> AnyClass? { return nil } +@discardableResult func object_setClass(_ obj: Any?, _ cls: AnyClass) -> AnyClass? { return nil } func method_getName(_ method: Method) -> Selector { Selector("") } func class_getSuperclass(_ cls: AnyClass?) -> AnyClass? { return nil } // swiftlint:disable:next identifier_name From 9965a849f158309d080f5daa3628e58acb663355 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 17:50:51 +0200 Subject: [PATCH 73/77] add objc to error --- Sources/InterposeKit/LinuxCompileSupport.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/InterposeKit/LinuxCompileSupport.swift b/Sources/InterposeKit/LinuxCompileSupport.swift index 9290f10..bf9489d 100644 --- a/Sources/InterposeKit/LinuxCompileSupport.swift +++ b/Sources/InterposeKit/LinuxCompileSupport.swift @@ -37,7 +37,7 @@ func objc_getClass(_: UnsafePointer!) -> Any! { return nil } func imp_implementationWithBlock(_ block: Any) -> IMP { IMP() } func imp_getBlock(_ anImp: IMP) -> Any? { return nil } @discardableResult func imp_removeBlock(_ anImp: IMP) -> Bool { false } -class NSError: NSObject {} +@objc class NSError: NSObject {} // AutoreleasingUnsafeMutablePointer is not available on Linux. typealias NSErrorPointer = UnsafeMutablePointer? extension NSObject { From a953f70f4a5f625482ceef18b510f6bad6adbd8e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 17:54:21 +0200 Subject: [PATCH 74/77] Linux support --- Sources/InterposeKit/ObjectHook.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index bb3a35d..82d9e1b 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -33,6 +33,14 @@ extension Interpose { } } + private static func isSupportedArchitectureForSuper() -> Bool { + #if os(Linux) + return false + #else + return NSClassFromString("SuperBuilder")?.value(forKey: "isSupportedArchitecure") as? Bool ?? false + #endif + } + /// A hook to an instance method of a single object, stores both the original and new implementation. /// Think about: Multiple hooks for one object final public class ObjectHook: TypedHook { @@ -41,7 +49,7 @@ extension Interpose { var dynamicSubclass: AnyClass? // Logic switch to use super builder - let generatesSuperIMP = NSClassFromString("SuperBuilder")?.value(forKey: "isSupportedArchitecure") as? Bool ?? false + let generatesSuperIMP = isSupportedArchitectureForSuper() /// Initialize a new hook to interpose an instance method. public init(object: AnyObject, selector: Selector, implementation:(ObjectHook) -> HookSignature?) throws { From e042d8f913f3fd48eb1beb0f16de5d66f288b667 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 18:03:37 +0200 Subject: [PATCH 75/77] linux --- Sources/InterposeKit/ObjectHook.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index 82d9e1b..9db1407 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -138,6 +138,7 @@ extension Interpose { _ = class_replaceMethod(object_getClass(`class`), ObjCSelector.getClass, impl, ObjCMethodEncoding.getClass) } + #if !os(Linux) private lazy var addSuperImpl: @convention(c) (AnyClass, Selector, NSErrorPointer) -> Bool = { let handle = dlopen(nil, RTLD_LAZY) let imp = dlsym(handle, "IKTAddSuperImplementationToClass") @@ -153,6 +154,10 @@ extension Interpose { Interpose.log("Added super for -[\(`class`).\(selector)]: \(imp)") } } + #else + private func addSuperTrampolineMethod(subclass: AnyClass) { } + #endif + /// The original implementation is looked up at runtime . public override var original: MethodSignature { From a202a0f90a7ea64a7407819388cd64c048cc5029 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 18:10:03 +0200 Subject: [PATCH 76/77] linux --- Sources/InterposeKit/ObjectHook.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index 9db1407..1f3ec0d 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -130,12 +130,14 @@ extension Interpose { } private func replaceGetClass(in class: AnyClass, decoy perceivedClass: AnyClass) { + #if !os(Linux) // crashes on linux let getClass: @convention(block) (UnsafeRawPointer?) -> AnyClass = { _ in perceivedClass } let impl = imp_implementationWithBlock(getClass as Any) _ = class_replaceMethod(`class`, ObjCSelector.getClass, impl, ObjCMethodEncoding.getClass) _ = class_replaceMethod(object_getClass(`class`), ObjCSelector.getClass, impl, ObjCMethodEncoding.getClass) + #endif } #if !os(Linux) From b90bdcdf4cf82a69fbbf2cedb6c704a0069a7e66 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2020 18:19:33 +0200 Subject: [PATCH 77/77] add docs --- Sources/InterposeKit/AnyHook.swift | 16 ++++++++++------ Sources/InterposeKit/LinuxCompileSupport.swift | 1 + Sources/InterposeKit/ObjectHook.swift | 12 +++++++----- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/Sources/InterposeKit/AnyHook.swift b/Sources/InterposeKit/AnyHook.swift index ebdc227..829b74d 100644 --- a/Sources/InterposeKit/AnyHook.swift +++ b/Sources/InterposeKit/AnyHook.swift @@ -1,15 +1,21 @@ import Foundation +/// Base class, represents a hook to exactly one method. public class AnyHook { + /// The class this hook is based on. public let `class`: AnyClass + + /// The selector this hook interposes. public let selector: Selector + + /// The current state of the hook. public internal(set) var state = State.prepared // else we validate init order - public internal(set) var replacementIMP: IMP! + var replacementIMP: IMP! // fetched at apply time, changes late, thus class requirement - public internal(set) var origIMP: IMP? + var origIMP: IMP? /// The possible task states public enum State: Equatable { @@ -50,10 +56,6 @@ public class AnyHook { try execute(newState: .prepared) { try resetImplementation() } return self } - - public func callAsFunction(_ type: U.Type) -> U { - unsafeBitCast(origIMP, to: type) - } /// Validate that the selector exists on the active class. @discardableResult func validate(expectedState: State = .prepared) throws -> Method { @@ -86,7 +88,9 @@ public class AnyHook { } } +/// Hook baseclass with generic signatures. public class TypedHook: AnyHook { + /// The original implementation of the hook. Might be looked up at runtime. Do not cache this. public var original: MethodSignature { preconditionFailure("Always override") } diff --git a/Sources/InterposeKit/LinuxCompileSupport.swift b/Sources/InterposeKit/LinuxCompileSupport.swift index bf9489d..43f901c 100644 --- a/Sources/InterposeKit/LinuxCompileSupport.swift +++ b/Sources/InterposeKit/LinuxCompileSupport.swift @@ -41,6 +41,7 @@ func imp_getBlock(_ anImp: IMP) -> Any? { return nil } // AutoreleasingUnsafeMutablePointer is not available on Linux. typealias NSErrorPointer = UnsafeMutablePointer? extension NSObject { + /// :nodoc: value open func value(forKey key: String) -> Any? { return nil } } /// :nodoc: objc_AssociationPolicy diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index 1f3ec0d..85bd0b0 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -22,13 +22,13 @@ extension Interpose { static var hookForBlock: UInt8 = 0 } - public class WeakObjectContainer: NSObject { + class WeakObjectContainer: NSObject { private weak var _object: T? - public var object: T? { + var object: T? { return _object } - public init(with object: T?) { + init(with object: T?) { _object = object } } @@ -44,7 +44,10 @@ extension Interpose { /// A hook to an instance method of a single object, stores both the original and new implementation. /// Think about: Multiple hooks for one object final public class ObjectHook: TypedHook { + + /// The object that is being hooked. public let object: AnyObject + /// Subclass that we create on the fly var dynamicSubclass: AnyClass? @@ -160,8 +163,7 @@ extension Interpose { private func addSuperTrampolineMethod(subclass: AnyClass) { } #endif - - /// The original implementation is looked up at runtime . + /// The original implementation of the hook. Might be looked up at runtime. Do not cache this. public override var original: MethodSignature { // If we switched implementations, return stored. if let savedOrigIMP = origIMP {