macro_rules! declare_class {
    {
        $(#[$m:meta])*
        $v:vis struct $name:ident {
            $($ivar_v:vis $ivar:ident: $ivar_ty:ty,)*
        }

        unsafe impl ClassType for $for:ty {
            $(#[inherits($($inheritance_rest:ty),+)])?
            type Super = $superclass:ty;

            $(const NAME: &'static str = $name_const:literal;)?
        }

        $($methods:tt)*
    } => { ... };
}
Expand description

Declare a new Objective-C class.

This is mostly just a convenience macro on top of extern_class! and the functionality in the declare module, but it can really help with cutting down on boilerplate, in particular when defining delegate classes!

Specification

This macro consists of three parts:

  • The class definition + ivar definition + inheritance specification.
  • A set of method definitions.
  • A set of protocol definitions.

Class and ivar definition

The class definition works a lot like extern_class!, with the added functionality that you can define custom instance variables on your class, which are then wrapped in a declare::Ivar and made accessible through the class. (E.g. you can use self.my_ivar as if it was a normal Rust struct).

Note that the class name should be unique across the entire application! You can declare the class with the desired unique name like "MyCrateCustomObject" by specifying it in ClassType::NAME, and then give the exposed type a different name like CustomObject.

The class is guaranteed to have been created and registered with the Objective-C runtime after the ClassType::class function has been called.

If any of the instance variables require being Drop’ed (e.g. are wrapped in declare::IvarDrop), this macro will generate a dealloc method automatically.

Method definitions

Within the impl block you can define two types of functions; “associated functions” and “methods”. These are then mapped to the Objective-C equivalents “class methods” and “instance methods”. In particular, if you use self your method will be registered as an instance method, and if you don’t it will be registered as a class method.

The desired selector can be specified using the #[sel(my:selector:)] attribute, similar to the extern_methods! macro.

A transformation step is performed on the functions (to make them have the correct ABI) and hence they shouldn’t really be called manually. (You can’t mark them as pub for the same reason). Instead, use the extern_methods! macro to create a Rust interface to the methods.

If the argument or return type is bool, a conversion is performed to make it behave similarly to the Objective-C BOOL. Use runtime::Bool if you want to control this manually.

Protocol definitions

You can specify protocols that the class should implement, along with any required/optional methods for said protocols.

The methods work exactly as normal, they’re only put “under” the protocol definition to make things easier to read.

Safety

Using this macro requires writing a few unsafe markers:

unsafe impl ClassType for T has the following safety requirements:

  • Same as extern_class! (the inheritance chain has to be correct).
  • Any instance variables you specify under the struct definition must either be able to be created using MaybeUninit::zeroed, or be properly initialized in an init method.

unsafe impl T { ... } asserts that the types match those that are expected when the method is invoked from Objective-C. Note that there are no safe-guards here; you can easily write i8, but if Objective-C thinks it’s an u32, it will cause UB when called!

unsafe impl Protocol<P> for T { ... } requires that all required methods of the specified protocol is implemented, and that any extra requirements (implicit or explicit) that the protocol has are upheld. The methods in this definition has the same safety requirements as above.

Examples

Declare a class MyCustomObject that inherits NSObject, has a few instance variables and methods, and implements the NSCopying protocol.

use std::os::raw::c_int;
use objc2::declare::{Ivar, IvarDrop};
use objc2::rc::{Id, Owned, Shared};
use objc2::foundation::{NSCopying, NSObject, NSString, NSZone};
use objc2::{declare_class, msg_send, msg_send_id, ns_string, ClassType};

declare_class!(
    struct MyCustomObject {
        foo: u8,
        pub bar: c_int,
        string: IvarDrop<Id<NSString, Shared>>,
    }

    unsafe impl ClassType for MyCustomObject {
        type Super = NSObject;
        // Optionally specify a different name
        // const NAME: &'static str = "MyCustomObject";
    }

    unsafe impl MyCustomObject {
        #[sel(initWithFoo:)]
        fn init_with(&mut self, foo: u8) -> Option<&mut Self> {
            let this: Option<&mut Self> = unsafe {
                msg_send![super(self), init]
            };

            // TODO: `ns_string` can't be used inside closures.
            let s = ns_string!("abc");

            this.map(|this| {
                // Initialize instance variables

                // Some types like `u8`, `bool`, `Option<Box<T>>` and
                // `Option<Id<T, O>>` are safe to zero-initialize, and
                // we can simply write to the variable as normal:
                *this.foo = foo;
                *this.bar = 42;

                // For others like `&u8`, `Box<T>` or `Id<T, O>`, we have
                // to initialize them with `Ivar::write`:
                Ivar::write(&mut this.string, s.copy());

                // All the instance variables have been initialized; our
                // initializer is sound
                this
            })
        }

        #[sel(foo)]
        fn __get_foo(&self) -> u8 {
            *self.foo
        }

        #[sel(string)]
        fn __get_string(&self) -> *mut NSString {
            Id::autorelease_return((*self.string).copy())
        }

        #[sel(myClassMethod)]
        fn __my_class_method() -> bool {
            true
        }
    }

    unsafe impl Protocol<NSCopying> for MyCustomObject {
        #[sel(copyWithZone:)]
        fn copy_with_zone(&self, _zone: *const NSZone) -> *mut Self {
            let mut obj = Self::new(*self.foo);
            *obj.bar = *self.bar;
            obj.autorelease_return()
        }
    }
);

impl MyCustomObject {
    pub fn new(foo: u8) -> Id<Self, Owned> {
        let cls = Self::class();
        unsafe { msg_send_id![msg_send_id![cls, alloc], initWithFoo: foo] }
    }

    pub fn get_foo(&self) -> u8 {
        unsafe { msg_send![self, foo] }
    }

    pub fn get_string(&self) -> Id<NSString, Shared> {
        unsafe { msg_send_id![self, string] }
    }

    pub fn my_class_method() -> bool {
        unsafe { msg_send![Self::class(), myClassMethod] }
    }
}

unsafe impl NSCopying for MyCustomObject {
    type Ownership = Owned;
    type Output = Self;
}

fn main() {
    let obj = MyCustomObject::new(3);
    assert_eq!(*obj.foo, 3);
    assert_eq!(*obj.bar, 42);
    assert_eq!(*obj.string, NSString::from_str("abc"));

    let obj = obj.copy();
    assert_eq!(obj.get_foo(), 3);
    assert_eq!(obj.get_string(), NSString::from_str("abc"));

    assert!(MyCustomObject::my_class_method());
}

Approximately equivalent to the following ARC-enabled Objective-C code.

#import <Foundation/Foundation.h>

@interface MyCustomObject: NSObject <NSCopying> {
    // Public ivar
    int bar;
}

- (instancetype)initWithFoo:(uint8_t)foo;
- (uint8_t)foo;
- (NSString*)string;
+ (BOOL)myClassMethod;

@end


@implementation MyCustomObject {
    // Private ivar
    uint8_t foo;
    NSString* _Nonnull string;
}

- (instancetype)initWithFoo:(uint8_t)foo_arg {
    self = [super init];
    if (self) {
        self->foo = foo_arg;
        self->bar = 42;
        self->string = @"abc";
    }
    return self;
}

- (uint8_t)foo {
    return self->foo; // Or just `foo`
}

- (NSString*)string {
    return self->string;
}

+ (BOOL)myClassMethod {
    return YES;
}

// NSCopying

- (id)copyWithZone:(NSZone *)_zone {
    MyCustomObject* obj = [[MyCustomObject alloc] initWithFoo: self->foo];
    obj->bar = self->bar;
    obj->string = self->string;
    return obj;
}

@end