Como stop () thread principal NSApp run () event loop? (incorporado em C ++)
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 applicationDidFinishLaunching
nunca é acionado.
Parando o
Encerramento
A questão:
Por que é isso? e como ter ambos os recursos funcionando?
Respostas
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.txt
aplicativos 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.