objc2 icon indicating copy to clipboard operation
objc2 copied to clipboard

Better class declaration

Open madsmtm opened this issue 2 years ago • 4 comments

~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 on Id which calls objc_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

madsmtm avatar Sep 05 '21 16:09 madsmtm

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.

madsmtm avatar Feb 26 '22 00:02 madsmtm

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

madsmtm avatar Jun 07 '22 23:06 madsmtm

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

madsmtm avatar Jun 16 '22 08:06 madsmtm

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).

madsmtm avatar Jun 19 '22 05:06 madsmtm

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.

madsmtm avatar Nov 03 '22 23:11 madsmtm