简体   繁体   中英

How to stop() main thread NSApp run() event loop? (embedded in C++)

Here is a minimal reproductible example, if too long to read, go to next section with the problem, and then explore the code if needed.

Minimal example:

Let suppose a simple C++ command line:

main.cpp

#include <iostream>
#include "Wrapper.h"
int main()
{
    Wrapper wrapper;
    wrapper.run();
    std::cout << "Exiting" << std::endl;
}

The Objective-C wrapper header: Wrapper.h

struct OCWrapper;
class Wrapper
{
public:
    Wrapper() noexcept;
    virtual ~Wrapper() noexcept;
    void run();
private:
    OCWrapper* impl=nullptr;
};

And it implementation: Wrapper.mm

#import "Wrapper.h"
#import "MyOCApp.h"

struct OCWrapper
{
    MyOCApp* wrapped=nullptr;
};

Wrapper::Wrapper() noexcept: impl(new OCWrapper)
{
    impl->wrapped = [[ MyOCApp alloc] init];
}

Wrapper::~Wrapper() noexcept
{
    [impl->wrapped release];
    delete impl;
}

void Wrapper::run()
{
    [impl->wrapped run];
}

And finally the interesting part, in Objective-C, MyOCApp.h:

#import <AppKit/AppKit.h>
#import <Foundation/Foundation.h>

@interface MyOCApp: NSObject
@end

@implementation MyOCApp
- (id)init 
{
    [[NSNotificationCenter defaultCenter] addObserver:self
                selector:@selector(applicationDidFinishLaunching:)
                name:NSApplicationDidFinishLaunchingNotification object:nil];
    
    return self;
}

- (void)run
{
    [self performSelector:@selector(shutdown:) withObject:nil afterDelay: 2];
    //CFRunLoopRun();
    
    [NSApplication sharedApplication];
    [NSApp run];
}

- (void) shutdown:(NSNotification *) notif
{
    NSLog(@"Stopping");
    //CFRunLoopStop(CFRunLoopGetCurrent());
    [NSApp stop:self];
}

- (void) applicationDidFinishLaunching:(NSNotification *) notif
{
    NSLog(@"Application ready");
}
@end

CMakeLists.txt

cmake_minimum_required (VERSION 3.10.0)
cmake_policy( SET CMP0076 NEW)

set(CMAKE_CXX_STANDARD 17)

project(ocapp)

add_executable(${PROJECT_NAME})

find_library(APP_KIT AppKit)
find_library(CORE_FOUNDATION CoreFoundation)
target_link_libraries( ${PROJECT_NAME} ${APP_KIT} ${CORE_FOUNDATION} )

target_sources( ${PROJECT_NAME} PRIVATE "main.cpp" "Wrapper.mm" PUBLIC "Wrapper.h" "MyOCApp.h" )

The project can be built with following commands:

$ cmake -G Xcode.
$ open ocapp.xcodeproj

The problem:

When using [NSApp run] and [NSApp stop:self] , I am unable to stop the event loop, so it keep running indefinitely.

Application finished launching
Stopping
.....
Killed: 9

When using CFRunLoopRun() and CFRunLoopStop(CFRunLoopGetCurrent()) , it start/stop correctly, but applicationDidFinishLaunching is never triggered.

Stopping
Terminating

The question:

Why is this? and how to have both feature working?

The problem isn't in the attached code. Your existing variant

[NSApplication sharedApplication];
[NSApp run];

and

[NSApp stop:self];

is correct.

The culprit is your CMakeLists.txt . The one you included creates an executable binary. That's fine for a console app but it's not a valid MacOS app consisting of AppName.app folder and bunch of other files. Since you're using AppKit API without proper scaffold of an MacOS app it doesn't work.

A bare minimum fix in your CMakeLists.txt is:

add_executable(
    ${PROJECT_NAME}
    MACOSX_BUNDLE
)

Now you will have a correct App target in Xcode. You can look up more advanced examples of CMakeLists.txt suitable for MacOS apps on the Internet.

Update
So I investigated it further and inspected the exit routine in
-[NSApplication run] ( +[NSApp run] is a synonym left for compatibility but the real implementation is in -[NSApplication run] ). We can set a symbolic breakpoint through lldb like this: b "-[NSApplication run]" the snippet of interest (for X86-64) is:

->  0x7fff4f5f96ff <+1074>: add    rsp, 0x98
    0x7fff4f5f9706 <+1081>: pop    rbx
    0x7fff4f5f9707 <+1082>: pop    r12
    0x7fff4f5f9709 <+1084>: pop    r13
    0x7fff4f5f970b <+1086>: pop    r14
    0x7fff4f5f970d <+1088>: pop    r15
    0x7fff4f5f970f <+1090>: pop    rbp
    0x7fff4f5f9710 <+1091>: ret  

We can verify that a breakpoint where arrow points is hit only in the bundled variant but not in the "naked" executable variant. After further research I found this answer https://stackoverflow.com/a/48064763/5329717 which is very helpful. The key quote by @Remko being:

it seems that the UI loop stop request is only processed after a UI event (so not just after a main loop event).

And that is indeed the case. If in the "naked" executable variant we add

- (void) shutdown:(NSNotification *) notif
{
    NSLog(@"Stopping");
    //CFRunLoopStop(CFRunLoopGetCurrent());
    [NSApp stop:self];
    [NSApp abortModal]; //this is used for generating a UI NSEvent
}

We get desired behavior and the app terminates normally. So your "naked" app variant isn't a correct MacOS app, hence it does not receive UI events (its runloop works correctly regardless).

On the other hand having proper MacOS app bundle with Info.plist etc is necessary for MacOS to setup an app window, Dock icon etc.

In the long run I do recommend either going for pure console app if you don't need AppKit at all or doing things by the book. Otherwise you will run into such anomalies.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM