objc2
objc2 copied to clipboard
Better class declaration
~Blocked on at least https://github.com/madsmtm/objc2/pull/21.~
Fundamentally cannot be made safe, since you're calling into unknown Objective-C classes. Not even sure add_ivar
is sound (since Objective-C could be using the same ivar in a superclass)?
-
[x] Fix https://github.com/SSheldon/rust-objc/issues/12 Fixed by https://github.com/madsmtm/objc2/pull/193.
-
[x] Rename
ClassDecl
->ClassBuilder
-
[x] Can it be made more ergonomic with macros? See https://github.com/SSheldon/rust-objc/issues/74. Done in various PRs (primarily
declare_class!
macro) -
[x] Returning an autoreleased object in a custom / overridden method is very unergonomic:
extern "C" valid_attributes(_this: &Object, _sel: Sel) -> *mut NSArray { let array = Id<NSArray, Owned>::from(vec![...]); array.autorelease(pool) // Uhh, we don't have a pool from anywhere; it is implicit in Objective-C } let mut decl = ClassDecl::new("MyView", class!(NSView)).unwrap(); decl.add_method( sel!(validAttributesForMarkedText), valid_attributes as extern "C" fn(&Object, Sel) -> *mut NSArray, );
Will also need something like an
autorelease_return
method onId
which callsobjc_autoreleaseReturnValue
.Fixed by https://github.com/madsmtm/objc2/pull/191. See https://github.com/madsmtm/objc2/pull/112 as well for an alternate solution.
-
[x] ~Differentiate between static class references and "registered" classes, see https://github.com/SSheldon/rust-objc/issues/49#issuecomment-830429031~ Moved to https://github.com/madsmtm/objc2/pull/185.
-
[ ] Memory management rules are not automatically followed. Fixed by https://github.com/madsmtm/objc2/pull/244
-
[x] Uninitialized data in
init
methods. Fixed by https://github.com/madsmtm/objc2/pull/252 -
[x] Allow
Drop
types in ivars. Fixed by https://github.com/madsmtm/objc2/pull/254
Work on syntax extension that would help with declaring new classes (ideas in part from objrs
, example from GNUStep's GSTitleView
):
unsafe pub struct MyTitleView: NSView {
pub close_button: Option<Id<NSButton, Owned>>,
pub miniaturize_button: Option<Id<NSButton, Owned>>,
pub text_attributes: Id<NSMutableDictionary, Owned>,
pub title_color: Id<NSColor, Owned>,
owner: *mut Object,
owned_by_menu: Bool,
is_key_window: Bool,
is_main_window: Bool,
is_active_application: Bool,
}
Approximate Objective-C equivalent.
/// Initialization & deallocation
unsafe impl MyTitleView {
fn height() -> CGFloat {
let h: CGFloat = unsafe { msg_send![class!(NSMenuView) menuBarHeight] };
h + 1.0
}
// TODO: How do we handle `self: Option<...>`?
fn init(self: Id<MaybeUninit<Self>, Owned>) -> Id<Self, Owned> {
let self = unsafe { msg_send_id![super(self, class!(NSView)), init] };
self.owner = ptr::null_mut();
self.owned_by_menu = Bool::NO;
self.is_key_window = Bool::NO;
self.is_main_window = Bool::NO;
self.is_active_application = Bool::NO;
unsafe { msg_send![self, setAutoresizingMask: NSViewWidthSizable | NSViewMinYMargin] };
self.text_attributes = unsafe { NSMutableDictionary::from(&[
(msg_send_id![class!(NSFont), boldSystemFontOfSize: 0], NSFontAttributeName),
(msg_send_id![class!(NSFont), boldSystemFontOfSize: 0], NSForegroundColorAttributeName),
])};
self.title_color = msg_send_id![NSColor, lightGrayColor];
unsafe { self.assume_init() }
}
#[selector(initWithOwner:)]
fn init_with_owner(self: Id<MaybeUninit<Self>, Owned>, owner: *mut Object) -> Id<Self, Owned> {
let self = unsafe { msg_send_id![self, init] };
let _: () = unsafe { msg_send![self, setOwner: owner] };
self
}
#[selector(setOwner:)]
fn set_owner(&mut self, owner: Id<NSObject, Shared>) {
let center = NSNotificationCenter::default_center();
if (owner.is_kind_of(class!(NSWindow)) {
log::debug("GSTitleView: owner is NSWindow or NSPanel");
self.owner = owner.as_ptr();
self.owned_by_menu = Bool::NO;
msg_send![
self,
setFrame: NSRect::new(
-1,
self.owner.frame().size.height - MyTitleView::height() - 40,
self.owner.frame().size.width + 2,
MyTitleView::height()
),
];
if (msg_send![self.owner, styleMask] & NSClosableWindowMask) {
msg_send![self, addCloseButtonWithAction: sel!(performClose:)];
}
if (msg_send![self.owner, styleMask] & NSMiniaturizableWindowMask) {
msg_send![self, addMiniaturizeButtonWithAction: sel!(performMiniaturize:)];
}
center.add_observer(self, sel!(windowBecomeKey:), NSWindowDidBecomeKeyNotification, self.owner);
center.add_observer(self, sel!(windowResignKey:), NSWindowDidResignKeyNotification, self.owner);
center.add_observer(self, sel!(windowBecomeMain:), NSWindowDidBecomeMainNotification, self.owner);
center.add_observer(self, sel!(windowResignMain:), NSWindowDidResignMainNotification, self.owner);
center.add_observer(self, sel!(applicationBecomeActive:), NSApplicationWillBecomeActiveNotification, self.owner);
center.add_observer(self, sel!(applicationResignActive:), NSApplicationWillResignActiveNotification, self.owner);
} else if (owner.is_kind_of(class!(NSMenu)) {
log::debug("GSTitleView: owner is NSMenu");
self.owner = owner.as_ptr();
self.owned_by_menu = Bool::YES;
let theme: Id<GSTheme, Unknown> = unsafe { msg_send_id![class!(GSTheme), theme] };
if let Some(color) = msg_send_id![theme, colorNamed: @"GSMenuBar", state: GSThemeNormalState] {
self.title_color = color;
} else {
self.title_color = msg_send_id![NSColor, blackColor];
}
let text_color = unsafe {
msg_send_id![theme, colorNamed: @"GSMenuBarTitle", state: GSThemeNormalState]
}.unwrap_or_else(|| {
msg_send_id![NSColor, whiteColor]
});
[self.text_attributes, setObject: text_color, forKey: NSForegroundColorAttributeName];
} else {
log::debug!("GSTitleView: {} owner is not NSMenu or NSWindow or NSPanel", owner.class().name());
}
}
fn owner(&self) -> *mut Object {
self.owner
}
fn dealloc(&mut self) {
if (self.owned_by_menu.is_false()) {
unsafe {
let center = msg_send_id![class!(NSNotificationCenter), defaultCenter];
msg_send![center, removeObserver: self];
};
}
unsafe { msg_send![msg_send_id![class!(GSTheme), theme], setName: ptr::null(), forElement: msg_send![self.close_button, cell], temporary: Bool::NO] };
unsafe { msg_send![super(self, class!(NSView)), dealloc] };
// Drop impl called after this
}
}
/// Drawing
unsafe impl MyTitleView {
fn title_size(&self) -> NSSize {
let s: Id<NSString, Shared> = unsafe { msg_send_id![self.owner, title] };
s.size_with_attributes(self.text_attributes)
}
fn title_size(&self) -> NSSize {
let s: Id<NSString, Shared> = unsafe { msg_send_id![self.owner, title] };
s.size_with_attributes(self.text_attributes)
}
}
/// Mouse actions
unsafe impl MyTitleView {
#[selector(acceptsFirstMouse:)]
fn accepts_first_mouse(&self, _event: &NSEvent) -> Bool {
Bool::YES
}
#[selector(mouseDown:)]
fn mouse_down(&self, _event: &NSEvent) {
todo!()
}
#[selector(rightMouseDown:)]
fn right_mouse_down(&self, _event: &NSEvent) {
// Explicitly does not call super
}
#[selector(menuForEvent:)]
fn menu_for_event(&self, _event: &NSEvent) -> Option<Id<NSMenu, Unknown>> {
None
}
}
/// NSWindow & NSApplication notifications
unsafe impl MyTitleView {
#[selector(applicationBecomeActive:)]
fn application_becomes_active(&self, _notification: &NSNotification) {
self.is_active_application = Bool::YES;
}
// ...
}
// ...
Approximate Objective-C equivalent.
Regarding instance variables, an idea would be to have a macro that expands to roughly:
struct MyObjectIvars {
ivar1: i32,
ivar2: Box<u8>,
}
impl MyObject {
fn ivars(&self) -> &MyObjectIvars { ... }
fn ivars_mut(&mut self) -> &mut MyObjectIvars { ... }
}
(Assuming that the order that ivars are laid out in are guaranteed, haven't researched this yet).
That would then make it trivial for users to access these as self.ivars().ivar1
, and we avoid having to write a complex macro that translates self.ivar1
to extract_ivar1(self)
like objrs
does.
EDIT: Found a better solution to this, see https://github.com/madsmtm/objc2/pull/190
Idea for init
: Add PartialInit<T>(NonNull<T>)
struct with helper methods for retrieving &mut MaybeUninit<U>
references to T
's instance variables:
fn init(this: Allocated<Self>) -> Id<Self, Owned> {
let this: Option<PartialInit<Self>> = unsafe { msg_send_id![super(self, class!(NSView)), init] };
this.map(|mut this| {
// this.owner is MaybeUninit<*mut Object>
this.owner.write(ptr::null_mut());
// this.owned_by_menu is MaybeUninit<Bool>
this.owned_by_menu.write(Bool::NO);
this.is_key_window.write(Bool::NO);
this.is_main_window.write(Bool::NO);
this.is_active_application.write(Bool::NO);
// Discouraged; no way to verify that `this` is initialized!
unsafe { msg_send![this, setAutoresizingMask: NSViewWidthSizable | NSViewMinYMargin] };
this.text_attributes.write(unsafe { NSMutableDictionary::from(&[
(msg_send_id![class!(NSFont), boldSystemFontOfSize: 0], NSFontAttributeName),
(msg_send_id![class!(NSFont), boldSystemFontOfSize: 0], NSForegroundColorAttributeName),
])});
this.title_color.write(unsafe { msg_send_id![NSColor, lightGrayColor] });
unsafe { this.assume_init() }
})
}
EDIT: Postponed, see https://github.com/madsmtm/objc2/pull/252 for the interim solution
We want to keep the "declare this class" and "use this class" use-cases separate (may at some point merge these together again, but at least for now). The "use this class" pattern will probably be implemented as part of https://github.com/madsmtm/objc2/pull/161.
Reasoning: Declared classes are often used for delegate classes, who don't need to be called from Rust, and it would be a waste trying to create methods for doing so. More importantly, if you do self.my_method()
inside a declared class method, it is ambiguous whether you want that to be a plain Rust-to-Rust method call, or you want it to go through the msg_send!
machinery (Rust-to-Objective-C-to-Rust).
The situation has gone a long way since I opened this issue!
A few remaining things are https://github.com/madsmtm/objc2/issues/282 and https://github.com/madsmtm/objc2/issues/283, but I went and opened separate issues for that to make it easier to track progress.
See also https://github.com/madsmtm/objc2/pull/250#issuecomment-1302772897 about improving safety when implementing protocols.