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
and
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.txtis:Now you will have a correct App target in Xcode. You can look up more advanced examples of
CMakeLists.txtsuitable 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: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:
And that is indeed the case. If in the "naked" executable variant we add
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.plistetc 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.