Coder Social home page Coder Social logo

steipete / interposekit Goto Github PK

View Code? Open in Web Editor NEW
981.0 16.0 49.0 5.59 MB

A modern library to swizzle elegantly in Swift.

Home Page: https://interposekit.com/

License: MIT License

Swift 75.85% Ruby 4.02% Objective-C 20.13%
swift swizzling interpose hook aspects

interposekit's Introduction

InterposeKit

SwiftPM xcodebuild pod lib lint Xcode 11.4+ Swift 5.2+

InterposeKit is a modern library to swizzle elegantly in Swift, supporting hooks on classes and individual objects. It is well-documented, tested, 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, which required tricky swizzling to fix, I also wrote up implementation thoughts on my blog.

Instead of adding new methods and exchanging implementations based on method_exchangeImplementations, this library replaces the implementation directly using class_replaceMethod. This avoids some of the usual problems with swizzling.

You can call the original implementation and add code before, instead or after a method call.
This is similar to the Aspects library, but doesn't yet do dynamic subclassing.

Compare: Swizzling a property without helper and with InterposeKit

Usage

Let's say you want to amend sayHi from TestClass:

class TestClass: NSObject {
    // Functions need to be marked as `@objc dynamic` or written in Objective-C.
    @objc dynamic func sayHi() -> String {
        print("Calling sayHi")
        return "Hi there πŸ‘‹"
    }
}

let interposer = try Interpose(TestClass.self) {
    try $0.prepareHook(
        #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) // free to skip
                print("After Interposing \(`self`)")
                return string + "and Interpose"
            }
    }
}

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

[Interposer] Swizzled -[TestClass.sayHi] IMP: 0x000000010d9f4430 -> 0x000000010db36020
Before Interposing <InterposeTests.TestClass: 0x7fa0b160c1e0>
Calling sayHi
After Interposing <InterposeTests.TestClass: 0x7fa0b160c1e0>
Hi there πŸ‘‹ and Interpose

Key Features

  • Interpose directly modifies the implementation of a Method, which is safer than selector-based swizzling.
  • 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.
  • 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.
  • Delayed Interposing helps when a class is loaded at runtime. This is useful for Mac Catalyst.

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.

try Interpose.whenAvailable(["RTIInput", "SystemSession"]) {
    let lock = DispatchQueue(label: "com.steipete.document-state-hack")
    try $0.hook("documentState", { store in { `self` in
        lock.sync {
            store((@convention(c) (AnyObject, Selector) -> 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})
}

FAQ

Why didn't you call it Interpose? "Kit" feels so old-school.

Naming it Interpose was the plan, but then SR-898 came. While having a class with the same name as the module works in most cases, this breaks when you enable build-for-distribution. There's some discussion 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. (See Dynamic function replacement #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 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".

It does not do X!

Pull Requests welcome! You might wanna open a draft before to lay out what you plan, I want to keep the feature-set minimal so it stays simple and no-magic.

Installation

Building InterposeKit requires Xcode 11.4+ or a Swift 5.2+ toolchain with the Swift Package Manager.

Swift Package Manager

Add .package(url: "https://github.com/steipete/InterposeKit.git", from: "0.0.1") to your Package.swift file's dependencies.

CocoaPods

InterposeKit is on CocoaPods. Add pod 'InterposeKit' to your Podfile.

Carthage

Add github "steipete/InterposeKit" to your Cartfile.

Improvement Ideas

  • Write proposal to allow to convert the calling convention of existing types.
  • Use the C block struct to perform type checking between Method type and C type (I do that in Aspects library), it's still a runtime crash but could be at hook time, not when we call it.
  • 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 to hook pure C functions
  • Combine Promise-API for Interpose.whenAvailable for better error bubbling.
  • Experiment with Swift function hooking? ⚑️
  • 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.
  • I'm sure there's more - Pull Requests or comments very welcome!

Make this happen: Carthage compatible CocoaPods

Thanks

Special thanks to JP Simard who did such a great job in setting up Yams with GitHub Actions - this was extremely helpful to build CI here fast.

License

InterposeKit is MIT Licensed.

interposekit's People

Contributors

colinhumber avatar dependabot[bot] avatar hannesoid avatar mattia avatar pedrovereza avatar rmigneco avatar steipete avatar zrzka avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

interposekit's Issues

Assert that swizzled implementation type signature matches original method

To make swizzling more robust, it would be ideal if the return/argument types of the swizzled implementation could be checked against the original method.

As for how to do that… I guess the only way would be to accept an Objective-C method (via target/selector parameters) instead of an Objective-C block?

How to use selector with number of parameters?

Hi there,
from example:
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" } }

But i need to set hook with the following signature:

methodSignature: (@convention(c) (AnyObject, Selector, AnyObject, AnyObject, AnyObject) -> Void).self, { store in {self in //to call the original method i need these three AnyObject values to pass. return store.original(self, store.selector, ?, ?, ?) } }

How to access these three AnyObject ?

Thanks in advance.

globalWatchers.append & filtering not synced via globalWatcherQueue

Quickly checked the code and I'm wondering ...

Global image watcher utilizes the globalWatcherQueue queue to filter globalWatchers:

InterposeWatcher.globalWatcherQueue.sync {

But there's another place where this queue isn't used, access to globalWatchers is not synced:

InterposeWatcher.globalWatchers.append(self)

Additional thing is that the globalWatchers.append(self) should be probably replaced by the InterposeWatcher.append(...) to encapsulate this whole watching stuff (globalWatchers).

Proposal: Match hook and callback type

Just a suggestion, perhaps you've thought of this and it won't work - I might be missing something.

Since the underlying implementation uses imp_implementationWithBlock, the hooked function has a different type than the block you supply (notably no selector). Would it be possible to instead pass an IMP directly, and then it might be possible to make sure they both have the same type? (type check that the callAsFunction and the block you supply have the same type)

Example here although I haven't tested it with the calling convention requirements:

func genericFunction<T>(_ selName: String, _ callback: @escaping (String, T.Type) -> T) {
  print("hi")
  callback("foo", T.self)
}

func noop() {}

genericFunction("test") { (str, type) -> () -> Void in
  print("Called with \(str)")
  print(type)
  return noop
}

This example passes the type into the method but you could potentially capture it into Task itself/make Task generic.

EDIT: Oh I think I see the issue, it has to be a block if you want to be able to reference the task.

Incompatible with bitcode

After adopting InterposeKit in my app, uploading a build to Apple fails when bitcode is enabled with the following error:

ITMS-90562: Invalid Bundle - The app submission can not be successfully recompiled from bitcode due to missing symbols during linking. You can try to reproduce and diagnose such issues locally by following the instructions from: https://developer.apple.com/library/archive/technotes/tn2432/_index.html

Uploading with bitcode disabled is fine. After reading the linked page I suspect it has something to do with the inline assembly in ITKSuperBuilder.m. I removed InterposeKit from my app and uploaded with bitcode without error to confirm that it was indeed InerposeKit causing the issue. Please challenge my assumption if you think something else is the issue.

Do you think this can be fixed? At least I think it would make sense to document this limitation in the readme.

Bad access when trying to call original selector

Trying to swizzle following:

  1. CLLocationManager.location [works fine]
  2. CLLocationManagerDelegate.locationManager(_:didUpdateLocations:) [Bad access when try to call original selector]

`

    do {
        _ = try Interpose(CLLocationManager.self, builder: {
            try $0.hook(#selector(getter: CLLocationManager.location),
                        methodSignature: (@convention(c) (AnyObject, Selector) -> CLLocation?).self,
                        hookSignature: (@convention(block) (AnyObject) -> CLLocation?).self) { store in {
                            `self` in
                            print("Before Interposing \(`self`)")
                            let originalLocation = store.original(`self`, store.selector) // free to skip
                            print("After Interposing \(`self`)")
                            
                            return originalLocation
                            }
                            
            }
        })
    } catch {
        print(error)
    }
    
    
    let classList = AppDelegate.getClassList().filter { class_conformsToProtocol($0, CLLocationManagerDelegate.self) }
    for c in classList {
        
        if !c.instancesRespond(to: #selector(CLLocationManagerDelegate.locationManager(_:didUpdateLocations:))) {
            continue
        }
        do {
            _ = try Interpose(c.self, builder: {
                try $0.hook(#selector(CLLocationManagerDelegate.locationManager(_:didUpdateLocations:)),
                            methodSignature: (@convention(c) (AnyObject, Selector, [CLLocation]) -> ()).self,
                            hookSignature: (@convention(block) (AnyObject, Selector, [CLLocation]) -> ()).self) { store in {

                                print("Before Interposing")
                                print($0)
                                print($1)
                                print($2)
                                
                                store.original($0, store.selector, $2) // free to skip
                                print("After Interposing \($0)")
                                }
                }
            })
        } catch {
            print(error)
        }
    }

`
Not able to figure out what is causing the issue.

Crash when compiled in Release

I am seeing crashes when compiled in Release mode with Xcode 15.3 and integrated with SPM with the following stack trace:

image (1)

Crash when hooking NSURL

let object = NSURL.init(string: "https://www.google.com")!
let hook = try? object.hook(
    #selector(getter: NSURL.host),
    methodSignature: (@convention(c) (AnyObject, Selector) -> String).self,
    hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { `self` in
    return "http://www.facebook.com"
        }
}
let host = object.host

Screenshot 2020-11-19 at 10 27 15 AM

it's similar with this issue. steipete/Aspects#177

`_dyld_register_func_for_add_image` callback is run before Objective-C classes are loaded

_dyld_register_func_for_add_image is called during dlopen but before the Objective-C classes in the image are loaded. The effect of this is that the interposition(s) for a given class are run on the next dlopen after the class is loaded. This may be too late depending on the interposition use case. Thoughts on clever ways to install a callback just after the Objective-C classes for a given image are loaded?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.