diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/InterposeKit.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/InterposeKit.xcscheme new file mode 100644 index 0000000..8c31f0b --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/InterposeKit.xcscheme @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/InterposeKit.xcworkspace/contents.xcworkspacedata b/InterposeKit.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..ca3329e --- /dev/null +++ b/InterposeKit.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Package.swift b/Package.swift index f0f4e94..fb2b112 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version: 5.9 import PackageDescription diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift new file mode 100644 index 0000000..ba62c5c --- /dev/null +++ b/Package@swift-6.0.swift @@ -0,0 +1,31 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "InterposeKit", + platforms: [ + .iOS(.v13), + .macOS(.v10_15), + .tvOS(.v13), + .watchOS(.v6) + ], + products: [ + .library( + name: "InterposeKit", + targets: ["InterposeKit"] + ), + ], + targets: [ + .target(name: "ITKSuperBuilder"), + .target( + name: "InterposeKit", + dependencies: ["ITKSuperBuilder"] + ), + .testTarget( + name: "InterposeKitTests", + dependencies: ["InterposeKit"] + ), + ], + swiftLanguageModes: [.v6] +) diff --git a/Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectHookRegistry.swift b/Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectHookRegistry.swift index 15385a1..5a81e3f 100644 --- a/Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectHookRegistry.swift +++ b/Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectHookRegistry.swift @@ -34,7 +34,11 @@ internal enum ObjectHookRegistry { } +#if compiler(>=5.10) +fileprivate nonisolated(unsafe) var ObjectHookRegistryKey: UInt8 = 0 +#else fileprivate var ObjectHookRegistryKey: UInt8 = 0 +#endif fileprivate class WeakReference: NSObject { diff --git a/Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectHookStrategy.swift b/Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectHookStrategy.swift index 81eef05..0c294fb 100644 --- a/Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectHookStrategy.swift +++ b/Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectHookStrategy.swift @@ -294,4 +294,8 @@ extension NSObject { } -private var ObjectHookCountKey: UInt8 = 0 +#if compiler(>=5.10) +fileprivate nonisolated(unsafe) var ObjectHookCountKey: UInt8 = 0 +#else +fileprivate var ObjectHookCountKey: UInt8 = 0 +#endif diff --git a/Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectSubclassManager.swift b/Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectSubclassManager.swift index 52a4ed2..a776973 100644 --- a/Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectSubclassManager.swift +++ b/Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectSubclassManager.swift @@ -136,9 +136,9 @@ internal enum ObjectSubclassManager { ) -> String { let className = NSStringFromClass(perceivedClass) - let counterSuffix: String = self.subclassCounterQueue.sync { - self.subclassCounter &+= 1 - return String(format: "%04llx", self.subclassCounter) + let counterSuffix: String = self.subclassCounter.withValue { counter in + counter &+= 1 + return String(format: "%04llx", counter) } return "\(self.namePrefix)_\(className)_\(counterSuffix)" @@ -147,10 +147,7 @@ internal enum ObjectSubclassManager { /// The prefix used for all dynamically created subclass names. private static let namePrefix = "InterposeKit" - /// A global counter for generating unique subclass name suffixes. - private static var subclassCounter: UInt64 = 0 - - /// A serial queue to ensure thread-safe access to `subclassCounter`. - private static let subclassCounterQueue = DispatchQueue(label: "com.steipete.InterposeKit.subclassCounter") + /// A lock-isolated global counter for generating unique subclass name suffixes. + private static let subclassCounter = LockIsolated(0) } diff --git a/Sources/InterposeKit/Interpose.swift b/Sources/InterposeKit/Interpose.swift index fa6f298..a2f7c3d 100644 --- a/Sources/InterposeKit/Interpose.swift +++ b/Sources/InterposeKit/Interpose.swift @@ -85,10 +85,18 @@ public enum Interpose { // MARK: Logging // ============================================================================ // - /// The flag indicating whether logging is enabled. + /// The flag that enables logging of InterposeKit internal operations to standard output + /// using the `print(…)` function. Defaults to `false`. + /// + /// It is recommended to set this flag only once early in your application lifecycle, + /// e.g. at app startup or in test setup. + #if compiler(>=5.10) + public nonisolated(unsafe) static var isLoggingEnabled = false + #else public static var isLoggingEnabled = false + #endif - internal static func log( + internal nonisolated static func log( _ message: @autoclosure () -> String ) { if self.isLoggingEnabled { @@ -96,7 +104,7 @@ public enum Interpose { } } - internal static func fail( + internal nonisolated static func fail( _ message: @autoclosure () -> String ) -> Never { fatalError("[InterposeKit] \(message())") diff --git a/Sources/InterposeKit/InterposeError.swift b/Sources/InterposeKit/InterposeError.swift index 2b55410..0211b67 100644 --- a/Sources/InterposeKit/InterposeError.swift +++ b/Sources/InterposeKit/InterposeError.swift @@ -1,6 +1,6 @@ import Foundation -public enum InterposeError: Error { +public enum InterposeError: Error, @unchecked Sendable { /// A hook operation failed and the hook is no longer usable. case hookInFailedState diff --git a/Sources/InterposeKit/Utilities/Concurrency/LockIsolated.swift b/Sources/InterposeKit/Utilities/Concurrency/LockIsolated.swift new file mode 100644 index 0000000..2eb5bb4 --- /dev/null +++ b/Sources/InterposeKit/Utilities/Concurrency/LockIsolated.swift @@ -0,0 +1,41 @@ +// A modified version from Point-Free’s Concurrency Extras package: +// https://github.com/pointfreeco/swift-concurrency-extras +// +// Copyright (c) 2023 Point-Free +// Licensed under the MIT license. + +import Foundation + +internal final class LockIsolated: @unchecked Sendable { + private var _value: Value + private let lock = NSRecursiveLock() + + internal init(_ value: @autoclosure @Sendable () throws -> Value) rethrows { + self._value = try value() + } + + internal var value: Value { + self.lock.sync { + self._value + } + } + + internal func withValue( + _ operation: @Sendable (inout Value) throws -> T + ) rethrows -> T { + try self.lock.sync { + var value = self._value + defer { self._value = value } + return try operation(&value) + } + } +} + +extension NSRecursiveLock { + @discardableResult + fileprivate func sync(work: () throws -> R) rethrows -> R { + self.lock() + defer { self.unlock() } + return try work() + } +} diff --git a/Sources/InterposeKit/Utilities/class_implementsInstanceMethod.swift b/Sources/InterposeKit/Utilities/ObjectiveC/class_implementsInstanceMethod.swift similarity index 100% rename from Sources/InterposeKit/Utilities/class_implementsInstanceMethod.swift rename to Sources/InterposeKit/Utilities/ObjectiveC/class_implementsInstanceMethod.swift diff --git a/Sources/InterposeKit/Utilities/class_setPerceivedClass.swift b/Sources/InterposeKit/Utilities/ObjectiveC/class_setPerceivedClass.swift similarity index 100% rename from Sources/InterposeKit/Utilities/class_setPerceivedClass.swift rename to Sources/InterposeKit/Utilities/ObjectiveC/class_setPerceivedClass.swift diff --git a/Sources/InterposeKit/Utilities/object_getClass.swift b/Sources/InterposeKit/Utilities/ObjectiveC/object_getClass.swift similarity index 100% rename from Sources/InterposeKit/Utilities/object_getClass.swift rename to Sources/InterposeKit/Utilities/ObjectiveC/object_getClass.swift diff --git a/Sources/InterposeKit/Utilities/object_isKVOActive.swift b/Sources/InterposeKit/Utilities/ObjectiveC/object_isKVOActive.swift similarity index 100% rename from Sources/InterposeKit/Utilities/object_isKVOActive.swift rename to Sources/InterposeKit/Utilities/ObjectiveC/object_isKVOActive.swift diff --git a/Tests/InterposeKitTests/UtilitiesTests.swift b/Tests/InterposeKitTests/UtilitiesTests.swift index 1fcaae1..8a38df0 100644 --- a/Tests/InterposeKitTests/UtilitiesTests.swift +++ b/Tests/InterposeKitTests/UtilitiesTests.swift @@ -20,12 +20,12 @@ extension NSObject { final class UtilitiesTests: XCTestCase { - static var hasRunTestSetPerceivedClass = false + static let hasRunTestSetPerceivedClass = LockIsolated(false) func test_setPerceivedClass() throws { // Runs only once to avoid leaking class swizzling across test runs. - try XCTSkipIf(Self.hasRunTestSetPerceivedClass, "Class override already applied.") - Self.hasRunTestSetPerceivedClass = true + try XCTSkipIf(Self.hasRunTestSetPerceivedClass.value, "Class override already applied.") + Self.hasRunTestSetPerceivedClass.withValue{ $0 = true } let object = RealClass()