Objective-C Selectors

PDF for offline use:

The Objective-C language is based upon selectors. A selector is a message that can be sent to an object or a class. Xamarin.iOS maps instance selectors to instance methods, and class selectors to static methods.

Unlike normal C functions (and like C++ member functions), you cannot directly invoke a selector using P/Invoke. (Aside: in theory you could use P/Invoke for non-virtual C++ member functions, but you'd need to worry about per-compiler name mangling, which is a world of pain better ignored.) Instead, selectors are sent to an Objective-C class or instance using the objc_msgSend function.

You may find this helpful guide on Objective-C messaging useful.

Example

Suppose you want to invoke the -[NSString sizeWithFont:forWidth:lineBreakMode:] selector. The declaration (from Apple's documentation) is:

- (CGSize)sizeWithFont:(UIFont *)font forWidth:(CGFloat)width lineBreakMode:(UILineBreakMode)lineBreakMode
  • The return type is GSize for the Unified API.
  • The font parameter is a UIFont (and a type (indirectly) derived from NSObject ), and is thus mapped to System.IntPtr .
  • The width parameter, a CGFloat , is mapped to nfloat.
  • The lineBreakMode parameter, a UILineBreakMode , has already been bound in Xamarin.iOS as the UILineBreakMode enumeration .

Put it all together, and we want an objc_msgSend declaration that matches:

CGSize objc_msgSend(IntPtr target, IntPtr selector,
    IntPtr font, nfloat width, UILineBreakMode mode);

Checking the ObjCRuntime.Messaging members, we don't see a match for this prototype. Consequently, we will need to declare it ourself:

[DllImport (Constants.ObjectiveCLibrary, EntryPoint="objc_msgSend")]
static extern CGSize cgsize_objc_msgSend_IntPtr_float_int (
    IntPtr target, IntPtr selector,
    IntPtr font,
    nfloat width,
    UILineBreakMode mode);

Once declared, we can invoke it once we have the appropriate parameters:

NSString      target = ...
Selector    selector = new Selector ("sizeWithFont:forWidth:lineBreakMode:");
UIFont          font = ...
nfloat          width = ...
UILineBreakMode mode = ...

CGSize size = cgsize_objc_msgSend_IntPtr_float_int(
    target.Handle, selector.Handle,
    font == null ? IntPtr.Zero : font.Handle,
    width,
    mode);

Had the returned value been a structure that was less than 8 bytes in size (like the older SizeF used before switching to the Unified APIs) the above code would have run on the simulator but crashed on the device. So to call a Selector that returns a value less than 8 bits in size, consequently we also need to declare the objc_msgSend_stret() function:

[DllImport (MonoTouch.Constants.ObjectiveCLibrary, EntryPoint="objc_msgSend_stret")]
static extern void cgsize_objc_msgSend_stret_IntPtr_float_int (
    out CGSize retval, 
    IntPtr target, IntPtr selector,
    IntPtr font,
    nfloat width,
    UILineBreakMode mode);

Invocation would then become:

NSString      target = ...
Selector    selector = new Selector ("sizeWithFont:forWidth:lineBreakMode:");
UIFont          font = ...
nfloat          width = ...
UILineBreakMode mode = ...

CGSize size;

if (Runtime.Arch == Arch.SIMULATOR)
    size = cgsize_objc_msgSend_IntPtr_float_int(
        target.Handle, selector.Handle,
        font == null ? IntPtr.Zero : font.Handle,
        width,
        mode);
else
    cgsize_objc_msgSend_stret_IntPtr_float_int(
        out size,
        target.Handle, selector.Handle,
        font == null ? IntPtr.Zero: font.Handle,
        width,
        mode);

Invoking a Selector

Invoking a selector has three steps:

  1. Get the selector target.
  2. Get the selector name.
  3. Call objc_msgSend() with the appropriate arguments.

Selector Targets

A selector target is either an object instance or an Objective-C class. If the target is an instance and came from a bound Xamarin.iOS type, use the ObjCRuntime.INativeObject.Handle property.

If the target is a class, use ObjCRuntime.Class to get a reference to the class instance, then use the Class.Handle property.

Selector Names

Selector names are listed within Apple's documentation. For example, the UIKit NSString extension methods include sizeWithFont: and sizeWithFont:forWidth:lineBreakMode:. The embedded and trailing colons are important, and are part of the selector name.

Once you have a selector name, you can create a ObjCRuntime.Selector instance for it.

Calling objc_msgSend()

objc_msgSend() is used to send a message (selector) to an object. This family of functions takes at least two required arguments: the selector target (an instance or class handle), the selector itself, and then any arguments required for the particular selector. The instance and selector arguments must be System.IntPtr, and all remaining arguments must match the type the selector expects, e.g. an nint for an int, or a System.IntPtr for all NSObject-derived types. Use the NSObject.Handle property to obtain an IntPtr for an Objective-C type instance.

Unfortunately, there is more than one objc_msgSend() function.

Use objcmsgSendstret() for selectors which return a structure. To keep things "interesting", on ARM this includes all return types that are not an enumeration or any of the C builtin types (char, short, int, long, float, double). On x86 (the Simulator), this needs to be used for all structures larger than 8 bytes in size. (CGSize -- used in the example above -- is 8 bytes exactly, and thus doesn't use objc_msgSend_stret() in the simulator.)

Use objcmsgSendfpret() for selectors which return a floating point value on x86 only. This function does not need to be used on ARM; instead, use objc_msgSend().

The main objc_msgSend() function is used for all other selectors.

Once you've decided which objc_msgSend() function(s) you need to call (and it may be more than one, e.g. for simulator and device), you can use a normal [DllImport] method to declare the function for later invocation.

A set of pre-made objc_msgSend() declarations can be found in ObjCRuntime.Messaging.

The Ugly

Objective-C has three kinds of objc_msgSend methods: one for regular invocations, one for invocations that return floating point values (x86 only), and one for invocations that return struct values. The latter includes the suffix _stret in ObjCRuntime.Messaging.

If you are invoking a method that will return certain structures (rules are described below), you must invoke the method with the return value as the first parameter as an out value, like this:

// The following returns a PointF structure:
PointF ret;
Messaging.PointF_objc_msgSend_stret_PointF_IntPtr (out ret, this.Handle, selConvertPointFromWindow.Handle, point, window.Handle);

Things get ugly here and because the rule for when you must use _stret is different on X86 and ARM, if you want your bindings to work on both the simulator and the device, you need to add code that looks like this:

if (Runtime.Arch == Arch.DEVICE){
    PointF ret;

    Messaging.PointF_objc_msgSend_stret_PointF_IntPtr (out ret, myHandle, selector.Handle);

    return ret;
} else
    return Messaging.PointF_objc_msgSend_PointF_IntPtr (myHandle, selector.Handle);

Using the objcmsgSendstret method

The rule for when to use the objc_msgSend_stret are like this for ARM:

  • Any value type that is not an enumeration or any of the base types for an enumeration (int, byte, short, long, double, float).

The rule for when to use the objc_msgSend_stret are like this for X86:

  • Any value type that is not an enumeration, or any of the base types for an enumeration (int, byte, short, long, double, float) and whose native size is larger than 8 bytes.