From c88cc49e6c6cb78283541c70b033bce18ff620d3 Mon Sep 17 00:00:00 2001 From: Benjamin Deckys Date: Thu, 27 Jul 2023 23:46:54 +0900 Subject: [PATCH] Add Interception Functionality --- Package.swift | 15 +- Sources/CombineCocoa/CombineCocoa.h | 13 - .../DelegateProxy/DelegateProxy.swift | 4 +- Sources/CombineCocoa/Exports.swift | 8 + .../NSObject+Association.swift | 167 ++++++ .../NSObject+Interseption.swift | 518 ++++++++++++++++++ .../NSObject+ObjCRuntime.swift | 19 + .../ObjC+Constants.swift | 56 ++ .../ObjC+Messages.swift | 60 ++ .../ObjC+Runtime.swift | 35 ++ .../ObjC+RuntimeSubclassing.swift | 166 ++++++ .../ObjC+Selector.swift | 53 ++ .../Synchronizing.swift | 17 + .../CombineCocoaRuntime/ObjcDelegateProxy.m | 219 ++++++++ .../CombineCocoaRuntime/ObjcRuntimeAliases.m | 21 + .../include/ObjcDelegateProxy.h | 1 + .../include/ObjcRuntimeAliases.h | 17 + .../include/module.modulemap | 2 +- Sources/Runtime/ObjcDelegateProxy.m | 220 -------- 19 files changed, 1373 insertions(+), 238 deletions(-) delete mode 100644 Sources/CombineCocoa/CombineCocoa.h create mode 100644 Sources/CombineCocoa/Exports.swift create mode 100644 Sources/CombineCocoaInterception/NSObject+Association.swift create mode 100644 Sources/CombineCocoaInterception/NSObject+Interseption.swift create mode 100644 Sources/CombineCocoaInterception/NSObject+ObjCRuntime.swift create mode 100644 Sources/CombineCocoaInterception/ObjC+Constants.swift create mode 100644 Sources/CombineCocoaInterception/ObjC+Messages.swift create mode 100644 Sources/CombineCocoaInterception/ObjC+Runtime.swift create mode 100644 Sources/CombineCocoaInterception/ObjC+RuntimeSubclassing.swift create mode 100644 Sources/CombineCocoaInterception/ObjC+Selector.swift create mode 100644 Sources/CombineCocoaInterception/Synchronizing.swift create mode 100644 Sources/CombineCocoaRuntime/ObjcDelegateProxy.m create mode 100644 Sources/CombineCocoaRuntime/ObjcRuntimeAliases.m rename Sources/{Runtime => CombineCocoaRuntime}/include/ObjcDelegateProxy.h (94%) create mode 100644 Sources/CombineCocoaRuntime/include/ObjcRuntimeAliases.h rename Sources/{Runtime => CombineCocoaRuntime}/include/module.modulemap (66%) delete mode 100644 Sources/Runtime/ObjcDelegateProxy.m diff --git a/Package.swift b/Package.swift index 564f05a..65b005b 100644 --- a/Package.swift +++ b/Package.swift @@ -9,7 +9,18 @@ let package = Package( ], dependencies: [], targets: [ - .target(name: "CombineCocoa", dependencies: ["Runtime"]), - .target(name: "Runtime", dependencies: []) + .target( + name: "CombineCocoa", + dependencies: [ + "CombineCocoaInterception", + "CombineCocoaRuntime" + ]), + .target( + name: "CombineCocoaInterception", + dependencies: [ + .target(name: "CombineCocoaRuntime") + ] + ), + .target(name: "CombineCocoaRuntime") ] ) diff --git a/Sources/CombineCocoa/CombineCocoa.h b/Sources/CombineCocoa/CombineCocoa.h deleted file mode 100644 index dd8db20..0000000 --- a/Sources/CombineCocoa/CombineCocoa.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// CombineCocoa.h -// CombineCocoa -// -// Created by Joan Disho on 25/09/2019. -// Copyright © 2020 Combine Community. All rights reserved. -// - -#import -#import - -FOUNDATION_EXPORT double CombineCocoaVersionNumber; -FOUNDATION_EXPORT const unsigned char CombineCocoaVersionString[]; diff --git a/Sources/CombineCocoa/DelegateProxy/DelegateProxy.swift b/Sources/CombineCocoa/DelegateProxy/DelegateProxy.swift index d3aae10..7a73454 100644 --- a/Sources/CombineCocoa/DelegateProxy/DelegateProxy.swift +++ b/Sources/CombineCocoa/DelegateProxy/DelegateProxy.swift @@ -10,8 +10,8 @@ import Foundation import Combine -#if canImport(Runtime) -import Runtime +#if canImport(CombineCocoaRuntime) +import CombineCocoaRuntime #endif @available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) diff --git a/Sources/CombineCocoa/Exports.swift b/Sources/CombineCocoa/Exports.swift new file mode 100644 index 0000000..8f0ad2b --- /dev/null +++ b/Sources/CombineCocoa/Exports.swift @@ -0,0 +1,8 @@ +// +// Exports.swift +// +// +// Created by Benjamin Deckys on 2023/07/27. +// + +@_exported import CombineCocoaInterception diff --git a/Sources/CombineCocoaInterception/NSObject+Association.swift b/Sources/CombineCocoaInterception/NSObject+Association.swift new file mode 100644 index 0000000..a22b384 --- /dev/null +++ b/Sources/CombineCocoaInterception/NSObject+Association.swift @@ -0,0 +1,167 @@ +// +// NSObject+Association.swift +// CombineCocoa +// +// Created by Maxim Krouk on 22.06.21. +// Copyright © 2020 Combine Community. All rights reserved. +// + +import Foundation + +#if canImport(CombineCocoaRuntime) + import CombineCocoaRuntime +#endif + +internal struct AssociationKey { + fileprivate let address: UnsafeRawPointer + fileprivate let `default`: Value! + + /// Create an ObjC association key. + /// + /// - warning: The key must be uniqued. + /// + /// - parameters: + /// - default: The default value, or `nil` to trap on undefined value. It is + /// ignored if `Value` is an optional. + init(default: Value? = nil) { + self.address = UnsafeRawPointer( + UnsafeMutablePointer.allocate(capacity: 1) + ) + self.default = `default` + } + + /// Create an ObjC association key from a `StaticString`. + /// + /// - precondition: `key` has a pointer representation. + /// + /// - parameters: + /// - default: The default value, or `nil` to trap on undefined value. It is + /// ignored if `Value` is an optional. + init(_ key: StaticString, default: Value? = nil) { + assert(key.hasPointerRepresentation) + self.address = UnsafeRawPointer(key.utf8Start) + self.default = `default` + } + + /// Create an ObjC association key from a `Selector`. + /// + /// - parameters: + /// - default: The default value, or `nil` to trap on undefined value. It is + /// ignored if `Value` is an optional. + init(_ key: Selector, default: Value? = nil) { + self.address = UnsafeRawPointer(key.utf8Start) + self.default = `default` + } +} + +internal struct Associations { + fileprivate let base: Base + + init(_ base: Base) { + self.base = base + } +} + +extension NSObjectProtocol { + /// Retrieve the associated value for the specified key. If the value does not + /// exist, `initial` would be called and the returned value would be + /// associated subsequently. + /// + /// - parameters: + /// - key: An optional key to differentiate different values. + /// - initial: The action that supples an initial value. + /// + /// - returns: The associated value for the specified key. + internal func associatedValue( + forKey key: StaticString = #function, + initial: (Self) -> T + ) -> T { + let key = AssociationKey(key) + + if let value = associations.value(forKey: key) { + return value + } + + let value = initial(self) + associations.setValue(value, forKey: key) + + return value + } +} + +extension NSObjectProtocol { + @nonobjc internal var associations: Associations { + return Associations(self) + } +} + +extension Associations { + /// Retrieve the associated value for the specified key. + /// + /// - parameters: + /// - key: The key. + /// + /// - returns: The associated value, or the default value if no value has been + /// associated with the key. + internal func value( + forKey key: AssociationKey + ) -> Value { + return (objc_getAssociatedObject(base, key.address) as! Value?) ?? key.default + } + + /// Retrieve the associated value for the specified key. + /// + /// - parameters: + /// - key: The key. + /// + /// - returns: The associated value, or `nil` if no value is associated with + /// the key. + internal func value( + forKey key: AssociationKey + ) -> Value? { + return objc_getAssociatedObject(base, key.address) as! Value? + } + + /// Set the associated value for the specified key. + /// + /// - parameters: + /// - value: The value to be associated. + /// - key: The key. + internal func setValue( + _ value: Value, + forKey key: AssociationKey + ) { + objc_setAssociatedObject(base, key.address, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + + /// Set the associated value for the specified key. + /// + /// - parameters: + /// - value: The value to be associated. + /// - key: The key. + internal func setValue( + _ value: Value?, + forKey key: AssociationKey + ) { + objc_setAssociatedObject(base, key.address, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } +} + +/// Set the associated value for the specified key. +/// +/// - parameters: +/// - value: The value to be associated. +/// - key: The key. +/// - address: The address of the object. +internal func unsafeSetAssociatedValue( + _ value: Value?, + forKey key: AssociationKey, + forObjectAt address: UnsafeRawPointer +) { + _combinecocoa_objc_setAssociatedObject( + address, + key.address, + value, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) +} diff --git a/Sources/CombineCocoaInterception/NSObject+Interseption.swift b/Sources/CombineCocoaInterception/NSObject+Interseption.swift new file mode 100644 index 0000000..fbe353f --- /dev/null +++ b/Sources/CombineCocoaInterception/NSObject+Interseption.swift @@ -0,0 +1,518 @@ +// +// NSObject+Interseption.swift +// CombineCocoa +// +// Created by Maxim Krouk on 22.06.21. +// Copyright © 2020 Combine Community. All rights reserved. +// + +#if canImport(Combine) +import Combine +import Foundation + +#if canImport(CombineCocoaRuntime) + import CombineCocoaRuntime +#endif + +/// Whether the runtime subclass has already been prepared for method +/// interception. +private let interceptedKey = AssociationKey(default: false) + +/// Holds the method signature cache of the runtime subclass. +private let signatureCacheKey = AssociationKey() + +/// Holds the method selector cache of the runtime subclass. +private let selectorCacheKey = AssociationKey() + +internal let noImplementation: IMP = unsafeBitCast(Int(0), to: IMP.self) + +extension NSObject { + /// Create a publisher which sends a `next` event at the end of every + /// invocation of `selector` on the object. + /// + /// It completes when the object deinitializes. + /// + /// - note: Observers to the resulting publisher should not call the method + /// specified by the selector. + /// + /// - parameters: + /// - selector: The selector to observe. + /// + /// - returns: A trigger publisher. + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + public func publisher(for selector: Selector) -> AnyPublisher<(), Never> { + return intercept(selector).map { (_: AnyObject) in }.eraseToAnyPublisher() + } + + /// Create a publisher which sends a `next` event, containing an array of + /// bridged arguments, at the end of every invocation of `selector` on the + /// object. + /// + /// It completes when the object deinitializes. + /// + /// - note: Observers to the resulting publisher should not call the method + /// specified by the selector. + /// + /// - parameters: + /// - selector: The selector to observe. + /// + /// - returns: A publisher that sends an array of bridged arguments. + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + public func intercept(_ selector: Selector) -> AnyPublisher<[Any?], Never> { + return intercept(selector).map(unpackInvocation).eraseToAnyPublisher() + } + + /// Setup the method interception. + /// + /// - parameters: + /// - object: The object to be intercepted. + /// - selector: The selector of the method to be intercepted. + /// + /// - returns: A publisher that sends the corresponding `NSInvocation` after + /// every invocation of the method. + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + @nonobjc fileprivate func intercept(_ selector: Selector) -> AnyPublisher { + guard let method = class_getInstanceMethod(objcClass, selector) else { + fatalError( + "Selector `\(selector)` does not exist in class `\(String(describing: objcClass))`." + ) + } + + let typeEncoding = method_getTypeEncoding(method)! + assert(checkTypeEncoding(typeEncoding)) + + return synchronized(self) { + let alias = selector.alias + let stateKey = AssociationKey(alias) + let interopAlias = selector.interopAlias + + if let state = associations.value(forKey: stateKey) { + return state.subject.eraseToAnyPublisher() + } + + let subclass: AnyClass = swizzleClass(self) + let subclassAssociations = Associations(subclass as AnyObject) + + synchronized(subclass) { + let isSwizzled = subclassAssociations.value(forKey: interceptedKey) + + let signatureCache: SignatureCache + let selectorCache: SelectorCache + + if isSwizzled { + signatureCache = subclassAssociations.value(forKey: signatureCacheKey) + selectorCache = subclassAssociations.value(forKey: selectorCacheKey) + } + else { + signatureCache = SignatureCache() + selectorCache = SelectorCache() + + subclassAssociations.setValue(signatureCache, forKey: signatureCacheKey) + subclassAssociations.setValue(selectorCache, forKey: selectorCacheKey) + subclassAssociations.setValue(true, forKey: interceptedKey) + + enableMessageForwarding(subclass, selectorCache) + setupMethodSignatureCaching(subclass, signatureCache) + } + + selectorCache.cache(selector) + + if signatureCache[selector] == nil { + let signature = NSMethodSignature.objcSignature(withObjCTypes: typeEncoding) + signatureCache[selector] = signature + } + + // If an immediate implementation of the selector is found in the + // runtime subclass the first time the selector is intercepted, + // preserve the implementation. + // + // Example: KVO setters if the instance is swizzled by KVO before RAC + // does. + if !class_respondsToSelector(subclass, interopAlias) { + let immediateImpl = class_getImmediateMethod(subclass, selector) + .flatMap(method_getImplementation) + .flatMap { $0 != _combinecocoa_objc_msgForward ? $0 : nil } + + if let impl = immediateImpl { + let succeeds = class_addMethod(subclass, interopAlias, impl, typeEncoding) + precondition( + succeeds, + "RAC attempts to swizzle a selector that has message forwarding enabled with a runtime injected implementation. This is unsupported in the current version." + ) + } + } + } + + let state = InterceptingState() + associations.setValue(state, forKey: stateKey) + + // Start forwarding the messages of the selector. + _ = class_replaceMethod(subclass, selector, _combinecocoa_objc_msgForward, typeEncoding) + + return state.subject.eraseToAnyPublisher() + } + } +} + +/// Swizzle `realClass` to enable message forwarding for method interception. +/// +/// - parameters: +/// - realClass: The runtime subclass to be swizzled. +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +private func enableMessageForwarding(_ realClass: AnyClass, _ selectorCache: SelectorCache) { + let perceivedClass: AnyClass = class_getSuperclass(realClass)! + + typealias ForwardInvocationImpl = @convention(block) (Unmanaged, AnyObject) -> Void + let newForwardInvocation: ForwardInvocationImpl = { objectRef, invocation in + let selector = invocation.selector! + let alias = selectorCache.alias(for: selector) + let interopAlias = selectorCache.interopAlias(for: selector) + + defer { + let stateKey = AssociationKey(alias) + if let state = objectRef.takeUnretainedValue().associations.value(forKey: stateKey) { + state.subject.send(invocation) + } + } + + let method = class_getInstanceMethod(perceivedClass, selector) + let typeEncoding: String + + if let runtimeTypeEncoding = method.flatMap(method_getTypeEncoding) { + typeEncoding = String(cString: runtimeTypeEncoding) + } + else { + let methodSignature = (objectRef.takeUnretainedValue() as AnyObject) + .objcMethodSignature(for: selector) + let encodings = (0.., Selector, AnyObject) -> + Void + let forwardInvocationImpl = class_getMethodImplementation( + perceivedClass, + ObjCSelector.forwardInvocation + ) + let forwardInvocation = unsafeBitCast(forwardInvocationImpl, to: SuperForwardInvocation.self) + forwardInvocation(objectRef, ObjCSelector.forwardInvocation, invocation) + } + + _ = class_replaceMethod( + realClass, + ObjCSelector.forwardInvocation, + imp_implementationWithBlock(newForwardInvocation as Any), + ObjCMethodEncoding.forwardInvocation + ) +} + +/// Swizzle `realClass` to accelerate the method signature retrieval, using a +/// signature cache that covers all known intercepted selectors of `realClass`. +/// +/// - parameters: +/// - realClass: The runtime subclass to be swizzled. +/// - signatureCache: The method signature cache. +private func setupMethodSignatureCaching(_ realClass: AnyClass, _ signatureCache: SignatureCache) { + let perceivedClass: AnyClass = class_getSuperclass(realClass)! + + let newMethodSignatureForSelector: + @convention(block) (Unmanaged, Selector) -> AnyObject? = { objectRef, selector in + if let signature = signatureCache[selector] { + return signature + } + + typealias SuperMethodSignatureForSelector = @convention(c) ( + Unmanaged, Selector, Selector + ) -> AnyObject? + let impl = class_getMethodImplementation( + perceivedClass, + ObjCSelector.methodSignatureForSelector + ) + let methodSignatureForSelector = unsafeBitCast(impl, to: SuperMethodSignatureForSelector.self) + return methodSignatureForSelector( + objectRef, + ObjCSelector.methodSignatureForSelector, + selector + ) + } + + _ = class_replaceMethod( + realClass, + ObjCSelector.methodSignatureForSelector, + imp_implementationWithBlock(newMethodSignatureForSelector as Any), + ObjCMethodEncoding.methodSignatureForSelector + ) +} + +/// The state of an intercepted method specific to an instance. +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +private final class InterceptingState { + let subject = PassthroughSubject() +} + +private final class SelectorCache { + private var map: [Selector: (main: Selector, interop: Selector)] = [:] + + init() {} + + /// Cache the aliases of the specified selector in the cache. + /// + /// - warning: Any invocation of this method must be synchronized against the + /// runtime subclass. + @discardableResult + func cache(_ selector: Selector) -> (main: Selector, interop: Selector) { + if let pair = map[selector] { + return pair + } + + let aliases = (selector.alias, selector.interopAlias) + map[selector] = aliases + + return aliases + } + + /// Get the alias of the specified selector. + /// + /// - parameters: + /// - selector: The selector alias. + func alias(for selector: Selector) -> Selector { + if let (main, _) = map[selector] { + return main + } + + return selector.alias + } + + /// Get the secondary alias of the specified selector. + /// + /// - parameters: + /// - selector: The selector alias. + func interopAlias(for selector: Selector) -> Selector { + if let (_, interop) = map[selector] { + return interop + } + + return selector.interopAlias + } +} + +// The signature cache for classes that have been swizzled for method +// interception. +// +// Read-copy-update is used here, since the cache has multiple readers but only +// one writer. +private final class SignatureCache { + // `Dictionary` takes 8 bytes for the reference to its storage and does CoW. + // So it should not encounter any corrupted, partially updated state. + private var map: [Selector: AnyObject] = [:] + + init() {} + + /// Get or set the signature for the specified selector. + /// + /// - warning: Any invocation of the setter must be synchronized against the + /// runtime subclass. + /// + /// - parameters: + /// - selector: The method signature. + subscript(selector: Selector) -> AnyObject? { + get { + return map[selector] + } + set { + if map[selector] == nil { + map[selector] = newValue + } + } + } +} + +/// Assert that the method does not contain types that cannot be intercepted. +/// +/// - parameters: +/// - types: The type encoding C string of the method. +/// +/// - returns: `true`. +private func checkTypeEncoding(_ types: UnsafePointer) -> Bool { + // Some types, including vector types, are not encoded. In these cases the + // signature starts with the size of the argument frame. + assert( + types.pointee < Int8(UInt8(ascii: "1")) || types.pointee > Int8(UInt8(ascii: "9")), + "unknown method return type not supported in type encoding: \(String(cString: types))" + ) + + assert(types.pointee != Int8(UInt8(ascii: "(")), "union method return type not supported") + assert(types.pointee != Int8(UInt8(ascii: "{")), "struct method return type not supported") + assert(types.pointee != Int8(UInt8(ascii: "[")), "array method return type not supported") + + assert(types.pointee != Int8(UInt8(ascii: "j")), "complex method return type not supported") + + return true +} + +/// Extract the arguments of an `NSInvocation` as an array of objects. +/// +/// - parameters: +/// - invocation: The `NSInvocation` to unpack. +/// +/// - returns: An array of objects. +private func unpackInvocation(_ invocation: AnyObject) -> [Any?] { + let invocation = invocation as AnyObject + let methodSignature = invocation.objcMethodSignature! + let count = methodSignature.objcNumberOfArguments! + + var bridged = [Any?]() + bridged.reserveCapacity(Int(count - 2)) + + // Ignore `self` and `_cmd` at index 0 and 1. + for position in 2..(_ type: U.Type) -> U { + let pointer = UnsafeMutableRawPointer.allocate( + byteCount: MemoryLayout.size, + alignment: MemoryLayout.alignment + ) + defer { + pointer.deallocate() + } + + invocation.objcCopy(to: pointer, forArgumentAt: Int(position)) + return pointer.assumingMemoryBound(to: type).pointee + } + + let value: Any? + + switch encoding { + case .char: + value = NSNumber(value: extract(CChar.self)) + case .int: + value = NSNumber(value: extract(CInt.self)) + case .short: + value = NSNumber(value: extract(CShort.self)) + case .long: + value = NSNumber(value: extract(CLong.self)) + case .longLong: + value = NSNumber(value: extract(CLongLong.self)) + case .unsignedChar: + value = NSNumber(value: extract(CUnsignedChar.self)) + case .unsignedInt: + value = NSNumber(value: extract(CUnsignedInt.self)) + case .unsignedShort: + value = NSNumber(value: extract(CUnsignedShort.self)) + case .unsignedLong: + value = NSNumber(value: extract(CUnsignedLong.self)) + case .unsignedLongLong: + value = NSNumber(value: extract(CUnsignedLongLong.self)) + case .float: + value = NSNumber(value: extract(CFloat.self)) + case .double: + value = NSNumber(value: extract(CDouble.self)) + case .bool: + value = NSNumber(value: extract(CBool.self)) + case .object: + value = extract((AnyObject?).self) + case .type: + value = extract((AnyClass?).self) + case .selector: + value = extract((Selector?).self) + case .undefined: + var size = 0 + var alignment = 0 + NSGetSizeAndAlignment(rawEncoding, &size, &alignment) + let buffer = UnsafeMutableRawPointer.allocate(byteCount: size, alignment: alignment) + defer { buffer.deallocate() } + + invocation.objcCopy(to: buffer, forArgumentAt: Int(position)) + value = NSValue(bytes: buffer, objCType: rawEncoding) + } + + bridged.append(value) + } + + return bridged +} +#endif diff --git a/Sources/CombineCocoaInterception/NSObject+ObjCRuntime.swift b/Sources/CombineCocoaInterception/NSObject+ObjCRuntime.swift new file mode 100644 index 0000000..fbc4a3a --- /dev/null +++ b/Sources/CombineCocoaInterception/NSObject+ObjCRuntime.swift @@ -0,0 +1,19 @@ +// +// NSObject+ObjCRuntime.swift +// CombineCocoa +// +// Created by Maxim Krouk on 22.06.21. +// Copyright © 2020 Combine Community. All rights reserved. +// + +import Foundation + +extension NSObject { + /// The class of the instance reported by the ObjC `-class:` message. + /// + /// - note: `type(of:)` might return the runtime subclass, while this property + /// always returns the original class. + @nonobjc internal var objcClass: AnyClass { + return (self as AnyObject).objcClass + } +} diff --git a/Sources/CombineCocoaInterception/ObjC+Constants.swift b/Sources/CombineCocoaInterception/ObjC+Constants.swift new file mode 100644 index 0000000..d761421 --- /dev/null +++ b/Sources/CombineCocoaInterception/ObjC+Constants.swift @@ -0,0 +1,56 @@ +// +// ObjC+Constants.swift +// CombineCocoa +// +// Created by Maxim Krouk on 22.06.21. +// Copyright © 2020 Combine Community. All rights reserved. +// + +import Foundation + +// Unavailable selectors in Swift. +internal enum ObjCSelector { + static let forwardInvocation = Selector((("forwardInvocation:"))) + static let methodSignatureForSelector = Selector((("methodSignatureForSelector:"))) + static let getClass = Selector((("class"))) +} + +// Method encoding of the unavailable selectors. +internal enum ObjCMethodEncoding { + static let forwardInvocation = extract("v@:@") + static let methodSignatureForSelector = extract("v@::") + static let getClass = extract("#@:") + + private static func extract(_ string: StaticString) -> UnsafePointer { + return UnsafeRawPointer(string.utf8Start).assumingMemoryBound(to: CChar.self) + } +} + +/// Objective-C type encoding. +/// +/// The enum does not cover all options, but only those that are expressive in +/// Swift. +internal enum ObjCTypeEncoding: Int8 { + case char = 99 + case int = 105 + case short = 115 + case long = 108 + case longLong = 113 + + case unsignedChar = 67 + case unsignedInt = 73 + case unsignedShort = 83 + case unsignedLong = 76 + case unsignedLongLong = 81 + + case float = 102 + case double = 100 + + case bool = 66 + + case object = 64 + case type = 35 + case selector = 58 + + case undefined = -1 +} diff --git a/Sources/CombineCocoaInterception/ObjC+Messages.swift b/Sources/CombineCocoaInterception/ObjC+Messages.swift new file mode 100644 index 0000000..253d495 --- /dev/null +++ b/Sources/CombineCocoaInterception/ObjC+Messages.swift @@ -0,0 +1,60 @@ +// +// ObjC+Messages.swift +// CombineCocoa +// +// Created by Maxim Krouk on 22.06.21. +// Copyright © 2020 Combine Community. All rights reserved. +// + +// Unavailable classes like `NSInvocation` can still be passed into Swift as +// `AnyClass` and `AnyObject`, and receive messages as `AnyClass` and +// `AnyObject` existentials. +// +// These `@objc` protocols host the method signatures so that they can be used +// with `AnyObject`. + +import Foundation + +internal let NSInvocation: AnyClass = NSClassFromString("NSInvocation")! +internal let NSMethodSignature: AnyClass = NSClassFromString("NSMethodSignature")! + +// Signatures defined in `@objc` protocols would be available for ObjC message +// sending via `AnyObject`. +@objc internal protocol ObjCClassReporting { + // An alias for `-class`, which is unavailable in Swift. + @objc(class) + var objcClass: AnyClass! { get } + + @objc(methodSignatureForSelector:) + func objcMethodSignature(for selector: Selector) -> AnyObject +} + +// Methods of `NSInvocation`. +@objc internal protocol ObjCInvocation { + @objc(setSelector:) + func objcSetSelector(_ selector: Selector) + + @objc(methodSignature) + var objcMethodSignature: AnyObject { get } + + @objc(getArgument:atIndex:) + func objcCopy(to buffer: UnsafeMutableRawPointer?, forArgumentAt index: Int) + + @objc(invoke) + func objcInvoke() + + @objc(invocationWithMethodSignature:) + static func objcInvocation(withMethodSignature signature: AnyObject) -> AnyObject +} + +// Methods of `NSMethodSignature`. +@objc internal protocol ObjCMethodSignature { + @objc(numberOfArguments) + var objcNumberOfArguments: UInt { get } + + @objc(getArgumentTypeAtIndex:) + func objcArgumentType(at index: UInt) -> UnsafePointer + + @objc(signatureWithObjCTypes:) + static func objcSignature(withObjCTypes typeEncoding: UnsafePointer) -> AnyObject +} diff --git a/Sources/CombineCocoaInterception/ObjC+Runtime.swift b/Sources/CombineCocoaInterception/ObjC+Runtime.swift new file mode 100644 index 0000000..acd30f5 --- /dev/null +++ b/Sources/CombineCocoaInterception/ObjC+Runtime.swift @@ -0,0 +1,35 @@ +// +// ObjC+Runtime.swift +// CombineCocoa +// +// Created by Maxim Krouk on 22.06.21. +// Copyright © 2020 Combine Community. All rights reserved. +// + +import Foundation + +/// Search in `class` for any method that matches the supplied selector without +/// propagating to the ancestors. +/// +/// - parameters: +/// - class: The class to search the method in. +/// - selector: The selector of the method. +/// +/// - returns: The matching method, or `nil` if none is found. +internal func class_getImmediateMethod(_ `class`: AnyClass, _ selector: Selector) -> Method? { + var total: UInt32 = 0 + + if let methods = class_copyMethodList(`class`, &total) { + defer { free(methods) } + + for index in 0..(default: nil) + +extension NSObject { + /// Swizzle the given selectors. + /// + /// - warning: The swizzling **does not** apply on a per-instance basis. In + /// other words, repetitive swizzling of the same selector would + /// overwrite previous swizzling attempts, despite a different + /// instance being supplied. + /// + /// - parameters: + /// - pairs: Tuples of selectors and the respective implementions to be + /// swapped in. + /// - key: An association key which determines if the swizzling has already + /// been performed. + internal func swizzle(_ pairs: (Selector, Any)..., key hasSwizzledKey: AssociationKey) { + let subclass: AnyClass = swizzleClass(self) + + synchronized(subclass) { + let subclassAssociations = Associations(subclass as AnyObject) + + if !subclassAssociations.value(forKey: hasSwizzledKey) { + subclassAssociations.setValue(true, forKey: hasSwizzledKey) + + for (selector, body) in pairs { + let method = class_getInstanceMethod(subclass, selector)! + let typeEncoding = method_getTypeEncoding(method)! + + if method_getImplementation(method) == _combinecocoa_objc_msgForward { + let succeeds = class_addMethod( + subclass, + selector.interopAlias, + imp_implementationWithBlock(body), + typeEncoding + ) + precondition( + succeeds, + "RAC attempts to swizzle a selector that has message forwarding enabled with a runtime injected implementation. This is unsupported in the current version." + ) + } + else { + let succeeds = class_addMethod( + subclass, + selector, + imp_implementationWithBlock(body), + typeEncoding + ) + precondition( + succeeds, + "RAC attempts to swizzle a selector that has already a runtime injected implementation. This is unsupported in the current version." + ) + } + } + } + } + } +} + +/// ISA-swizzle the class of the supplied instance. +/// +/// - note: If the instance has already been isa-swizzled, the swizzling happens +/// in place in the runtime subclass created by external parties. +/// +/// - warning: The swizzling **does not** apply on a per-instance basis. In +/// other words, repetitive swizzling of the same selector would +/// overwrite previous swizzling attempts, despite a different +/// instance being supplied. +/// +/// - parameters: +/// - instance: The instance to be swizzled. +/// +/// - returns: The runtime subclass of the perceived class of the instance. +internal func swizzleClass(_ instance: NSObject) -> AnyClass { + if let knownSubclass = instance.associations.value(forKey: knownRuntimeSubclassKey) { + return knownSubclass + } + + let perceivedClass: AnyClass = instance.objcClass + let realClass: AnyClass = object_getClass(instance)! + let realClassAssociations = Associations(realClass as AnyObject) + + if perceivedClass != realClass { + // If the class is already lying about what it is, it's probably a KVO + // dynamic subclass or something else that we shouldn't subclass at runtime. + synchronized(realClass) { + let isSwizzled = realClassAssociations.value(forKey: runtimeSubclassedKey) + if !isSwizzled { + replaceGetClass(in: realClass, decoy: perceivedClass) + realClassAssociations.setValue(true, forKey: runtimeSubclassedKey) + } + } + + return realClass + } + else { + let name = subclassName(of: perceivedClass) + let subclass: AnyClass = name.withCString { cString in + if let existingClass = objc_getClass(cString) as! AnyClass? { + return existingClass + } + else { + let subclass: AnyClass = objc_allocateClassPair(perceivedClass, cString, 0)! + replaceGetClass(in: subclass, decoy: perceivedClass) + objc_registerClassPair(subclass) + return subclass + } + } + + object_setClass(instance, subclass) + instance.associations.setValue(subclass, forKey: knownRuntimeSubclassKey) + return subclass + } +} + +private func subclassName(of class: AnyClass) -> String { + return String(cString: class_getName(`class`)).appending("_RACSwift") +} + +/// Swizzle the `-class` and `+class` methods. +/// +/// - parameters: +/// - class: The class to swizzle. +/// - perceivedClass: The class to be reported by the methods. +private func replaceGetClass(in class: AnyClass, decoy perceivedClass: AnyClass) { + let getClass: @convention(block) (UnsafeRawPointer?) -> AnyClass = { _ in + return perceivedClass + } + + let impl = imp_implementationWithBlock(getClass as Any) + + _ = class_replaceMethod( + `class`, + ObjCSelector.getClass, + impl, + ObjCMethodEncoding.getClass + ) + + _ = class_replaceMethod( + object_getClass(`class`), + ObjCSelector.getClass, + impl, + ObjCMethodEncoding.getClass + ) +} +#endif diff --git a/Sources/CombineCocoaInterception/ObjC+Selector.swift b/Sources/CombineCocoaInterception/ObjC+Selector.swift new file mode 100644 index 0000000..4ee73c3 --- /dev/null +++ b/Sources/CombineCocoaInterception/ObjC+Selector.swift @@ -0,0 +1,53 @@ +// +// ObjC+Selector.swift +// CombineCocoa +// +// Created by Maxim Krouk on 22.06.21. +// Copyright © 2020 Combine Community. All rights reserved. +// + +import Foundation + +extension Selector { + /// `self` as a pointer. It is uniqued across instances, similar to + /// `StaticString`. + internal var utf8Start: UnsafePointer { + return unsafeBitCast(self, to: UnsafePointer.self) + } + + /// An alias of `self`, used in method interception. + internal var alias: Selector { + return prefixing("rac0_") + } + + /// An alias of `self`, used in method interception specifically for + /// preserving (if found) an immediate implementation of `self` in the + /// runtime subclass. + internal var interopAlias: Selector { + return prefixing("rac1_") + } + + /// An alias of `self`, used for delegate proxies. + internal var delegateProxyAlias: Selector { + return prefixing("rac2_") + } + + internal func prefixing(_ prefix: StaticString) -> Selector { + let length = Int(strlen(utf8Start)) + let prefixedLength = length + prefix.utf8CodeUnitCount + + let asciiPrefix = UnsafeRawPointer(prefix.utf8Start).assumingMemoryBound(to: Int8.self) + + let cString = UnsafeMutablePointer.allocate(capacity: prefixedLength + 1) + defer { + cString.deinitialize(count: prefixedLength + 1) + cString.deallocate() + } + + cString.initialize(from: asciiPrefix, count: prefix.utf8CodeUnitCount) + (cString + prefix.utf8CodeUnitCount).initialize(from: utf8Start, count: length) + (cString + prefixedLength).initialize(to: Int8(UInt8(ascii: "\0"))) + + return sel_registerName(cString) + } +} diff --git a/Sources/CombineCocoaInterception/Synchronizing.swift b/Sources/CombineCocoaInterception/Synchronizing.swift new file mode 100644 index 0000000..72d3d5f --- /dev/null +++ b/Sources/CombineCocoaInterception/Synchronizing.swift @@ -0,0 +1,17 @@ +// +// Synchronizing.swift +// CombineCocoa +// +// Created by Maxim Krouk on 22.06.21. +// Copyright © 2020 Combine Community. All rights reserved. +// + +import Foundation + +internal func synchronized(_ token: AnyObject, execute: () throws -> Result) rethrows + -> Result +{ + objc_sync_enter(token) + defer { objc_sync_exit(token) } + return try execute() +} diff --git a/Sources/CombineCocoaRuntime/ObjcDelegateProxy.m b/Sources/CombineCocoaRuntime/ObjcDelegateProxy.m new file mode 100644 index 0000000..44963c8 --- /dev/null +++ b/Sources/CombineCocoaRuntime/ObjcDelegateProxy.m @@ -0,0 +1,219 @@ +// +// ObjcDelegateProxy.m +// CombineCocoa +// +// Created by Joan Disho & Shai Mishali on 25/09/2019. +// Copyright © 2020 Combine Community. All rights reserved. +// + +#import +#import "include/ObjcDelegateProxy.h" +#import + +#define OBJECT_VALUE(object) [NSValue valueWithNonretainedObject:(object)] + +static NSMutableDictionary *> *allSelectors; + +@implementation ObjcDelegateProxy + +- (NSSet *)selectors { + return allSelectors[OBJECT_VALUE(self.class)]; +} + ++ (void)initialize +{ + @synchronized (ObjcDelegateProxy.class) { + if (!allSelectors) { + allSelectors = [NSMutableDictionary new]; + } + allSelectors[OBJECT_VALUE(self)] = [self selectorsOfClass:self + withEncodedReturnType:[NSString stringWithFormat:@"%s", @encode(void)]]; + } +} + +- (BOOL)respondsToSelector:(SEL _Nonnull)aSelector { + return [super respondsToSelector:aSelector] || [self canRespondToSelector:aSelector]; +} + +- (BOOL)canRespondToSelector:(SEL _Nonnull)selector { + for (id current in allSelectors[OBJECT_VALUE(self.class)]) { + if (selector == (SEL) [current pointerValue]) { + return true; + } + } + + return false; +} + +- (void)interceptedSelector:(SEL _Nonnull)selector arguments:(NSArray * _Nonnull)arguments {} + +- (void)forwardInvocation:(NSInvocation *)anInvocation +{ + NSArray * _Nonnull arguments = unpackInvocation(anInvocation); + [self interceptedSelector:anInvocation.selector arguments:arguments]; +} + +NSArray * _Nonnull unpackInvocation(NSInvocation * _Nonnull invocation) { + NSUInteger numberOfArguments = invocation.methodSignature.numberOfArguments; + NSMutableArray *arguments = [NSMutableArray arrayWithCapacity:numberOfArguments - 2]; + + // Ignore `self` and `_cmd` at index 0 and 1. + for (NSUInteger index = 2; index < numberOfArguments; ++index) { + const char *argumentType = [invocation.methodSignature getArgumentTypeAtIndex:index]; + + // Skip const type qualifier. + if (argumentType[0] == 'r') { + argumentType++; + } + +#define isArgumentType(type) \ +strcmp(argumentType, @encode(type)) == 0 + +#define extractTypeAndSetValue(type, value) \ +type argument = 0; \ +[invocation getArgument:&argument atIndex:index]; \ +value = @(argument); \ + + id _Nonnull value; + + if (isArgumentType(id) || isArgumentType(Class) || isArgumentType(void (^)(void))) { + __unsafe_unretained id argument = nil; + [invocation getArgument:&argument atIndex:index]; + value = argument; + } + else if (isArgumentType(char)) { + extractTypeAndSetValue(char, value); + } + else if (isArgumentType(short)) { + extractTypeAndSetValue(short, value); + } + else if (isArgumentType(int)) { + extractTypeAndSetValue(int, value); + } + else if (isArgumentType(long)) { + extractTypeAndSetValue(long, value); + } + else if (isArgumentType(long long)) { + extractTypeAndSetValue(long long, value); + } + else if (isArgumentType(unsigned char)) { + extractTypeAndSetValue(unsigned char, value); + } + else if (isArgumentType(unsigned short)) { + extractTypeAndSetValue(unsigned short, value); + } + else if (isArgumentType(unsigned int)) { + extractTypeAndSetValue(unsigned int, value); + } + else if (isArgumentType(unsigned long)) { + extractTypeAndSetValue(unsigned long, value); + } + else if (isArgumentType(unsigned long long)) { + extractTypeAndSetValue(unsigned long long, value); + } + else if (isArgumentType(float)) { + extractTypeAndSetValue(float, value); + } + else if (isArgumentType(double)) { + extractTypeAndSetValue(double, value); + } + else if (isArgumentType(BOOL)) { + extractTypeAndSetValue(BOOL, value); + } + else if (isArgumentType(const char *)) { + extractTypeAndSetValue(const char *, value); + } + else { + NSUInteger size = 0; + NSGetSizeAndAlignment(argumentType, &size, NULL); + NSCParameterAssert(size > 0); + uint8_t data[size]; + [invocation getArgument:&data atIndex:index]; + + value = [NSValue valueWithBytes:&data objCType:argumentType]; + } + + [arguments addObject:value]; + } + + return arguments; +} + ++ (NSSet *) selectorsOfClass: (Class _Nonnull __unsafe_unretained) class + withEncodedReturnType: (NSString *) encodedReturnType { + unsigned int protocolsCount = 0; + Protocol * __unsafe_unretained _Nonnull * _Nullable protocolPointer = class_copyProtocolList(class, &protocolsCount); + + NSMutableSet *allSelectors = [[self selectorsOfProtocolPointer:protocolPointer + count:protocolsCount + andEncodedReturnType:encodedReturnType] mutableCopy]; + + Class _Nonnull __unsafe_unretained superclass = class_getSuperclass(class); + + if(superclass != nil) { + NSSet *superclassSelectors = [self selectorsOfClass:superclass + withEncodedReturnType:encodedReturnType]; + [allSelectors unionSet:superclassSelectors]; + } + + free(protocolPointer); + + return allSelectors; +} + ++ (NSSet *) selectorsOfProtocol: (Protocol * __unsafe_unretained) protocol + andEncodedReturnType: (NSString *) encodedReturnType { + unsigned int protocolMethodCount = 0; + struct objc_method_description * _Nullable methodDescriptions = protocol_copyMethodDescriptionList(protocol, false, true, &protocolMethodCount); + + // Protocol pointers + unsigned int protocolsCount = 0; + Protocol * __unsafe_unretained _Nonnull * _Nullable protocols = protocol_copyProtocolList(protocol, &protocolsCount); + + NSMutableSet *allSelectors = [NSMutableSet new]; + + // Protocol methods + for (NSInteger idx = 0; idx < protocolMethodCount; idx++) { + struct objc_method_description description = methodDescriptions[idx]; + + if ([self encodedMethodReturnTypeForMethod:description] == encodedReturnType) { + [allSelectors addObject: [NSValue valueWithPointer:description.name]]; + } + } + + if (protocols != nil) { + [allSelectors unionSet: [self selectorsOfProtocolPointer:protocols + count:protocolsCount + andEncodedReturnType:encodedReturnType]]; + } + + free(methodDescriptions); + free(protocols); + + return allSelectors; +} + ++ (NSSet *) selectorsOfProtocolPointer: (Protocol * __unsafe_unretained * _Nullable) pointer + count: (NSInteger) count + andEncodedReturnType: (NSString *) encodedReturnType { + NSMutableSet *allSelectors = [NSMutableSet new]; + + for (NSInteger i = 0; i < count; i++) { + Protocol * __unsafe_unretained _Nullable protocol = pointer[i]; + + if (protocol == nil) { continue; } + [allSelectors unionSet:[self selectorsOfProtocol:protocol + andEncodedReturnType:encodedReturnType]]; + } + + return allSelectors; +} + ++ (NSString *)encodedMethodReturnTypeForMethod: (struct objc_method_description) method { + return [[NSString alloc] initWithBytes:method.types + length:1 + encoding:NSASCIIStringEncoding]; +} + + +@end diff --git a/Sources/CombineCocoaRuntime/ObjcRuntimeAliases.m b/Sources/CombineCocoaRuntime/ObjcRuntimeAliases.m new file mode 100644 index 0000000..6c5dd98 --- /dev/null +++ b/Sources/CombineCocoaRuntime/ObjcRuntimeAliases.m @@ -0,0 +1,21 @@ +// +// ObjcRuntimeAliases.m +// +// +// Created by Benjamin Deckys on 2023/07/27. +// + +#import +#import + +const IMP _combinecocoa_objc_msgForward = _objc_msgForward; + +void _combinecocoa_objc_setAssociatedObject( + const void* object, + const void* key, + id value, + objc_AssociationPolicy policy + ) { + __unsafe_unretained id obj = (__bridge typeof(obj)) object; + objc_setAssociatedObject(obj, key, value, policy); +} diff --git a/Sources/Runtime/include/ObjcDelegateProxy.h b/Sources/CombineCocoaRuntime/include/ObjcDelegateProxy.h similarity index 94% rename from Sources/Runtime/include/ObjcDelegateProxy.h rename to Sources/CombineCocoaRuntime/include/ObjcDelegateProxy.h index a7d7d2f..62d5668 100644 --- a/Sources/Runtime/include/ObjcDelegateProxy.h +++ b/Sources/CombineCocoaRuntime/include/ObjcDelegateProxy.h @@ -7,6 +7,7 @@ // #import +#import @interface ObjcDelegateProxy: NSObject diff --git a/Sources/CombineCocoaRuntime/include/ObjcRuntimeAliases.h b/Sources/CombineCocoaRuntime/include/ObjcRuntimeAliases.h new file mode 100644 index 0000000..93aa744 --- /dev/null +++ b/Sources/CombineCocoaRuntime/include/ObjcRuntimeAliases.h @@ -0,0 +1,17 @@ +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +extern const IMP _combinecocoa_objc_msgForward; + +/// A trampoline of `objc_setAssociatedObject` that is made to circumvent the +/// reference counting calls in the imported version in Swift. +void _combinecocoa_objc_setAssociatedObject( + const void* object, + const void* key, + id _Nullable value, + objc_AssociationPolicy policy +); + +NS_ASSUME_NONNULL_END diff --git a/Sources/Runtime/include/module.modulemap b/Sources/CombineCocoaRuntime/include/module.modulemap similarity index 66% rename from Sources/Runtime/include/module.modulemap rename to Sources/CombineCocoaRuntime/include/module.modulemap index e628955..4986328 100644 --- a/Sources/Runtime/include/module.modulemap +++ b/Sources/CombineCocoaRuntime/include/module.modulemap @@ -1,4 +1,4 @@ -module Runtime { +module CombineCocoaRuntime { umbrella header "ObjcDelegateProxy.h" export * } diff --git a/Sources/Runtime/ObjcDelegateProxy.m b/Sources/Runtime/ObjcDelegateProxy.m deleted file mode 100644 index 9ae5624..0000000 --- a/Sources/Runtime/ObjcDelegateProxy.m +++ /dev/null @@ -1,220 +0,0 @@ -// -// ObjcDelegateProxy.m -// CombineCocoa -// -// Created by Joan Disho & Shai Mishali on 25/09/2019. -// Copyright © 2020 Combine Community. All rights reserved. -// - -#import -#import "include/ObjcDelegateProxy.h" -#import - -#define OBJECT_VALUE(object) [NSValue valueWithNonretainedObject:(object)] - -static NSMutableDictionary *> *allSelectors; - -@implementation ObjcDelegateProxy - -- (NSSet *)selectors { - return allSelectors[OBJECT_VALUE(self.class)]; -} - -+ (void)initialize -{ - @synchronized (ObjcDelegateProxy.class) { - if (!allSelectors) { - allSelectors = [NSMutableDictionary new]; - } - allSelectors[OBJECT_VALUE(self)] = [self selectorsOfClass:self - withEncodedReturnType:[NSString stringWithFormat:@"%s", @encode(void)]]; - } -} - -- (BOOL)respondsToSelector:(SEL _Nonnull)aSelector { - return [super respondsToSelector:aSelector] || [self canRespondToSelector:aSelector]; -} - -- (BOOL)canRespondToSelector:(SEL _Nonnull)selector { - for (id current in allSelectors[OBJECT_VALUE(self.class)]) { - if (selector == (SEL) [current pointerValue]) { - return true; - } - } - - return false; -} - -- (void)interceptedSelector:(SEL _Nonnull)selector arguments:(NSArray * _Nonnull)arguments {} - -- (void)forwardInvocation:(NSInvocation *)anInvocation -{ - NSArray * _Nonnull arguments = unpackInvocation(anInvocation); - [self interceptedSelector:anInvocation.selector arguments:arguments]; -} - -NSArray * _Nonnull unpackInvocation(NSInvocation * _Nonnull invocation) { - NSUInteger numberOfArguments = invocation.methodSignature.numberOfArguments; - NSMutableArray *arguments = [NSMutableArray arrayWithCapacity:numberOfArguments - 2]; - - // Ignore `self` and `_cmd` at index 0 and 1. - for (NSUInteger index = 2; index < numberOfArguments; ++index) { - const char *argumentType = [invocation.methodSignature getArgumentTypeAtIndex:index]; - - // Skip const type qualifier. - if (argumentType[0] == 'r') { - argumentType++; - } - - #define isArgumentType(type) \ - strcmp(argumentType, @encode(type)) == 0 - - #define extractTypeAndSetValue(type, value) \ - type argument = 0; \ - [invocation getArgument:&argument atIndex:index]; \ - value = @(argument); \ - - id _Nonnull value; - - if (isArgumentType(id) || isArgumentType(Class) || isArgumentType(void (^)(void))) { - __unsafe_unretained id argument = nil; - [invocation getArgument:&argument atIndex:index]; - value = argument; - } - else if (isArgumentType(char)) { - extractTypeAndSetValue(char, value); - } - else if (isArgumentType(short)) { - extractTypeAndSetValue(short, value); - } - else if (isArgumentType(int)) { - extractTypeAndSetValue(int, value); - } - else if (isArgumentType(long)) { - extractTypeAndSetValue(long, value); - } - else if (isArgumentType(long long)) { - extractTypeAndSetValue(long long, value); - } - else if (isArgumentType(unsigned char)) { - extractTypeAndSetValue(unsigned char, value); - } - else if (isArgumentType(unsigned short)) { - extractTypeAndSetValue(unsigned short, value); - } - else if (isArgumentType(unsigned int)) { - extractTypeAndSetValue(unsigned int, value); - } - else if (isArgumentType(unsigned long)) { - extractTypeAndSetValue(unsigned long, value); - } - else if (isArgumentType(unsigned long long)) { - extractTypeAndSetValue(unsigned long long, value); - } - else if (isArgumentType(float)) { - extractTypeAndSetValue(float, value); - } - else if (isArgumentType(double)) { - extractTypeAndSetValue(double, value); - } - else if (isArgumentType(BOOL)) { - extractTypeAndSetValue(BOOL, value); - } - else if (isArgumentType(const char *)) { - extractTypeAndSetValue(const char *, value); - } - else { - NSUInteger size = 0; - NSGetSizeAndAlignment(argumentType, &size, NULL); - NSCParameterAssert(size > 0); - uint8_t data[size]; - [invocation getArgument:&data atIndex:index]; - - value = [NSValue valueWithBytes:&data objCType:argumentType]; - } - - [arguments addObject:value]; - } - - return arguments; -} - -+ (NSSet *) selectorsOfClass: (Class _Nonnull __unsafe_unretained) class - withEncodedReturnType: (NSString *) encodedReturnType { - unsigned int protocolsCount = 0; - Protocol * __unsafe_unretained _Nonnull * _Nullable protocolPointer = class_copyProtocolList(class, &protocolsCount); - - NSMutableSet *allSelectors = [[self selectorsOfProtocolPointer:protocolPointer - count:protocolsCount - andEncodedReturnType:encodedReturnType] mutableCopy]; - - Class _Nonnull __unsafe_unretained superclass = class_getSuperclass(class); - - if(superclass != nil) { - NSSet *superclassSelectors = [self selectorsOfClass:superclass - withEncodedReturnType:encodedReturnType]; - [allSelectors unionSet:superclassSelectors]; - } - - free(protocolPointer); - - return allSelectors; -} - -+ (NSSet *) selectorsOfProtocol: (Protocol * __unsafe_unretained) protocol - andEncodedReturnType: (NSString *) encodedReturnType { - unsigned int protocolMethodCount = 0; - struct objc_method_description * _Nullable methodDescriptions = protocol_copyMethodDescriptionList(protocol, false, true, &protocolMethodCount); - - // Protocol pointers - unsigned int protocolsCount = 0; - Protocol * __unsafe_unretained _Nonnull * _Nullable protocols = protocol_copyProtocolList(protocol, &protocolsCount); - - NSMutableSet *allSelectors = [NSMutableSet new]; - - // Protocol methods - for (NSInteger idx = 0; idx < protocolMethodCount; idx++) { - struct objc_method_description description = methodDescriptions[idx]; - - if ([self encodedMethodReturnTypeForMethod:description] == encodedReturnType) { - [allSelectors addObject: [NSValue valueWithPointer:description.name]]; - } - } - - if (protocols != nil) { - [allSelectors unionSet: [self selectorsOfProtocolPointer:protocols - count:protocolsCount - andEncodedReturnType:encodedReturnType]]; - } - - free(methodDescriptions); - free(protocols); - - return allSelectors; -} - -+ (NSSet *) selectorsOfProtocolPointer: (Protocol * __unsafe_unretained * _Nullable) pointer - count: (NSInteger) count - andEncodedReturnType: (NSString *) encodedReturnType { - NSMutableSet *allSelectors = [NSMutableSet new]; - - for (NSInteger i = 0; i < count; i++) { - Protocol * __unsafe_unretained _Nullable protocol = pointer[i]; - - if (protocol == nil) { continue; } - [allSelectors unionSet:[self selectorsOfProtocol:protocol - andEncodedReturnType:encodedReturnType]]; - } - - return allSelectors; -} - -+ (NSString *)encodedMethodReturnTypeForMethod: (struct objc_method_description) method { - return [[NSString alloc] initWithBytes:method.types - length:1 - encoding:NSASCIIStringEncoding]; -} - - -@end -