Как остановить () цикл событий run () основного потока NSApp? (встроен в C ++)

Dec 21 2020

Вот минимальный воспроизводимый пример: если он слишком длинный для чтения, перейдите к следующему разделу с проблемой, а затем при необходимости изучите код.

Минимальный пример:

Допустим, простая командная строка C ++:

main.cpp

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

Заголовок оболочки Objective-C: Wrapper.h

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

И это реализация: 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];
}

И, наконец, интересная часть в 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" )

Проект можно построить с помощью следующих команд:

$ cmake -G Xcode.
$ открыть ocapp.xcodeproj

Проблема:

При использовании [NSApp run]и [NSApp stop:self]я не могу остановить цикл событий, поэтому он продолжает работать бесконечно.

Приложение завершило запуск
Остановка
.....
Убито: 9

При использовании CFRunLoopRun()и CFRunLoopStop(CFRunLoopGetCurrent())он запускается / останавливается правильно, но applicationDidFinishLaunchingникогда не срабатывает.

Остановка
Прекращение

Вопрос:

Почему это? и как заставить работать обе функции?

Ответы

2 Kamil.S Jan 06 2021 at 03:56

Проблема не в прилагаемом коде. Ваш существующий вариант

[NSApplication sharedApplication];
[NSApp run];

а также

[NSApp stop:self];

верно.

Виновник твой CMakeLists.txt. Тот, который вы включили, создает исполняемый двоичный файл. Это нормально для консольного приложения, но это недопустимое приложение для MacOS, состоящее из папки AppName.app и множества других файлов. Поскольку вы используете API AppKit без надлежащего каркаса приложения MacOS, он не работает.

Голый минимум исправить в ваших CMakeLists.txtсоставляет:

add_executable(
    ${PROJECT_NAME}
    MACOSX_BUNDLE
)

Теперь у вас будет правильная цель приложения в Xcode. Вы можете найти более сложные примеры приложений, CMakeLists.txtподходящих для MacOS, в Интернете.

Обновление
Итак, я исследовал это дальше и проверил процедуру выхода в
-[NSApplication run]( +[NSApp run]это синоним, оставленный для совместимости, но реальная реализация находится в -[NSApplication run]). Мы можем установить символическую точку останова через lldb следующим образом: b "-[NSApplication run]"интересующий фрагмент (для X86-64):

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

Мы можем проверить, что точка останова, на которую указывает стрелка, срабатывает только в связанном варианте, но не в «голом» исполняемом варианте. После дальнейших исследований я нашел этот ответhttps://stackoverflow.com/a/48064763/5329717что очень полезно. Ключевая цитата @Remko:

кажется, что запрос остановки цикла пользовательского интерфейса обрабатывается только после события пользовательского интерфейса (а не сразу после события основного цикла).

И это действительно так. Если в «голом» исполняемом варианте добавим

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

Мы получаем желаемое поведение, и приложение завершается нормально. Таким образом, ваш «голый» вариант приложения не является правильным приложением для MacOS, следовательно, он не получает событий пользовательского интерфейса (его цикл выполнения работает правильно, несмотря на это).

С другой стороны, наличие соответствующего пакета приложений MacOS с и Info.plistт. Д. Необходимо для MacOS для настройки окна приложения, значка док-станции и т. Д.

В конечном итоге я рекомендую либо перейти на чистое консольное приложение, если вам вообще не нужен AppKit, либо делать что-то по инструкции. Иначе вы столкнетесь с такими аномалиями.