From 37fdd3bca39dcb0cae317b14a025ad9697daae86 Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Sun, 30 Mar 2025 17:30:39 +0200 Subject: [PATCH 1/4] Initial take on signature tests --- Tests/InterposeKitTests/SignatureTests.swift | 209 +++++++++++++++++ .../ToBePolished/ObjectInterposeTests.swift | 221 ------------------ .../ToBePolished/TestClass.swift | 78 ------- 3 files changed, 209 insertions(+), 299 deletions(-) create mode 100644 Tests/InterposeKitTests/SignatureTests.swift delete mode 100644 Tests/InterposeKitTests/ToBePolished/ObjectInterposeTests.swift delete mode 100644 Tests/InterposeKitTests/ToBePolished/TestClass.swift diff --git a/Tests/InterposeKitTests/SignatureTests.swift b/Tests/InterposeKitTests/SignatureTests.swift new file mode 100644 index 0000000..b841391 --- /dev/null +++ b/Tests/InterposeKitTests/SignatureTests.swift @@ -0,0 +1,209 @@ +import InterposeKit +import XCTest + +fileprivate class ExampleClass: NSObject { + + @objc dynamic func passthroughInt(_ input: Int) -> Int { input } + @objc dynamic func passthroughDouble(_ input: Double) -> Double { input } + @objc dynamic func passthroughPoint(_ input: CGPoint) -> CGPoint { input } + @objc dynamic func passthroughRect(_ input: CGRect) -> CGRect { input } + @objc dynamic func passthroughTransform3D(_ input: CATransform3D) -> CATransform3D { input } + @objc dynamic func passthroughString(_ input: String) -> String { input } + @objc dynamic func passthroughObject(_ input: NSObject) -> NSObject { input } + + @objc dynamic func sum3(var1: Int, var2: Int, var3: Int) -> Int { + var1 + var2 + var3 + } + + @objc dynamic func sum6(var1: Int, var2: Int, var3: Int, var4: Int, var5: Int, var6: Int) -> Int { + var1 + var2 + var3 + var4 + var5 + var6 + } + +} + +final class SignatureTests: XCTestCase { + + override func setUpWithError() throws { + Interpose.isLoggingEnabled = true + } + + func testPassthroughInt() throws { + let object = ExampleClass() + + let hook = try object.applyHook( + for: #selector(ExampleClass.passthroughInt(_:)), + methodSignature: (@convention(c) (NSObject, Selector, Int) -> Int).self, + hookSignature: (@convention(block) (NSObject, Int) -> Int).self + ) { hook in + return { `self`, input in + hook.original(self, hook.selector, input) + 1 + } + } + XCTAssertEqual(object.passthroughInt(42), 43) + + try hook.revert() + XCTAssertEqual(object.passthroughInt(42), 42) + } + + func testPassthroughDouble() throws { + let object = ExampleClass() + + let hook = try object.applyHook( + for: #selector(ExampleClass.passthroughDouble(_:)), + methodSignature: (@convention(c) (NSObject, Selector, Double) -> Double).self, + hookSignature: (@convention(block) (NSObject, Double) -> Double).self + ) { hook in + return { `self`, input in + hook.original(self, hook.selector, input) + 0.5 + } + } + XCTAssertEqual(object.passthroughDouble(1.5), 2.0) + + try hook.revert() + XCTAssertEqual(object.passthroughDouble(1.5), 1.5) + } + + func testPassthroughPoint() throws { + let object = ExampleClass() + + let hook = try object.applyHook( + for: #selector(ExampleClass.passthroughPoint(_:)), + methodSignature: (@convention(c) (NSObject, Selector, CGPoint) -> CGPoint).self, + hookSignature: (@convention(block) (NSObject, CGPoint) -> CGPoint).self + ) { hook in + return { `self`, input in + var point = hook.original(self, hook.selector, input) + point.x += 1 + point.y += 1 + return point + } + } + XCTAssertEqual(object.passthroughPoint(CGPoint(x: 1, y: 2)), CGPoint(x: 2, y: 3)) + + try hook.revert() + XCTAssertEqual(object.passthroughPoint(CGPoint(x: 1, y: 2)), CGPoint(x: 1, y: 2)) + } + + func testPassthroughRect() throws { + let object = ExampleClass() + + let hook = try object.applyHook( + for: #selector(ExampleClass.passthroughRect(_:)), + methodSignature: (@convention(c) (NSObject, Selector, CGRect) -> CGRect).self, + hookSignature: (@convention(block) (NSObject, CGRect) -> CGRect).self + ) { hook in + { `self`, rect in + var rect = hook.original(self, hook.selector, rect) + rect.origin.x += 1 + rect.size.width += 1 + return rect + } + } + XCTAssertEqual( + object.passthroughRect(CGRect(x: 1, y: 1, width: 10, height: 10)), + CGRect(x: 2, y: 1, width: 11, height: 10) + ) + + try hook.revert() + XCTAssertEqual( + object.passthroughRect(CGRect(x: 1, y: 2, width: 10, height: 10)), + CGRect(x: 1, y: 2, width: 10, height: 10) + ) + } + + func testPassthroughTransform3D() throws { + let object = ExampleClass() + let input = CATransform3DMakeTranslation(1, 2, 3) + + let hook = try object.applyHook( + for: #selector(ExampleClass.passthroughTransform3D(_:)), + methodSignature: (@convention(c) (NSObject, Selector, CATransform3D) -> CATransform3D).self, + hookSignature: (@convention(block) (NSObject, CATransform3D) -> CATransform3D).self + ) { hook in + { `self`, transform in + var modified = hook.original(self, hook.selector, transform) + modified.m44 += 1 + return modified + } + } + + var expected = input + expected.m44 += 1 + XCTAssertTrue(CATransform3DEqualToTransform(object.passthroughTransform3D(input), expected)) + + try hook.revert() + XCTAssertTrue(CATransform3DEqualToTransform(object.passthroughTransform3D(input), input)) + } + + func testPassthroughString() throws { + let object = ExampleClass() + + let hook = try object.applyHook( + for: #selector(ExampleClass.passthroughString(_:)), + methodSignature: (@convention(c) (NSObject, Selector, String) -> String).self, + hookSignature: (@convention(block) (NSObject, String) -> String).self + ) { hook in + { `self`, input in hook.original(self, hook.selector, input) + "!" } + } + XCTAssertEqual(object.passthroughString("Test"), "Test!") + + try hook.revert() + XCTAssertEqual(object.passthroughString("Test"), "Test") + } + + func testPassthroughObject() throws { + let object = ExampleClass() + let input = NSObject() + + let hook = try object.applyHook( + for: #selector(ExampleClass.passthroughObject(_:)), + methodSignature: (@convention(c) (NSObject, Selector, NSObject) -> NSObject).self, + hookSignature: (@convention(block) (NSObject, NSObject) -> NSObject).self + ) { hook in + { `self`, _ in NSObject() } + } + XCTAssertTrue(object.passthroughObject(input) !== input) + + try hook.revert() + XCTAssertTrue(object.passthroughObject(input) === input) + } + + func testSum3Ints() throws { + let object = ExampleClass() + + let hook = try object.applyHook( + for: #selector(ExampleClass.sum3(var1:var2:var3:)), + methodSignature: (@convention(c) (NSObject, Selector, Int, Int, Int) -> Int).self, + hookSignature: (@convention(block) (NSObject, Int, Int, Int) -> Int).self + ) { hook in + { `self`, var1, var2, var3 in + hook.original(self, hook.selector, var1, var2, var3) + 1 + } + } + + XCTAssertEqual(object.sum3(var1: 1, var2: 2, var3: 3), 7) + + try hook.revert() + XCTAssertEqual(object.sum3(var1: 1, var2: 2, var3: 3), 6) + } + + func testSum6Ints() throws { + let object = ExampleClass() + + let hook = try object.applyHook( + for: #selector(ExampleClass.sum6(var1:var2:var3:var4:var5:var6:)), + methodSignature: (@convention(c) (NSObject, Selector, Int, Int, Int, Int, Int, Int) -> Int).self, + hookSignature: (@convention(block) (NSObject, Int, Int, Int, Int, Int, Int) -> Int).self + ) { hook in + { `self`, var1, var2, var3, var4, var5, var6 in + hook.original(self, hook.selector, var1, var2, var3, var4, var5, var6) + 1 + } + } + + XCTAssertEqual(object.sum6(var1: 1, var2: 1, var3: 1, var4: 1, var5: 1, var6: 1), 7) + + try hook.revert() + XCTAssertEqual(object.sum6(var1: 1, var2: 1, var3: 1, var4: 1, var5: 1, var6: 1), 6) + } + +} diff --git a/Tests/InterposeKitTests/ToBePolished/ObjectInterposeTests.swift b/Tests/InterposeKitTests/ToBePolished/ObjectInterposeTests.swift deleted file mode 100644 index 6014c36..0000000 --- a/Tests/InterposeKitTests/ToBePolished/ObjectInterposeTests.swift +++ /dev/null @@ -1,221 +0,0 @@ -import Foundation -import XCTest -@testable import InterposeKit - -final class ObjectInterposeTests: XCTestCase { - - func testInterposeSingleObjectInt() throws { - let testObj = TestClass() - let returnIntDefault = testObj.returnInt() - let returnIntOverrideOffset = 2 - XCTAssertEqual(testObj.returnInt(), returnIntDefault) - - let hook = try testObj.applyHook( - for: #selector(TestClass.returnInt), - methodSignature: (@convention(c) (NSObject, Selector) -> Int).self, - hookSignature: (@convention(block) (NSObject) -> Int).self - ) { hook in - return { `self` in - let int = hook.original(self, hook.selector) - return int + returnIntOverrideOffset - } - } - - XCTAssertEqual(testObj.returnInt(), returnIntDefault + returnIntOverrideOffset) - try hook.revert() - XCTAssertEqual(testObj.returnInt(), returnIntDefault) - try hook.apply() - // ensure we really don't leak into another object - let testObj2 = TestClass() - XCTAssertEqual(testObj2.returnInt(), returnIntDefault) - XCTAssertEqual(testObj.returnInt(), returnIntDefault + returnIntOverrideOffset) - try hook.revert() - XCTAssertEqual(testObj.returnInt(), returnIntDefault) - } - - func testDoubleIntegerInterpose() throws { - let testObj = TestClass() - let returnIntDefault = testObj.returnInt() - let returnIntOverrideOffset = 2 - let returnIntClassMultiplier = 4 - XCTAssertEqual(testObj.returnInt(), returnIntDefault) - - // Functions need to be `@objc dynamic` to be hookable. - let hook = try testObj.applyHook( - for: #selector(TestClass.returnInt), - methodSignature: (@convention(c) (NSObject, Selector) -> Int).self, - hookSignature: (@convention(block) (NSObject) -> Int).self - ) { hook in - return { `self` in - // You're free to skip calling the original implementation. - hook.original(self, hook.selector) + returnIntOverrideOffset - } - } - XCTAssertEqual(testObj.returnInt(), returnIntDefault + returnIntOverrideOffset) - - // Interpose on TestClass itself! - let classHook = try Interpose.applyHook( - on: TestClass.self, - for: #selector(TestClass.returnInt), - methodSignature: (@convention(c) (NSObject, Selector) -> Int).self, - hookSignature: (@convention(block) (NSObject) -> Int).self - ) { hook in - return { - hook.original($0, hook.selector) * returnIntClassMultiplier - } - } - - XCTAssertEqual(testObj.returnInt(), (returnIntDefault * returnIntClassMultiplier) + returnIntOverrideOffset) - - // ensure we really don't leak into another object - let testObj2 = TestClass() - XCTAssertEqual(testObj2.returnInt(), returnIntDefault * returnIntClassMultiplier) - - try hook.revert() - XCTAssertEqual(testObj.returnInt(), returnIntDefault * returnIntClassMultiplier) - try classHook.revert() - XCTAssertEqual(testObj.returnInt(), returnIntDefault) - } - - func test3IntParameters() throws { - let testObj = TestClass() - XCTAssertEqual(testObj.calculate(var1: 1, var2: 2, var3: 3), 1 + 2 + 3) - - // Functions need to be `@objc dynamic` to be hookable. - let hook = try testObj.applyHook( - for: #selector(TestClass.calculate), - methodSignature: (@convention(c) (NSObject, Selector, Int, Int, Int) -> Int).self, - hookSignature: (@convention(block) (NSObject, Int, Int, Int) -> Int).self - ) { hook in - return { - let orig = hook.original($0, hook.selector, $1, $2, $3) - return orig + 1 - } - } - XCTAssertEqual(testObj.calculate(var1: 1, var2: 2, var3: 3), 1 + 2 + 3 + 1) - try hook.revert() - } - - func test6IntParameters() throws { - let testObj = TestClass() - - XCTAssertEqual(testObj.calculate2(var1: 1, var2: 2, var3: 3, - var4: 4, var5: 5, var6: 6), 1 + 2 + 3 + 4 + 5 + 6) - - // Functions need to be `@objc dynamic` to be hookable. - let hook = try testObj.applyHook( - for: #selector(TestClass.calculate2), - methodSignature: (@convention(c) (NSObject, Selector, Int, Int, Int, Int, Int, Int) -> Int).self, - hookSignature: (@convention(block) (NSObject, Int, Int, Int, Int, Int, Int) -> Int).self - ) { hook in - return { - // You're free to skip calling the original implementation. - let orig = hook.original($0, hook.selector, $1, $2, $3, $4, $5, $6) - return orig + 1 - } - } - XCTAssertEqual(testObj.calculate2(var1: 1, var2: 2, var3: 3, - var4: 4, var5: 5, var6: 6), 1 + 2 + 3 + 4 + 5 + 6 + 1) - try hook.revert() - } - - func testObjectCallReturn() throws { - let testObj = TestClass() - let str = "foo" - XCTAssertEqual(testObj.doubleString(string: str), str + str) - - // Functions need to be `@objc dynamic` to be hookable. - let hook = try testObj.applyHook( - for: #selector(TestClass.doubleString), - methodSignature: (@convention(c) (NSObject, Selector, String) -> String).self, - hookSignature: (@convention(block) (NSObject, String) -> String).self - ) { hook in - return { `self`, parameter in - hook.original(self, hook.selector, parameter) + str - } - } - XCTAssertEqual(testObj.doubleString(string: str), str + str + str) - try hook.revert() - XCTAssertEqual(testObj.doubleString(string: str), str + str) - } - - func testHook_getPoint() throws { - let object = TestClass() - XCTAssertEqual(object.getPoint(), CGPoint(x: -1, y: 1)) - - let hook = try object.applyHook( - for: #selector(TestClass.getPoint), - methodSignature: (@convention(c) (NSObject, Selector) -> CGPoint).self, - hookSignature: (@convention(block) (NSObject) -> CGPoint).self - ) { hook in - return { `self` in - var point = hook.original(self, hook.selector) - point.x += 2 - point.y += 2 - return point - } - } - - XCTAssertEqual(object.getPoint(), CGPoint(x: 1, y: 3)) - - try hook.revert() - XCTAssertEqual(object.getPoint(), CGPoint(x: -1, y: 1)) - } - - func testHook_passthroughPoint() throws { - let object = TestClass() - - XCTAssertEqual( - object.passthroughPoint(CGPoint(x: 1, y: 1)), - CGPoint(x: 1, y: 1) - ) - - let hook = try object.applyHook( - for: #selector(TestClass.passthroughPoint(_:)), - methodSignature: (@convention(c) (NSObject, Selector, CGPoint) -> CGPoint).self, - hookSignature: (@convention(block) (NSObject, CGPoint) -> CGPoint).self - ) { hook in - return { `self`, inPoint in - var outPoint = hook.original(self, hook.selector, inPoint) - outPoint.x += 1 - outPoint.y += 1 - return outPoint - } - } - - XCTAssertEqual( - object.passthroughPoint(CGPoint(x: 1, y: 1)), - CGPoint(x: 2, y: 2) - ) - - try hook.revert() - - XCTAssertEqual( - object.passthroughPoint(CGPoint(x: 1, y: 1)), - CGPoint(x: 1, y: 1) - ) - } - -// func testLargeStructReturn() throws { -// let testObj = TestClass() -// let transform = CATransform3D() -// XCTAssertEqual(testObj.invert3DTransform(transform), transform.inverted) -// -// func transformMatrix(_ matrix: CATransform3D) -> CATransform3D { -// matrix.translated(x: 10, y: 5, z: 2) -// } -// -// // Functions need to be `@objc dynamic` to be hookable. -// let hook = try testObj.hook(#selector(TestClass.invert3DTransform)) { (store: TypedHook -// <@convention(c)(NSObject, Selector, CATransform3D) -> CATransform3D, -// @convention(block) (NSObject, CATransform3D) -> CATransform3D>) in { -// let matrix = store.original($0, store.selector, $1) -// return transformMatrix(matrix) -// } -// } -// XCTAssertEqual(testObj.invert3DTransform(transform), transformMatrix(transform.inverted)) -// try hook.revert() -// XCTAssertEqual(testObj.invert3DTransform(transform), transform.inverted) -// } - -} diff --git a/Tests/InterposeKitTests/ToBePolished/TestClass.swift b/Tests/InterposeKitTests/ToBePolished/TestClass.swift deleted file mode 100644 index 185b416..0000000 --- a/Tests/InterposeKitTests/ToBePolished/TestClass.swift +++ /dev/null @@ -1,78 +0,0 @@ -import Foundation -import QuartzCore - -let testClassHi = "Hi from TestClass!" -let testString = " and Interpose" -let testString2 = " testString2" -let testSubclass = "Subclass is here!" - -public func == (lhs: CATransform3D, rhs: CATransform3D) -> Bool { - return CATransform3DEqualToTransform(lhs, rhs) -} - -public extension CATransform3D { - - // swiftlint:disable:next identifier_name - func translated(x: CGFloat = 0, y: CGFloat = 0, z: CGFloat = 0) -> CATransform3D { - return CATransform3DTranslate(self, x, y, z) - } - - var inverted: CATransform3D { - return CATransform3DInvert(self) - } -} - -class TestClass: NSObject { - - @objc dynamic static var staticInt = 42 - - @objc dynamic var age: Int = 1 - @objc dynamic var name: String = "Tim Apple" - - @objc dynamic func sayHi() -> String { - print(testClassHi) - return testClassHi - } - - @objc dynamic func doNothing() { } - - @objc dynamic func doubleString(string: String) -> String { - string + string - } - - @objc dynamic func returnInt() -> Int { - 7 - } - - @objc dynamic func calculate(var1: Int, var2: Int, var3: Int) -> Int { - var1 + var2 + var3 - } - - // swiftlint:disable:next function_parameter_count - @objc dynamic func calculate2(var1: Int, var2: Int, var3: Int, var4: Int, var5: Int, var6: Int) -> Int { - var1 + var2 + var3 + var4 + var5 + var6 - } - - @objc dynamic func getPoint() -> CGPoint { - CGPoint(x: -1, y: 1) - } - - @objc dynamic func passthroughPoint(_ point: CGPoint) -> CGPoint { - point - } - - // This requires _objc_msgSendSuper_stret on x64, returns a large struct - @objc dynamic func invert3DTransform(_ input: CATransform3D) -> CATransform3D { - input.inverted - } -} - -class TestSubclass: TestClass { - override func sayHi() -> String { - return super.sayHi() + testSubclass - } - - override func doNothing() { - super.doNothing() - } -} From ca6ce3fa25673f314fdd06b8f621b0118d10f88f Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Sun, 30 Mar 2025 17:43:29 +0200 Subject: [PATCH 2/4] Fixed typos in ci.yml --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b982c1..e0a8ce6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,9 +16,9 @@ jobs: include: - os: macos-13 # x86_64 (Intel) xcode: 15.2 # Swift 5.9.2 - - os: macos-14 # arch64 (Apple Silicon) + - os: macos-14 # arm64 (Apple Silicon) xcode: 15.3 # Swift 5.10 - - os: macos-latest # arch64 (Apple Silicon) + - os: macos-latest # arm64 (Apple Silicon) xcode: 16.2 # Swift 6.0 steps: From 7ac47d1ce61ff976317c24cb6e5abe9d6d0f4fdb Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Sun, 30 Mar 2025 17:47:37 +0200 Subject: [PATCH 3/4] Added CI step for printing system info --- .github/workflows/ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0a8ce6..5f8d46e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,14 @@ jobs: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app xcodebuild -version + - name: Print system and architecture info + run: | + echo "Architecture: $(uname -m)" + echo "Processor: $(sysctl -n machdep.cpu.brand_string 2>/dev/null || echo 'N/A')" + echo "macOS Version: $(sw_vers -productVersion) ($(sw_vers -buildVersion))" + echo "Xcode Path: $(xcode-select -p)" + echo "macOS SDK: $(xcrun --sdk macosx --show-sdk-path)" + - name: Build run: swift build -v From 85a4118171d490c98ed2f4975de4fcc1522d3b32 Mon Sep 17 00:00:00 2001 From: Lukas Kubanek Date: Sun, 30 Mar 2025 21:35:55 +0200 Subject: [PATCH 4/4] Ignored large struct test in Xcode due to instrumentation --- Sources/ITKSuperBuilder/src/ITKSuperBuilder.m | 2 +- Tests/InterposeKitTests/SignatureTests.swift | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Sources/ITKSuperBuilder/src/ITKSuperBuilder.m b/Sources/ITKSuperBuilder/src/ITKSuperBuilder.m index 9d91647..5f9c0d9 100644 --- a/Sources/ITKSuperBuilder/src/ITKSuperBuilder.m +++ b/Sources/ITKSuperBuilder/src/ITKSuperBuilder.m @@ -132,7 +132,7 @@ static BOOL ITKMethodIsSuperTrampoline(Method method) { } clazz = superclazz; superclazz = class_getSuperclass(clazz); - }while (1); + } while (1); struct objc_super *_super = &_threadSuperStorage; _super->receiver = obj; diff --git a/Tests/InterposeKitTests/SignatureTests.swift b/Tests/InterposeKitTests/SignatureTests.swift index b841391..ac37598 100644 --- a/Tests/InterposeKitTests/SignatureTests.swift +++ b/Tests/InterposeKitTests/SignatureTests.swift @@ -112,6 +112,28 @@ final class SignatureTests: XCTestCase { } func testPassthroughTransform3D() throws { + // This test crashes in Xcode Debug builds due to compiler-injected instrumentation into + // `msgSendSuperTrampoline()`. Although the function is marked `__attribute__((naked))` + // and is defined entirely in inline assembly, Xcode injects code at the beginning that + // overwrites the `x8` register. `x8` is used by the ABI as the indirect return pointer + // for large structs like `CATransform3D`. Overwriting it causes the trampoline to write + // to an invalid address, leading to a crash. + // + // The injected code looks like this: + // + // ``` + // adrp x8, 43 + // ldr x9, [x8, #0xce0] + // add x9, x9, #0x1 + // str x9, [x8, #0xce0] + // ``` + // + // This only happens when tests are run inside Xcode. Running `swift test` works correctly + // in both Debug and Release configurations. + if ProcessInfo.processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] != nil { + throw XCTSkip("Skipping test: trampoline is instrumented in Xcode Debug builds.") + } + let object = ExampleClass() let input = CATransform3DMakeTranslation(1, 2, 3)