Jak zatrzymać () główny wątek pętli zdarzeń NSApp run ()? (osadzone w C ++)

Dec 21 2020

Oto minimalny możliwy do odtworzenia przykład, jeśli jest zbyt długi, aby go przeczytać, przejdź do następnej sekcji z problemem, a następnie zbadaj kod, jeśli to konieczne.

Minimalny przykład:

Załóżmy, że prosta linia poleceń C ++:

main.cpp

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

Nagłówek opakowania Objective-C: Wrapper.h

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

I to implementacja: 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];
}

I wreszcie interesująca część, w 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" )

Projekt można zbudować za pomocą następujących poleceń:

$ cmake -G Xcode.
$ open ocapp.xcodeproj

Problem:

Podczas używania [NSApp run]i [NSApp stop:self]nie mogę zatrzymać pętli zdarzeń, więc działa ona w nieskończoność.

Zakończono uruchamianie aplikacji
Zatrzymywanie
.....
Zabity: 9

Podczas używania CFRunLoopRun()i CFRunLoopStop(CFRunLoopGetCurrent())uruchamia / zatrzymuje się poprawnie, ale applicationDidFinishLaunchingnigdy nie jest wyzwalany.

Zatrzymywanie
Kończenie

Pytanie:

Dlaczego to? i jak sprawić, by obie te funkcje działały?

Odpowiedzi

2 Kamil.S Jan 06 2021 at 03:56

Problemu nie ma w załączonym kodzie. Twój istniejący wariant

[NSApplication sharedApplication];
[NSApp run];

i

[NSApp stop:self];

jest poprawne.

Winowajcą jest twój CMakeLists.txt. Ten, który dołączyłeś, tworzy wykonywalny plik binarny. To dobrze dla aplikacji konsolowej, ale nie jest to poprawna aplikacja MacOS składająca się z folderu AppName.app i wielu innych plików. Ponieważ używasz AppKit API bez odpowiedniego szkieletu aplikacji MacOS, nie działa.

Absolutna minimalna poprawka w Twoim CMakeLists.txtto:

add_executable(
    ${PROJECT_NAME}
    MACOSX_BUNDLE
)

Teraz będziesz mieć poprawny cel aplikacji w Xcode. W CMakeLists.txtInternecie można znaleźć bardziej zaawansowane przykłady odpowiednich aplikacji dla systemu MacOS.

Aktualizacja
Więc zbadałem to dokładniej i sprawdziłem procedurę wyjścia w
-[NSApplication run]( +[NSApp run]jest to synonim pozostawiony dla kompatybilności, ale prawdziwa implementacja jest w -[NSApplication run]). Możemy ustawić symboliczny punkt przerwania przez lldb w następujący sposób: b "-[NSApplication run]"interesujący fragment (dla X86-64) to:

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

Możemy zweryfikować, że punkt przerwania, w którym punkty strzałek są trafiane tylko w wariancie pakietowym, ale nie w wariancie „nagiego” pliku wykonywalnego. Po dalszych badaniach znalazłem tę odpowiedźhttps://stackoverflow.com/a/48064763/5329717co jest bardzo pomocne. Kluczowym cytatem @Remko jest:

wydaje się, że żądanie zatrzymania pętli interfejsu użytkownika jest przetwarzane dopiero po zdarzeniu interfejsu użytkownika (a więc nie tylko po zdarzeniu pętli głównej).

I rzeczywiście tak jest. Jeśli w wariancie „nagiego” pliku wykonywalnego dodajemy

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

Otrzymujemy pożądane zachowanie, a aplikacja kończy się normalnie. Tak więc wariant „nagiej” aplikacji nie jest poprawną aplikacją na MacOS, dlatego nie odbiera zdarzeń interfejsu użytkownika (niezależnie od tego, jego runloop działa poprawnie).

Z drugiej strony posiadanie odpowiedniego pakietu aplikacji MacOS z Info.plistitp. Jest konieczne, aby MacOS mógł skonfigurować okno aplikacji, ikonę Dock itp.

Na dłuższą metę polecam albo wybranie czystej aplikacji konsolowej, jeśli w ogóle nie potrzebujesz AppKit, albo robienie rzeczy według książki. W przeciwnym razie napotkasz takie anomalie.