Macro objc2::declare_class

source ·
macro_rules! declare_class {
    {
        $(#[$m:meta])*
        $v:vis struct $name:ident;

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

            type Mutability = $mutability:ty;

            const NAME: &'static str = $name_const:expr;
        }

        impl DeclaredClass for $for_declared:ty {
            $(type Ivars = $ivars:ty;)?
        }

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

Declare a new class.

This is useful in many cases since Objective-C frameworks tend to favour a design pattern using “delegates”, where to hook into a piece of functionality in a class, you implement that class’ delegate protocol in a custom class.

This macro is the declarative way of creating classes, in contrast with the declare module which mostly contain ways of declaring classes in an imperative fashion. It is highly recommended that you use this macro though, since it contains a lot of extra debug assertions and niceties that help ensure the soundness of your code.

§Specification

This macro consists of the following parts (the first three are required):

  • The type declaration.
  • The ClassType implementation.
  • The DeclaredClass implementation.
  • Any number of inherent implementations.
  • Any number of protocol implementations.

With the syntax generally resembling a combination of that in extern_class! and extern_methods!.

§Type declaration

The type declaration works a lot like in extern_class!, an opaque struct is created and a lot of traits is implemented for that struct.

You are allowed to add most common attributes to the declaration, including #[cfg(...)] and doc comments. ABI-modifying attributes like #[repr(...)] are not allowed.

#[derive(...)] attributes are allowed, but heavily discouraged, as they are likely to not work as you’d expect them to. This is being worked on in #267.

If the type implements Drop, the macro will generate a dealloc method for you, which will call drop automatically.

§ClassType implementation

This also resembles the syntax in extern_class!, except that ClassType::NAME must be specified, and it must be unique across the entire application.

If you’re developing a library, good practice here would be to include your crate name in the prefix (something like "MyLibrary_MyClass").

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

§DeclaredClass implementation

The syntax here is as if you were implementing the trait yourself.

You may optionally specify the associated type Ivars; this is the intended way to specify the data your class stores. If you don’t specify any ivars, the macro will default to ().

Beware that if you want to use the class’ inherited initializers (such as init), you must override the subclass’ designated initializers, and initialize your ivars properly in there.

§Inherent 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 or the special name this (or _this), your method will be registered as an instance method, and if you don’t it will be registered as a class method.

On instance methods, you can freely choose between different types of receivers, e.g. &self, this: *const Self, &mut self, and so on. Note though that using &mut self requires the class’ mutability to be IsAllowedMutable. If you need mutation of your class’ instance variables, consider using Cell or similar instead.

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

If the #[method_id(...)] attribute is used, the return type must be Option<Retained<T>> or Retained<T>. Additionally, if the selector is in the “init”-family, the self/this parameter must be Allocated<Self>.

Putting other attributes on the method such as cfg, allow, doc, deprecated and so on is supported. However, note that cfg_attr may not work correctly, due to implementation difficulty - if you have a concrete use-case, please open an issue, then we can discuss it.

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 parameter 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.

Note that &mut Retained<_> and other such out parameters are not yet supported, and may generate a panic at runtime.

§Protocol implementations

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

The protocol must have been previously defined with extern_protocol!.

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

Putting attributes on the impl item such as cfg, allow, doc, deprecated and so on is supported.

§Panics

The implemented ClassType::class method may panic in a few cases, such as if:

  • A class with the specified name already exists.
  • Debug assertions are enabled, and an overriden method’s signature is not equal to the one on the superclass.
  • Debug assertions are enabled, and the protocol’s required methods are not implemented.

And possibly more similar cases in the future.

§Safety

Using this macro requires writing a few unsafe markers:

unsafe impl ClassType for T has the following safety requirements:

  • Any invariants that the superclass ClassType::Super may have must be upheld.
  • ClassType::Mutability must be correct.
  • If your type implements Drop, the implementation must abide by the following rules:
    • It must not call any overridden methods.
    • It must not retain the object past the lifetime of the drop.
    • It must not retain in the same scope that &mut self is active.
    • TODO: And probably a few more. Open an issue if you would like guidance on whether your implementation is correct.

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

unsafe impl 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_foundation::{NSCopying, NSObject, NSObjectProtocol, NSZone};
use objc2::rc::{Allocated, Retained};
use objc2::{
    declare_class, extern_protocol, msg_send, msg_send_id, mutability, ClassType,
    DeclaredClass, ProtocolType,
};

#[derive(Clone)]
struct Ivars {
    foo: u8,
    bar: c_int,
    object: Retained<NSObject>,
}

declare_class!(
    struct MyCustomObject;

    // SAFETY:
    // - The superclass NSObject does not have any subclassing requirements.
    // - Interior mutability is a safe default.
    // - `MyCustomObject` does not implement `Drop`.
    unsafe impl ClassType for MyCustomObject {
        type Super = NSObject;
        type Mutability = mutability::InteriorMutable;
        const NAME: &'static str = "MyCustomObject";
    }

    impl DeclaredClass for MyCustomObject {
        type Ivars = Ivars;
    }

    unsafe impl MyCustomObject {
        #[method_id(initWithFoo:)]
        fn init_with(this: Allocated<Self>, foo: u8) -> Option<Retained<Self>> {
            let this = this.set_ivars(Ivars {
                foo,
                bar: 42,
                object: NSObject::new(),
            });
            unsafe { msg_send_id![super(this), init] }
        }

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

        #[method_id(object)]
        fn __get_object(&self) -> Retained<NSObject> {
            self.ivars().object.clone()
        }

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

    unsafe impl NSObjectProtocol for MyCustomObject {}

    unsafe impl NSCopying for MyCustomObject {
        #[method_id(copyWithZone:)]
        fn copyWithZone(&self, _zone: *const NSZone) -> Retained<Self> {
            let new = Self::alloc().set_ivars(self.ivars().clone());
            unsafe { msg_send_id![super(new), init] }
        }

        // If we have tried to add other methods here, or had forgotten
        // to implement the method, we would have gotten an error.
    }
);

impl MyCustomObject {
    pub fn new(foo: u8) -> Retained<Self> {
        unsafe { msg_send_id![Self::alloc(), initWithFoo: foo] }
    }

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

    pub fn get_object(&self) -> Retained<NSObject> {
        unsafe { msg_send_id![self, object] }
    }

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

fn main() {
    let obj = MyCustomObject::new(3);
    assert_eq!(obj.ivars().foo, 3);
    assert_eq!(obj.ivars().bar, 42);
    assert!(obj.ivars().object.is_kind_of::<NSObject>());

    let obj = obj.copy();

    assert_eq!(obj.get_foo(), 3);
    assert!(obj.get_object().is_kind_of::<NSObject>());

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

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

#import <Foundation/Foundation.h>

@interface MyCustomObject: NSObject <NSCopying>
- (instancetype)initWithFoo:(uint8_t)foo;
- (uint8_t)foo;
- (NSObject*)object;
+ (BOOL)myClassMethod;
@end


@implementation MyCustomObject {
    // Instance variables
    uint8_t foo;
    int bar;
    NSObject* _Nonnull object;
}

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

- (uint8_t)foo {
    return self->foo;
}

- (NSObject*)object {
    return self->object;
}

+ (BOOL)myClassMethod {
    return YES;
}

// NSCopying

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

@end