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