Como stop () thread principal NSApp run () event loop? (incorporado em C ++)

Dec 21 2020

Aqui está um exemplo reproduzível mínimo; se for muito longo para ler, vá para a próxima seção com o problema e explore o código, se necessário.

Exemplo mínimo:

Suponhamos uma linha de comando simples em C ++:

main.cpp

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

O cabeçalho do wrapper Objective-C: Wrapper.h

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

E sua implementação: 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];
}

E, finalmente, a parte interessante, em 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" )

O projeto pode ser construído com os seguintes comandos:

$ cmake -G Xcode.
$ open ocapp.xcodeproj

O problema:

Ao usar [NSApp run]e [NSApp stop:self], não consigo interromper o loop de eventos, portanto, ele continua em execução indefinidamente.

Lançamento do aplicativo concluído
Parando
...
Mortos: 9

Ao usar CFRunLoopRun()e CFRunLoopStop(CFRunLoopGetCurrent()), ele inicia / para corretamente, mas applicationDidFinishLaunchingnunca é acionado.

Parando o
Encerramento

A questão:

Por que é isso? e como ter ambos os recursos funcionando?

Respostas

2 Kamil.S Jan 06 2021 at 03:56

O problema não está no código anexado. Sua variante existente

[NSApplication sharedApplication];
[NSApp run];

e

[NSApp stop:self];

está correto.

O culpado é seu CMakeLists.txt. O que você incluiu cria um binário executável. Isso é bom para um aplicativo de console, mas não é um aplicativo MacOS válido que consiste na pasta AppName.app e um monte de outros arquivos. Como você está usando a API AppKit sem o suporte adequado de um aplicativo MacOS, ele não funciona.

Uma correção mínima em seu CMakeLists.txté:

add_executable(
    ${PROJECT_NAME}
    MACOSX_BUNDLE
)

Agora você terá um destino de aplicativo correto no Xcode. Você pode procurar exemplos mais avançados de CMakeLists.txtaplicativos adequados para MacOS na Internet.

Atualizar
Então eu investiguei mais e inspecionei a rotina de saída em
-[NSApplication run]( +[NSApp run]é um sinônimo para compatibilidade, mas a implementação real está em -[NSApplication run]). Podemos definir um ponto de interrupção simbólico por meio do lldb como este: b "-[NSApplication run]"o snippet de interesse (para 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  

Podemos verificar que um ponto de interrupção onde a seta aponta é atingido apenas na variante empacotada, mas não na variante executável "simples". Depois de mais pesquisas, encontrei esta respostahttps://stackoverflow.com/a/48064763/5329717o que é muito útil. A citação chave de @Remko é:

parece que a solicitação de parada do loop de IU só é processada após um evento de IU (não apenas após um evento de loop principal).

E esse é realmente o caso. Se na variante executável "simples", adicionarmos

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

Obtemos o comportamento desejado e o aplicativo é encerrado normalmente. Portanto, sua variante de aplicativo "simples" não é um aplicativo MacOS correto, portanto, não recebe eventos de IU (seu runloop funciona corretamente de qualquer maneira).

Por outro lado, Info.plisté necessário ter o pacote de aplicativos MacOS adequado com etc, para que o MacOS configure uma janela de aplicativo, ícone Dock etc.

No longo prazo, eu recomendo ir para o aplicativo de console puro se você não precisar do AppKit ou seguir as regras. Caso contrário, você encontrará tais anomalias.