4

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?

Adrian Maire
  • 14,354
  • 9
  • 45
  • 85
  • 1
    The signature of `didFinishLaunching` should be `- (void)applicationDidFinishLaunching:(NSNotification *)aNotification` instead. I'm surprised you'd need to subscribe to it manually. – Kamil.S Dec 22 '20 at 20:38
  • I fixed the code in the question, and added a CMAKE file. – Adrian Maire Jan 05 '21 at 07:59
  • Is there any reason why are you not calling `NSApplicationMain` from your `main` instead and presumably designating your `MyOCApp` as the `NSApplicationDelegate`? Are you trying to make a GUI Cocoa/AppKit App? – Kamil.S Jan 05 '21 at 12:04
  • This is just a minimal example, in the real project the C++ part is much larger, and the Objective-C part is just to gather some events from macOS (also most of the project is cross-platform). Answering your question, no Cocoa, as minimal as possible AppKit. So using NSApplicationMain makes the Objective_C part even more intrusive in the project (which already needed significant refactoring to allow the main thread to be monopolized by NSApp::run). – Adrian Maire Jan 05 '21 at 14:06
  • But obviously, if a solution without changing main.cpp works, I don't really care about NSApplicationMain, NSApplicationDelegate, or NSWhatever. Feel free to show your ideas as answers. – Adrian Maire Jan 05 '21 at 14:12

1 Answers1

6

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.

Kamil.S
  • 5,205
  • 2
  • 22
  • 51
  • Hi Kamil.S, Thanks for your answer. Do you have an explanation why executing the bundle (`open ...`) works, while executing the binary inside (`./ocapp.app/...../ocapp`) does not? Up to now, to me, the bundles was just a way to pack conveniently libraries and other required files for the executable. – Adrian Maire Jan 08 '21 at 07:43
  • So I did check and the Chess app works only when the executable is inside the `Chess.app` bundle folder. If you move the executable somewhere else it won't work. That mimics your problem. And the MacOS when starting an UI app needs `Info.plist` file. – Kamil.S Jan 10 '21 at 14:54
  • I still miss some explanation: Why is this? It is IMHO not acceptable to push a software which freeze while run through the binary. I feel like the bundle is solving the problem "by casualty" rather than for a specific OS requirement. – Adrian Maire Jan 11 '21 at 13:34
  • Thank you. This bring light to the topic. – Adrian Maire Jan 12 '21 at 11:22