Macro msg_send

Source
macro_rules! msg_send {
    [super($obj:expr), $($selector_and_arguments:tt)+] => { ... };
    [super($obj:expr, $superclass:expr), $($selector_and_arguments:tt)+] => { ... };
    [$obj:expr, $($selector_and_arguments:tt)+] => { ... };
}
Expand description

Send a message to an object or class.

This is wildly unsafe, even more so than sending messages in Objective-C, because this macro can’t inspect header files to see the expected types, and because Rust has more safety invariants to uphold. Make sure to review the safety section below!

The recommended way of using this macro is by defining a wrapper function:

unsafe fn do_something(obj: &NSObject, arg: c_int) -> *const c_char {
    msg_send![obj, doSomething: arg]
}

This way we are clearly communicating to Rust that: The method doSomething: works with a shared reference to the object. It takes a C-style signed integer, and returns a pointer to what is probably a C-compatible string. Now it’s much, much easier to make a safe abstraction around this!

The extern_methods! macro can help with coding this pattern.

§Memory management

If an Objective-C method returns id, NSObject*, or similar object pointers, you should use Retained<T> on the Rust side, or Option<Retained<T>> if the pointer is nullable.

This is necessary because object pointers in Objective-C have certain rules for when they should be retained and released across function calls.

§A little history

Objective-C’s type system is… limited, so you can’t tell without consulting the documentation who is responsible for releasing an object. To remedy this problem, Apple/Cocoa introduced (approximately) the following rule:

The caller is responsible for releasing objects return from methods that begin with new, alloc, copy, mutableCopy or init, and method that begins with init takes ownership of the receiver. See Cocoa’s Memory Management Policy for a user-friendly introduction to this concept.

In the past, users had to do retain and release calls themselves to properly follow these rules. To avoid the memory management problems associated with manual stuff like that, they introduced “ARC”, which codifies the rules as part of the language, and inserts the required retain and release calls automatically.

Returning a *const T pointer is similar to pre-ARC; you have to know when to retain and when to release an object. Returning Retained is similar to ARC; the rules are simple enough that we can do them automatically!

§Specification

The syntax is somewhat similar to the message syntax in Objective-C, except with a comma between arguments. Eliding the comma is possible, but deprecated, and may be removed in a future version of objc2.

The first expression, know as the “receiver”, can be any type that implements MessageReceiver, like a reference or a pointer to an object. Additionally, it can even be a reference to an Retained containing an object.

The expression can be wrapped in super, with an optional superclass as the second argument. If no specific superclass is specified, the direct superclass is retrieved from ClassType.

All arguments, as well as the return type, must implement Encode (bar the exceptions below).

If the last argument is the special marker _, the macro will return a Result<_, Retained<E>>, see below.

This macro roughly translates into a call to sel!, and afterwards a fully qualified call to MessageReceiver::send_message. Note that this means that auto-dereferencing of the receiver is not supported, and that the receiver is consumed. You may encounter a little trouble with &mut references, try refactoring into a separate method or reborrowing the reference.

Variadic arguments are currently not supported.

§Memory management details

The accepted receiver and return types, and how we handle them, differ depending on which, if any, of the recognized selector families the selector belongs to:

  • The new family: The receiver may be anything that implements MessageReceiver (though often you’ll want to use &AnyClass). The return type is a generic Retained<T> or Option<Retained<T>>.

  • The alloc family: The receiver must be &AnyClass, and the return type is a generic Allocated<T>.

  • The init family: The receiver must be Allocated<T> as returned from alloc, or if sending messages to the superclass, it must be PartialInit<T>.

    The receiver is consumed, and a the now-initialized Retained<T> or Option<Retained<T>> (with the same T) is returned.

  • The copy family: The receiver may be anything that implements MessageReceiver and the return type is a generic Retained<T> or Option<Retained<T>>.

  • The mutableCopy family: Same as the copy family.

  • No family: The receiver may be anything that implements MessageReceiver. The result is retained using Retained::retain_autoreleased, and a generic Retained<T> or Option<Retained<T>> is returned. This retain is in most cases faster than using autorelease pools!

See the clang documentation for the precise specification of Objective-C’s ownership rules.

As you may have noticed, the return type is usually either Retained or Option<Retained>. Internally, the return type is always Option<Retained> (for example: almost all new methods can fail if the allocation failed), but for convenience, if the return type is Retained<T>, this macro will automatically unwrap the object, or panic with an error message if it couldn’t be retrieved.

As a special case, if the last argument is the marker _, the macro will return a Result<Retained<T>, Retained<E>>, see below.

The retain, release and autorelease selectors are not supported, use Retained::retain, Retained::drop and Retained::autorelease_ptr for that.

§bool handling

Objective-C’s BOOL is slightly different from Rust’s bool, and hence a conversion step must be performed before using it. This is very easy to forget (because it’ll happen to work in most cases), so this macro does the conversion step automatically whenever an argument or the return type is bool.

That means that any Objective-C method that take or return BOOL can be translated to use bool on the Rust side.

If you want to handle the conversion explicitly, or the Objective-C method expects e.g. a pointer to a BOOL, use runtime::Bool instead.

§Out-parameters

Parameters like NSString** in Objective-C are passed by “writeback”, which means that the callee autoreleases any value that they may write into the parameter.

This macro has support for passing such parameters using the following types:

  • &mut Retained<_>
  • Option<&mut Retained<_>>
  • &mut Option<Retained<_>>,
  • Option<&mut Option<Retained<_>>>

Beware with the first two, since they will cause undefined behaviour if the method overwrites the value with nil.

See clang’s documentation for more details.

§Errors

The most common place you’ll see out-parameters is as NSError** the last parameter, which is used to communicate errors to the caller, see Error Handling Programming Guide For Cocoa.

Similar to Swift’s importing of error parameters, this macro supports an even more convenient version than the out-parameter support, which transforms methods whose last parameter is NSError** into the Rust equivalent, the Result type.

In particular, if you make the last argument the special marker _, then the macro will return a Result<R, Retained<E>>. The error type E must be either NSObject or objc2_foundation::NSError.

The success type R must be either () or Retained<T>.

At runtime, we create the temporary error variable for you on the stack and send it as the out-parameter to the method. If the method then returns NO/false, or in the case of an object pointer, NULL, the error variable is loaded and returned in Err.

§Panics

Unwinds if the underlying method throws and exception. If the "catch-all" Cargo feature is enabled, the Objective-C exception is converted into a Rust panic, with potentially a bit better stack trace.

Finally, panics if the return type is specified as Retained<_>, but the method actually returned NULL. If this happens, you should change the signature to instead return Option<Retained<_>> to handle the error yourself.

§Type verification

To make message sending safer, all arguments and return values for messages must implement encode::Encode. This allows the Rust compiler to prevent you from passing e.g. a Vec into Objective-C, which would both be UB and leak the vector.

When debug_assertions are enabled, this macro will check the encoding of the given arguments and return every time you send a message, and will panic if they are not equivalent.

This is not a perfect solution for ensuring safety (some Rust types have the same Objective-C encoding, but are not equivalent, such as &T and *const T), but it gets us much closer to it!

This behaviour can be tweaked with the "relax-void-encoding", "relax-sign-encoding" or "disable-encoding-assertions" Cargo feature flags if it is causing you trouble.

§Safety

Similar to defining and calling an extern function in a foreign function interface. In particular, you must uphold the following requirements:

  1. The selector corresponds to a valid method that is available on the receiver.

  2. The argument types match what the receiver excepts for this selector.

  3. The return type match what the receiver returns for this selector.

  4. The call must not violate Rust’s mutability rules, for example if passing an &T, the Objective-C method must not mutate the variable (except if the variable is inside std::cell::UnsafeCell or derivatives).

  5. If the receiver is a raw pointer it must be valid (aligned, dereferenceable, initialized and so on). Messages to null pointers are allowed (though heavily discouraged), but only if the return type itself is a pointer.

  6. You must uphold any additional safety requirements (explicit and implicit) that the method has. For example:

    • Methods that take pointers usually require that the pointer is valid, and sometimes non-null.
    • Sometimes, a method may only be called on the main thread.
    • The lifetime of returned pointers usually follows certain rules, and may not be valid outside of an autoreleasepool (returning Retained usually helps with these cases).
  7. Each out-parameter must have the correct nullability, and the method must not have any attributes that changes the how it handles memory management for these.

  8. If using the automatic memory management facilities of this macro, the method must not have any attributes such as objc_method_family, ns_returns_retained, ns_consumed that changes the how it handles memory management.

  9. TODO: Maybe more?

§Examples

Interacting with NSURLComponents, NSString and NSNumber.

use objc2::rc::Retained;
use objc2::{msg_send, ClassType};
use objc2_foundation::{NSNumber, NSString, NSURLComponents};


// Create an empty `NSURLComponents` by calling the class method `new`.
let components: Retained<NSURLComponents> = unsafe {
    //          ^^^^^^^^^^^^^^^^^^^^^^^^^ the return type, a memory-managed
    //                                    `NSURLComponents` instance
    //
    msg_send![NSURLComponents::class(), new]
    //        ------------------------  ^^^ the selector `new`
    //        |
    //        the receiver, in this case the class itself
};


// Create a new `NSNumber` from an integer.
let port: Retained<NSNumber> = unsafe {
    msg_send![NSNumber::class(), numberWithInt: 8080i32]
    //                           -------------- ^^^^^^^ the argument to the method
    //                           |
    //                           the selector `numberWithInt:`
    //
    // Note how we must fully specify the argument as `8080i32` instead of just `8080`.
};


// Set the port property of the URL.
let _: () = unsafe { msg_send![&components, setPort: &*port] };
//     --                                   -------- ^^^^^^ the port is deref'd to
//     |                                    |               become the correct type
//     |                                    |
//     |                                    the selector `setPort:` is derived
//     |                                    from the property name `port`.
//     |
//     return type (i.e. nothing / void)
//
// Note that even return types of `void` must be explicitly specified as `()`.


// Set the `host` property of the URL.
let host: Retained<NSString> = unsafe {
    msg_send![NSString::class(), stringWithUTF8String: c"example.com".as_ptr()]
};
let _: () = unsafe { msg_send![&components, setHost: &*host] };


// Set the `scheme` property of the URL.
let scheme: Retained<NSString> = unsafe {
    msg_send![NSString::class(), stringWithUTF8String: c"http".as_ptr()]
};
let _: () = unsafe { msg_send![&components, setScheme: &*scheme] };


// Get the combined URL in string form.
let string: Option<Retained<NSString>> = unsafe { msg_send![&components, string] };
//          ^^^^^^ the method can return NULL, so we specify an option here


assert_eq!(string.unwrap().to_string(), "http://example.com:8080");

The example above uses only msg_send! for demonstration purposes; note that usually the interface you seek is already present in the framework crates and then the equivalent code can be as simple as:

use objc2_foundation::{NSNumber, NSString, NSURLComponents};

let components = unsafe { NSURLComponents::new() };
unsafe { components.setPort(Some(&NSNumber::new_i32(8080))) };
unsafe { components.setHost(Some(&NSString::from_str("example.com"))) };
unsafe { components.setScheme(Some(&NSString::from_str("http"))) };
let string = unsafe { components.string() };

assert_eq!(string.unwrap().to_string(), "http://example.com:8080");

Sending messages to the superclass of an object.

use objc2::runtime::NSObject;
use objc2::{msg_send, ClassType};

// Call `someMethod` on the direct super class.
let _: () = unsafe { msg_send![super(&obj), someMethod] };

// Or lower-level, a method on a specific superclass.
let superclass = NSObject::class();
let arg3: u32 = unsafe { msg_send![super(&obj, superclass), getArg3] };

Sending a message with automatic error handling.

use objc2::msg_send;
use objc2::rc::Retained;
use objc2_foundation::{NSBundle, NSError};

let bundle = NSBundle::mainBundle();

let res: Result<(), Retained<NSError>> = unsafe {
    //          --  -------- ^^^^^^^ must be NSError or NSObject
    //          |   |
    //          |   always retained
    //          |
    //          `()` means that the method returns `bool`, we check
    //          that and return success if `true`, an error if `false`
    //
    msg_send![&bundle, preflightAndReturnError: _]
    //                                          ^ activate error handling
};

Sending a message with an out parameter and automatic error handling.

use objc2::msg_send;
use objc2::rc::Retained;

let obj: &NSFileManager;
let url: &NSURL;
let mut result_url: Option<Retained<NSURL>> = None;
unsafe {
    msg_send![
        obj,
        trashItemAtURL: url,
        resultingItemURL: Some(&mut result_url),
        error: _
    ]?
//   ^ is possible on error-returning methods, if the return type is specified
};

// Use `result_url` here

Attempt to do an invalid message send. This is undefined behaviour, but will panic with debug_assertions enabled.

use objc2::msg_send;
use objc2::runtime::NSObject;

let obj = NSObject::new();

// Wrong return type - this is UB!
//
// But it will be caught with `debug_assertions` enabled, stating that
// the return type's encoding is not correct.
let hash: f32 = unsafe { msg_send![&obj, hash] };