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

        $(
            $(#[$impl_m:meta])*
            unsafe impl $(protocol $protocol:ident)? {
                $($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 objc2::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, the method definition, and the protocol definition.

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 objc2::runtime::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! As a tip, you can declare the class with the desired unique name like MyCrateCustomObject using this macro, and then expose a renamed type alias like pub type CustomObject = MyCrateCustomObject; instead.

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

Method definition

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 a special @sel(my:selector:) directive directly before the function definition.

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, define a new function that calls it via. objc2::msg_send!.

Protocol definition

You can specify the protocols that the class should implement, along with any required 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 struct ... has the following safety requirements:

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

unsafe impl { ... } 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 ... { ... } 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::{msg_send, msg_send_bool, msg_send_id};
use objc2::rc::{Id, Owned};
use objc2::runtime::Bool;
use objc2_foundation::{declare_class, NSCopying, NSObject, NSZone};

declare_class! {
    unsafe struct MyCustomObject: NSObject {
        foo: u8,
        pub bar: c_int,
    }

    unsafe impl {
        @sel(initWithFoo:)
        fn init_with(&mut self, foo: u8) -> Option<&mut Self> {
            let this: Option<&mut Self> = unsafe {
                msg_send![super(self, NSObject::class()), init]
            };
            this.map(|this| {
                // TODO: Initialization through MaybeUninit
                // (The below is only safe because these variables are
                // safe to initialize with `MaybeUninit::zeroed`).
                *this.foo = foo;
                *this.bar = 42;
                this
            })
        }

        @sel(foo)
        fn __get_foo(&self) -> u8 {
            *self.foo
        }

        @sel(myClassMethod)
        fn __my_class_method() -> Bool {
            Bool::YES
        }
    }

    unsafe impl protocol NSCopying {
        @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].unwrap() }
    }

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

    pub fn my_class_method() -> bool {
        unsafe { msg_send_bool![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);

    let obj = obj.copy();
    assert_eq!(obj.get_foo(), 3);

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

Approximately equivalent to the following Objective-C code.

#import <Foundation/Foundation.h>

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

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

@end


@implementation MyCustomObject {
    // Private ivar
    uint8_t foo;
}

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

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

+ (BOOL)myClassMethod {
    return YES;
}

// NSCopying

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

@end