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