diff --git a/InterposeKit.xcodeproj/project.pbxproj b/InterposeKit.xcodeproj/project.pbxproj index 81865b4..31f96df 100644 --- a/InterposeKit.xcodeproj/project.pbxproj +++ b/InterposeKit.xcodeproj/project.pbxproj @@ -17,9 +17,13 @@ 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, ); }; }; + 781EB556249BEF54002545B4 /* ObjCInvocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781EB555249BEF54002545B4 /* ObjCInvocation.swift */; }; + 781EB558249BF0D1002545B4 /* DynamicHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781EB557249BF0D1002545B4 /* DynamicHook.swift */; }; + 781EB55A249BFA58002545B4 /* DynamicHookTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781EB559249BFA58002545B4 /* DynamicHookTests.swift */; }; 78A2F265249635B100F5AC5F /* KVOTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F264249635B100F5AC5F /* KVOTests.swift */; }; 78A2F26724964AF200F5AC5F /* InterposeKitTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F26624964AF200F5AC5F /* InterposeKitTestCase.swift */; }; 78A2F26E2496B54B00F5AC5F /* InterposeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A2F26D2496B54B00F5AC5F /* InterposeError.swift */; }; + 78AB64B5249EAED3002394CD /* InterposeRuntime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78AB64B4249EAED3002394CD /* InterposeRuntime.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 */; }; @@ -78,11 +82,15 @@ 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 = ""; }; + 781EB555249BEF54002545B4 /* ObjCInvocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ObjCInvocation.swift; path = Sources/InterposeKit/ObjCInvocation.swift; sourceTree = SOURCE_ROOT; }; + 781EB557249BF0D1002545B4 /* DynamicHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DynamicHook.swift; path = Sources/InterposeKit/DynamicHook.swift; sourceTree = SOURCE_ROOT; }; + 781EB559249BFA58002545B4 /* DynamicHookTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DynamicHookTests.swift; path = Tests/InterposeKitTests/DynamicHookTests.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 = ""; }; 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; }; 78A2F26D2496B54B00F5AC5F /* InterposeError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InterposeError.swift; path = Sources/InterposeKit/InterposeError.swift; sourceTree = SOURCE_ROOT; }; + 78AB64B4249EAED3002394CD /* InterposeRuntime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InterposeRuntime.swift; path = Sources/InterposeKit/InterposeRuntime.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; }; @@ -184,10 +192,13 @@ 78EDB8DC248BAA5600D2F6C1 /* AnyHook.swift */, 7810959D248D43DC008A943C /* ClassHook.swift */, 78EDB8FE248D0A9900D2F6C1 /* ObjectHook.swift */, - 78EDB902248D42CD00D2F6C1 /* LinuxCompileSupport.swift */, + 781EB557249BF0D1002545B4 /* DynamicHook.swift */, 7810959F248D50C1008A943C /* Watcher.swift */, 78E20D9724981B2A0021552C /* InterposeSubclass.swift */, 780FC9F9249822C900DA5A14 /* HookFinder.swift */, + 781EB555249BEF54002545B4 /* ObjCInvocation.swift */, + 78EDB902248D42CD00D2F6C1 /* LinuxCompileSupport.swift */, + 78AB64B4249EAED3002394CD /* InterposeRuntime.swift */, ); path = InterposeKit; sourceTree = ""; @@ -208,6 +219,7 @@ 78EDB8D4248B9BB500D2F6C1 /* TestClass.swift */, 78C39D922483169300B46395 /* InterposeKitTests.swift */, 78EDB8D6248B9C1200D2F6C1 /* ObjectInterposeTests.swift */, + 781EB559249BFA58002545B4 /* DynamicHookTests.swift */, 78C5A4A72494D75100EE9756 /* MultipleInterposing.swift */, 78A2F264249635B100F5AC5F /* KVOTests.swift */, 78A2F26624964AF200F5AC5F /* InterposeKitTestCase.swift */, @@ -405,7 +417,10 @@ 78C39D912483165600B46395 /* InterposeKit.swift in Sources */, 78EDB8FF248D0A9900D2F6C1 /* ObjectHook.swift in Sources */, 78EDB903248D42CD00D2F6C1 /* LinuxCompileSupport.swift in Sources */, + 781EB556249BEF54002545B4 /* ObjCInvocation.swift in Sources */, 78E20D9824981B2A0021552C /* InterposeSubclass.swift in Sources */, + 781EB558249BF0D1002545B4 /* DynamicHook.swift in Sources */, + 78AB64B5249EAED3002394CD /* InterposeRuntime.swift in Sources */, 78EDB8DD248BAA5600D2F6C1 /* AnyHook.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -418,6 +433,7 @@ 78C5A4A82494D75100EE9756 /* MultipleInterposing.swift in Sources */, 78EDB8DA248BA9B300D2F6C1 /* TestClass.swift in Sources */, 78EDB8DB248BA9BB00D2F6C1 /* ObjectInterposeTests.swift in Sources */, + 781EB55A249BFA58002545B4 /* DynamicHookTests.swift in Sources */, 78A2F26724964AF200F5AC5F /* InterposeKitTestCase.swift in Sources */, 78A2F265249635B100F5AC5F /* KVOTests.swift in Sources */, ); diff --git a/Sources/InterposeKit/AnyHook.swift b/Sources/InterposeKit/AnyHook.swift index 1c8d581..69d8fab 100644 --- a/Sources/InterposeKit/AnyHook.swift +++ b/Sources/InterposeKit/AnyHook.swift @@ -37,6 +37,11 @@ public class AnyHook { try validate() } + /// Helper to get class wrapper + var klass: InterposeClass { + InterposeClass(`class`) + } + func replaceImplementation() throws { preconditionFailure("Not implemented") } diff --git a/Sources/InterposeKit/ClassHook.swift b/Sources/InterposeKit/ClassHook.swift index fdbb748..1206a68 100644 --- a/Sources/InterposeKit/ClassHook.swift +++ b/Sources/InterposeKit/ClassHook.swift @@ -14,15 +14,17 @@ extension Interpose { 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) } + + origIMP = try klass.replace(method: method, imp: replacementIMP) 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)) + + + let previousIMP = try klass.replace(method: method, imp: origIMP!) guard previousIMP == replacementIMP else { throw InterposeError.unexpectedImplementation(`class`, selector, previousIMP) } diff --git a/Sources/InterposeKit/DynamicHook.swift b/Sources/InterposeKit/DynamicHook.swift new file mode 100644 index 0000000..e15bca7 --- /dev/null +++ b/Sources/InterposeKit/DynamicHook.swift @@ -0,0 +1,132 @@ +import Foundation + +extension Interpose { + + public enum AspectStrategy { + case before /// Called before the original implementation. + case instead /// Called insted of the original implementation. + case after /// Called after the original implementation. + } + + /// Hook that uses `NSInvocation `to not require specific signatures + /// The call is converted into an invocation via `_objc_msgForward`. + final public class DynamicHook: AnyHook { + + /// The object that is being hooked. + public let object: AnyObject + + /// The position of this hook. + public let strategy: AspectStrategy + + /// The stored action to be called + public let action: (AnyObject) -> Void + + /// Subclass that we create on the fly + var subclass: InterposeSubclass! + + // Logic switch to use super builder + let makesSuperIMP: Bool + + public init(object: AnyObject, + selector: Selector, + strategy: AspectStrategy = .before, + makeSuper: Bool = true, + implementation: @escaping (AnyObject) -> Void) throws { + if makeSuper && !InterposeSubclass.supportsSuperTrampolines { + throw InterposeError.superTrampolineNotAvailable + } + + self.object = object + self.strategy = strategy + self.action = implementation + self.makesSuperIMP = makeSuper + try super.init(class: type(of: object), selector: selector) + } + + private lazy var forwardIMP: IMP = { + resolve(symbol: "_objc_msgForward") + }() + + // stret is needed for x86-64 struct returns but not for ARM64 + private lazy var forwardStretIMP: IMP = { + resolve(symbol: "_objc_msgForward_stret") + }() + + override func replaceImplementation() throws { + let method = try validate() + + // Check if there's an existing subclass we can reuse. + // Create one at runtime if there is none. + let subclass = try InterposeSubclass(object: object) + try subclass.prepareDynamicInvocation() + self.subclass = subclass + + // If there is no existing implementation, add one. + if !subclass.implementsExact(selector: selector) { + // Add super trampoline, then swizzle + subclass.addSuperTrampoline(selector: selector) + let superCallingMethod = subclass.instanceMethod(selector)! + + // add a prefixed copy of the method + let aspectSelector = InterposeSubclass.aspectPrefixed(selector) + let origImp = superCallingMethod.implementation + + try subclass.add(selector: aspectSelector, imp: origImp, encoding: method.typeEncoding) + + Interpose.log("maked -[\(`class`).\(aspectSelector)]: \(origImp)") + } + + // append hook as copy + let newContainer = DynamicHookContainer() + var hooks = subclass.hookContainer?.hooks ?? [] + hooks.append(self) + newContainer.hooks = hooks + subclass.hookContainer = newContainer + + try subclass.replace(method: method, imp: self.forwardIMP) + Interpose.log("Added dynamic -[\(`class`).\(selector)]") + } + + override func resetImplementation() throws { + let method = try validate(expectedState: .interposed) + + // Get the super-implementation via the prefixed method... + let aspectSelector = InterposeSubclass.aspectPrefixed(selector) + + let superIMP = try subclass.methodImplementation(aspectSelector) + + // ... and replace the original + // The subclassed method can't be removed, but will be unused. + let origIMP = try subclass.replace(method: method, imp: superIMP) + + // If the IMP does not match our expectations, throw! + // TODO: guard for dynamic + static hook mix! + guard origIMP == forwardIMP else { + throw InterposeError.unexpectedImplementation(subclass.class, selector, origIMP) + } + + Interpose.log("Removed dynamic -[\(`class`).\(selector)]") + } + } + + /// Store all hooks + class DynamicHookContainer { + var hooks: [DynamicHook] = [] + + var before: [DynamicHook] { + hooks.filter { $0.strategy == .before } + } + var instead: [DynamicHook] { + hooks.filter { $0.strategy == .instead } + } + var after: [DynamicHook] { + hooks.filter { $0.strategy == .after } + } + } +} + +extension Collection where Iterator.Element == Interpose.DynamicHook { + func executeAll(_ bSelf: AnyObject) { + forEach { $0.action(bSelf) } + } +} diff --git a/Sources/InterposeKit/HookFinder.swift b/Sources/InterposeKit/HookFinder.swift index fccf06f..7e0d0c0 100644 --- a/Sources/InterposeKit/HookFinder.swift +++ b/Sources/InterposeKit/HookFinder.swift @@ -2,8 +2,9 @@ import Foundation extension Interpose { - private struct AssociatedKeys { + struct AssociatedKeys { static var hookForBlock: UInt8 = 0 + static var hookContainer: UInt8 = 0 } private class WeakObjectContainer: NSObject { @@ -17,6 +18,12 @@ extension Interpose { } } + /// Helper to resolve an implementation + static func resolve(symbol: String) -> IMP { + let imp = dlsym(dlopen(nil, RTLD_LAZY), symbol) + return unsafeBitCast(imp, to: IMP.self) + } + static func storeHook(hook: HookType, to block: AnyObject) { // Weakly store reference to hook inside the block of the IMP. objc_setAssociatedObject(block, &AssociatedKeys.hookForBlock, diff --git a/Sources/InterposeKit/InterposeError.swift b/Sources/InterposeKit/InterposeError.swift index e19a372..4be86f8 100644 --- a/Sources/InterposeKit/InterposeError.swift +++ b/Sources/InterposeKit/InterposeError.swift @@ -34,6 +34,9 @@ public enum InterposeError: LocalizedError { /// Can't revert or apply if already done so. case invalidState(expectedState: AnyHook.State) + /// SuperBuilder is not in the runtime but required for this configuration. + case superTrampolineNotAvailable + /// Unable to remove hook. case resetUnsupported(_ reason: String) @@ -51,22 +54,34 @@ extension InterposeError: Equatable { 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 \(type(of: obj)) posing as \(NSStringFromClass(actualClass))/" + case .invalidState(let expectedState): return "Invalid State. Expected: \(expectedState)" + case .resetUnsupported(let reason): return "Reset Unsupported: \(reason)" + + case .superTrampolineNotAvailable: + return "SuperBuilder is required but not available at runtime." + case .unknownError(let reason): return reason } diff --git a/Sources/InterposeKit/InterposeKit.swift b/Sources/InterposeKit/InterposeKit.swift index 24c2a61..7b8ef09 100644 --- a/Sources/InterposeKit/InterposeKit.swift +++ b/Sources/InterposeKit/InterposeKit.swift @@ -15,13 +15,22 @@ extension NSObject { } } + /// Hook an `@objc dynamic` instance method via selector on the current object or class.. + @discardableResult public func hook ( + _ selector: Selector, + strategy: Interpose.AspectStrategy = .before, + _ implementation: @escaping (AnyObject) -> Void) throws -> AnyHook { + try Interpose.DynamicHook(object: self, selector: selector, + strategy: strategy, 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, + try Interpose.ClassHook(class: self as AnyClass, selector: selector, implementation: implementation).apply() } } @@ -51,7 +60,7 @@ final public class Interpose { } // This is based on observation, there is no documented way - private func isKVORuntimeGeneratedClass(_ klass: AnyClass) -> Bool { + private func isKVORuntimemakedClass(_ klass: AnyClass) -> Bool { NSStringFromClass(klass).hasPrefix("NSKVO") } @@ -73,7 +82,7 @@ final public class Interpose { self.class = type(of: object) if let actualClass = checkObjectPosingAsDifferentClass(object) { - if isKVORuntimeGeneratedClass(actualClass) { + if isKVORuntimemakedClass(actualClass) { throw InterposeError.keyValueObservationDetected(object) } else { throw InterposeError.objectPosingAsDifferentClass(object, actualClass: actualClass) @@ -97,8 +106,8 @@ final public class Interpose { hookSignature: HookSignature.Type = HookSignature.self, _ implementation: (TypedHook) -> HookSignature?) throws -> TypedHook { - try hook(NSSelectorFromString(selName), - methodSignature: methodSignature, hookSignature: hookSignature, implementation) + try hook(NSSelectorFromString(selName), + methodSignature: methodSignature, hookSignature: hookSignature, implementation) } /// Hook an `@objc dynamic` instance method via selector on the current class. diff --git a/Sources/InterposeKit/InterposeRuntime.swift b/Sources/InterposeKit/InterposeRuntime.swift new file mode 100644 index 0000000..6945b03 --- /dev/null +++ b/Sources/InterposeKit/InterposeRuntime.swift @@ -0,0 +1,87 @@ +import Foundation + +extension Method { + var implementation: IMP { + method_getImplementation(self) + } + + var typeEncoding: UnsafePointer? { + method_getTypeEncoding(self) + } + + var name: Selector { + method_getName(self) + } +} + +/// Looks for an instance method in `klass`, without looking up the hierarchy. +func implementsExactClass(klass: AnyClass, selector: Selector) -> Bool { + var methodCount: CUnsignedInt = 0 + guard let methodsInAClass = class_copyMethodList(klass, &methodCount) else { return false } + defer { free(methodsInAClass) } + for index in 0 ..< Int(methodCount) { + let method = methodsInAClass[index] + if method_getName(method) == selector { + return true + } + } + return false +} + +struct InterposeClass: ModifyableClass { + let `class`: AnyClass + + init(_ class: AnyClass) { + self.`class` = `class` + } +} + +protocol ModifyableClass { + var `class`: AnyClass { get } + func add(selector: Selector, imp: IMP, encoding: UnsafePointer?) throws + @discardableResult func replace(method: Method, imp: IMP) throws -> IMP + @discardableResult func replace(selector: Selector, imp: IMP, encoding: UnsafePointer?) throws -> IMP + func implementsExact(selector: Selector) -> Bool + func instanceMethod(_ selector: Selector) -> Method? + func methodImplementation(_ selector: Selector) throws -> IMP +} + +extension ModifyableClass { + var superclass: AnyClass? { + class_getSuperclass(`class`) + } + + func add(selector: Selector, imp: IMP, encoding: UnsafePointer?) throws { + if !class_addMethod(`class`, selector, imp, encoding) { + Interpose.log("Unable to add: -[\(`class`).\(selector)] IMP: \(imp)") + throw InterposeError.unableToAddMethod(`class`, selector) + } + } + + @discardableResult func replace(method: Method, imp: IMP) throws -> IMP { + try replace(selector: method.name, imp: imp, encoding: method.typeEncoding) + } + + @discardableResult func replace(selector: Selector, imp: IMP, encoding: UnsafePointer?) throws -> IMP { + guard let imp = class_replaceMethod(`class`, selector, imp, encoding) else { + throw InterposeError.unableToAddMethod(`class`, selector) + } + return imp + } + + /// Looks for an instance method in this subclass, without looking up the hierarchy. + func implementsExact(selector: Selector) -> Bool { + implementsExactClass(klass: `class`, selector: selector) + } + + func instanceMethod(_ selector: Selector) -> Method? { + class_getInstanceMethod(`class`, selector) + } + + func methodImplementation(_ selector: Selector) throws -> IMP { + guard let imp = class_getMethodImplementation(`class`, selector) else { + throw InterposeError.unknownError("Unable to get method implementation") + } + return imp + } +} diff --git a/Sources/InterposeKit/InterposeSubclass.swift b/Sources/InterposeKit/InterposeSubclass.swift index 7752ce1..8ee52a9 100644 --- a/Sources/InterposeKit/InterposeSubclass.swift +++ b/Sources/InterposeKit/InterposeSubclass.swift @@ -1,6 +1,6 @@ import Foundation -class InterposeSubclass { +class InterposeSubclass: ModifyableClass { private enum Constants { static let subclassSuffix = "InterposeKit_" @@ -8,10 +8,14 @@ class InterposeSubclass { enum ObjCSelector { static let getClass = Selector((("class"))) + static let forwardInvocation = Selector((("forwardInvocation:"))) + //static let methodSignatureForSelector = Selector((("methodSignatureForSelector:"))) } enum ObjCMethodEncoding { static let getClass = extract("#@:") + static let forwardInvocation = extract("v@:@") + //static let methodSignatureForSelector = extract("v@::") private static func extract(_ string: StaticString) -> UnsafePointer { return UnsafeRawPointer(string.utf8Start).assumingMemoryBound(to: CChar.self) @@ -22,7 +26,17 @@ class InterposeSubclass { let object: AnyObject /// Subclass that we create on the fly - private(set) var dynamicClass: AnyClass + private(set) var `class`: AnyClass + + /// Hooks that have to be called dynamically. + var hookContainer: Interpose.DynamicHookContainer? { + get { objc_getAssociatedObject(object, &Interpose.AssociatedKeys.hookContainer) + as? Interpose.DynamicHookContainer } + set { + objc_setAssociatedObject(object, &Interpose.AssociatedKeys.hookContainer, + newValue, .OBJC_ASSOCIATION_RETAIN) + } + } /// If the class has been altered (e.g. via NSKVONotifying_ KVO logic) /// then perceived and actual class don't match. @@ -31,8 +45,8 @@ class InterposeSubclass { /// If we make a dynamic subclass over KVO, invalidating the token crashes in cache_getImp. init(object: AnyObject) throws { self.object = object - dynamicClass = type(of: object) // satisfy set to something - dynamicClass = try getExistingSubclass() ?? createSubclass() + `class` = type(of: object) // satisfy set to something + `class` = try getExistingSubclass() ?? createSubclass() } private func createSubclass() throws -> AnyClass { @@ -62,7 +76,7 @@ class InterposeSubclass { object_setClass(object, nnSubclass) let oldName = NSStringFromClass(class_getSuperclass(object_getClass(object)!)!) - Interpose.log("Generated \(NSStringFromClass(nnSubclass)) for object (was: \(oldName))") + Interpose.log("maked \(NSStringFromClass(nnSubclass)) for object (was: \(oldName))") return nnSubclass } @@ -75,7 +89,63 @@ class InterposeSubclass { return nil } + /// Overrides the invocation forwarding machinery to support dynamic invocation. + func prepareDynamicInvocation() throws { + guard InterposeSubclass.supportsSuperTrampolines else { throw InterposeError.unknownError("SuperBuilder is required for dynamic invocation")} + + try replaceForwardInvocation() + } + #if !os(Linux) + + class func aspectPrefixed(_ selector: Selector) -> Selector { + Selector("interpose_" + selector.description) + } + + /// Test if the class requires adding dynamic implementation preparation hooks. + private func requiresPrepareDynamicInvocation() -> Bool { + implementsExact(selector: ObjCSelector.forwardInvocation) == false + } + + private func replaceForwardInvocation() throws { + guard requiresPrepareDynamicInvocation() else { return } + + // Add super trampoline + addSuperTrampoline(selector: ObjCSelector.forwardInvocation) + + // Replace with custom handler that calls our hooks + var origImp: IMP? + let forwardInvocation: @convention(block) (AnyObject, ObjCInvocation) -> Void = { bSelf, invocation in + + if let hookContainer = self.hookContainer { + hookContainer.before.executeAll(bSelf) + + // Call instead hooks or original + let instead = hookContainer.instead + if instead.isEmpty { + let selector = invocation.selector() + let prefixedSelector = InterposeSubclass.aspectPrefixed(selector) + invocation.setSelector(prefixedSelector) + invocation.invoke() + } else { + instead.executeAll(bSelf) + } + + hookContainer.after.executeAll(bSelf) + + } else { + // Call original forward + // - (void)forwardInvocation:(NSInvocation *)anInvocation + let originalInvocation = unsafeBitCast(origImp!, to: (@convention(c) (AnyObject, Selector, AnyObject) -> Void).self) + originalInvocation(bSelf, ObjCSelector.forwardInvocation, invocation) + + } + } + + let impl = imp_implementationWithBlock(forwardInvocation as Any) + origImp = try replace(selector: ObjCSelector.forwardInvocation, imp: impl, encoding: ObjCMethodEncoding.getClass) + } + private func replaceGetClass(in class: AnyClass, decoy perceivedClass: AnyClass) { // crashes on linux let getClass: @convention(block) (AnyObject) -> AnyClass = { _ in @@ -91,18 +161,17 @@ class InterposeSubclass { } private lazy var addSuperImpl: @convention(c) (AnyClass, Selector, NSErrorPointer) -> Bool = { - let handle = dlopen(nil, RTLD_LAZY) - let imp = dlsym(handle, "IKTAddSuperImplementationToClass") + let imp = Interpose.resolve(symbol: "IKTAddSuperImplementationToClass") return unsafeBitCast(imp, to: (@convention(c) (AnyClass, Selector, NSErrorPointer) -> Bool).self) }() func addSuperTrampoline(selector: Selector) { var error: NSError? - if addSuperImpl(dynamicClass, selector, &error) == false { - Interpose.log("Failed to add super implementation to -[\(dynamicClass).\(selector)]: \(error!)") + if addSuperImpl(`class`, selector, &error) == false { + Interpose.log("Failed to add super implementation to -[\(`class`).\(selector)]: \(error!)") } else { - let imp = class_getMethodImplementation(dynamicClass, selector)! - Interpose.log("Added super for -[\(dynamicClass).\(selector)]: \(imp)") + let imp = class_getMethodImplementation(`class`, selector)! + Interpose.log("Added super for -[\(`class`).\(selector)]: \(imp)") } } #else diff --git a/Sources/InterposeKit/ObjCInvocation.swift b/Sources/InterposeKit/ObjCInvocation.swift new file mode 100644 index 0000000..814c696 --- /dev/null +++ b/Sources/InterposeKit/ObjCInvocation.swift @@ -0,0 +1,31 @@ +import Foundation + +/// `NSInvocation` is not directly accessible in Swift so we use a protocol. +@objc internal protocol ObjCInvocation { + @objc(setSelector:) + func setSelector(_ selector: Selector) + + @objc(selector) + func selector() -> Selector + + @objc(target) + var objcTarget: AnyObject { get } + + @objc(methodSignature) + var objcMethodSignature: AnyObject { get } + + @objc(getArgument:atIndex:) + func getArgument(_ argumentLocation: UnsafeMutableRawPointer, atIndex idx: Int) + + @objc(setArgument:atIndex:) + func setArgument(_ argumentLocation: UnsafeMutableRawPointer, atIndex idx: Int) + + @objc(invoke) + func invoke() + + @objc(invokeWithTarget:) + func invoke(target: AnyObject) + + @objc(invocationWithMethodSignature:) + static func invocation(methodSignature: AnyObject) -> AnyObject +} diff --git a/Sources/InterposeKit/ObjectHook.swift b/Sources/InterposeKit/ObjectHook.swift index 51bfd83..d6a49c2 100644 --- a/Sources/InterposeKit/ObjectHook.swift +++ b/Sources/InterposeKit/ObjectHook.swift @@ -10,14 +10,22 @@ extension Interpose { public let object: AnyObject /// Subclass that we create on the fly - var interposeSubclass: InterposeSubclass? + /// maked when replace is called, checked via state. + var subclass: InterposeSubclass! // Logic switch to use super builder - let generatesSuperIMP = InterposeSubclass.supportsSuperTrampolines + let makesSuperIMP: Bool /// Initialize a new hook to interpose an instance method. - public init(object: AnyObject, selector: Selector, + public init(object: AnyObject, + selector: Selector, + makeSuper: Bool = true, implementation: (ObjectHook) -> HookSignature?) throws { + if makeSuper && !InterposeSubclass.supportsSuperTrampolines { + throw InterposeError.superTrampolineNotAvailable + } + self.makesSuperIMP = makeSuper + self.object = object try super.init(class: type(of: object), selector: selector) let block = implementation(self) as AnyObject @@ -55,7 +63,7 @@ extension Interpose { repeat { if let currentClass = currentClass, let method = class_getInstanceMethod(currentClass, self.selector) { - let origIMP = method_getImplementation(method) + let origIMP = method.implementation return unsafeBitCast(origIMP, to: MethodSignature.self) } currentClass = class_getSuperclass(currentClass) @@ -63,30 +71,12 @@ 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 index in 0 ..< Int(methodCount) { - let method = methodsInAClass[index] - if method_getName(method) == selector { - return true - } - } - return false - } - - var dynamicSubclass: AnyClass { - interposeSubclass!.dynamicClass - } - override func replaceImplementation() throws { let method = try validate() // Check if there's an existing subclass we can reuse. // Create one at runtime if there is none. - interposeSubclass = try InterposeSubclass(object: object) + subclass = try InterposeSubclass(object: object) // The implementation of the call that is hooked must exist. guard lookupOrigIMP != nil else { @@ -94,40 +84,26 @@ extension Interpose { } // This function searches superclasses for implementations - let hasExistingMethod = exactClassImplementsSelector(dynamicSubclass, selector) - let encoding = method_getTypeEncoding(method) + let hasExistingMethod = subclass!.implementsExact(selector: selector) - if self.generatesSuperIMP { + if self.makesSuperIMP { // If the subclass is empty, we create a super trampoline first. // If a hook already exists, we must skip this. if !hasExistingMethod { - interposeSubclass!.addSuperTrampoline(selector: selector) + subclass!.addSuperTrampoline(selector: selector) } // 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) - } + origIMP = try subclass.replace(method: method, imp: replacementIMP) 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 { - Interpose.log("Added -[\(`class`).\(selector)] IMP: \(replacementIMP!) via replacement") - } else { - Interpose.log("Unable to replace: -[\(`class`).\(selector)] IMP: \(replacementIMP!)") - throw InterposeError.unableToAddMethod(`class`, selector) - } + origIMP = try subclass.replace(method: method, imp: replacementIMP) + Interpose.log("Added -[\(`class`).\(selector)] IMP: \(replacementIMP!) via replacement") } else { - 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) - } + try subclass.add(selector: selector, imp: replacementIMP, encoding: method.typeEncoding) + Interpose.log("Added -[\(`class`).\(selector)] IMP: \(replacementIMP!)") } } } @@ -147,16 +123,13 @@ extension Interpose { throw InterposeError.resetUnsupported("No Original IMP found. SuperBuilder missing?") } - guard let currentIMP = class_getMethodImplementation(dynamicSubclass, selector) else { - throw InterposeError.unknownError("No Implementation found") - } + let currentIMP = try subclass.methodImplementation(selector) // We are the topmost hook, replace method. if currentIMP == replacementIMP { - let previousIMP = class_replaceMethod( - dynamicSubclass, selector, origIMP!, method_getTypeEncoding(method)) + let previousIMP = try subclass.replace(method: method, imp: origIMP!) guard previousIMP == replacementIMP else { - throw InterposeError.unexpectedImplementation(dynamicSubclass, selector, previousIMP) + throw InterposeError.unexpectedImplementation(subclass.class, selector, previousIMP) } Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") } else { diff --git a/Sources/SuperBuilder/src/ITKSuperBuilder.m b/Sources/SuperBuilder/src/ITKSuperBuilder.m index 9101052..57b998a 100644 --- a/Sources/SuperBuilder/src/ITKSuperBuilder.m +++ b/Sources/SuperBuilder/src/ITKSuperBuilder.m @@ -112,6 +112,21 @@ static BOOL ITKMethodIsSuperTrampoline(Method method) { return methodIMP == (IMP)msgSendSuperTrampoline || methodIMP == (IMP)msgSendSuperStretTrampoline; } +bool prefix(const char *pre, const char *str) { + return strncmp(pre, str, strlen(pre)) == 0; +} + +let ITKInterposePrefix = "interpose_"; + +/// If prefix was found, forward the char pointer and get a new selector +SEL ITKReplaceCmd(__unsafe_unretained id obj, SEL _cmd); +SEL ITKReplaceCmd(__unsafe_unretained id obj, SEL _cmd) { + let len = strlen(ITKInterposePrefix); + let sel = sel_getName(_cmd); + let hasPrefix = strncmp(ITKInterposePrefix, sel, len) == 0; + return hasPrefix ? sel_getUid(sel + len) : _cmd; +} + struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj, SEL _cmd); struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj, SEL _cmd) { /** @@ -197,6 +212,7 @@ asm volatile ( // 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" @@ -249,14 +265,20 @@ asm volatile ( // // 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 %%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) + // replace _cmd + "callq _ITKReplaceCmd \n" + "movq %%rax, %%rsi \n" + "movq %%rsi, -16(%%rbp) \n" // store modified _cmd + // fetch filled struct objc_super, call with self + _cmd + "movq -8(%%rbp), %%rdi \n" "callq _ITKReturnThreadSuper \n" // first param is now struct objc_super "movq %%rax, %%rdi \n" diff --git a/Tests/InterposeKitTests/DynamicHookTests.swift b/Tests/InterposeKitTests/DynamicHookTests.swift new file mode 100644 index 0000000..bf2f11b --- /dev/null +++ b/Tests/InterposeKitTests/DynamicHookTests.swift @@ -0,0 +1,38 @@ +import Foundation +import XCTest +@testable import InterposeKit + +final class DynamicInterposeTests: InterposeKitTestCase { + + func testDynamicSingleObject() throws { + let testObj = TestClass() + + // Test regular usage, calls block immediately + var executed = false + testObj.executeBlock { + executed = true + } + XCTAssertTrue(executed) + + // Add hook that is called before the block + var hookExecuted = false + let hook = try testObj.hook(#selector(TestClass.executeBlock)) { bSelf in + print("Before Interposing Dynamic Hook for \(bSelf)") + hookExecuted = true + } + + // Ensure that hook is called before the block + XCTAssertFalse(hookExecuted) + testObj.executeBlock { + // A before aspect is called before the block is executed + XCTAssertTrue(hookExecuted) + } + XCTAssertTrue(hookExecuted) + + try hook.revert() + + hookExecuted = false + testObj.executeBlock { } + XCTAssertFalse(hookExecuted) + } +} diff --git a/Tests/InterposeKitTests/TestClass.swift b/Tests/InterposeKitTests/TestClass.swift index f754ef0..2c03de2 100644 --- a/Tests/InterposeKitTests/TestClass.swift +++ b/Tests/InterposeKitTests/TestClass.swift @@ -34,6 +34,10 @@ class TestClass: NSObject { return testClassHi } + @objc dynamic func executeBlock(_ block: () -> ()) { + block() + } + @objc dynamic func doNothing() { } @objc dynamic func doubleString(string: String) -> String {