Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion Sources/ITKSuperBuilder/src/ITKSuperBuilder.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
231 changes: 231 additions & 0 deletions Tests/InterposeKitTests/SignatureTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
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 {
// 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)

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)
}

}
Loading