Macro objc2::declare_class
source · macro_rules! declare_class { { $(#[$m:meta])* $v:vis struct $name:ident { $($ivar_v:vis $ivar:ident: $ivar_ty:ty,)* } unsafe impl ClassType for $for:ty { $(#[inherits($($inheritance_rest:ty),+)])? type Super = $superclass:ty; $(const NAME: &'static str = $name_const:literal;)? } $($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 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 + ivar definition + inheritance specification.
- A set of method definitions.
- A set of protocol definitions.
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 declare::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!
You can declare the class with the desired unique name like
"MyCrateCustomObject"
by specifying it in ClassType::NAME
, and then
give the exposed type a different name like CustomObject
.
The class is guaranteed to have been created and registered with the
Objective-C runtime after the ClassType::class
function has been
called.
If any of the instance variables require being Drop
’ed (e.g. are wrapped
in declare::IvarDrop
), this macro will generate a dealloc
method
automatically.
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
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 the #[sel(my:selector:)]
attribute, similar to the extern_methods!
macro.
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 argument 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.
Protocol definitions
You can specify protocols that the class should implement, along with any required/optional 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 impl ClassType for T
has the following safety requirements:
- Same as
extern_class!
(the inheritance chain has to be correct). - Any instance variables you specify under the struct definition must
either be able to be created using
MaybeUninit::zeroed
, or be properly initialized in aninit
method.
unsafe impl T { ... }
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<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::declare::{Ivar, IvarDrop};
use objc2::rc::{Id, Owned, Shared};
use objc2::foundation::{NSCopying, NSObject, NSString, NSZone};
use objc2::{declare_class, msg_send, msg_send_id, ns_string, ClassType};
declare_class!(
struct MyCustomObject {
foo: u8,
pub bar: c_int,
string: IvarDrop<Id<NSString, Shared>>,
}
unsafe impl ClassType for MyCustomObject {
type Super = NSObject;
// Optionally specify a different name
// const NAME: &'static str = "MyCustomObject";
}
unsafe impl MyCustomObject {
#[sel(initWithFoo:)]
fn init_with(&mut self, foo: u8) -> Option<&mut Self> {
let this: Option<&mut Self> = unsafe {
msg_send![super(self), init]
};
// TODO: `ns_string` can't be used inside closures.
let s = ns_string!("abc");
this.map(|this| {
// Initialize instance variables
// Some types like `u8`, `bool`, `Option<Box<T>>` and
// `Option<Id<T, O>>` are safe to zero-initialize, and
// we can simply write to the variable as normal:
*this.foo = foo;
*this.bar = 42;
// For others like `&u8`, `Box<T>` or `Id<T, O>`, we have
// to initialize them with `Ivar::write`:
Ivar::write(&mut this.string, s.copy());
// All the instance variables have been initialized; our
// initializer is sound
this
})
}
#[sel(foo)]
fn __get_foo(&self) -> u8 {
*self.foo
}
#[sel(string)]
fn __get_string(&self) -> *mut NSString {
Id::autorelease_return((*self.string).copy())
}
#[sel(myClassMethod)]
fn __my_class_method() -> bool {
true
}
}
unsafe impl Protocol<NSCopying> for MyCustomObject {
#[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] }
}
pub fn get_foo(&self) -> u8 {
unsafe { msg_send![self, foo] }
}
pub fn get_string(&self) -> Id<NSString, Shared> {
unsafe { msg_send_id![self, string] }
}
pub fn my_class_method() -> bool {
unsafe { msg_send![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);
assert_eq!(*obj.string, NSString::from_str("abc"));
let obj = obj.copy();
assert_eq!(obj.get_foo(), 3);
assert_eq!(obj.get_string(), NSString::from_str("abc"));
assert!(MyCustomObject::my_class_method());
}
Approximately equivalent to the following ARC-enabled Objective-C code.
#import <Foundation/Foundation.h>
@interface MyCustomObject: NSObject <NSCopying> {
// Public ivar
int bar;
}
- (instancetype)initWithFoo:(uint8_t)foo;
- (uint8_t)foo;
- (NSString*)string;
+ (BOOL)myClassMethod;
@end
@implementation MyCustomObject {
// Private ivar
uint8_t foo;
NSString* _Nonnull string;
}
- (instancetype)initWithFoo:(uint8_t)foo_arg {
self = [super init];
if (self) {
self->foo = foo_arg;
self->bar = 42;
self->string = @"abc";
}
return self;
}
- (uint8_t)foo {
return self->foo; // Or just `foo`
}
- (NSString*)string {
return self->string;
}
+ (BOOL)myClassMethod {
return YES;
}
// NSCopying
- (id)copyWithZone:(NSZone *)_zone {
MyCustomObject* obj = [[MyCustomObject alloc] initWithFoo: self->foo];
obj->bar = self->bar;
obj->string = self->string;
return obj;
}
@end