I previously wrote about how to interoperate between Objective-C and Rust
and use a Rust macro to pass a variable number of arguments to objc_msgSend
.
Well, there’s a problem with this: objc_msgSend
isn’t a variadic function!
objc_msgSend
is actually a “trampoline” that works by jumping directly to
the implementation of the method, not calling it and passing parameters.
Safely calling it requires first casting it to the type of the underlying
method implementation, like:
// id result = [obj description];
id result = ((id (*)(id, SEL))objc_msgSend)(obj, @selector(description));
If you try to call objc_msgSend
as if it were a variadic function,
weird things can happen!
Handling arguments
How can we handle objc_msgSend
in Rust? Let’s try to write a
wrapper function that calls it correctly.
It’s easy to see how we could do this for, say, two arguments:
unsafe fn msg_send<A, B, R>(obj: *mut Object, op: Sel, arg1: A, arg2: B) -> R {
// Transmute objc_msgSend to the type of the method implementation
let msg_send_fn: unsafe extern fn(*mut Object, Sel, A, B) -> R =
mem::transmute(objc_msgSend);
msg_send_fn(obj, op, arg1, arg2)
}
But our function needs to be able to accept one argument, or no arguments, or more than 2. This sounds a lot like function overloading, which Rust doesn’t support. However, there’s a pattern that can emulate function overloading in Rust: functions with a generic parameter. In our case, our function could take a generic parameter that is the arguments, represented as a tuple:
unsafe fn msg_send<T, R>(obj: *mut Object, op: Sel, args: T) -> R { ... }
How do we implement this now?
Well, it was easy to implement for a fixed number of arguments,
so we can let the arguments themselves handle calling objc_msgSend
.
Let’s add a MessageArguments
trait and implement it for tuples:
impl<A, B> MessageArguments for (A, B) {
unsafe fn send<R>(self, obj: *mut Object, op: Sel) -> R {
// Transmute objc_msgSend to the type of the method implementation
let msg_send_fn: unsafe extern fn(*mut Object, Sel, A, B) -> R =
mem::transmute(objc_msgSend);
let (arg1, arg2) = self;
msg_send_fn(obj, op, arg1, arg2)
}
}
Using a macro, we can easily implement this trait for tuples from size 0 to some upper bound. Our function simply becomes:
unsafe fn msg_send<T, R>(obj: *mut Object, op: Sel, args: T) -> R
where T: MessageArguments {
args.send(obj, op)
}
And now we’re no longer pretending that objc_msgSend
is variadic!
Return types
There’s one other caveat with objc_msgSend
, though:
different versions of it are used for different return types.
Which version is used depends on the calling conventions of the architecture.
Let’s encapsulate this with a simple function that returns the correct
version of objc_msgSend
for the return type:
fn msg_send_fn<R>() -> unsafe extern fn(*mut Object, Sel, ...) -> R { ... }
We can implement this for different architectures with a
cfg
attribute.
x86’s calling conventions are arguably the most complicated,
so for it this function would look like:
#[cfg(target_arch = "x86")]
fn msg_send_fn<R: Any>() -> unsafe extern fn(*mut Object, Sel, ...) -> R {
use std::any::TypeId;
let type_id = TypeId::of::<R>();
let size = mem::size_of::<R>();
if type_id == TypeId::of::<f32>() || type_id == TypeId::of::<f64>() {
unsafe { mem::transmute(objc_msgSend_fpret) }
} else if size == 0 || size == 1 || size == 2 || size == 4 || size == 8 {
unsafe { mem::transmute(objc_msgSend) }
} else {
unsafe { mem::transmute(objc_msgSend_stret) }
}
}
With this msg_send_fn
function defined for each architecture,
we can use it to get the correct version of objc_msgSend
for
our return type.
At this point we’re invisibly handling objc_msgSend
correctly for any
combination of argument types and any return type. We can further
wrap it with a macro
to make it more ergonomic; in the end, calling Objective-C methods is nearly
as easy from Rust as it is in Objective-C!