From 809800060ff7298244f6640c7816f701e829ee38 Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Fri, 11 Apr 2025 20:12:44 +0200 Subject: [PATCH 01/13] Initial README rework --- README-legacy.md | 126 +++++++++++++++++++++++++++++ README.md | 202 ++++++++--------------------------------------- 2 files changed, 158 insertions(+), 170 deletions(-) create mode 100644 README-legacy.md diff --git a/README-legacy.md b/README-legacy.md new file mode 100644 index 0000000..f45d185 --- /dev/null +++ b/README-legacy.md @@ -0,0 +1,126 @@ +Instead of [adding new methods and exchanging implementations](https://nshipster.com/method-swizzling/) based on [`method_exchangeImplementations`](https://developer.apple.com/documentation/objectivec/1418769-method_exchangeimplementations), this library replaces the implementation directly using [`class_replaceMethod`](https://developer.apple.com/documentation/objectivec/1418677-class_replacemethod). This avoids some of [the usual problems with swizzling](https://pspdfkit.com/blog/2019/swizzling-in-swift/). + +## Usage + +Let's say you want to amend `sayHi` from `TestClass`: + +```swift +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! + +```swift +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 +Calling sayHi +After Interposing +Hi there 👋 and Interpose +``` + +## 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 + } + } +} +``` + +## FAQ + +### Why didn't you call it Interpose? "Kit" feels so old-school. +Naming it Interpose was the plan, but then [SR-898](https://bugs.swift.org/browse/SR-898) came. While having a class with the same name as the module works [in most cases](https://forums.swift.org/t/frameworkname-is-not-a-member-type-of-frameworkname-errors-inside-swiftinterface/28962), [this breaks](https://twitter.com/BalestraPatrick/status/1260928023357878273) when you enable build-for-distribution. There's some [discussion](https://forums.swift.org/t/pitch-fully-qualified-name-syntax/28482/81) to get that fixed, but this will be more towards end of 2020, if even. + +### I want to hook into Swift! You made another ObjC swizzle thingy, why? +UIKit and AppKit won't go away, and the bugs won't go away either. I see this as a rarely-needed instrument to fix system-level issues. There are ways to do some of that in Swift, but that's a separate (and much more difficult!) project. (See [Dynamic function replacement #20333](https://github.com/apple/swift/pull/20333) aka `@_dynamicReplacement` for details.) + +### Can I ship this? +Yes, absolutely. The goal for this one project is a simple library that doesn't try to be too smart. I did this in [Aspects](https://github.com/steipete/Aspects) and while I loved this to no end, it's problematic and can cause side-effects with other code that tries to be clever. InterposeKit is boring, so you don't have to worry about conditions like "We added New Relic to our app and now [your thing crashes](https://github.com/steipete/Aspects/issues/21)". + +### 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 15+ or a Swift 5.9+ 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`. + +## Improvement Ideas + +- Write proposal to allow to [convert the calling convention of existing types](https://twitter.com/steipete/status/1266799174563041282?s=21). +- Use the C block struct to perform type checking between Method type and C type (I do that in [Aspects library](https://github.com/steipete/Aspects)), it's still a runtime crash but could be at hook time, not when we call it. +- Add a way to get all current hooks from an object/class. +- Add a way to revert hooks without super helper. +- Add a way to apply multiple hooks to classes +- Enable hooking of class methods. +- Add [dyld_dynamic_interpose](https://twitter.com/steipete/status/1258482647933870080) to hook pure C functions +- Combine Promise-API for `Interpose.whenAvailable` for better error bubbling. +- Experiment with [Swift function hooking](https://github.com/rodionovd/SWRoute/wiki/Function-hooking-in-Swift)? ⚡️ +- Test against Swift Nightly as Cron Job +- Switch to Trampolines to manage cases where other code overrides super, so we end up with a super call that's [not on top of the class hierarchy](https://github.com/steipete/InterposeKit/pull/15#discussion_r439871752). diff --git a/README.md b/README.md index ac27ae6..4d89318 100644 --- a/README.md +++ b/README.md @@ -1,186 +1,48 @@ # InterposeKit -[![CI](https://github.com/structuredpath/InterposeKit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/structuredpath/InterposeKit/actions/workflows/ci.yml) -![Xcode 15+](https://img.shields.io/badge/Xcode-15%2B-blue.svg) -![Swift 5.9+](https://img.shields.io/badge/Swift-5.9%2B-orange.svg) +[![CI](https://github.com/structuredpath/InterposeKit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/structuredpath/InterposeKit/actions/workflows/ci.yml) ![Xcode 15+](https://img.shields.io/badge/Xcode-15%2B-blue.svg) ![Swift 5.9+](https://img.shields.io/badge/Swift-5.9%2B-orange.svg) -InterposeKit is a modern library to swizzle elegantly in Swift, supporting hooks on classes and individual objects. It is [well-documented](http://interposekit.com/), [tested](https://github.com/steipete/InterposeKit/actions?query=workflow%3ASwiftPM), written in "pure" Swift and works on `@objc dynamic` Swift functions or Objective-C instance methods. The Inspiration for InterposeKit was [a race condition in Mac Catalyst](https://steipete.com/posts/mac-catalyst-crash-hunt/), which required tricky swizzling to fix, I also wrote up [implementation thoughts on my blog](https://steipete.com/posts/interposekit/). +**InterposeKit** is a modern library for hooking Objective-C methods in Swift, also known as method swizzling. It supports both class-based and object-based hooks and it provides a clean, block-based, Swift-friendly API. -Instead of [adding new methods and exchanging implementations](https://nshipster.com/method-swizzling/) based on [`method_exchangeImplementations`](https://developer.apple.com/documentation/objectivec/1418769-method_exchangeimplementations), this library replaces the implementation directly using [`class_replaceMethod`](https://developer.apple.com/documentation/objectivec/1418677-class_replacemethod). This avoids some of [the usual problems with swizzling](https://pspdfkit.com/blog/2019/swizzling-in-swift/). - -You can call the original implementation and add code before, instead or after a method call. -This is similar to the [Aspects library](https://github.com/steipete/Aspects), but doesn't yet do dynamic subclassing. - -Compare: [Swizzling a property without helper and with InterposeKit](https://gist.github.com/steipete/f955aaa0742021af15add0133d8482b9) - -## Usage - -Let's say you want to amend `sayHi` from `TestClass`: - -```swift -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! - -```swift -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 -Calling sayHi -After Interposing -Hi there 👋 and Interpose -``` +This is a continuation and modernization of [Peter Steinberger’s original implementation](https://github.com/steipete/InterposeKit). For the background on why and how this revamp came about, see [my blog post](#). If you’re migrating, check out [what’s changed](#). ## Key Features -- Interpose directly modifies the implementation of a `Method`, which is [safer than selector-based swizzling]((https://pspdfkit.com/blog/2019/swizzling-in-swift/)). -- Interpose works on classes and individual objects. -- Hooks can easily be undone via calling `revert()`. This also checks and errors if someone else changed stuff in between. -- 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](https://steipete.com/posts/mac-catalyst-crash-hunt/). - -## Object Hooking - -InterposeKit can hook classes and object. Class hooking is similar to swizzling, but object-based hooking offers a variety of new ways to set hooks. This is achieved via creating a dynamic subclass at runtime. - -Caveat: Hooking will fail with an error if the object uses KVO. The KVO machinery is fragile and it's to easy to cause a crash. Using KVO after a hook was created is supported and will not cause issues. - -## Various ways to define the signature - -Next to using `methodSignature` and `hookSignature`, following variants to define the signature are also possible: - -### methodSignature + casted block -``` -let interposer = try Interpose(testObj) { - try $0.hook( - #selector(TestClass.sayHi), - methodSignature: (@convention(c) (AnyObject, Selector) -> String).self) { store in { `self` in - let string = store.original(`self`, store.selector) - return string + testString - } as @convention(block) (AnyObject) -> String } -} -``` - -### Define type via store object -``` -// Functions need to be `@objc dynamic` to be hookable. -let interposer = try Interpose(testObj) { - try $0.hook(#selector(TestClass.returnInt)) { (store: TypedHook<@convention(c) (AnyObject, Selector) -> Int, @convention(block) (AnyObject) -> Int>) in { +- Swift-friendly, minimal, thread-safe API. +- Block-based hooks targeting both classes and individual objects. +- Support for both instance and class methods. +- Hooks get access to the original implementation via a proxy. +- Object hooks are safely isolated using runtime subclassing, similar to the KVO mechanism. +- Hooks can be applied immediately or prepared and applied later, and safely reverted at any time[^1]. +- Direct `Method` implementation replacement rather than less-safe [selector-based swizzling](https://pspdfkit.com/blog/2019/swizzling-in-swift/). +- Typed signatures must be provided for both the original method and the hook block, enabling an ergonomic API. +- There’s no runtime type checking, and the signature has to be written twice—a trade-off to avoid `NSInvocation`. +- Written almost entirely in Swift on top of the Objective-C runtime[^2]. - // You're free to skip calling the original implementation. - let int = store.original($0, store.selector) - return int + returnIntOverrideOffset - } - } -} -``` +## Getting Started -## Delayed Hooking +- Installation Swift Package Manager +- Swift 5.9+, Xcode 15+ +- arm64 and x86_64 architectures +- Examples: instance method on class, class method on class, object -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. +## Changes from [Original Implementation](https://github.com/steipete/InterposeKit) -```swift -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}) -} -``` +## References - -## FAQ - -### Why didn't you call it Interpose? "Kit" feels so old-school. -Naming it Interpose was the plan, but then [SR-898](https://bugs.swift.org/browse/SR-898) came. While having a class with the same name as the module works [in most cases](https://forums.swift.org/t/frameworkname-is-not-a-member-type-of-frameworkname-errors-inside-swiftinterface/28962), [this breaks](https://twitter.com/BalestraPatrick/status/1260928023357878273) when you enable build-for-distribution. There's some [discussion](https://forums.swift.org/t/pitch-fully-qualified-name-syntax/28482/81) to get that fixed, but this will be more towards end of 2020, if even. - -### I want to hook into Swift! You made another ObjC swizzle thingy, why? -UIKit and AppKit won't go away, and the bugs won't go away either. I see this as a rarely-needed instrument to fix system-level issues. There are ways to do some of that in Swift, but that's a separate (and much more difficult!) project. (See [Dynamic function replacement #20333](https://github.com/apple/swift/pull/20333) aka `@_dynamicReplacement` for details.) - -### Can I ship this? -Yes, absolutely. The goal for this one project is a simple library that doesn't try to be too smart. I did this in [Aspects](https://github.com/steipete/Aspects) and while I loved this to no end, it's problematic and can cause side-effects with other code that tries to be clever. InterposeKit is boring, so you don't have to worry about conditions like "We added New Relic to our app and now [your thing crashes](https://github.com/steipete/Aspects/issues/21)". - -### 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 15+ or a Swift 5.9+ 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`. - -### Carthage - -Add `github "steipete/InterposeKit"` to your `Cartfile`. - -## Improvement Ideas - -- Write proposal to allow to [convert the calling convention of existing types](https://twitter.com/steipete/status/1266799174563041282?s=21). -- Use the C block struct to perform type checking between Method type and C type (I do that in [Aspects library](https://github.com/steipete/Aspects)), it's still a runtime crash but could be at hook time, not when we call it. -- Add a way to get all current hooks from an object/class. -- Add a way to revert hooks without super helper. -- Add a way to apply multiple hooks to classes -- Enable hooking of class methods. -- Add [dyld_dynamic_interpose](https://twitter.com/steipete/status/1258482647933870080) to hook pure C functions -- Combine Promise-API for `Interpose.whenAvailable` for better error bubbling. -- Experiment with [Swift function hooking](https://github.com/rodionovd/SWRoute/wiki/Function-hooking-in-Swift)? ⚡️ -- Test against Swift Nightly as Cron Job -- Switch to Trampolines to manage cases where other code overrides super, so we end up with a super call that's [not on top of the class hierarchy](https://github.com/steipete/InterposeKit/pull/15#discussion_r439871752). -- I'm sure there's more - Pull Requests or [comments](https://twitter.com/steipete) very welcome! - -Make this happen: -[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) -![CocoaPods](https://img.shields.io/cocoapods/v/SwiftyJSON.svg) - -## Thanks - -Special thanks to [JP Simard](https://github.com/jpsim/Yams) who did such a great job in setting up [Yams](https://github.com/jpsim/Yams) with GitHub Actions - this was extremely helpful to build CI here fast. +- [Peter’s original implementation](https://github.com/steipete/InterposeKit) +- [Peter’s introductory blog post](https://steipete.me/posts/interposekit/) +- [Peter’s blog post on swizzling in Swift](https://www.nutrient.io/blog/swizzling-in-swift/) +- [Peter’s blog post on calling super at runtime](https://steipete.me/posts/calling-super-at-runtime/) +- [Aspects - Objective-C predecessor to InterposeKit](https://github.com/steipete/Aspects) +- [NSHipster on method swizzling](https://nshipster.com/method-swizzling/) +- [Stack Overflow: How do I remove instance methods at runtime in Objective-C 2.0?](https://stackoverflow.com/questions/1315169/how-do-i-remove-instance-methods-at-runtime-in-objective-c-2-0) ## License -InterposeKit is MIT Licensed. +InterposeKit is available under the [MIT license](LICENSE). + +[^1]: Both applying and reverting a hook include safety checks. InterposeKit detects if the method was modified externally—such as by KVO or other swizzling—and prevents the operation if it would lead to inconsistent behavior. +[^2]: The most advanced part of this library is `ITKSuperBuilder`, a component for constructing method implementations that simply call `super`, which is [surprisingly hard to do](https://steipete.me/posts/calling-super-at-runtime/). It’s written in Objective-C and assembly, lives in its own SPM target, and is invoked from Swift. All credit goes to Peter who originally came up with this masterpiece! From c34e9e7f4142e6c9629b7d47e8ef0ce5fddb9c9a Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Fri, 11 Apr 2025 21:49:00 +0200 Subject: [PATCH 02/13] Added list of changes over original implementation --- README-legacy.md | 30 ------------------------------ README.md | 31 +++++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/README-legacy.md b/README-legacy.md index f45d185..21372d7 100644 --- a/README-legacy.md +++ b/README-legacy.md @@ -58,36 +58,6 @@ InterposeKit can hook classes and object. Class hooking is similar to swizzling, 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 - } - } -} -``` - ## FAQ ### Why didn't you call it Interpose? "Kit" feels so old-school. diff --git a/README.md b/README.md index 4d89318..b9ad11d 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,36 @@ This is a continuation and modernization of [Peter Steinberger’s original impl - arm64 and x86_64 architectures - Examples: instance method on class, class method on class, object -## Changes from [Original Implementation](https://github.com/steipete/InterposeKit) +## What’s Changed -- ... +Compared to the [original implementation](https://github.com/steipete/InterposeKit), this fork introduces several API and internal changes. Here is a summary of key differences with migration hints. + +### Environment + +- Switched the library to Swift Package Manager only. Carthage and CocoaPods support was removed. +- Raised minimum Swift version to 5.9. +- Limited to Apple platforms with arm64 and x86_64 architectures. Support for Linux was removed. + +### API Changes + +- Class hooks now support class methods. +- Removed the builder-based API `Interpose(…) { … }` for applying hooks in batches. Each hook must now be individually prepared, applied, and reverted. +- Signature types must now be specified via parameters, improving clarity and streamlining the API. +- Renamed `hook(…)` methods to `applyHook(…)`. +- Removed fluent-style `Hook` API, meaning that methods no longer return `self`. +- Introduced `HookProxy`, which is passed into the hook builder. It still provides access to the original method implementation and selector, but hides irrelevant APIs like `revert()`. +- Hook types now use composition instead of inheritance. The public `Hook` class delegates to internal strategies conforming to `HookStrategy`. +- Object hooks now use a global counter instead of UUIDs for dynamic subclass names. +- Dynamic subclasses created at runtime are now cleaned up when the last hook is reverted on an object. +- Class hooks must now target the exact class that actually implements the method to ensure the revert functionality works correctly. +- Added initial Swift 6 compatibility with some thread-safety checks. Most usage is still expected to be single-threaded, though. +- Removed support for [delayed hooking](https://steipete.com/posts/mac-catalyst-crash-hunt/) (`whenAvailable(…)`) to keep the library laser-focused. +- …and heavily refactored the Swift part of the codebase: cleaner use of Objective-C runtime APIs, a revamped `InterposeError` enum, and new supporting types like `HookScope` or `HookState`. + +### Fixes + +- Fixed a crash where `IKTAddSuperImplementationToClass` was stripped in release builds per [steipete/InterposeKit#29](https://github.com/steipete/InterposeKit#29) by using the fix from [steipete/InterposeKit#30](https://github.com/steipete/InterposeKit#30) submitted by @Thomvis, which replaces a call via dynamic library with a direct Swift call to `IKTSuperBuilder.addSuperInstanceMethod(to:selector:)`. +- Fixed floating-point register handling on arm64 using the patch from [steipete/InterposeKit#37](https://github.com/steipete/InterposeKit#37) submitted by @ishutinvv, which resolves an issue affecting swizzled methods with `CGFloat` parameters or structs like `CGPoint` and `CGRect` due to floating-point registers not being restored in the correct order after the trampoline call. ## References From 06ee36807051d887aea39c4d8bba9ecf5d647341 Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Fri, 11 Apr 2025 21:58:34 +0200 Subject: [PATCH 03/13] README: Various tweaks --- README-legacy.md | 2 -- README.md | 27 ++++++++++++++++++++------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/README-legacy.md b/README-legacy.md index 21372d7..cca5701 100644 --- a/README-legacy.md +++ b/README-legacy.md @@ -1,5 +1,3 @@ -Instead of [adding new methods and exchanging implementations](https://nshipster.com/method-swizzling/) based on [`method_exchangeImplementations`](https://developer.apple.com/documentation/objectivec/1418769-method_exchangeimplementations), this library replaces the implementation directly using [`class_replaceMethod`](https://developer.apple.com/documentation/objectivec/1418677-class_replacemethod). This avoids some of [the usual problems with swizzling](https://pspdfkit.com/blog/2019/swizzling-in-swift/). - ## Usage Let's say you want to amend `sayHi` from `TestClass`: diff --git a/README.md b/README.md index b9ad11d..4137fc4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # InterposeKit -[![CI](https://github.com/structuredpath/InterposeKit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/structuredpath/InterposeKit/actions/workflows/ci.yml) ![Xcode 15+](https://img.shields.io/badge/Xcode-15%2B-blue.svg) ![Swift 5.9+](https://img.shields.io/badge/Swift-5.9%2B-orange.svg) +[![CI](https://github.com/structuredpath/InterposeKit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/structuredpath/InterposeKit/actions/workflows/ci.yml) ![Swift 5.9+](https://img.shields.io/badge/Swift-5.9%2B-orange.svg) ![Xcode 15+](https://img.shields.io/badge/Xcode-15%2B-blue.svg) **InterposeKit** is a modern library for hooking Objective-C methods in Swift, also known as method swizzling. It supports both class-based and object-based hooks and it provides a clean, block-based, Swift-friendly API. @@ -8,7 +8,7 @@ This is a continuation and modernization of [Peter Steinberger’s original impl ## Key Features -- Swift-friendly, minimal, thread-safe API. +- Swift-friendly, modern, and minimal API. - Block-based hooks targeting both classes and individual objects. - Support for both instance and class methods. - Hooks get access to the original implementation via a proxy. @@ -19,12 +19,25 @@ This is a continuation and modernization of [Peter Steinberger’s original impl - There’s no runtime type checking, and the signature has to be written twice—a trade-off to avoid `NSInvocation`. - Written almost entirely in Swift on top of the Objective-C runtime[^2]. -## Getting Started +## Installation - Installation Swift Package Manager - Swift 5.9+, Xcode 15+ - arm64 and x86_64 architectures -- Examples: instance method on class, class method on class, object + +## Usage + +### Class Hook: Instance Method + +… + +### Class Hook: Class Method + +… + +### Object Hook + +… ## What’s Changed @@ -48,14 +61,14 @@ Compared to the [original implementation](https://github.com/steipete/InterposeK - Object hooks now use a global counter instead of UUIDs for dynamic subclass names. - Dynamic subclasses created at runtime are now cleaned up when the last hook is reverted on an object. - Class hooks must now target the exact class that actually implements the method to ensure the revert functionality works correctly. -- Added initial Swift 6 compatibility with some thread-safety checks. Most usage is still expected to be single-threaded, though. +- Added initial Swift 6 support with basic concurrency checks. Should be thread-safe, but most usage is still expected to be single-threaded. - Removed support for [delayed hooking](https://steipete.com/posts/mac-catalyst-crash-hunt/) (`whenAvailable(…)`) to keep the library laser-focused. - …and heavily refactored the Swift part of the codebase: cleaner use of Objective-C runtime APIs, a revamped `InterposeError` enum, and new supporting types like `HookScope` or `HookState`. ### Fixes -- Fixed a crash where `IKTAddSuperImplementationToClass` was stripped in release builds per [steipete/InterposeKit#29](https://github.com/steipete/InterposeKit#29) by using the fix from [steipete/InterposeKit#30](https://github.com/steipete/InterposeKit#30) submitted by @Thomvis, which replaces a call via dynamic library with a direct Swift call to `IKTSuperBuilder.addSuperInstanceMethod(to:selector:)`. -- Fixed floating-point register handling on arm64 using the patch from [steipete/InterposeKit#37](https://github.com/steipete/InterposeKit#37) submitted by @ishutinvv, which resolves an issue affecting swizzled methods with `CGFloat` parameters or structs like `CGPoint` and `CGRect` due to floating-point registers not being restored in the correct order after the trampoline call. +- Fixed a crash where `IKTAddSuperImplementationToClass` was stripped in release builds per [steipete/InterposeKit#29](https://github.com/steipete/InterposeKit/issues/29) by using the fix from [steipete/InterposeKit#30](https://github.com/steipete/InterposeKit/issues/30) submitted by @Thomvis, which replaces a call via dynamic library with a direct Swift call to `IKTSuperBuilder.addSuperInstanceMethod(to:selector:)`. +- Fixed floating-point register handling on arm64 using the patch from [steipete/InterposeKit#37](https://github.com/steipete/InterposeKit/issues/37) submitted by @ishutinvv, which resolves an issue affecting swizzled methods with `CGFloat` parameters or structs like `CGPoint` and `CGRect` due to floating-point registers not being restored in the correct order after the trampoline call. ## References From 52da7ac7b89bdd89d787798663140b2f60aa5069 Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Fri, 11 Apr 2025 22:16:26 +0200 Subject: [PATCH 04/13] README: Added Requirements and Installation sections --- README.md | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4137fc4..a0ec90d 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,37 @@ This is a continuation and modernization of [Peter Steinberger’s original impl - There’s no runtime type checking, and the signature has to be written twice—a trade-off to avoid `NSInvocation`. - Written almost entirely in Swift on top of the Objective-C runtime[^2]. +## Requirements + +- Swift 5.9 or later +- Xcode 15 or later +- Apple platforms only (macOS, iOS, tvOS, watchOS) +- arm64 or x86_64 architectures + ## Installation -- Installation Swift Package Manager -- Swift 5.9+, Xcode 15+ -- arm64 and x86_64 architectures +You can add InterposeKit to your project using the Swift Package Manager. + +In Xcode, open your project settings, select the *Package Dependencies* tab, click the *+* button, and enter the URL `https://github.com/structuredpath/InterposeKit`. Then select the latest version and add the package to your desired target. + +If you’re adding InterposeKit using a `Package.swift` manifest, include it in your `dependencies` like this: + +```swift +dependencies: [ + .package(url: "https://github.com/structuredpath/InterposeKit", from: "1.0.0") +] +``` + +Then add the product to any target that needs it: + +```swift +.target( + name: "YourTarget", + dependencies: [ + .product(name: "InterposeKit", package: "InterposeKit") + ] +) +``` ## Usage From d1521dc3afd44bdc765056dae124f48d8192dfe1 Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Fri, 11 Apr 2025 22:25:37 +0200 Subject: [PATCH 05/13] README: Added basic examples --- README-legacy.md | 38 ------------------------------ README.md | 60 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 40 deletions(-) diff --git a/README-legacy.md b/README-legacy.md index cca5701..881b67a 100644 --- a/README-legacy.md +++ b/README-legacy.md @@ -1,34 +1,5 @@ ## Usage -Let's say you want to amend `sayHi` from `TestClass`: - -```swift -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! ```swift @@ -41,15 +12,6 @@ let hook = try testObj.hook( } ``` -Here's what we get when calling `print(TestClass().sayHi())` -``` -[Interposer] Swizzled -[TestClass.sayHi] IMP: 0x000000010d9f4430 -> 0x000000010db36020 -Before Interposing -Calling sayHi -After Interposing -Hi there 👋 and Interpose -``` - ## 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. diff --git a/README.md b/README.md index a0ec90d..e553c32 100644 --- a/README.md +++ b/README.md @@ -55,16 +55,72 @@ Then add the product to any target that needs it: ### Class Hook: Instance Method -… +```swift +class MyClass: NSObject { + @objc dynamic func getValue() -> Int { + return 42 + } +} + +let object = MyClass() +print(object.getValue()) // => 42 + +let hook = try Interpose.applyHook( + on: MyClass.self, + for: #selector(MyClass.getValue), + methodSignature: (@convention(c) (MyClass, Selector) -> Int).self, + hookSignature: (@convention(block) (MyClass) -> Int).self +) { hook in + return { `self` in + // Retrieve the original result and add 1 to it. This can be skipped. + return hook.original(`self`, hook.selector) + 1 + } +} + +print(object.getValue()) // => 43 + +try hook.revert() +print(object.getValue()) // => 42 +``` ### Class Hook: Class Method -… +```swift +class MyClass: NSObject { + @objc dynamic class func getStaticValue() -> Int { + return 42 + } +} + +print(MyClass.getStaticValue()) // => 42 + +let hook = try Interpose.applyHook( + on: MyClass.self, + for: #selector(MyClass.getStaticValue), + methodKind: .class, + methodSignature: (@convention(c) (MyClass.Type, Selector) -> Int).self, + hookSignature: (@convention(block) (MyClass.Type) -> Int).self +) { hook in + return { `class` in + // Retrieve the original result and add 1 to it. This can be skipped. + return hook.original(`class`, hook.selector) + 1 + } +} + +print(MyClass.getStaticValue()) // => 43 + +try hook.revert() +print(MyClass.getStaticValue()) // => 42 +``` ### Object Hook … +### More Examples + +You can check out the extensive test suite to see more advanced examples or the example Xcode project to see more real-life examples of tweaking AppKit classes. + ## What’s Changed Compared to the [original implementation](https://github.com/steipete/InterposeKit), this fork introduces several API and internal changes. Here is a summary of key differences with migration hints. From 4afa6f1a7abf01c127f6768c3a288e1eb1197928 Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Sat, 12 Apr 2025 08:14:20 +0200 Subject: [PATCH 06/13] README: Minor tweaks --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e553c32..4d169b7 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ In Xcode, open your project settings, select the *Package Dependencies* tab, cli If you’re adding InterposeKit using a `Package.swift` manifest, include it in your `dependencies` like this: -```swift +``` dependencies: [ .package(url: "https://github.com/structuredpath/InterposeKit", from: "1.0.0") ] @@ -42,7 +42,7 @@ dependencies: [ Then add the product to any target that needs it: -```swift +``` .target( name: "YourTarget", dependencies: [ @@ -53,7 +53,7 @@ Then add the product to any target that needs it: ## Usage -### Class Hook: Instance Method +### Class Hook on Instance Method ```swift class MyClass: NSObject { @@ -83,7 +83,7 @@ try hook.revert() print(object.getValue()) // => 42 ``` -### Class Hook: Class Method +### Class Hook on Class Method ```swift class MyClass: NSObject { @@ -144,7 +144,7 @@ Compared to the [original implementation](https://github.com/steipete/InterposeK - Dynamic subclasses created at runtime are now cleaned up when the last hook is reverted on an object. - Class hooks must now target the exact class that actually implements the method to ensure the revert functionality works correctly. - Added initial Swift 6 support with basic concurrency checks. Should be thread-safe, but most usage is still expected to be single-threaded. -- Removed support for [delayed hooking](https://steipete.com/posts/mac-catalyst-crash-hunt/) (`whenAvailable(…)`) to keep the library laser-focused. +- Removed support for [delayed hooking](https://github.com/steipete/InterposeKit?tab=readme-ov-file#delayed-hooking) (`Interpose.whenAvailable(…)`) to keep the library laser-focused. - …and heavily refactored the Swift part of the codebase: cleaner use of Objective-C runtime APIs, a revamped `InterposeError` enum, and new supporting types like `HookScope` or `HookState`. ### Fixes @@ -164,7 +164,7 @@ Compared to the [original implementation](https://github.com/steipete/InterposeK ## License -InterposeKit is available under the [MIT license](LICENSE). +This library is released under the MIT license. See [LICENSE](LICENSE) for details. [^1]: Both applying and reverting a hook include safety checks. InterposeKit detects if the method was modified externally—such as by KVO or other swizzling—and prevents the operation if it would lead to inconsistent behavior. [^2]: The most advanced part of this library is `ITKSuperBuilder`, a component for constructing method implementations that simply call `super`, which is [surprisingly hard to do](https://steipete.me/posts/calling-super-at-runtime/). It’s written in Objective-C and assembly, lives in its own SPM target, and is invoked from Swift. All credit goes to Peter who originally came up with this masterpiece! From ddd48448258dcd122cd3660e2090fe0251759df5 Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Sat, 12 Apr 2025 08:50:45 +0200 Subject: [PATCH 07/13] README: Initial take on Q&A section --- README-legacy.md | 19 ------------------- README.md | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/README-legacy.md b/README-legacy.md index 881b67a..fb60f0c 100644 --- a/README-legacy.md +++ b/README-legacy.md @@ -29,28 +29,9 @@ UIKit and AppKit won't go away, and the bugs won't go away either. I see this as ### Can I ship this? Yes, absolutely. The goal for this one project is a simple library that doesn't try to be too smart. I did this in [Aspects](https://github.com/steipete/Aspects) and while I loved this to no end, it's problematic and can cause side-effects with other code that tries to be clever. InterposeKit is boring, so you don't have to worry about conditions like "We added New Relic to our app and now [your thing crashes](https://github.com/steipete/Aspects/issues/21)". -### 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 15+ or a Swift 5.9+ 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`. - ## Improvement Ideas - Write proposal to allow to [convert the calling convention of existing types](https://twitter.com/steipete/status/1266799174563041282?s=21). - Use the C block struct to perform type checking between Method type and C type (I do that in [Aspects library](https://github.com/steipete/Aspects)), it's still a runtime crash but could be at hook time, not when we call it. - Add a way to get all current hooks from an object/class. -- Add a way to revert hooks without super helper. -- Add a way to apply multiple hooks to classes -- Enable hooking of class methods. -- Add [dyld_dynamic_interpose](https://twitter.com/steipete/status/1258482647933870080) to hook pure C functions -- Combine Promise-API for `Interpose.whenAvailable` for better error bubbling. -- Experiment with [Swift function hooking](https://github.com/rodionovd/SWRoute/wiki/Function-hooking-in-Swift)? ⚡️ -- Test against Swift Nightly as Cron Job - Switch to Trampolines to manage cases where other code overrides super, so we end up with a super call that's [not on top of the class hierarchy](https://github.com/steipete/InterposeKit/pull/15#discussion_r439871752). diff --git a/README.md b/README.md index 4d169b7..5ed0d17 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,22 @@ Compared to the [original implementation](https://github.com/steipete/InterposeK - Fixed a crash where `IKTAddSuperImplementationToClass` was stripped in release builds per [steipete/InterposeKit#29](https://github.com/steipete/InterposeKit/issues/29) by using the fix from [steipete/InterposeKit#30](https://github.com/steipete/InterposeKit/issues/30) submitted by @Thomvis, which replaces a call via dynamic library with a direct Swift call to `IKTSuperBuilder.addSuperInstanceMethod(to:selector:)`. - Fixed floating-point register handling on arm64 using the patch from [steipete/InterposeKit#37](https://github.com/steipete/InterposeKit/issues/37) submitted by @ishutinvv, which resolves an issue affecting swizzled methods with `CGFloat` parameters or structs like `CGPoint` and `CGRect` due to floating-point registers not being restored in the correct order after the trampoline call. +## Q&A + +### Why did Peter call it InterposeKit? +### Why another Objective-C swizzling library? +### Can I hook pure C functions or Swift methods? + +No. Peter had plans to experiment with [Swift method hooking](https://github.com/rodionovd/SWRoute) and hooking C functions via [`dyld_dynamic_interpose`](https://twitter.com/steipete/status/1258482647933870080), but neither made it into the library. And honestly, it doesn’t really fit the scope of this library anyway. + +### What the fork? +### Can I ship this? + +## Improvement Ideas + +- Signature type checking at hook construction +- Add support for reverting multiple hooks on a class in arbitrary order + ## References - [Peter’s original implementation](https://github.com/steipete/InterposeKit) From 107ac62ef4bca6594a59325e7a059163a93a2f55 Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Sat, 26 Apr 2025 09:10:59 +0200 Subject: [PATCH 08/13] README: Various tweaks --- README.md | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 5ed0d17..eb9637d 100644 --- a/README.md +++ b/README.md @@ -2,29 +2,28 @@ [![CI](https://github.com/structuredpath/InterposeKit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/structuredpath/InterposeKit/actions/workflows/ci.yml) ![Swift 5.9+](https://img.shields.io/badge/Swift-5.9%2B-orange.svg) ![Xcode 15+](https://img.shields.io/badge/Xcode-15%2B-blue.svg) -**InterposeKit** is a modern library for hooking Objective-C methods in Swift, also known as method swizzling. It supports both class-based and object-based hooks and it provides a clean, block-based, Swift-friendly API. +**InterposeKit** is a modern library for hooking Objective-C methods in Swift, also known as method swizzling. It supports both class-based and object-based hooks, and it provides a clean, block-based, Swift-friendly API. -This is a continuation and modernization of [Peter Steinberger’s original implementation](https://github.com/steipete/InterposeKit). For the background on why and how this revamp came about, see [my blog post](#). If you’re migrating, check out [what’s changed](#). +This is a continuation and modernization of [Peter Steinberger’s original implementation](https://github.com/steipete/InterposeKit). If you’re migrating, check out [what’s changed](#what-has-changed). ## Key Features - Swift-friendly, modern, and minimal API. -- Block-based hooks targeting both classes and individual objects. +- Block-based hooks using direct `Method` implementation replacement under the hood rather than less-safe [selector-based swizzling](https://pspdfkit.com/blog/2019/swizzling-in-swift/). +- Ability to target both classes and individual objects. - Support for both instance and class methods. -- Hooks get access to the original implementation via a proxy. - Object hooks are safely isolated using runtime subclassing, similar to the KVO mechanism. +- Hooks get access to the original method implementation via a proxy. - Hooks can be applied immediately or prepared and applied later, and safely reverted at any time[^1]. -- Direct `Method` implementation replacement rather than less-safe [selector-based swizzling](https://pspdfkit.com/blog/2019/swizzling-in-swift/). -- Typed signatures must be provided for both the original method and the hook block, enabling an ergonomic API. -- There’s no runtime type checking, and the signature has to be written twice—a trade-off to avoid `NSInvocation`. -- Written almost entirely in Swift on top of the Objective-C runtime[^2]. +- Typed signatures must be explicitly provided for both the original method and the hook block. This adds some boilerplate but ensures a clean API and better performance compared to using `NSInvocation`[^2]. +- Written almost entirely in Swift on top of the Objective-C runtime[^3]. ## Requirements - Swift 5.9 or later - Xcode 15 or later - Apple platforms only (macOS, iOS, tvOS, watchOS) -- arm64 or x86_64 architectures +- `arm64` or `x86_64` architectures ## Installation @@ -36,7 +35,7 @@ If you’re adding InterposeKit using a `Package.swift` manifest, include it in ``` dependencies: [ - .package(url: "https://github.com/structuredpath/InterposeKit", from: "1.0.0") + .package(url: "https://github.com/structuredpath/InterposeKit", from: "0.5.0") ] ``` @@ -121,7 +120,7 @@ print(MyClass.getStaticValue()) // => 42 You can check out the extensive test suite to see more advanced examples or the example Xcode project to see more real-life examples of tweaking AppKit classes. -## What’s Changed +

What’s Changed

Compared to the [original implementation](https://github.com/steipete/InterposeKit), this fork introduces several API and internal changes. Here is a summary of key differences with migration hints. @@ -129,7 +128,7 @@ Compared to the [original implementation](https://github.com/steipete/InterposeK - Switched the library to Swift Package Manager only. Carthage and CocoaPods support was removed. - Raised minimum Swift version to 5.9. -- Limited to Apple platforms with arm64 and x86_64 architectures. Support for Linux was removed. +- Limited to Apple platforms with `arm64` and `x86_64` architectures. Support for Linux was removed. ### API Changes @@ -138,25 +137,25 @@ Compared to the [original implementation](https://github.com/steipete/InterposeK - Signature types must now be specified via parameters, improving clarity and streamlining the API. - Renamed `hook(…)` methods to `applyHook(…)`. - Removed fluent-style `Hook` API, meaning that methods no longer return `self`. -- Introduced `HookProxy`, which is passed into the hook builder. It still provides access to the original method implementation and selector, but hides irrelevant APIs like `revert()`. +- Introduced `HookProxy`, which is passed into the hook builder. It still provides access to the original method implementation and selector, but hides other irrelevant APIs like `revert()`. - Hook types now use composition instead of inheritance. The public `Hook` class delegates to internal strategies conforming to `HookStrategy`. - Object hooks now use a global counter instead of UUIDs for dynamic subclass names. - Dynamic subclasses created at runtime are now cleaned up when the last hook is reverted on an object. - Class hooks must now target the exact class that actually implements the method to ensure the revert functionality works correctly. -- Added initial Swift 6 support with basic concurrency checks. Should be thread-safe, but most usage is still expected to be single-threaded. -- Removed support for [delayed hooking](https://github.com/steipete/InterposeKit?tab=readme-ov-file#delayed-hooking) (`Interpose.whenAvailable(…)`) to keep the library laser-focused. +- Added initial Swift 6 support with basic concurrency checks. Should be thread-safe but most usage is still expected to be single-threaded. +- Removed support for [delayed hooking](https://github.com/steipete/InterposeKit?tab=readme-ov-file#delayed-hooking) via `Interpose.whenAvailable(…)` to keep the library laser-focused. - …and heavily refactored the Swift part of the codebase: cleaner use of Objective-C runtime APIs, a revamped `InterposeError` enum, and new supporting types like `HookScope` or `HookState`. ### Fixes -- Fixed a crash where `IKTAddSuperImplementationToClass` was stripped in release builds per [steipete/InterposeKit#29](https://github.com/steipete/InterposeKit/issues/29) by using the fix from [steipete/InterposeKit#30](https://github.com/steipete/InterposeKit/issues/30) submitted by @Thomvis, which replaces a call via dynamic library with a direct Swift call to `IKTSuperBuilder.addSuperInstanceMethod(to:selector:)`. -- Fixed floating-point register handling on arm64 using the patch from [steipete/InterposeKit#37](https://github.com/steipete/InterposeKit/issues/37) submitted by @ishutinvv, which resolves an issue affecting swizzled methods with `CGFloat` parameters or structs like `CGPoint` and `CGRect` due to floating-point registers not being restored in the correct order after the trampoline call. +- Fixed a crash where `IKTAddSuperImplementationToClass` was stripped in release builds per [steipete/InterposeKit#29](https://github.com/steipete/InterposeKit/issues/29) by using the fix from [steipete/InterposeKit#30](https://github.com/steipete/InterposeKit/issues/30) submitted by [@Thomvis](https://github.com/Thomvis), which replaces a call via dynamic library with a direct Swift call to `IKTSuperBuilder.addSuperInstanceMethod(to:selector:)`. +- Fixed floating-point register handling on arm64 using the patch from [steipete/InterposeKit#37](https://github.com/steipete/InterposeKit/issues/37) submitted by [@ishutinvv](https://github.com/ishutinvv), which resolves an issue affecting swizzled methods with `CGFloat` parameters or structs like `CGPoint` and `CGRect` due to floating-point registers not being restored in the correct order after the trampoline call. ## Q&A ### Why did Peter call it InterposeKit? ### Why another Objective-C swizzling library? -### Can I hook pure C functions or Swift methods? +### Can I hook Swift methods? And what about pure C functions? No. Peter had plans to experiment with [Swift method hooking](https://github.com/rodionovd/SWRoute) and hooking C functions via [`dyld_dynamic_interpose`](https://twitter.com/steipete/status/1258482647933870080), but neither made it into the library. And honestly, it doesn’t really fit the scope of this library anyway. @@ -165,8 +164,9 @@ No. Peter had plans to experiment with [Swift method hooking](https://github.com ## Improvement Ideas +- Support for hooking KVO-enabled objects - Signature type checking at hook construction -- Add support for reverting multiple hooks on a class in arbitrary order +- Support for reverting multiple hooks on a class in an arbitrary order ## References @@ -183,4 +183,5 @@ No. Peter had plans to experiment with [Swift method hooking](https://github.com This library is released under the MIT license. See [LICENSE](LICENSE) for details. [^1]: Both applying and reverting a hook include safety checks. InterposeKit detects if the method was modified externally—such as by KVO or other swizzling—and prevents the operation if it would lead to inconsistent behavior. -[^2]: The most advanced part of this library is `ITKSuperBuilder`, a component for constructing method implementations that simply call `super`, which is [surprisingly hard to do](https://steipete.me/posts/calling-super-at-runtime/). It’s written in Objective-C and assembly, lives in its own SPM target, and is invoked from Swift. All credit goes to Peter who originally came up with this masterpiece! +[^2]: There’s currently no runtime type checking. If the specified types don’t match, it will cause a runtime crash. +[^3]: The most advanced part of this library is `ITKSuperBuilder`, a component for constructing method implementations that simply call `super`, which is [surprisingly hard to do](https://steipete.me/posts/calling-super-at-runtime/). It’s written in Objective-C and assembly, lives in its own SPM target, and is invoked from Swift. All credit goes to Peter, who originally came up with this masterpiece! From f5ef7240f9ba5afaf8254379d6154086069d3b24 Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Sat, 26 Apr 2025 09:47:52 +0200 Subject: [PATCH 09/13] README: Finalized the Q&A section --- README-legacy.md | 11 ----------- README.md | 19 ++++++++++++++++--- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/README-legacy.md b/README-legacy.md index fb60f0c..6999365 100644 --- a/README-legacy.md +++ b/README-legacy.md @@ -18,17 +18,6 @@ InterposeKit can hook classes and object. Class hooking is similar to swizzling, 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. -## FAQ - -### Why didn't you call it Interpose? "Kit" feels so old-school. -Naming it Interpose was the plan, but then [SR-898](https://bugs.swift.org/browse/SR-898) came. While having a class with the same name as the module works [in most cases](https://forums.swift.org/t/frameworkname-is-not-a-member-type-of-frameworkname-errors-inside-swiftinterface/28962), [this breaks](https://twitter.com/BalestraPatrick/status/1260928023357878273) when you enable build-for-distribution. There's some [discussion](https://forums.swift.org/t/pitch-fully-qualified-name-syntax/28482/81) to get that fixed, but this will be more towards end of 2020, if even. - -### I want to hook into Swift! You made another ObjC swizzle thingy, why? -UIKit and AppKit won't go away, and the bugs won't go away either. I see this as a rarely-needed instrument to fix system-level issues. There are ways to do some of that in Swift, but that's a separate (and much more difficult!) project. (See [Dynamic function replacement #20333](https://github.com/apple/swift/pull/20333) aka `@_dynamicReplacement` for details.) - -### Can I ship this? -Yes, absolutely. The goal for this one project is a simple library that doesn't try to be too smart. I did this in [Aspects](https://github.com/steipete/Aspects) and while I loved this to no end, it's problematic and can cause side-effects with other code that tries to be clever. InterposeKit is boring, so you don't have to worry about conditions like "We added New Relic to our app and now [your thing crashes](https://github.com/steipete/Aspects/issues/21)". - ## Improvement Ideas - Write proposal to allow to [convert the calling convention of existing types](https://twitter.com/steipete/status/1266799174563041282?s=21). diff --git a/README.md b/README.md index eb9637d..c067a02 100644 --- a/README.md +++ b/README.md @@ -153,15 +153,28 @@ Compared to the [original implementation](https://github.com/steipete/InterposeK ## Q&A -### Why did Peter call it InterposeKit? +### Why is it called InterposeKit? + +Peter originally wanted to go with _Interpose_, but [Swift had (and still has) a bug](https://github.com/swiftlang/swift/issues/43510) where using the same name for a module and a type can break things in certain scenarios. + ### Why another Objective-C swizzling library? -### Can I hook Swift methods? And what about pure C functions? -No. Peter had plans to experiment with [Swift method hooking](https://github.com/rodionovd/SWRoute) and hooking C functions via [`dyld_dynamic_interpose`](https://twitter.com/steipete/status/1258482647933870080), but neither made it into the library. And honestly, it doesn’t really fit the scope of this library anyway. +UIKit, AppKit, and other system frameworks written in Objective-C won’t go away and sometimes you still need to swizzle to fix bugs or tweak internal behavior. InterposeKit is meant as a rarely-needed tool for these cases, providing a simple, Swift-friendly API. ### What the fork? + +This version of InterposeKit started as a fork of [Peter Steinberger’s original library](https://github.com/steipete/InterposeKit) but has since evolved into a significantly reworked and modernized version. The core ideas and underlying runtime techniques remain, but large parts of the Swift codebase were restructured and rewritten. + +### Can I hook Swift methods? And what about pure C functions? + +No. Peter had plans to experiment with [Swift method hooking](https://github.com/rodionovd/SWRoute), [Swift dynamic function replacement](https://github.com/swiftlang/swift/pull/20333), and hooking C functions via [`dyld_dynamic_interpose`](https://twitter.com/steipete/status/1258482647933870080), but none made it into the library. And honestly, it doesn’t really fit the scope of this library anyway. + ### Can I ship this? +Modifying the internal behavior of system frameworks always carries risks. You should know what you’re doing, use defensive programming techniques, and assume that future OS updates might break your hooks. + +That said, InterposeKit is designed to be safe for production use. It includes guardrails that verify method state before applying or reverting hooks, helping catch unexpected conditions early. The focus is on simplicity and predictability, avoiding clever tricks that could introduce hidden side effects. + ## Improvement Ideas - Support for hooking KVO-enabled objects From 6f111a1a90b8a7b3d8b2a74b63d998bc0171fd7d Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Sat, 26 Apr 2025 12:28:57 +0200 Subject: [PATCH 10/13] README: Improvement ideas section --- README-legacy.md | 7 ------- README.md | 8 +++++--- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/README-legacy.md b/README-legacy.md index 6999365..bb0b45d 100644 --- a/README-legacy.md +++ b/README-legacy.md @@ -17,10 +17,3 @@ let hook = try testObj.hook( 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. - -## Improvement Ideas - -- Write proposal to allow to [convert the calling convention of existing types](https://twitter.com/steipete/status/1266799174563041282?s=21). -- Use the C block struct to perform type checking between Method type and C type (I do that in [Aspects library](https://github.com/steipete/Aspects)), it's still a runtime crash but could be at hook time, not when we call it. -- Add a way to get all current hooks from an object/class. -- Switch to Trampolines to manage cases where other code overrides super, so we end up with a super call that's [not on top of the class hierarchy](https://github.com/steipete/InterposeKit/pull/15#discussion_r439871752). diff --git a/README.md b/README.md index c067a02..dcee86e 100644 --- a/README.md +++ b/README.md @@ -177,9 +177,11 @@ That said, InterposeKit is designed to be safe for production use. It includes g ## Improvement Ideas -- Support for hooking KVO-enabled objects -- Signature type checking at hook construction -- Support for reverting multiple hooks on a class in an arbitrary order +- Support for hooking KVO-enabled objects ([#19](https://github.com/structuredpath/InterposeKit/issues/19)) +- Correct super lookup when injecting a trampoline into classes with overridden methods ([#21](https://github.com/structuredpath/InterposeKit/issues/21)) +- Signature type checking at hook construction ([#20](https://github.com/structuredpath/InterposeKit/issues/20)) +- A way for retrieving all hooks on an object/class +- Support for reverting multiple hooks on a class in an arbitrary order ([#12](https://github.com/structuredpath/InterposeKit/issues/12)) ## References From a0fb5720a80837dff023a9bd05c7460656d44b1e Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Sat, 26 Apr 2025 19:31:17 +0200 Subject: [PATCH 11/13] README: Code sample for object hook --- README-legacy.md | 19 ------------------- README.md | 40 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 21 deletions(-) delete mode 100644 README-legacy.md diff --git a/README-legacy.md b/README-legacy.md deleted file mode 100644 index bb0b45d..0000000 --- a/README-legacy.md +++ /dev/null @@ -1,19 +0,0 @@ -## Usage - -Want to hook just a single instance? No problem! - -```swift -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" - } -} -``` - -## 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. diff --git a/README.md b/README.md index dcee86e..d98fb27 100644 --- a/README.md +++ b/README.md @@ -114,11 +114,47 @@ print(MyClass.getStaticValue()) // => 42 ### Object Hook -… +```swift +class MyClass: NSObject { + @objc dynamic func getValue() -> Int { + return 42 + } +} + +let object1 = MyClass() +let object2 = MyClass() + +print(object1.getValue()) // => 42 +print(object2.getValue()) // => 42 + +let hook = try Interpose.applyHook( + on: object1, + for: #selector(MyClass.getValue), + methodSignature: (@convention(c) (MyClass, Selector) -> Int).self, + hookSignature: (@convention(block) (MyClass) -> Int).self +) { hook in + return { `self` in + return hook.original(`self`, hook.selector) + 1 + } +} + +print(object1.getValue()) // => 43 +print(object2.getValue()) // => 42 + +try hook.revert() + +print(object1.getValue()) // => 42 +print(object2.getValue()) // => 42 +``` + +> [!IMPORTANT] +> If the object is already being observed via KVO when you apply or revert the hook, the operation will fail safely by throwing `InterposeError.kvoDetected(object:)`. Using KVO after the hook is installed is fully supported. ### More Examples -You can check out the extensive test suite to see more advanced examples or the example Xcode project to see more real-life examples of tweaking AppKit classes. +You can check out the extensive test suite to see more advanced examples. The repository also comes with and example Xcode project, which showcases more real-life examples of tweaking AppKit classes. + +

What’s Changed

From a73736864f39224750fbd3a5f0730c898f42fd5f Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Sat, 26 Apr 2025 19:32:57 +0200 Subject: [PATCH 12/13] README: Aligned spacing in code samples --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d98fb27..ee84180 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ If you’re adding InterposeKit using a `Package.swift` manifest, include it in ``` dependencies: [ - .package(url: "https://github.com/structuredpath/InterposeKit", from: "0.5.0") + .package(url: "https://github.com/structuredpath/InterposeKit", from: "0.5.0") ] ``` @@ -43,10 +43,10 @@ Then add the product to any target that needs it: ``` .target( - name: "YourTarget", - dependencies: [ - .product(name: "InterposeKit", package: "InterposeKit") - ] + name: "YourTarget", + dependencies: [ + .product(name: "InterposeKit", package: "InterposeKit") + ] ) ``` @@ -134,6 +134,7 @@ let hook = try Interpose.applyHook( hookSignature: (@convention(block) (MyClass) -> Int).self ) { hook in return { `self` in + // Retrieve the original result and add 1 to it. This can be skipped. return hook.original(`self`, hook.selector) + 1 } } From 0c4d9487458b8b79fd5aab3450c48c9fb937c68d Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Sat, 26 Apr 2025 19:39:55 +0200 Subject: [PATCH 13/13] README: Final polish --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ee84180..cf562ab 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ If you’re adding InterposeKit using a `Package.swift` manifest, include it in ``` dependencies: [ - .package(url: "https://github.com/structuredpath/InterposeKit", from: "0.5.0") + .package(url: "https://github.com/structuredpath/InterposeKit", from: "1.0.0") ] ``` @@ -153,7 +153,7 @@ print(object2.getValue()) // => 42 ### More Examples -You can check out the extensive test suite to see more advanced examples. The repository also comes with and example Xcode project, which showcases more real-life examples of tweaking AppKit classes. +You can check out the extensive test suite to see more advanced examples. The repository also comes with an example Xcode project, which showcases more real-life examples of tweaking AppKit classes. @@ -196,21 +196,21 @@ Peter originally wanted to go with _Interpose_, but [Swift had (and still has) a ### Why another Objective-C swizzling library? -UIKit, AppKit, and other system frameworks written in Objective-C won’t go away and sometimes you still need to swizzle to fix bugs or tweak internal behavior. InterposeKit is meant as a rarely-needed tool for these cases, providing a simple, Swift-friendly API. +UIKit, AppKit, and other system frameworks written in Objective-C won’t go away and sometimes you still need to swizzle to fix bugs or tweak internal behavior. InterposeKit is meant as a rarely needed tool for these cases, providing a simple, Swift-friendly API. ### What the fork? -This version of InterposeKit started as a fork of [Peter Steinberger’s original library](https://github.com/steipete/InterposeKit) but has since evolved into a significantly reworked and modernized version. The core ideas and underlying runtime techniques remain, but large parts of the Swift codebase were restructured and rewritten. +This version of InterposeKit started as a fork of [Peter Steinberger’s original library](https://github.com/steipete/InterposeKit) but has since evolved into a *significantly reworked and modernized* version. The core ideas and underlying runtime techniques remain, but large parts of the Swift codebase were restructured and rewritten. ### Can I hook Swift methods? And what about pure C functions? -No. Peter had plans to experiment with [Swift method hooking](https://github.com/rodionovd/SWRoute), [Swift dynamic function replacement](https://github.com/swiftlang/swift/pull/20333), and hooking C functions via [`dyld_dynamic_interpose`](https://twitter.com/steipete/status/1258482647933870080), but none made it into the library. And honestly, it doesn’t really fit the scope of this library anyway. +No. Peter had plans to experiment with [Swift method hooking](https://github.com/rodionovd/SWRoute), [Swift dynamic function replacement](https://github.com/swiftlang/swift/pull/20333), and hooking C functions via [`dyld_dynamic_interpose`](https://twitter.com/steipete/status/1258482647933870080), but none of these made it into InterposeKit, and frankly, they wouldn’t really fit the scope of this library anyway. ### Can I ship this? Modifying the internal behavior of system frameworks always carries risks. You should know what you’re doing, use defensive programming techniques, and assume that future OS updates might break your hooks. -That said, InterposeKit is designed to be safe for production use. It includes guardrails that verify method state before applying or reverting hooks, helping catch unexpected conditions early. The focus is on simplicity and predictability, avoiding clever tricks that could introduce hidden side effects. +That said, InterposeKit is designed to be safe for production use. It includes guardrails that verify the method state before applying or reverting hooks, helping catch unexpected conditions early. The focus is on simplicity and predictability, avoiding clever tricks that could introduce hidden side effects. ## Improvement Ideas