madsmtm / objc2

Bindings to Apple's frameworks in Rust
https://docs.rs/objc2/
MIT License
280 stars 35 forks source link

Problems with creating a NSUserNotificationCenterDelegate using declare_class!() #606

Closed dscso closed 3 weeks ago

dscso commented 3 weeks ago

I am trying to port the mac-notificaiton-sys to objc2, to be able to respond on multiple notifications at the same time while not blocking the whole thread (as mac-notification-sys does)

I have some problems declaring the delegate for NSUserNotificationCenter see in detail here. It seems to declare the class, but when interacting with a notification, callback does not get called. I have tried quite a lot, and tried to get inspired by multiple different sources (winit, etc.) but I can't figure it out :(.

When running my creation I sometimes get a segfault, sometimes nothing and one time I got this:

     Running `target/debug/examples/send`
objc[48022]: Attempt to use unknown class 0x600002467340.
[1]    48022 abort      cargo run --example send

Might there be a problem with the NSUserNotificationCenterDelegate implementation since it is deprecated? I would also use the User Notifications framework, but it requires signing the binaries[1]. Therefore, I try to stick with the deprecated API, since Electron and many other are still using it.

If you want to try it just download it and run

cargo run --example send 

btw thanks for this crate, it makes development with objc much easier (i've tried a lot now :D).

use objc2::msg_send_id;
use objc2::mutability::InteriorMutable;
use objc2::rc::Id;
use objc2::runtime::{NSObject, NSObjectProtocol};
use objc2::{declare_class, ClassType, DeclaredClass};
use objc2_foundation::{
    MainThreadMarker, NSUserNotification, NSUserNotificationCenter,
    NSUserNotificationCenterDelegate,
};

#[derive(Debug, Default)]
pub(super) struct State {}

declare_class! {
    pub(super) struct RustNotificationDelegate;

    unsafe impl ClassType for RustNotificationDelegate {
        type Super = NSObject;
        type Mutability = InteriorMutable;
        const NAME: &'static str = "RustNotificationDelegate";
    }

    impl DeclaredClass for RustNotificationDelegate {
        type Ivars = State;
    }

    unsafe impl NSObjectProtocol for RustNotificationDelegate {}

    unsafe impl NSUserNotificationCenterDelegate for RustNotificationDelegate {
        #[method(userNotificationCenter:didActivateNotification:)]
        fn user_notification_center_did_activate_notification(
            &self,
            _center: &NSUserNotificationCenter,
            notification: &NSUserNotification,
        ) {
            println!("Notification activated: {:?}", notification);
        }
    }
}

impl RustNotificationDelegate {
    pub fn new() -> Id<Self> {
        let this = MainThreadMarker::new().unwrap().alloc().set_ivars(State {
            ..Default::default()
        });
        unsafe { msg_send_id![super(this), init] }
    }
}
madsmtm commented 3 weeks ago

port the mac-notificaiton-sys to objc2

I think there's a PR up for that already, btw: https://github.com/h4llow3En/mac-notification-sys/pull/51

I try to stick with the deprecated API

Makes sense, though I think that might be your issue? I haven't used NSUserNotification myself, but from my testing it seems like the existing mac-notification-sys doesn't work either, and haven't for a while.

Maybe it's the __bundleIdentifier hack that's broken? Have you tried running it inside an application bundle?

When running my creation I sometimes get a segfault

Hmm, that sounds problematic! Could you hook up a debugger and see where the crash occurs? I tried running it myself a few times, including using AddressSanitizer, and couldn't reproduce the crash.

dscso commented 3 weeks ago

I think there's a PR up for that already, btw: https://github.com/h4llow3En/mac-notification-sys/pull/51

Yes but the PR uses the User Notification Framework, so I want to get it running in the old deprecated way.

from my testing it seems like the existing mac-notification-sys doesn't work either, and haven't https://github.com/h4llow3En/mac-notification-sys/issues/33.

I got it working on my M1 Macbook running on MacOS 14.2.1 so it seems to still be working. Did it send notifications for you when you tried my code? Because it should send a notification and crash (sometimes/immediately after)

Maybe it's the __bundleIdentifier hack that's broken? Have you tried running it inside an application bundle?

Maybe I am doing something wrong here, but if I declare the delegate in the .m file, there are no problems, and I get my notification interactions. The problem starts if I go pure rust on the delegate and try to write the NSUserNotificationCenterDelegate in pure Rust.

If you want to try, I created a branch: objc-delegate

The thing that is troubling me is if I add this to the notification.m file, it works perfectly. I would like to not use some dirty callback solution. Also my previous solution resulted in a lot of memory leaks.

@interface NotificationCenterDelegate: NSObject <NSUserNotificationCenterDelegate>
@end

@implementation NotificationCenterDelegate

// Implement the delegate method to handle the user's response to the notification
- (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification {
    NSLog(@"Notification clicked");
    [center removeDeliveredNotification:notification];
}

@end

and add this:

NotificationCenterDelegate *delegate = [[NotificationCenterDelegate alloc] init];
NSUserNotificationCenter* center = [NSUserNotificationCenter defaultUserNotificationCenter];
center.delegate = delegate;

Regarding the debugger: It seems to always fail in a different spot in a native library. maybe you know a better way to get more debug info, that might be helpful :).

$ lldb target/debug/examples/send
(lldb) target create "target/debug/examples/send"
Current executable set to '/Users/me/code/rust/notifications/target/debug/examples/send' (arm64).
(lldb) run 
Process 54189 launched: '/Users/me/code/rust/notifications/target/debug/examples/send' (arm64)
aProcess 54189 exited with status = 0 (0x00000000) 
(lldb) run
Process 54198 launched: '/Users/me/code/rust/notifications/target/debug/examples/send' (arm64)
Process 54198 stopped
* thread #1, name = 'main', queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x1e)
    frame #0: 0x000000018cb648d8 libobjc.A.dylib`objc_opt_respondsToSelector + 52
libobjc.A.dylib`objc_opt_respondsToSelector:
->  0x18cb648d8 <+52>: ldrsh  w8, [x16, #0x1e]
    0x18cb648dc <+56>: tbz    w8, #0x1f, 0x18cb6496c    ; <+200>
    0x18cb648e0 <+60>: mov    x1, x2
    0x18cb648e4 <+64>: mov    x2, x16
Target 0: (send) stopped.
(lldb) run
There is a running process, kill it and restart?: [Y/n] y
Process 54198 exited with status = 9 (0x00000009) killed
Process 54212 launched: '/Users/me/code/rust/notifications/target/debug/examples/send' (arm64)
Process 54212 stopped
* thread #1, name = 'main', queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x1f48aa0003f0)
    frame #0: 0x000000018cb36ed8 libobjc.A.dylib`class_respondsToSelector_inst + 64
libobjc.A.dylib`class_respondsToSelector_inst:
->  0x18cb36ed8 <+64>: ldr    w9, [x9]
    0x18cb36edc <+68>: tbnz   w9, #0x0, 0x18cb36ef8     ; <+96>
    0x18cb36ee0 <+72>: ldr    x9, [x19]
    0x18cb36ee4 <+76>: and    x9, x9, #0x7ffffffffff8
Target 0: (send) stopped.
(lldb) run
There is a running process, kill it and restart?: [Y/n] y
Process 54212 exited with status = 9 (0x00000009) killed
Process 54226 launched: '/Users/me/code/rust/notifications/target/debug/examples/send' (arm64)
Process 54226 stopped
* thread #1, name = 'main', queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x1e)
    frame #0: 0x000000018cb648d8 libobjc.A.dylib`objc_opt_respondsToSelector + 52
libobjc.A.dylib`objc_opt_respondsToSelector:
->  0x18cb648d8 <+52>: ldrsh  w8, [x16, #0x1e]
    0x18cb648dc <+56>: tbz    w8, #0x1f, 0x18cb6496c    ; <+200>
    0x18cb648e0 <+60>: mov    x1, x2
    0x18cb648e4 <+64>: mov    x2, x16
Target 0: (send) stopped.
(lldb) 
dscso commented 3 weeks ago

After hours and hours I found the solution, and it was not due to this crate: I thought that if you pass a reference, it would retain delegate, as there is one ref existent. Of course this is not the case, and delegate gets dropped as soon as the scope ends.

notification_center.setDelegate(Some(ProtocolObject::from_ref(delegate.as_ref())));

I found a workaround using a static variable like so:

unsafe fn get_delegate() -> &'static Id<RustNotificationDelegate> {
    static mut DELEGATE: MaybeUninit<Id<RustNotificationDelegate>> = MaybeUninit::uninit();
    static ONCE: Once = Once::new();

    ONCE.call_once(|| {
        DELEGATE.write(RustNotificationDelegate::new());
    });

    DELEGATE.assume_init_ref()
}

fn init() {
    //...
    let notification_center = NSUserNotificationCenter::defaultUserNotificationCenter();
    let delegate = get_delegate();
    notification_center.setDelegate(Some(ProtocolObject::from_ref(delegate.as_ref())));
}

Thank you very much for your support and the library, as already said: it makes my live much easier!

madsmtm commented 3 weeks ago

if you pass a reference, it would retain delegate, as there is one ref existent

Yeah, delegates are usually only weakly retained, that's not something that objc2 has control over.

An alternative to the static would be to use ManuallyDrop, and just leak the Id<RustNotificationDelegate>.

Glad you figured it out though!