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

Check warning on line 36 in Sources/InterposeKit/DynamicHook.swift

View check run for this annotation

Codecov / codecov/patch

Sources/InterposeKit/DynamicHook.swift#L36

Added line #L36 was not covered by tests
}

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")
}()

Check warning on line 53 in Sources/InterposeKit/DynamicHook.swift

View check run for this annotation

Codecov / codecov/patch

Sources/InterposeKit/DynamicHook.swift#L51-L53

Added lines #L51 - L53 were not covered by tests

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)

Check warning on line 105 in Sources/InterposeKit/DynamicHook.swift

View check run for this annotation

Codecov / codecov/patch

Sources/InterposeKit/DynamicHook.swift#L105

Added line #L105 was not covered by tests
}

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 @@
/// 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 @@
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."

Check warning on line 83 in Sources/InterposeKit/InterposeError.swift

View check run for this annotation

Codecov / codecov/patch

Sources/InterposeKit/InterposeError.swift#L83

Added line #L83 was not covered by tests

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 @@
}
}

/// 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,

Check warning on line 33 in Sources/InterposeKit/InterposeKit.swift

View check run for this annotation

Codecov / codecov/patch

Sources/InterposeKit/InterposeKit.swift#L33

Added line #L33 was not covered by tests
selector: selector, implementation: implementation).apply()
}
}
Expand Down Expand Up @@ -51,7 +60,7 @@
}

// 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 @@
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 @@
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)

Check warning on line 110 in Sources/InterposeKit/InterposeKit.swift

View check run for this annotation

Codecov / codecov/patch

Sources/InterposeKit/InterposeKit.swift#L109-L110

Added lines #L109 - L110 were not covered by tests
}

/// Hook an `@objc dynamic` instance method via selector on the current class.
Expand Down
Loading