Comment arrêter () la boucle d'événements NSApp run () du thread principal? (intégré dans C ++)

Dec 21 2020

Voici un exemple reproductible minimal, s'il est trop long à lire, passez à la section suivante avec le problème, puis explorez le code si nécessaire.

Exemple minimal:

Supposons une simple ligne de commande C ++:

main.cpp

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

L'en-tête du wrapper Objective-C: Wrapper.h

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

Et sa mise en œuvre: 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];
}

Et enfin la partie intéressante, en 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" )

Le projet peut être construit avec les commandes suivantes:

$ cmake -G Xcode.
$ open ocapp.xcodeproj

Le problème:

Lorsque vous utilisez [NSApp run]et [NSApp stop:self], je ne parviens pas à arrêter la boucle d'événements, elle continue donc à fonctionner indéfiniment.

L' application a terminé son lancement
Stopping
.....
Tué: 9

Lors de l'utilisation de CFRunLoopRun()et CFRunLoopStop(CFRunLoopGetCurrent()), il démarre / s'arrête correctement, mais applicationDidFinishLaunchingn'est jamais déclenché.

Arrêt de la
résiliation

La question:

Pourquoi est-ce? et comment faire fonctionner les deux fonctionnalités?

Réponses

2 Kamil.S Jan 06 2021 at 03:56

Le problème ne se trouve pas dans le code ci-joint. Votre variante existante

[NSApplication sharedApplication];
[NSApp run];

et

[NSApp stop:self];

est correct.

Le coupable est le vôtre CMakeLists.txt. Celui que vous avez inclus crée un exécutable binaire. C'est bien pour une application console, mais ce n'est pas une application MacOS valide composée du dossier AppName.app et d'un tas d'autres fichiers. Puisque vous utilisez l'API AppKit sans échafaudage approprié d'une application MacOS, cela ne fonctionne pas.

Une solution minimale pour vous CMakeLists.txtest:

add_executable(
    ${PROJECT_NAME}
    MACOSX_BUNDLE
)

Vous aurez maintenant une cible d'application correcte dans Xcode. Vous pouvez rechercher des exemples plus avancés d' CMakeLists.txtapplications adaptées à MacOS sur Internet.

Mise à jour
J'ai donc enquêté plus avant et inspecté la routine de sortie dans
-[NSApplication run]( +[NSApp run]est un synonyme laissé pour la compatibilité mais la véritable implémentation est dans -[NSApplication run]). Nous pouvons définir un point d'arrêt symbolique via lldb comme ceci: b "-[NSApplication run]"l'extrait de code d'intérêt (pour X86-64) est:

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

Nous pouvons vérifier qu'un point d'arrêt où la flèche pointe est atteint uniquement dans la variante fournie mais pas dans la variante exécutable "nue". Après de plus amples recherches, j'ai trouvé cette réponsehttps://stackoverflow.com/a/48064763/5329717ce qui est très utile. La citation clé de @Remko étant:

il semble que la demande d'arrêt de la boucle de l'interface utilisateur n'est traitée qu'après un événement de l'interface utilisateur (donc pas seulement après un événement de la boucle principale).

Et c'est bien le cas. Si dans la variante exécutable "nue" nous ajoutons

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

Nous obtenons le comportement souhaité et l'application se termine normalement. Ainsi, votre variante d'application "nue" n'est pas une application MacOS correcte, par conséquent, elle ne reçoit pas d'événements d'interface utilisateur (sa boucle d'exécution fonctionne correctement malgré tout).

D'un autre côté, il Info.plistest nécessaire pour MacOS de configurer une fenêtre d'application, une icône du Dock, etc.

À long terme, je recommande d'utiliser une application console pure si vous n'avez pas du tout besoin d'AppKit ou de faire les choses comme prévu. Sinon, vous rencontrerez de telles anomalies.