├── .gitignore ├── LICENSE ├── Package.swift ├── README.md └── Sources ├── CombineInterception ├── NSObject+Association.swift ├── NSObject+Interception.swift ├── NSObject+ObjCRuntime.swift ├── ObjC+Constants.swift ├── ObjC+Messages.swift ├── ObjC+Runtime.swift ├── ObjC+RuntimeSubclassing.swift ├── ObjC+Selector.swift └── Synchronizing.swift └── CombineInterceptionObjC ├── ObjcRuntimeAliases.m └── include ├── ObjcRuntimeAliases.h └── module.modulemap /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CombineInterception", 8 | platforms: [.iOS(.v13), .macOS(.v10_15)], 9 | products: [ 10 | .library(name: "CombineInterception", targets: ["CombineInterception"]), 11 | ], 12 | dependencies: [], 13 | targets: [ 14 | .target(name: "CombineInterception", dependencies: ["CombineInterceptionObjC"]), 15 | .target(name: "CombineInterceptionObjC", dependencies: []) 16 | ] 17 | ) 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CombineInterception 2 | 3 | ## WARNING 4 | ### If that [PR](https://github.com/CombineCommunity/CombineCocoa/pull/88) is merged, it will add functionality to CombineCocoa. 5 | ### Please use CombineCocoa in future. 6 | 7 | ## 8 |

9 | CombineCocoa supports Swift Package Manager (SPM) 10 | 11 |

12 | 13 | CombineInterception is a library that provides functionality similar to the methodInvoked function from RxCocoa, allowing you to subscribe to the invocation of Objective-C methods. 14 | 15 | Reference from https://github.com/EduDo-Inc/CombineCocoa 16 | 17 | ## Usage 18 | TL;DR 19 | 20 | ```swift 21 | import UIKit 22 | import Combine 23 | import CombineInterception 24 | 25 | extension UIViewController { 26 | var viewDidLoadPublisher: AnyPublisher { 27 | let selector = #selector(UIViewController.viewDidLoad) 28 | return publisher(for: selector) 29 | .map { _ in () } 30 | .eraseToAnyPublisher() 31 | } 32 | 33 | var viewWillAppearPublisher: AnyPublisher { 34 | let selector = #selector(UIViewController.viewWillAppear(_:)) 35 | return intercept(selector) 36 | .map { $0[0] as? Bool ?? false } 37 | .eraseToAnyPublisher() 38 | } 39 | } 40 | 41 | class ViewController: UIViewController { 42 | private var subscriptions = Set() 43 | 44 | private func bind() { 45 | viewDidLoadPublisher 46 | .sink(receiveValue: { _ in 47 | print("viewDidLoad") 48 | }) 49 | .store(in: &subscriptions) 50 | 51 | viewWillAppearPublisher 52 | .sink(receiveValue: { isAnimated in 53 | print("viewWillAppearPublisher", isAnimated) 54 | }) 55 | .store(in: &subscriptions) 56 | } 57 | } 58 | ``` 59 | 60 | ## Dependencies 61 | 62 | This project has the following dependencies: 63 | 64 | * Combine.framework 65 | 66 | ## Installation 67 | 68 | ### Swift Package Manager 69 | 70 | Add the following dependency to your Package.swift file: 71 | 72 | ```swift 73 | .package(url: "https://github.com/chorim/CombineInterception.git", from: "0.1.0") 74 | ``` 75 | 76 | ## Acknowledgments 77 | 78 | * CombineInterception was created by referencing https://github.com/EduDo-Inc/CombineCocoa 79 | 80 | ## License 81 | 82 | MIT, See the [LICENSE](LICENSE) file. 83 | 84 | The Combine framework are property of Apple Inc. 85 | -------------------------------------------------------------------------------- /Sources/CombineInterception/NSObject+Association.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSObject+Association.swift 3 | // 4 | // 5 | // Created by Insu Byeon on 2023/01/09. 6 | // 7 | 8 | import Foundation 9 | 10 | #if canImport(CombineInterceptionObjC) 11 | import CombineInterceptionObjC 12 | #endif 13 | 14 | internal struct AssociationKey { 15 | fileprivate let address: UnsafeRawPointer 16 | fileprivate let `default`: Value! 17 | 18 | /// Create an ObjC association key. 19 | /// 20 | /// - warning: The key must be uniqued. 21 | /// 22 | /// - parameters: 23 | /// - default: The default value, or `nil` to trap on undefined value. It is 24 | /// ignored if `Value` is an optional. 25 | init(default: Value? = nil) { 26 | self.address = UnsafeRawPointer( 27 | UnsafeMutablePointer.allocate(capacity: 1) 28 | ) 29 | self.default = `default` 30 | } 31 | 32 | /// Create an ObjC association key from a `StaticString`. 33 | /// 34 | /// - precondition: `key` has a pointer representation. 35 | /// 36 | /// - parameters: 37 | /// - default: The default value, or `nil` to trap on undefined value. It is 38 | /// ignored if `Value` is an optional. 39 | init(_ key: StaticString, default: Value? = nil) { 40 | assert(key.hasPointerRepresentation) 41 | self.address = UnsafeRawPointer(key.utf8Start) 42 | self.default = `default` 43 | } 44 | 45 | /// Create an ObjC association key from a `Selector`. 46 | /// 47 | /// - parameters: 48 | /// - default: The default value, or `nil` to trap on undefined value. It is 49 | /// ignored if `Value` is an optional. 50 | init(_ key: Selector, default: Value? = nil) { 51 | self.address = UnsafeRawPointer(key.utf8Start) 52 | self.default = `default` 53 | } 54 | } 55 | 56 | internal struct Associations { 57 | fileprivate let base: Base 58 | 59 | init(_ base: Base) { 60 | self.base = base 61 | } 62 | } 63 | 64 | extension NSObjectProtocol { 65 | /// Retrieve the associated value for the specified key. If the value does not 66 | /// exist, `initial` would be called and the returned value would be 67 | /// associated subsequently. 68 | /// 69 | /// - parameters: 70 | /// - key: An optional key to differentiate different values. 71 | /// - initial: The action that supples an initial value. 72 | /// 73 | /// - returns: The associated value for the specified key. 74 | internal func associatedValue( 75 | forKey key: StaticString = #function, 76 | initial: (Self) -> T 77 | ) -> T { 78 | let key = AssociationKey(key) 79 | 80 | if let value = associations.value(forKey: key) { 81 | return value 82 | } 83 | 84 | let value = initial(self) 85 | associations.setValue(value, forKey: key) 86 | 87 | return value 88 | } 89 | } 90 | 91 | extension NSObjectProtocol { 92 | @nonobjc internal var associations: Associations { 93 | return Associations(self) 94 | } 95 | } 96 | 97 | extension Associations { 98 | /// Retrieve the associated value for the specified key. 99 | /// 100 | /// - parameters: 101 | /// - key: The key. 102 | /// 103 | /// - returns: The associated value, or the default value if no value has been 104 | /// associated with the key. 105 | internal func value( 106 | forKey key: AssociationKey 107 | ) -> Value { 108 | return (objc_getAssociatedObject(base, key.address) as! Value?) ?? key.default 109 | } 110 | 111 | /// Retrieve the associated value for the specified key. 112 | /// 113 | /// - parameters: 114 | /// - key: The key. 115 | /// 116 | /// - returns: The associated value, or `nil` if no value is associated with 117 | /// the key. 118 | internal func value( 119 | forKey key: AssociationKey 120 | ) -> Value? { 121 | return objc_getAssociatedObject(base, key.address) as! Value? 122 | } 123 | 124 | /// Set the associated value for the specified key. 125 | /// 126 | /// - parameters: 127 | /// - value: The value to be associated. 128 | /// - key: The key. 129 | internal func setValue( 130 | _ value: Value, 131 | forKey key: AssociationKey 132 | ) { 133 | objc_setAssociatedObject(base, key.address, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 134 | } 135 | 136 | /// Set the associated value for the specified key. 137 | /// 138 | /// - parameters: 139 | /// - value: The value to be associated. 140 | /// - key: The key. 141 | internal func setValue( 142 | _ value: Value?, 143 | forKey key: AssociationKey 144 | ) { 145 | objc_setAssociatedObject(base, key.address, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 146 | } 147 | } 148 | 149 | /// Set the associated value for the specified key. 150 | /// 151 | /// - parameters: 152 | /// - value: The value to be associated. 153 | /// - key: The key. 154 | /// - address: The address of the object. 155 | internal func unsafeSetAssociatedValue( 156 | _ value: Value?, 157 | forKey key: AssociationKey, 158 | forObjectAt address: UnsafeRawPointer 159 | ) { 160 | _combinecocoa_objc_setAssociatedObject( 161 | address, 162 | key.address, 163 | value, 164 | .OBJC_ASSOCIATION_RETAIN_NONATOMIC 165 | ) 166 | } 167 | -------------------------------------------------------------------------------- /Sources/CombineInterception/NSObject+Interception.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSObject+Interception.swift 3 | // 4 | // 5 | // Created by Insu Byeon on 2023/01/09. 6 | // 7 | 8 | #if canImport(Combine) 9 | import Combine 10 | import Foundation 11 | 12 | #if canImport(CombineInterceptionObjC) 13 | import CombineInterceptionObjC 14 | #endif 15 | 16 | /// Whether the runtime subclass has already been prepared for method 17 | /// interception. 18 | private let interceptedKey = AssociationKey(default: false) 19 | 20 | /// Holds the method signature cache of the runtime subclass. 21 | private let signatureCacheKey = AssociationKey() 22 | 23 | /// Holds the method selector cache of the runtime subclass. 24 | private let selectorCacheKey = AssociationKey() 25 | 26 | internal let noImplementation: IMP = unsafeBitCast(Int(0), to: IMP.self) 27 | 28 | extension NSObject { 29 | /// Create a publisher which sends a `next` event at the end of every 30 | /// invocation of `selector` on the object. 31 | /// 32 | /// It completes when the object deinitializes. 33 | /// 34 | /// - note: Observers to the resulting publisher should not call the method 35 | /// specified by the selector. 36 | /// 37 | /// - parameters: 38 | /// - selector: The selector to observe. 39 | /// 40 | /// - returns: A trigger publisher. 41 | @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 42 | public func publisher(for selector: Selector) -> AnyPublisher<(), Never> { 43 | return intercept(selector).map { (_: AnyObject) in }.eraseToAnyPublisher() 44 | } 45 | 46 | /// Create a publisher which sends a `next` event, containing an array of 47 | /// bridged arguments, at the end of every invocation of `selector` on the 48 | /// object. 49 | /// 50 | /// It completes when the object deinitializes. 51 | /// 52 | /// - note: Observers to the resulting publisher should not call the method 53 | /// specified by the selector. 54 | /// 55 | /// - parameters: 56 | /// - selector: The selector to observe. 57 | /// 58 | /// - returns: A publisher that sends an array of bridged arguments. 59 | @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 60 | public func intercept(_ selector: Selector) -> AnyPublisher<[Any?], Never> { 61 | return intercept(selector).map(unpackInvocation).eraseToAnyPublisher() 62 | } 63 | 64 | /// Setup the method interception. 65 | /// 66 | /// - parameters: 67 | /// - object: The object to be intercepted. 68 | /// - selector: The selector of the method to be intercepted. 69 | /// 70 | /// - returns: A publisher that sends the corresponding `NSInvocation` after 71 | /// every invocation of the method. 72 | @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 73 | @nonobjc fileprivate func intercept(_ selector: Selector) -> AnyPublisher { 74 | guard let method = class_getInstanceMethod(objcClass, selector) else { 75 | fatalError( 76 | "Selector `\(selector)` does not exist in class `\(String(describing: objcClass))`." 77 | ) 78 | } 79 | 80 | let typeEncoding = method_getTypeEncoding(method)! 81 | assert(checkTypeEncoding(typeEncoding)) 82 | 83 | return synchronized(self) { 84 | let alias = selector.alias 85 | let stateKey = AssociationKey(alias) 86 | let interopAlias = selector.interopAlias 87 | 88 | if let state = associations.value(forKey: stateKey) { 89 | return state.subject.eraseToAnyPublisher() 90 | } 91 | 92 | let subclass: AnyClass = swizzleClass(self) 93 | let subclassAssociations = Associations(subclass as AnyObject) 94 | 95 | synchronized(subclass) { 96 | let isSwizzled = subclassAssociations.value(forKey: interceptedKey) 97 | 98 | let signatureCache: SignatureCache 99 | let selectorCache: SelectorCache 100 | 101 | if isSwizzled { 102 | signatureCache = subclassAssociations.value(forKey: signatureCacheKey) 103 | selectorCache = subclassAssociations.value(forKey: selectorCacheKey) 104 | } 105 | else { 106 | signatureCache = SignatureCache() 107 | selectorCache = SelectorCache() 108 | 109 | subclassAssociations.setValue(signatureCache, forKey: signatureCacheKey) 110 | subclassAssociations.setValue(selectorCache, forKey: selectorCacheKey) 111 | subclassAssociations.setValue(true, forKey: interceptedKey) 112 | 113 | enableMessageForwarding(subclass, selectorCache) 114 | setupMethodSignatureCaching(subclass, signatureCache) 115 | } 116 | 117 | selectorCache.cache(selector) 118 | 119 | if signatureCache[selector] == nil { 120 | let signature = NSMethodSignature.objcSignature(withObjCTypes: typeEncoding) 121 | signatureCache[selector] = signature 122 | } 123 | 124 | // If an immediate implementation of the selector is found in the 125 | // runtime subclass the first time the selector is intercepted, 126 | // preserve the implementation. 127 | // 128 | // Example: KVO setters if the instance is swizzled by KVO before RAC 129 | // does. 130 | if !class_respondsToSelector(subclass, interopAlias) { 131 | let immediateImpl = class_getImmediateMethod(subclass, selector) 132 | .flatMap(method_getImplementation) 133 | .flatMap { $0 != _combinecocoa_objc_msgForward ? $0 : nil } 134 | 135 | if let impl = immediateImpl { 136 | let succeeds = class_addMethod(subclass, interopAlias, impl, typeEncoding) 137 | precondition( 138 | succeeds, 139 | "RAC attempts to swizzle a selector that has message forwarding enabled with a runtime injected implementation. This is unsupported in the current version." 140 | ) 141 | } 142 | } 143 | } 144 | 145 | let state = InterceptingState() 146 | associations.setValue(state, forKey: stateKey) 147 | 148 | // Start forwarding the messages of the selector. 149 | _ = class_replaceMethod(subclass, selector, _combinecocoa_objc_msgForward, typeEncoding) 150 | 151 | return state.subject.eraseToAnyPublisher() 152 | } 153 | } 154 | } 155 | 156 | /// Swizzle `realClass` to enable message forwarding for method interception. 157 | /// 158 | /// - parameters: 159 | /// - realClass: The runtime subclass to be swizzled. 160 | @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 161 | private func enableMessageForwarding(_ realClass: AnyClass, _ selectorCache: SelectorCache) { 162 | let perceivedClass: AnyClass = class_getSuperclass(realClass)! 163 | 164 | typealias ForwardInvocationImpl = @convention(block) (Unmanaged, AnyObject) -> Void 165 | let newForwardInvocation: ForwardInvocationImpl = { objectRef, invocation in 166 | let selector = invocation.selector! 167 | let alias = selectorCache.alias(for: selector) 168 | let interopAlias = selectorCache.interopAlias(for: selector) 169 | 170 | defer { 171 | let stateKey = AssociationKey(alias) 172 | if let state = objectRef.takeUnretainedValue().associations.value(forKey: stateKey) { 173 | state.subject.send(invocation) 174 | } 175 | } 176 | 177 | let method = class_getInstanceMethod(perceivedClass, selector) 178 | let typeEncoding: String 179 | 180 | if let runtimeTypeEncoding = method.flatMap(method_getTypeEncoding) { 181 | typeEncoding = String(cString: runtimeTypeEncoding) 182 | } 183 | else { 184 | let methodSignature = (objectRef.takeUnretainedValue() as AnyObject) 185 | .objcMethodSignature(for: selector) 186 | let encodings = (0.., Selector, AnyObject) -> 270 | Void 271 | let forwardInvocationImpl = class_getMethodImplementation( 272 | perceivedClass, 273 | ObjCSelector.forwardInvocation 274 | ) 275 | let forwardInvocation = unsafeBitCast(forwardInvocationImpl, to: SuperForwardInvocation.self) 276 | forwardInvocation(objectRef, ObjCSelector.forwardInvocation, invocation) 277 | } 278 | 279 | _ = class_replaceMethod( 280 | realClass, 281 | ObjCSelector.forwardInvocation, 282 | imp_implementationWithBlock(newForwardInvocation as Any), 283 | ObjCMethodEncoding.forwardInvocation 284 | ) 285 | } 286 | 287 | /// Swizzle `realClass` to accelerate the method signature retrieval, using a 288 | /// signature cache that covers all known intercepted selectors of `realClass`. 289 | /// 290 | /// - parameters: 291 | /// - realClass: The runtime subclass to be swizzled. 292 | /// - signatureCache: The method signature cache. 293 | private func setupMethodSignatureCaching(_ realClass: AnyClass, _ signatureCache: SignatureCache) { 294 | let perceivedClass: AnyClass = class_getSuperclass(realClass)! 295 | 296 | let newMethodSignatureForSelector: 297 | @convention(block) (Unmanaged, Selector) -> AnyObject? = { objectRef, selector in 298 | if let signature = signatureCache[selector] { 299 | return signature 300 | } 301 | 302 | typealias SuperMethodSignatureForSelector = @convention(c) ( 303 | Unmanaged, Selector, Selector 304 | ) -> AnyObject? 305 | let impl = class_getMethodImplementation( 306 | perceivedClass, 307 | ObjCSelector.methodSignatureForSelector 308 | ) 309 | let methodSignatureForSelector = unsafeBitCast(impl, to: SuperMethodSignatureForSelector.self) 310 | return methodSignatureForSelector( 311 | objectRef, 312 | ObjCSelector.methodSignatureForSelector, 313 | selector 314 | ) 315 | } 316 | 317 | _ = class_replaceMethod( 318 | realClass, 319 | ObjCSelector.methodSignatureForSelector, 320 | imp_implementationWithBlock(newMethodSignatureForSelector as Any), 321 | ObjCMethodEncoding.methodSignatureForSelector 322 | ) 323 | } 324 | 325 | /// The state of an intercepted method specific to an instance. 326 | @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 327 | private final class InterceptingState { 328 | let subject = PassthroughSubject() 329 | } 330 | 331 | private final class SelectorCache { 332 | private var map: [Selector: (main: Selector, interop: Selector)] = [:] 333 | 334 | init() {} 335 | 336 | /// Cache the aliases of the specified selector in the cache. 337 | /// 338 | /// - warning: Any invocation of this method must be synchronized against the 339 | /// runtime subclass. 340 | @discardableResult 341 | func cache(_ selector: Selector) -> (main: Selector, interop: Selector) { 342 | if let pair = map[selector] { 343 | return pair 344 | } 345 | 346 | let aliases = (selector.alias, selector.interopAlias) 347 | map[selector] = aliases 348 | 349 | return aliases 350 | } 351 | 352 | /// Get the alias of the specified selector. 353 | /// 354 | /// - parameters: 355 | /// - selector: The selector alias. 356 | func alias(for selector: Selector) -> Selector { 357 | if let (main, _) = map[selector] { 358 | return main 359 | } 360 | 361 | return selector.alias 362 | } 363 | 364 | /// Get the secondary alias of the specified selector. 365 | /// 366 | /// - parameters: 367 | /// - selector: The selector alias. 368 | func interopAlias(for selector: Selector) -> Selector { 369 | if let (_, interop) = map[selector] { 370 | return interop 371 | } 372 | 373 | return selector.interopAlias 374 | } 375 | } 376 | 377 | // The signature cache for classes that have been swizzled for method 378 | // interception. 379 | // 380 | // Read-copy-update is used here, since the cache has multiple readers but only 381 | // one writer. 382 | private final class SignatureCache { 383 | // `Dictionary` takes 8 bytes for the reference to its storage and does CoW. 384 | // So it should not encounter any corrupted, partially updated state. 385 | private var map: [Selector: AnyObject] = [:] 386 | 387 | init() {} 388 | 389 | /// Get or set the signature for the specified selector. 390 | /// 391 | /// - warning: Any invocation of the setter must be synchronized against the 392 | /// runtime subclass. 393 | /// 394 | /// - parameters: 395 | /// - selector: The method signature. 396 | subscript(selector: Selector) -> AnyObject? { 397 | get { 398 | return map[selector] 399 | } 400 | set { 401 | if map[selector] == nil { 402 | map[selector] = newValue 403 | } 404 | } 405 | } 406 | } 407 | 408 | /// Assert that the method does not contain types that cannot be intercepted. 409 | /// 410 | /// - parameters: 411 | /// - types: The type encoding C string of the method. 412 | /// 413 | /// - returns: `true`. 414 | private func checkTypeEncoding(_ types: UnsafePointer) -> Bool { 415 | // Some types, including vector types, are not encoded. In these cases the 416 | // signature starts with the size of the argument frame. 417 | assert( 418 | types.pointee < Int8(UInt8(ascii: "1")) || types.pointee > Int8(UInt8(ascii: "9")), 419 | "unknown method return type not supported in type encoding: \(String(cString: types))" 420 | ) 421 | 422 | assert(types.pointee != Int8(UInt8(ascii: "(")), "union method return type not supported") 423 | assert(types.pointee != Int8(UInt8(ascii: "{")), "struct method return type not supported") 424 | assert(types.pointee != Int8(UInt8(ascii: "[")), "array method return type not supported") 425 | 426 | assert(types.pointee != Int8(UInt8(ascii: "j")), "complex method return type not supported") 427 | 428 | return true 429 | } 430 | 431 | /// Extract the arguments of an `NSInvocation` as an array of objects. 432 | /// 433 | /// - parameters: 434 | /// - invocation: The `NSInvocation` to unpack. 435 | /// 436 | /// - returns: An array of objects. 437 | private func unpackInvocation(_ invocation: AnyObject) -> [Any?] { 438 | let invocation = invocation as AnyObject 439 | let methodSignature = invocation.objcMethodSignature! 440 | let count = methodSignature.objcNumberOfArguments! 441 | 442 | var bridged = [Any?]() 443 | bridged.reserveCapacity(Int(count - 2)) 444 | 445 | // Ignore `self` and `_cmd` at index 0 and 1. 446 | for position in 2..(_ type: U.Type) -> U { 451 | let pointer = UnsafeMutableRawPointer.allocate( 452 | byteCount: MemoryLayout.size, 453 | alignment: MemoryLayout.alignment 454 | ) 455 | defer { 456 | pointer.deallocate() 457 | } 458 | 459 | invocation.objcCopy(to: pointer, forArgumentAt: Int(position)) 460 | return pointer.assumingMemoryBound(to: type).pointee 461 | } 462 | 463 | let value: Any? 464 | 465 | switch encoding { 466 | case .char: 467 | value = NSNumber(value: extract(CChar.self)) 468 | case .int: 469 | value = NSNumber(value: extract(CInt.self)) 470 | case .short: 471 | value = NSNumber(value: extract(CShort.self)) 472 | case .long: 473 | value = NSNumber(value: extract(CLong.self)) 474 | case .longLong: 475 | value = NSNumber(value: extract(CLongLong.self)) 476 | case .unsignedChar: 477 | value = NSNumber(value: extract(CUnsignedChar.self)) 478 | case .unsignedInt: 479 | value = NSNumber(value: extract(CUnsignedInt.self)) 480 | case .unsignedShort: 481 | value = NSNumber(value: extract(CUnsignedShort.self)) 482 | case .unsignedLong: 483 | value = NSNumber(value: extract(CUnsignedLong.self)) 484 | case .unsignedLongLong: 485 | value = NSNumber(value: extract(CUnsignedLongLong.self)) 486 | case .float: 487 | value = NSNumber(value: extract(CFloat.self)) 488 | case .double: 489 | value = NSNumber(value: extract(CDouble.self)) 490 | case .bool: 491 | value = NSNumber(value: extract(CBool.self)) 492 | case .object: 493 | value = extract((AnyObject?).self) 494 | case .type: 495 | value = extract((AnyClass?).self) 496 | case .selector: 497 | value = extract((Selector?).self) 498 | case .undefined: 499 | var size = 0 500 | var alignment = 0 501 | NSGetSizeAndAlignment(rawEncoding, &size, &alignment) 502 | let buffer = UnsafeMutableRawPointer.allocate(byteCount: size, alignment: alignment) 503 | defer { buffer.deallocate() } 504 | 505 | invocation.objcCopy(to: buffer, forArgumentAt: Int(position)) 506 | value = NSValue(bytes: buffer, objCType: rawEncoding) 507 | } 508 | 509 | bridged.append(value) 510 | } 511 | 512 | return bridged 513 | } 514 | #endif 515 | -------------------------------------------------------------------------------- /Sources/CombineInterception/NSObject+ObjCRuntime.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSObject+ObjCRuntime.swift 3 | // 4 | // 5 | // Created by Insu Byeon on 2023/01/09. 6 | // 7 | 8 | import Foundation 9 | 10 | extension NSObject { 11 | /// The class of the instance reported by the ObjC `-class:` message. 12 | /// 13 | /// - note: `type(of:)` might return the runtime subclass, while this property 14 | /// always returns the original class. 15 | @nonobjc internal var objcClass: AnyClass { 16 | return (self as AnyObject).objcClass 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/CombineInterception/ObjC+Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjC+Constants.swift 3 | // 4 | // 5 | // Created by Insu Byeon on 2023/01/09. 6 | // 7 | 8 | import Foundation 9 | 10 | // Unavailable selectors in Swift. 11 | internal enum ObjCSelector { 12 | static let forwardInvocation = Selector((("forwardInvocation:"))) 13 | static let methodSignatureForSelector = Selector((("methodSignatureForSelector:"))) 14 | static let getClass = Selector((("class"))) 15 | } 16 | 17 | // Method encoding of the unavailable selectors. 18 | internal enum ObjCMethodEncoding { 19 | static let forwardInvocation = extract("v@:@") 20 | static let methodSignatureForSelector = extract("v@::") 21 | static let getClass = extract("#@:") 22 | 23 | private static func extract(_ string: StaticString) -> UnsafePointer { 24 | return UnsafeRawPointer(string.utf8Start).assumingMemoryBound(to: CChar.self) 25 | } 26 | } 27 | 28 | /// Objective-C type encoding. 29 | /// 30 | /// The enum does not cover all options, but only those that are expressive in 31 | /// Swift. 32 | internal enum ObjCTypeEncoding: Int8 { 33 | case char = 99 34 | case int = 105 35 | case short = 115 36 | case long = 108 37 | case longLong = 113 38 | 39 | case unsignedChar = 67 40 | case unsignedInt = 73 41 | case unsignedShort = 83 42 | case unsignedLong = 76 43 | case unsignedLongLong = 81 44 | 45 | case float = 102 46 | case double = 100 47 | 48 | case bool = 66 49 | 50 | case object = 64 51 | case type = 35 52 | case selector = 58 53 | 54 | case undefined = -1 55 | } 56 | -------------------------------------------------------------------------------- /Sources/CombineInterception/ObjC+Messages.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjC+Messages.swift 3 | // 4 | // 5 | // Created by Insu Byeon on 2023/01/09. 6 | // 7 | 8 | // Unavailable classes like `NSInvocation` can still be passed into Swift as 9 | // `AnyClass` and `AnyObject`, and receive messages as `AnyClass` and 10 | // `AnyObject` existentials. 11 | // 12 | // These `@objc` protocols host the method signatures so that they can be used 13 | // with `AnyObject`. 14 | import Foundation 15 | 16 | internal let NSInvocation: AnyClass = NSClassFromString("NSInvocation")! 17 | internal let NSMethodSignature: AnyClass = NSClassFromString("NSMethodSignature")! 18 | 19 | // Signatures defined in `@objc` protocols would be available for ObjC message 20 | // sending via `AnyObject`. 21 | @objc internal protocol ObjCClassReporting { 22 | // An alias for `-class`, which is unavailable in Swift. 23 | @objc(class) 24 | var objcClass: AnyClass! { get } 25 | 26 | @objc(methodSignatureForSelector:) 27 | func objcMethodSignature(for selector: Selector) -> AnyObject 28 | } 29 | 30 | // Methods of `NSInvocation`. 31 | @objc internal protocol ObjCInvocation { 32 | @objc(setSelector:) 33 | func objcSetSelector(_ selector: Selector) 34 | 35 | @objc(methodSignature) 36 | var objcMethodSignature: AnyObject { get } 37 | 38 | @objc(getArgument:atIndex:) 39 | func objcCopy(to buffer: UnsafeMutableRawPointer?, forArgumentAt index: Int) 40 | 41 | @objc(invoke) 42 | func objcInvoke() 43 | 44 | @objc(invocationWithMethodSignature:) 45 | static func objcInvocation(withMethodSignature signature: AnyObject) -> AnyObject 46 | } 47 | 48 | // Methods of `NSMethodSignature`. 49 | @objc internal protocol ObjCMethodSignature { 50 | @objc(numberOfArguments) 51 | var objcNumberOfArguments: UInt { get } 52 | 53 | @objc(getArgumentTypeAtIndex:) 54 | func objcArgumentType(at index: UInt) -> UnsafePointer 55 | 56 | @objc(signatureWithObjCTypes:) 57 | static func objcSignature(withObjCTypes typeEncoding: UnsafePointer) -> AnyObject 58 | } 59 | -------------------------------------------------------------------------------- /Sources/CombineInterception/ObjC+Runtime.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjC+Runtime.swift 3 | // 4 | // 5 | // Created by Insu Byeon on 2023/01/09. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Search in `class` for any method that matches the supplied selector without 11 | /// propagating to the ancestors. 12 | /// 13 | /// - parameters: 14 | /// - class: The class to search the method in. 15 | /// - selector: The selector of the method. 16 | /// 17 | /// - returns: The matching method, or `nil` if none is found. 18 | internal func class_getImmediateMethod(_ `class`: AnyClass, _ selector: Selector) -> Method? { 19 | var total: UInt32 = 0 20 | 21 | if let methods = class_copyMethodList(`class`, &total) { 22 | defer { free(methods) } 23 | 24 | for index in 0..(default: nil) 22 | 23 | extension NSObject { 24 | /// Swizzle the given selectors. 25 | /// 26 | /// - warning: The swizzling **does not** apply on a per-instance basis. In 27 | /// other words, repetitive swizzling of the same selector would 28 | /// overwrite previous swizzling attempts, despite a different 29 | /// instance being supplied. 30 | /// 31 | /// - parameters: 32 | /// - pairs: Tuples of selectors and the respective implementions to be 33 | /// swapped in. 34 | /// - key: An association key which determines if the swizzling has already 35 | /// been performed. 36 | internal func swizzle(_ pairs: (Selector, Any)..., key hasSwizzledKey: AssociationKey) { 37 | let subclass: AnyClass = swizzleClass(self) 38 | 39 | synchronized(subclass) { 40 | let subclassAssociations = Associations(subclass as AnyObject) 41 | 42 | if !subclassAssociations.value(forKey: hasSwizzledKey) { 43 | subclassAssociations.setValue(true, forKey: hasSwizzledKey) 44 | 45 | for (selector, body) in pairs { 46 | let method = class_getInstanceMethod(subclass, selector)! 47 | let typeEncoding = method_getTypeEncoding(method)! 48 | 49 | if method_getImplementation(method) == _combinecocoa_objc_msgForward { 50 | let succeeds = class_addMethod( 51 | subclass, 52 | selector.interopAlias, 53 | imp_implementationWithBlock(body), 54 | typeEncoding 55 | ) 56 | precondition( 57 | succeeds, 58 | "RAC attempts to swizzle a selector that has message forwarding enabled with a runtime injected implementation. This is unsupported in the current version." 59 | ) 60 | } 61 | else { 62 | let succeeds = class_addMethod( 63 | subclass, 64 | selector, 65 | imp_implementationWithBlock(body), 66 | typeEncoding 67 | ) 68 | precondition( 69 | succeeds, 70 | "RAC attempts to swizzle a selector that has already a runtime injected implementation. This is unsupported in the current version." 71 | ) 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | /// ISA-swizzle the class of the supplied instance. 80 | /// 81 | /// - note: If the instance has already been isa-swizzled, the swizzling happens 82 | /// in place in the runtime subclass created by external parties. 83 | /// 84 | /// - warning: The swizzling **does not** apply on a per-instance basis. In 85 | /// other words, repetitive swizzling of the same selector would 86 | /// overwrite previous swizzling attempts, despite a different 87 | /// instance being supplied. 88 | /// 89 | /// - parameters: 90 | /// - instance: The instance to be swizzled. 91 | /// 92 | /// - returns: The runtime subclass of the perceived class of the instance. 93 | internal func swizzleClass(_ instance: NSObject) -> AnyClass { 94 | if let knownSubclass = instance.associations.value(forKey: knownRuntimeSubclassKey) { 95 | return knownSubclass 96 | } 97 | 98 | let perceivedClass: AnyClass = instance.objcClass 99 | let realClass: AnyClass = object_getClass(instance)! 100 | let realClassAssociations = Associations(realClass as AnyObject) 101 | 102 | if perceivedClass != realClass { 103 | // If the class is already lying about what it is, it's probably a KVO 104 | // dynamic subclass or something else that we shouldn't subclass at runtime. 105 | synchronized(realClass) { 106 | let isSwizzled = realClassAssociations.value(forKey: runtimeSubclassedKey) 107 | if !isSwizzled { 108 | replaceGetClass(in: realClass, decoy: perceivedClass) 109 | realClassAssociations.setValue(true, forKey: runtimeSubclassedKey) 110 | } 111 | } 112 | 113 | return realClass 114 | } 115 | else { 116 | let name = subclassName(of: perceivedClass) 117 | let subclass: AnyClass = name.withCString { cString in 118 | if let existingClass = objc_getClass(cString) as! AnyClass? { 119 | return existingClass 120 | } 121 | else { 122 | let subclass: AnyClass = objc_allocateClassPair(perceivedClass, cString, 0)! 123 | replaceGetClass(in: subclass, decoy: perceivedClass) 124 | objc_registerClassPair(subclass) 125 | return subclass 126 | } 127 | } 128 | 129 | object_setClass(instance, subclass) 130 | instance.associations.setValue(subclass, forKey: knownRuntimeSubclassKey) 131 | return subclass 132 | } 133 | } 134 | 135 | private func subclassName(of class: AnyClass) -> String { 136 | return String(cString: class_getName(`class`)).appending("_RACSwift") 137 | } 138 | 139 | /// Swizzle the `-class` and `+class` methods. 140 | /// 141 | /// - parameters: 142 | /// - class: The class to swizzle. 143 | /// - perceivedClass: The class to be reported by the methods. 144 | private func replaceGetClass(in class: AnyClass, decoy perceivedClass: AnyClass) { 145 | let getClass: @convention(block) (UnsafeRawPointer?) -> AnyClass = { _ in 146 | return perceivedClass 147 | } 148 | 149 | let impl = imp_implementationWithBlock(getClass as Any) 150 | 151 | _ = class_replaceMethod( 152 | `class`, 153 | ObjCSelector.getClass, 154 | impl, 155 | ObjCMethodEncoding.getClass 156 | ) 157 | 158 | _ = class_replaceMethod( 159 | object_getClass(`class`), 160 | ObjCSelector.getClass, 161 | impl, 162 | ObjCMethodEncoding.getClass 163 | ) 164 | } 165 | #endif 166 | -------------------------------------------------------------------------------- /Sources/CombineInterception/ObjC+Selector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjC+Selector.swift 3 | // 4 | // 5 | // Created by Insu Byeon on 2023/01/09. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Selector { 11 | /// `self` as a pointer. It is uniqued across instances, similar to 12 | /// `StaticString`. 13 | internal var utf8Start: UnsafePointer { 14 | return unsafeBitCast(self, to: UnsafePointer.self) 15 | } 16 | 17 | /// An alias of `self`, used in method interception. 18 | internal var alias: Selector { 19 | return prefixing("rac0_") 20 | } 21 | 22 | /// An alias of `self`, used in method interception specifically for 23 | /// preserving (if found) an immediate implementation of `self` in the 24 | /// runtime subclass. 25 | internal var interopAlias: Selector { 26 | return prefixing("rac1_") 27 | } 28 | 29 | /// An alias of `self`, used for delegate proxies. 30 | internal var delegateProxyAlias: Selector { 31 | return prefixing("rac2_") 32 | } 33 | 34 | internal func prefixing(_ prefix: StaticString) -> Selector { 35 | let length = Int(strlen(utf8Start)) 36 | let prefixedLength = length + prefix.utf8CodeUnitCount 37 | 38 | let asciiPrefix = UnsafeRawPointer(prefix.utf8Start).assumingMemoryBound(to: Int8.self) 39 | 40 | let cString = UnsafeMutablePointer.allocate(capacity: prefixedLength + 1) 41 | defer { 42 | cString.deinitialize(count: prefixedLength + 1) 43 | cString.deallocate() 44 | } 45 | 46 | cString.initialize(from: asciiPrefix, count: prefix.utf8CodeUnitCount) 47 | (cString + prefix.utf8CodeUnitCount).initialize(from: utf8Start, count: length) 48 | (cString + prefixedLength).initialize(to: Int8(UInt8(ascii: "\0"))) 49 | 50 | return sel_registerName(cString) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/CombineInterception/Synchronizing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Synchronizing.swift 3 | // 4 | // 5 | // Created by Insu Byeon on 2023/01/09. 6 | // 7 | 8 | import Foundation 9 | 10 | internal func synchronized(_ token: AnyObject, execute: () throws -> Result) rethrows -> Result { 11 | objc_sync_enter(token) 12 | defer { objc_sync_exit(token) } 13 | return try execute() 14 | } 15 | -------------------------------------------------------------------------------- /Sources/CombineInterceptionObjC/ObjcRuntimeAliases.m: -------------------------------------------------------------------------------- 1 | // 2 | // ObjcRuntimeAliases.m 3 | // 4 | // 5 | // Created by Insu Byeon on 2023/01/09. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | const IMP _combinecocoa_objc_msgForward = _objc_msgForward; 12 | 13 | void _combinecocoa_objc_setAssociatedObject(const void* object, 14 | const void* key, 15 | id value, 16 | objc_AssociationPolicy policy) { 17 | __unsafe_unretained id obj = (__bridge typeof(obj)) object; 18 | objc_setAssociatedObject(obj, key, value, policy); 19 | } 20 | -------------------------------------------------------------------------------- /Sources/CombineInterceptionObjC/include/ObjcRuntimeAliases.h: -------------------------------------------------------------------------------- 1 | // 2 | // ObjcRuntimeAliases.h 3 | // 4 | // 5 | // Created by Insu Byeon on 2023/01/09. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | extern const IMP _combinecocoa_objc_msgForward; 12 | 13 | /// A trampoline of `objc_setAssociatedObject` that is made to circumvent the 14 | /// reference counting calls in the imported version in Swift. 15 | void _combinecocoa_objc_setAssociatedObject(const void* object, 16 | const void* key, 17 | id _Nullable value, 18 | objc_AssociationPolicy policy); 19 | -------------------------------------------------------------------------------- /Sources/CombineInterceptionObjC/include/module.modulemap: -------------------------------------------------------------------------------- 1 | module CombineInterceptionObjC { 2 | umbrella header "ObjcRuntimeAliases.h" 3 | export * 4 | } 5 | --------------------------------------------------------------------------------