I am attempting to use the Objective-C runtime in Swift to dynamically call Objective-C methods using method_getImplementation
. My approach works well for methods that either return void or that return an object, but it breaks when attempting to return a value type such as an NSUInteger.
My implementation will not crash for that value type, instead it will return an UnsafePointer where the memory address is actually the value (e.g. instead of an UnsafePointer with the pointee
value of 2, the memory address is 0x0000000000000002).
How do I get the method to return the correct value instead of an invalid memory type?
My test code
// Initialize a NSMutable Array
var nsMutableArrayClass: AnyObject? = objc_lookUpClass("NSMutableArray")
nsMutableArrayClass = call(selector: "alloc", on: nsMutableArrayClass)
nsMutableArrayClass = call(selector: "init", on: nsMutableArrayClass)
// Append 0 through 7 to the array
call(selector: "addObject:", on: nsMutableArrayClass, with: [0])
call(selector: "addObject:", on: nsMutableArrayClass, with: [1])
call(selector: "addObject:", on: nsMutableArrayClass, with: [2])
call(selector: "addObject:", on: nsMutableArrayClass, with: [3])
call(selector: "addObject:", on: nsMutableArrayClass, with: [4])
call(selector: "addObject:", on: nsMutableArrayClass, with: [5])
call(selector: "addObject:", on: nsMutableArrayClass, with: [6])
call(selector: "addObject:", on: nsMutableArrayClass, with: [7])
// the value now of nsMutableArrayClass is an NSMutableArray containing all 8 elements
// FIXME: Breaks on return of NSUInteger
let indexOfTwo = call(selector: "indexOfObject:", on: nsMutableArrayClass, with: [2]) // returns Unsafe Pointer with memory address of 0x0000000000000002
let indexOfFive = call(selector: "indexOfObject:", on: nsMutableArrayClass, with: [5]) // returns Unsafe Pointer with memory address of 0x0000000000000005
My implementation
typealias ObjcMethodWithoutArguements = @convention(c) (AnyObject?, Selector) -> UnsafeRawPointer?
typealias ObjcMethod1Arg = @convention(c) (AnyObject?, Selector, AnyObject?) -> UnsafeRawPointer?
// Use this to dynamically call a selector exposed to the Objective-C
@discardableResult func call(selector selectorString: String, on target: AnyObject?, with arguments: [Any] = []) -> AnyObject? {
let selector = sel_getUid(selectorString)
guard let isa = object_getClass(target),
let i = class_getInstanceMethod(isa, selector) else {
return nil
}
let m = method_getImplementation(i)
var buf = [ Int8 ](repeating: 0, count: 46)
method_getReturnType(i, &buf, buf.count)
let returnType = String(cString: &buf)
let result : UnsafeRawPointer?
switch arguments.count {
case 0:
let typedMethod = unsafeBitCast(m, to: ObjcMethodWithoutArguements.self)
result = typedMethod(target, selector)
case 1:
let typedMethod = unsafeBitCast(m, to: ObjcMethod1Arg.self)
result = typedMethod(target, selector, arguments[0] as AnyObject)
default:
fatalError("Not yet implemented")
}
// Handle the return type of objects
if returnType == "@" {
guard let result = result else {
return nil
}
let p = Unmanaged<AnyObject>.fromOpaque(result)
return shouldReleaseResult(of: selectorString)
? p.takeRetainedValue()
: p.takeUnretainedValue()
} else if returnType == "Q" {
// Handle the return type of a value
return result as AnyObject
}
return target
}
func shouldReleaseResult(of selector: String) -> Bool {
return selector.starts(with: "alloc")
|| selector.starts(with: "init")
|| selector.starts(with: "new")
|| selector.starts(with: "copy")
}