define_class

Macro define_class 

Source
macro_rules! define_class {
    {
        // The following special attributes are supported:
        // - #[unsafe(super($($superclasses:path),*))]
        // - #[unsafe(super = $superclass:path)]
        // - #[thread_kind = $thread_kind:path]
        // - #[name = $name:literal]
        // - #[ivars = $ivars:path]
        $(#[$($attrs:tt)*])*
        $v:vis struct $class:ident;

        // unsafe impl Protocol for $class { ... }
        // impl $class { ... }
        $($impls:tt)*
    } => { ... };
}
Expand description

Create a new Objective-C 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 ClassBuilder, which allows creating 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.

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

See Apple’s documentation on defining classes for a more in-depth introduction.

§Specification

This macro consists of the following parts:

  • The type definition, along with special attributes.
  • 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!.

This macro creates an opaque struct with implementations in a similar manner as the extern_class! macro. Additionally, it implements the DefinedClass trait, as well as any protocols specified in the protocol implementations.

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

The macro does not support generic types.

§Attributes

You can add most normal attributes to the class, including #[cfg(...)], #[allow(...)] and doc comments.

Exceptions and special attributes are noted below.

§#[unsafe(super(...))] (required)

Same as in extern_class!.

§#[thread_kind = ...] (optional)

Same as in extern_class!.

§#[name = "..."] (optional)

Specify the runtime-name for the class. Must be unique across the entire application. This is useful if the name of a class is used elsewhere, such as when defining a delegate that needs to be named in e.g. Info.plist.

If not set, this will default to:

concat!(module_path!(), "::", $class, env!("CARGO_PKG_VERSION"));

E.g. for example "my_crate::my_module::MyClass0.1.0".

If you’re developing a library, it is recommended that you do not set this, and instead rely on the default naming, since that usually works better with users having multiple SemVer-incompatible versions of your library in the same binary.

§#[ivars = ...] (optional)

Controls the instance variables of the class; this is the intended way to specify the data your class stores. If you don’t set this attribute, the macro will default to ().

It is recommended that you wrap your instance variables in Cell, RefCell, atomics or other similar interior mutability abstractions to allow mutating your instance variables. See the docs on interior mutability for further details.

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.

§#[derive(...)]

This is overridden, and only works with PartialEq, Eq, Hash and Debug.

The implementations delegate to the superclass’ implementation, so if you want to change how they work, you should override the isEqual: and hash methods instead.

The Debug implementation currently also debug print your ivars, but that may change in the future. Prefer to override description (and potentially debugDescription) instead.

§#[cfg_attr(..., ...)]

Same as in extern_class!.

§#[repr(...)]

Same as in extern_class!.

§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, self: *const Self, this: *const Self, and so on. Note that using &mut self is not possible, if you need mutation of your class’ instance variables, consider using Cell or similar instead.

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

If the #[unsafe(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 overridden 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 lot of unsafe markers:

When writing #[unsafe(super(...))], you must ensure that:

  • Any invariants that the superclass ClassType::Super may have must be upheld.
  • 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(method(...))] 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.

§Thread safety

The Objective-C runtime is thread-safe, so any classes you create yourself will automatically be Send and Sync (via auto traits) provided that:

  1. The superclass is thread-safe, or is NSObject.
  2. The ivars are thread-safe.
  3. The thread kind is not MainThreadOnly.

Note though that in many cases, the frameworks you will be interacting with will not be thread-safe, and so in many cases it will make sense to use interior mutability in your custom classes.

§Examples

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

use std::ffi::c_int;

use objc2_foundation::{CopyingHelper, NSCopying, NSObject, NSObjectProtocol, NSZone};
use objc2::rc::{Allocated, Retained};
use objc2::{
    define_class, extern_methods, extern_protocol, msg_send, AnyThread,
    ClassType, DefinedClass, ProtocolType,
};

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

define_class!(
    // SAFETY:
    // - The superclass NSObject does not have any subclassing requirements.
    // - `MyCustomObject` does not implement `Drop`.
    #[unsafe(super(NSObject))]

    // If we were implementing delegate methods like `NSApplicationDelegate`,
    // we would specify the object to only be usable on the main thread:
    // #[thread_kind = MainThreadOnly]

    // If we needed to refer to the class from elsewhere, we'd give it a
    // name here explicitly.
    // #[name = "MyCustomObject"]

    // Specify the instance variables this class has.
    #[ivars = Ivars]
    struct MyCustomObject;

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

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

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

    unsafe impl NSObjectProtocol for MyCustomObject {}

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

// Specially required for `NSCopying`, but otherwise not needed.
unsafe impl CopyingHelper for MyCustomObject {
    type Result = Self;
}

// Add creation method.
impl MyCustomObject {
    fn new(foo: u8) -> Retained<Self> {
        // Initialize instance variables.
        let this = Self::alloc().set_ivars(Ivars {
            foo,
            bar: 42,
            object: NSObject::new(),
        });
        // Call `NSObject`'s `init` method.
        unsafe { msg_send![super(this), init] }
    }
}

// Make an interface to the methods we defined.
impl MyCustomObject {
    extern_methods!(
        #[unsafe(method(foo))]
        pub fn get_foo(&self) -> u8;

        #[unsafe(method(object))]
        pub fn get_object(&self) -> Retained<NSObject>;

        #[unsafe(method(myClassMethod))]
        pub fn my_class_method() -> bool;
    );
}

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

    let obj = obj.copy();

    assert_eq!(obj.get_foo(), 3);
    assert!(obj.get_object().isKindOfClass(NSObject::class()));

    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