Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic Invocation #19

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion InterposeKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -78,11 +82,15 @@
781095B3248D6DFD008A943C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
781095B5248D6DFD008A943C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
781095B9248D6E0A008A943C /* InterposeTestHost.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = InterposeTestHost.entitlements; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
78C39D8E2483164500B46395 /* InterposeKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = InterposeKit.h; path = Sources/InterposeKit/InterposeKit.h; sourceTree = SOURCE_ROOT; };
Expand Down Expand Up @@ -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 = "<group>";
Expand All @@ -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 */,
Expand Down Expand Up @@ -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;
Expand All @@ -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 */,
);
Expand Down
5 changes: 5 additions & 0 deletions Sources/InterposeKit/AnyHook.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
8 changes: 5 additions & 3 deletions Sources/InterposeKit/ClassHook.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
132 changes: 132 additions & 0 deletions Sources/InterposeKit/DynamicHook.swift
Original file line number Diff line number Diff line change
@@ -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) }
}
}
9 changes: 8 additions & 1 deletion Sources/InterposeKit/HookFinder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: AnyObject>: NSObject {
Expand All @@ -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<HookType: AnyHook>(hook: HookType, to block: AnyObject) {
// Weakly store reference to hook inside the block of the IMP.
objc_setAssociatedObject(block, &AssociatedKeys.hookForBlock,
Expand Down
15 changes: 15 additions & 0 deletions Sources/InterposeKit/InterposeError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
}
Expand Down
19 changes: 14 additions & 5 deletions Sources/InterposeKit/InterposeKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<MethodSignature, HookSignature> (
_ selector: Selector,
methodSignature: MethodSignature.Type = MethodSignature.self,
hookSignature: HookSignature.Type = HookSignature.self,
_ implementation: (TypedHook<MethodSignature, HookSignature>) -> HookSignature?) throws -> AnyHook {
return try Interpose.ClassHook(class: self as AnyClass,
try Interpose.ClassHook(class: self as AnyClass,
selector: selector, implementation: implementation).apply()
}
}
Expand Down Expand Up @@ -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")
}

Expand All @@ -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)
Expand All @@ -97,8 +106,8 @@ final public class Interpose {
hookSignature: HookSignature.Type = HookSignature.self,
_ implementation: (TypedHook<MethodSignature, HookSignature>) -> HookSignature?)
throws -> TypedHook<MethodSignature, HookSignature> {
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.
Expand Down
Loading