Articles

Servicios de XPC en la app de macOS

Lê Điền Phúc
Lê Điền Phúc

Sigue

Sep 4, 2020 – 9 min read

Antes de XPC cogíamos Sockets y Mach Messages (Mach Ports).

El mecanismo XPC ofrece una alternativa a los sockets (o a los Servicios Mach usando MIG) para el IPC. Podríamos tener, por ejemplo, un proceso que actúe como «servidor» a la espera de que los clientes accedan a su API y presten algún servicio.

Servicios XPC en aplicaciones

Cuando hablamos de Servicios XPC (S mayúscula), nos referimos al bundle llamado Servicio XPC. Los Bundles en el ecosistema Apple se refieren a entidades representadas por una estructura de directorios específica. El Bundle más común que se encuentra son los Bundles de aplicaciones. Si haces clic con el botón derecho del ratón sobre cualquier aplicación (por ejemplo Chess.app) y seleccionas Mostrar contenido, lo que encontrarás es una estructura de directorios. Volviendo a XPC, las aplicaciones pueden tener varios bundles de Servicios XPC. Los encontrará dentro del directorio Contents/XPCServices/ dentro del paquete de aplicaciones. Puedes buscar en tu directorio /Applications y ver cuántas de las aplicaciones dependen de los Servicios XPC.

También puedes tener Servicios XPC dentro de Frameworks (que son otro tipo de Bundle).

Beneficios adicionales de los Servicios XPC

Usar Servicios XPC en nuestras aplicaciones nos permite romper algunas funcionalidades en módulos separados (El Servicio XPC). Podríamos crear un Servicio XPC que se encargue de ejecutar algunas tareas costosas pero poco frecuentes. Por ejemplo, alguna tarea de criptografía para generar números aleatorios.

Otra ventaja adicional es que el Servicio XPC se ejecuta en su propio proceso. Si ese proceso se cae o se mata, no afecta a nuestra aplicación principal. Imagina que tu aplicación soporta plugins definidos por el usuario. Y los plugins son construidos usando Servicios XPC. Si están mal codificados y se caen, no afectarán la integridad de su aplicación principal.

Un beneficio adicional del Servicio XPC es que pueden tener sus propios derechos. La aplicación sólo requerirá el derecho cuando hace uso de un servicio proporcionado por el Servicio XPC que requiere el derecho. Imagínese que tiene una aplicación que utiliza la localización, pero sólo para funciones específicas. Podría mover esas funciones a un Servicio XPC y añadir el derecho de localización sólo a ese Servicio XPC. Si su usuario nunca necesita la característica que usa la localización, no se le pedirán permisos, haciendo que el uso de su aplicación sea más confiable.

XPC y nuestro amigo launchd

launchd es el primer proceso que se ejecuta en nuestro sistema. Se encarga de lanzar y gestionar otros procesos, servicios y demonios. launchd también se encarga de programar tareas. Así que tiene sentido que launchd también sea responsable de la gestión de los Servicios XPC.

El Servicio XPC puede ser detenido si ha estado inactivo durante mucho tiempo, o ser engendrado bajo demanda. Toda la gestión la hace launchd, y no necesitamos hacer nada para que funcione.

launchd tiene información sobre la disponibilidad de recursos en todo el sistema y la presión de la memoria, quién mejor que launchd para tomar decisiones sobre cómo utilizar más eficazmente los recursos de nuestro sistema

Implementar servicios XPC

Un servicio XPC es un bundle en el directorio Contents/XPCServices del bundle principal de la aplicación; el bundle del servicio XPC contiene un archivo Info.plist, un ejecutable y cualquier recurso que necesite el servicio. El servicio XPC indica a qué función llamar cuando el servicio recibe mensajes llamando a xpc_main(3) Mac OS X Developer Tools Manual Page desde su función principal.

Para crear un servicio XPC en Xcode, haga lo siguiente:

  1. Añada un nuevo objetivo a su proyecto, utilizando la plantilla XPC Service.
  2. Agrega una fase de Copiar Archivos a la configuración de construcción de tu aplicación, que copie el servicio XPC en el directorio Contents/XPCServices del paquete principal de la aplicación.
  3. Agrega una dependencia a la configuración de construcción de tu aplicación, para indicar que depende del paquete del servicio XPC.
  4. Si estás escribiendo un servicio XPC de bajo nivel (basado en C), implementa una función principal mínima para registrar tu manejador de eventos, como se muestra en el siguiente listado de código. Reemplace mi_evento_manejador con el nombre de su función manejadora de eventos.
int main(int argc, const char *argv) {
xpc_main(my_event_handler);
// The xpc_main() function never returns.
exit(EXIT_FAILURE);
}

Si está escribiendo un servicio de alto nivel (basado en Objective-C) usando NSXPCConnection, primero cree una clase delegada de conexión que se ajuste al protocolo NSXPCListenerDelegate. A continuación, implemente una función principal mínima que cree y configure un objeto listener, como se muestra en el siguiente listado de código.

int main(int argc, const char *argv) {
MyDelegateClass *myDelegate = ...
NSXPCListener *listener =
;
listener.delegate = myDelegate;
;
// The resume method never returns.
exit(EXIT_FAILURE);
}

Usando el servicio

La forma de utilizar un servicio XPC depende de si se trabaja con la API de C (Servicios XPC) o con la API de Objective-C (NSXPCConnection).

Usando la API NSXPCConnection de Objective-C La API NSXPCConnection de Objective-C proporciona una interfaz de alto nivel de llamada a procedimientos remotos que permite llamar a métodos de objetos en un proceso desde otro proceso (normalmente una aplicación que llama a un método en un servicio XPC). La API NSXPCConnection serializa automáticamente las estructuras de datos y los objetos para su transmisión y los deserializa en el otro extremo. Como resultado, llamar a un método en un objeto remoto se comporta como llamar a un método en un objeto local.

Para utilizar la API NSXPCConnection, debe crear lo siguiente:

  • Una interfaz. Esto consiste principalmente en un protocolo que describe los métodos que deben ser llamados desde el proceso remoto. Esto se describe en Diseño de una interfaz
  • Un objeto de conexión en ambos lados. En el lado del servicio, esto fue descrito previamente en Crear el Servicio. En el lado del cliente, esto se describe en Conexión y uso de una interfaz.
  • Un oyente. Este código en el servicio XPC acepta conexiones. Esto se describe en Aceptar una conexión en el Helper. Mensajes.

Arquitectura general

Arquitectura general

Cuando se trabaja con aplicaciones helper basadas en NSXPCConnection, tanto la aplicación principal como la helper tienen una instancia de NSXPCConnection. La aplicación principal crea su objeto de conexión por sí misma, lo que provoca el lanzamiento del helper. Un método delegado en el helper recibe su objeto de conexión cuando se establece la conexión. Esto se ilustra en la Figura 4-1.

Cada objeto NSXPCConnection proporciona tres características clave:

  • Una propiedad exportedInterface que describe los métodos que deben estar disponibles para el lado opuesto de la conexión.
  • Una propiedad exportedObject que contiene un objeto local para manejar las llamadas de métodos que llegan desde el otro lado de la conexión.
  • La capacidad de obtener un objeto proxy para llamar a los métodos en el otro lado de la conexión.

Cuando la aplicación principal llama a un método en un objeto proxy, el objeto NSXPCConnection del servicio XPC llama a ese método en el objeto almacenado en su propiedad exportedObject.

De manera similar, si el servicio XPC obtiene un objeto proxy y llama a un método en ese objeto, el objeto NSXPCConnection de la aplicación principal llama a ese método en el objeto almacenado en su propiedad exportedObject

Diseño de una interfaz

La API NSXPCConnection aprovecha los protocolos de Objective-C para definir la interfaz programática entre la aplicación que llama y el servicio. Cualquier método de instancia que se quiera llamar desde el lado opuesto de una conexión debe definirse explícitamente en un protocolo formal. Por ejemplo:

@protocol FeedMeACookie
- (void)feedMeACookie: (Cookie *)cookie;
@end

Como la comunicación a través de XPC es asíncrona, todos los métodos del protocolo deben tener un tipo de retorno void. Si necesita devolver datos, puede definir un bloque de respuesta así:

@protocol FeedMeAWatermelon
- (void)feedMeAWatermelon: (Watermelon *)watermelon
reply:(void (^)(Rind *))reply;
@end

Un método sólo puede tener un bloque de respuesta. Sin embargo, debido a que las conexiones son bidireccionales, el ayudante de servicio XPC también puede responder llamando a los métodos de la interfaz proporcionada por la aplicación principal, si se desea.

Cada método debe tener un tipo de retorno de void, y todos los parámetros de los métodos o bloques de respuesta deben ser:

  • Tipos aritméticos (int, char, float, double, uint64_t, NSUInteger, y así sucesivamente)
  • BOOL
  • Cadenas
  • C estructuras y arrays que contengan sólo los tipos enumerados anteriormente
  • Objetos Objective-C que implementen el protocolo NSSecureCoding.

Importante: Si un método (o su bloque de respuesta) tiene parámetros que son clases de colección de Objective-C (NSDictionary, NSArray, etc.), y si necesita pasar sus propios objetos personalizados dentro de una colección, debe decirle explícitamente a XPC que permita esa clase como miembro de ese parámetro de colección.

Conexión y uso de una interfaz

Una vez que haya definido el protocolo, debe crear un objeto de interfaz que lo describa. Para ello, llame al método interfaceWithProtocol: de la clase NSXPCInterface. Por ejemplo:

NSXPCInterface *myCookieInterface =
;

Una vez que haya creado el objeto de interfaz, dentro de la aplicación principal, debe configurar una conexión con él llamando al método initWithServiceName:. Por ejemplo:

NSXPCConnection *myConnection = 
initWithServiceName:@"com.example.monster"];
myConnection.remoteObjectInterface = myCookieInterface;
;

Nota: Para comunicarse con servicios XPC fuera de su paquete de aplicaciones, también puede configurar una conexión XPC con el método initWithMachServiceName:.

En este punto, la aplicación principal puede llamar a los métodos remoteObjectProxy o remoteObjectProxyWithErrorHandler: en el objeto myConnection para obtener un objeto proxy.

Este objeto actúa como un proxy para el objeto que el servicio XPC ha establecido como su objeto exportado (estableciendo la propiedad exportedObject). Este objeto debe cumplir con el protocolo definido por la propiedad remoteObjectInterface.

Cuando su aplicación llama a un método en el objeto proxy, el método correspondiente es llamado en el objeto exportado dentro del servicio XPC. Cuando el método del servicio llama al bloque de respuesta, los valores de los parámetros son serializados y enviados de vuelta a la aplicación, donde los valores de los parámetros son deserializados y pasados al bloque de respuesta. (El bloque de respuesta se ejecuta dentro del espacio de direcciones de la aplicación.)

Nota: Si quieres permitir que el proceso helper llame a métodos de un objeto de tu aplicación, debes establecer las propiedades exportedInterface y exportedObject antes de llamar a resume. Estas propiedades se describen con más detalle en la siguiente sección.

Aceptación de una conexión en el helper

Cuando un helper basado en NSXPCConnection recibe el primer mensaje de una conexión, se llama al método listener:shouldAcceptNewConnection: del delegado del listener con un objeto listener y un objeto connection. Este método le permite decidir si acepta la conexión o no; debe devolver YES para aceptar la conexión o NO para rechazarla.

Nota: El helper recibe una solicitud de conexión cuando se envía el primer mensaje real. El método de reanudación del objeto de conexión no hace que se envíe un mensaje.

Además de tomar decisiones de política, este método debe configurar el objeto de conexión. En particular, asumiendo que el helper decide aceptar la conexión, debe establecer las siguientes propiedades en la conexión:

  • exportedInterface – un objeto de interfaz que describe el protocolo para el objeto que se desea exportar. (La creación de este objeto se describió anteriormente en Conexión y uso de una interfaz.)
  • exportedObject – el objeto local (normalmente en el helper) al que deben entregarse las llamadas al método del cliente remoto. Cada vez que el extremo opuesto de la conexión (normalmente en la aplicación) llama a un método en el objeto proxy de la conexión, se llama al método correspondiente en el objeto especificado por la propiedad exportedObject.

Después de establecer esas propiedades, debe llamar al método resume del objeto de conexión antes de devolver YES. Aunque el delegado puede aplazar la llamada a resume, la conexión no recibirá ningún mensaje hasta que lo haga.

Enviar mensajes

Enviar mensajes con NSXPC es tan sencillo como hacer una llamada a un método. Por ejemplo, dada la interfaz myCookieInterface (descrita en secciones anteriores) en el objeto de conexión XPC myConnection, se puede llamar al método feedMeACookie así:

Cookie *myCookie = ...
feedMeACookie: myCookie];

Cuando se llama a ese método, se llama automáticamente al método correspondiente en el XPC helper. Ese método, a su vez, podría utilizar el objeto de conexión del helper XPC de manera similar para llamar a un método en el objeto exportado por la aplicación principal.

Manejo de errores

Además de cualquier método de manejo de errores específico de una tarea determinada del helper, tanto el servicio XPC como la aplicación principal también deberían proporcionar los siguientes bloques de manejo de errores XPC:

  • Manejo de interrupciones – se llama cuando el proceso en el otro extremo de la conexión se ha estrellado o ha cerrado su conexión de otra manera. El objeto de conexión local suele seguir siendo válido – cualquier llamada futura generará automáticamente una nueva instancia del helper a menos que sea imposible hacerlo – pero puede ser necesario restablecer cualquier estado que el helper hubiera mantenido de otro modo.

El handler se invoca en la misma cola que los mensajes de respuesta y otros handlers, y siempre se ejecuta después de cualquier otro mensaje o handler de bloque de respuesta (excepto el handler de invalidación). Es seguro hacer nuevas peticiones en la conexión desde un manejador de interrupción.

  • Manejador de invalidación – llamado cuando el método de invalidación es llamado o cuando un ayudante XPC no pudo ser iniciado. Cuando se llama a este manejador, el objeto de conexión local ya no es válido y debe ser recreado. Este es siempre el último manejador llamado en un objeto de conexión. Cuando se llama a este bloque, el objeto de conexión ha sido destruido. No es posible enviar más mensajes sobre la conexión en ese momento, ya sea dentro del manejador o en cualquier otra parte de su código.

En ambos casos, debe utilizar variables de alcance de bloque para proporcionar suficiente información contextual – tal vez una cola de operaciones pendientes y el propio objeto de conexión – para que su código manejador pueda hacer algo sensato, como reintentar las operaciones pendientes, derribar la conexión, mostrar un diálogo de error, o cualquier otra acción que tenga sentido en su aplicación particular.