├── .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 |
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 |
--------------------------------------------------------------------------------