¿Cómo detener () el bucle de eventos NSApp run () del hilo principal? (incrustado en C ++)
Aquí hay un ejemplo mínimo reproducible, si es demasiado largo para leer, vaya a la siguiente sección con el problema y luego explore el código si es necesario.
Ejemplo mínimo:
Supongamos una línea de comando simple de C ++:
main.cpp
#include <iostream>
#include "Wrapper.h"
int main()
{
Wrapper wrapper;
wrapper.run();
std::cout << "Exiting" << std::endl;
}
El encabezado contenedor de Objective-C: Wrapper.h
struct OCWrapper;
class Wrapper
{
public:
Wrapper() noexcept;
virtual ~Wrapper() noexcept;
void run();
private:
OCWrapper* impl=nullptr;
};
Y su implementación: 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];
}
Y finalmente la parte interesante, 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" )
El proyecto se puede construir con los siguientes comandos:
$ cmake -G Xcode.
$ open ocapp.xcodeproj
El problema:
Cuando uso [NSApp run]
y [NSApp stop:self]
, no puedo detener el bucle de eventos, por lo que sigue ejecutándose indefinidamente.
La aplicación terminó de iniciarse
Deteniendo
...
Muerto: 9
Cuando se usa CFRunLoopRun()
y CFRunLoopStop(CFRunLoopGetCurrent())
, se inicia / detiene correctamente, pero applicationDidFinishLaunching
nunca se activa.
Detener
Terminar
La pregunta:
¿Por qué es esto? y cómo hacer que ambas funciones funcionen?
Respuestas
El problema no está en el código adjunto. Tu variante existente
[NSApplication sharedApplication];
[NSApp run];
y
[NSApp stop:self];
es correcto.
El culpable es tuyo CMakeLists.txt
. El que incluyó crea un binario ejecutable. Eso está bien para una aplicación de consola, pero no es una aplicación MacOS válida que consta de la carpeta AppName.app y muchos otros archivos. Dado que está utilizando la API de AppKit sin el andamio adecuado de una aplicación MacOS, no funciona.
Una solución mínima en su CMakeLists.txt
es:
add_executable(
${PROJECT_NAME}
MACOSX_BUNDLE
)
Ahora tendrá un objetivo de aplicación correcto en Xcode. Puede buscar ejemplos más avanzados de CMakeLists.txt
aplicaciones adecuadas para MacOS en Internet.
Actualización
Así que lo investigué más a fondo e inspeccioné la rutina de salida en
-[NSApplication run]
( +[NSApp run]
es un sinónimo dejado para compatibilidad, pero la implementación real está adentro -[NSApplication run]
). Podemos establecer un punto de interrupción simbólico a través de lldb como este: b "-[NSApplication run]"
el fragmento de interés (para X86-64) es:
-> 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 un punto de interrupción donde los puntos de flecha se golpean solo en la variante incluida, pero no en la variante ejecutable "desnuda". Después de investigar más, encontré esta respuesta.https://stackoverflow.com/a/48064763/5329717lo cual es muy útil. La cita clave de @Remko es:
parece que la solicitud de detención del bucle de IU solo se procesa después de un evento de IU (por lo tanto, no solo después de un evento de bucle principal).
Y ese es de hecho el caso. Si en la variante ejecutable "desnuda" agregamos
- (void) shutdown:(NSNotification *) notif
{
NSLog(@"Stopping");
//CFRunLoopStop(CFRunLoopGetCurrent());
[NSApp stop:self];
[NSApp abortModal]; //this is used for generating a UI NSEvent
}
Obtenemos el comportamiento deseado y la aplicación termina normalmente. Por lo tanto, su variante de aplicación "desnuda" no es una aplicación MacOS correcta, por lo que no recibe eventos de IU (su runloop funciona correctamente independientemente).
Por otro lado, Info.plist
es necesario tener un paquete de aplicaciones MacOS adecuado con, etc., para que MacOS configure una ventana de aplicación, un icono de Dock, etc.
A largo plazo, recomiendo optar por una aplicación de consola pura si no necesita AppKit en absoluto o hacer las cosas según las reglas. De lo contrario, se encontrará con tales anomalías.