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::Supermay 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
retainthe object past the lifetime of the drop. - It must not
retainin the same scope that&mut selfis 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:
- The superclass is thread-safe, or is
NSObject. - The ivars are thread-safe.
- 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