¿Cómo detener () el bucle de eventos NSApp run () del hilo principal? (incrustado en C ++)

Dec 21 2020

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 applicationDidFinishLaunchingnunca se activa.

Detener
Terminar

La pregunta:

¿Por qué es esto? y cómo hacer que ambas funciones funcionen?

Respuestas

2 Kamil.S Jan 06 2021 at 03:56

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.txtes:

add_executable(
    ${PROJECT_NAME}
    MACOSX_BUNDLE
)

Ahora tendrá un objetivo de aplicación correcto en Xcode. Puede buscar ejemplos más avanzados de CMakeLists.txtaplicaciones 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.plistes 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.