Articles

Services XPC sur l’app macOS

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

Follow

4 sept, 2020 – 9 min de lecture

Avant XPC, nous ramassions des Sockets et des messages Mach (Mach Ports).

Le mécanisme XPC offre une alternative aux sockets (ou aux services Mach utilisant MIG) pour l’IPC. Nous pourrions avoir, par exemple, un processus qui agit comme un « serveur » attendant que les clients accèdent à son API et fournissent un certain service.

Services XPC sur les applications

Lorsque nous parlons de services XPC (S majuscule), nous faisons référence au bundle appelé service XPC. Les bundles dans l’écosystème Apple font référence à des entités représentées par une structure de répertoire spécifique. Les bundles les plus courants que vous rencontrez sont les bundles d’applications. Si vous cliquez avec le bouton droit de la souris sur une application (par exemple Chess.app) et que vous sélectionnez Afficher le contenu, vous trouverez une structure de répertoire. Pour en revenir à XPC, les applications peuvent avoir plusieurs bundles de services XPC. Vous les trouverez dans le répertoire Contents/XPCServices/ à l’intérieur du bundle de l’application. Vous pouvez effectuer une recherche dans votre répertoire /Applications et voir combien d’applications s’appuient sur les services XPC.

Vous pouvez également avoir des services XPC à l’intérieur des Frameworks (qui sont un autre type de Bundle).

Avantages supplémentaires des services XPC

L’utilisation des services XPC dans nos apps nous permet de casser certaines fonctionnalités dans des modules séparés (Le service XPC). Nous pourrions créer un service XPC qui peut être chargé d’exécuter certaines tâches coûteuses mais peu fréquentes. Par exemple, une certaine tâche cryptographique pour générer des nombres aléatoires.

Un autre avantage supplémentaire est que le service XPC s’exécute sur son propre processus. Si ce processus se plante ou s’il est tué, cela n’affecte pas notre application principale. Imaginez que votre application supporte des plugins définis par l’utilisateur. Et les plugins sont construits à l’aide de services XPC. S’ils sont mal codés et se plantent, ils n’affecteront pas l’intégrité de votre application principale.

Un avantage supplémentaire du service XPC est qu’ils peuvent avoir leurs propres droits. L’application n’aura besoin de l’habilitation que lorsqu’elle fera usage d’un service fourni par le service XPC qui nécessite l’habilitation. Imaginez que vous ayez une application qui utilise la localisation, mais uniquement pour des fonctions spécifiques. Vous pouvez déplacer ces fonctions vers un service XPC et ajouter le droit de localisation uniquement à ce service XPC. Si votre utilisateur n’a jamais besoin de la fonctionnalité qui utilise la localisation, il ne sera pas invité à demander des autorisations, ce qui rendra l’utilisation de votre app plus digne de confiance.

XPC et notre ami launchd

launchd est le premier processus à s’exécuter sur notre système. Il est chargé de lancer et de gérer les autres processus, services et démons. launchd est également chargé de planifier les tâches. Il est donc logique que launchd soit également responsable de la gestion des services XPC.

Le service XPC peut être arrêté s’il est resté inactif pendant une longue période, ou être spawné à la demande. Toute la gestion est faite par launchd, et nous n’avons pas besoin de faire quoi que ce soit pour que cela fonctionne.

launchd dispose d’informations sur la disponibilité des ressources à l’échelle du système et sur la pression mémoire, qui de mieux que launchd pour prendre des décisions sur la façon d’utiliser le plus efficacement les ressources de notre système

Implémenter les services XPC

Un service XPC est un bundle dans le répertoire Contents/XPCServices du bundle d’application principal ; le bundle de service XPC contient un fichier Info.plist, un exécutable et toutes les ressources nécessaires au service. Le service XPC indique la fonction à appeler lorsque le service reçoit des messages en appelant xpc_main(3) Manuel des outils de développement Mac OS X Page depuis sa fonction principale.

Pour créer un service XPC dans Xcode, procédez comme suit :

  1. Ajouter une nouvelle cible à votre projet, en utilisant le modèle de service XPC.
  2. Ajouter une phase de copie des fichiers aux paramètres de construction de votre application, qui copie le service XPC dans le répertoire Contents/XPCServices du bundle d’application principal.
  3. Ajouter une dépendance aux paramètres de construction de votre application, pour indiquer qu’elle dépend du bundle de service XPC.
  4. Si vous écrivez un service XPC de bas niveau (basé sur le langage C), mettez en œuvre une fonction principale minimale pour enregistrer votre gestionnaire d’événements, comme indiqué dans le listing de code suivant. Remplacez my_event_handler par le nom de votre fonction de gestion d’événements.
int main(int argc, const char *argv) {
xpc_main(my_event_handler);
// The xpc_main() function never returns.
exit(EXIT_FAILURE);
}

Si vous écrivez un service de haut niveau (basé sur Objective-C) utilisant NSXPCConnection, créez d’abord une classe de délégué de connexion conforme au protocole NSXPCListenerDelegate. Ensuite, implémentez une fonction principale minimale qui crée et configure un objet écouteur, comme indiqué dans le listing de code suivant.

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

Utilisation du service

La façon dont vous utilisez un service XPC dépend de la façon dont vous travaillez avec l’API C (services XPC) ou l’API Objective-C (NSXPCConnection).

Utilisation de l’API Objective-C NSXPCConnection L’API Objective-C NSXPCConnection fournit une interface d’appel de procédure à distance de haut niveau qui vous permet d’appeler des méthodes sur des objets dans un processus depuis un autre processus (généralement une application appelant une méthode dans un service XPC). L’API NSXPCConnection sérialise automatiquement les structures de données et les objets pour la transmission et les désérialise à l’autre bout. Par conséquent, l’appel d’une méthode sur un objet distant se comporte comme l’appel d’une méthode sur un objet local.

Pour utiliser l’API NSXPCConnection, vous devez créer les éléments suivants :

  • Une interface. Celle-ci consiste principalement en un protocole qui décrit les méthodes qui doivent être appelables depuis le processus distant. Ceci est décrit dans Conception d’une interface
  • Un objet de connexion des deux côtés. Du côté du service, cela a été décrit précédemment dans Création du service. Du côté du client, cela est décrit dans Connexion à une interface et utilisation de celle-ci.
  • Un écouteur. Ce code dans le service XPC accepte les connexions. Ceci est décrit dans Accepter une connexion dans l’aide. Messages.

Architecture globale

Architecture globale

Lorsque l’on travaille avec des applications d’aide basées sur la NSXPCConnection, l’application principale et l’aide ont toutes deux une instance de la NSXPCConnection. L’application principale crée elle-même son objet de connexion, ce qui entraîne le lancement de l’application auxiliaire. Une méthode déléguée de l’application d’assistance reçoit son objet de connexion lorsque la connexion est établie. Ceci est illustré dans la figure 4-1.

Chaque objet NSXPCConnection fournit trois caractéristiques clés :

  • Une propriété exportéeInterface qui décrit les méthodes qui doivent être mises à la disposition de l’autre côté de la connexion.
  • Une propriété exportéeObject qui contient un objet local pour gérer les appels de méthode provenant de l’autre côté de la connexion.
  • La possibilité d’obtenir un objet proxy pour appeler les méthodes de l’autre côté de la connexion.

Lorsque l’application principale appelle une méthode sur un objet proxy, l’objet NSXPCConnection du service XPC appelle cette méthode sur l’objet stocké dans sa propriété exportedObject.

De même, si le service XPC obtient un objet proxy et appelle une méthode sur cet objet, l’objet NSXPCConnection de l’application principale appelle cette méthode sur l’objet stocké dans sa propriété exportedObject

Conception d’une interface

L’API NSXPCConnection tire parti des protocoles Objective-C pour définir l’interface programmatique entre l’application appelante et le service. Toute méthode d’instance que vous souhaitez appeler depuis le côté opposé d’une connexion doit être explicitement définie dans un protocole formel. Par exemple :

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

Parce que la communication sur XPC est asynchrone, toutes les méthodes du protocole doivent avoir un type de retour de void. Si vous devez retourner des données, vous pouvez définir un bloc de réponse comme ceci:

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

Une méthode ne peut avoir qu’un seul bloc de réponse. Cependant, comme les connexions sont bidirectionnelles, l’aide de service XPC peut également répondre en appelant des méthodes dans l’interface fournie par l’application principale, si vous le souhaitez.

Chaque méthode doit avoir un type de retour de void, et tous les paramètres des méthodes ou des blocs de réponse doivent être soit :

  • Types arithmétiques (int, char, float, double, uint64_t, NSUInteger, et ainsi de suite)
  • BOOL
  • C chaînes de caractères
  • C structures et tableaux contenant uniquement les types énumérés ci-dessus
  • Objets C qui mettent en œuvre le protocole NSSecureCoding.

Important : si une méthode (ou son bloc de réponse) a des paramètres qui sont des classes de collection Objective-C (NSDictionary, NSArray, et ainsi de suite), et si vous devez passer vos propres objets personnalisés dans une collection, vous devez indiquer explicitement à XPC d’autoriser cette classe comme membre de ce paramètre de collection.

Connexion à et utilisation d’une interface

Une fois que vous avez défini le protocole, vous devez créer un objet d’interface qui le décrit. Pour ce faire, appelez la méthode interfaceWithProtocol : sur la classe NSXPCInterface. Par exemple:

NSXPCInterface *myCookieInterface =
;

Une fois que vous avez créé l’objet interface, dans l’application principale, vous devez configurer une connexion avec celui-ci en appelant la méthode initWithServiceName :. Par exemple :

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

Note : Pour communiquer avec les services XPC en dehors de votre bundle d’app, vous pouvez également configurer une connexion XPC avec la méthode initWithMachServiceName :.

À ce stade, l’application principale peut appeler les méthodes remoteObjectProxy ou remoteObjectProxyWithErrorHandler : sur l’objet myConnection pour obtenir un objet proxy.

Cet objet agit comme un proxy pour l’objet que le service XPC a défini comme son objet exporté (en définissant la propriété exportedObject). Cet objet doit se conformer au protocole défini par la propriété remoteObjectInterface.

Lorsque votre application appelle une méthode sur l’objet proxy, la méthode correspondante est appelée sur l’objet exporté à l’intérieur du service XPC. Lorsque la méthode du service appelle le bloc de réponse, les valeurs des paramètres sont sérialisées et renvoyées à l’application, où les valeurs des paramètres sont désérialisées et transmises au bloc de réponse. (Le bloc de réponse s’exécute dans l’espace d’adressage de l’application.)

Remarque : si vous voulez permettre au processus d’aide d’appeler des méthodes sur un objet de votre application, vous devez définir les propriétés exportedInterface et exportedObject avant d’appeler resume. Ces propriétés sont décrites plus en détail dans la section suivante.

Acceptation d’une connexion dans l’assistant

Lorsqu’un assistant basé sur NSXPCConnection reçoit le premier message d’une connexion, la méthode listener:shouldAcceptNewConnection: du délégué de l’auditeur est appelée avec un objet d’auditeur et un objet de connexion. Cette méthode vous permet de décider d’accepter ou non la connexion ; elle doit renvoyer YES pour accepter la connexion ou NO pour refuser la connexion.

Note : L’assistant reçoit une demande de connexion lorsque le premier message réel est envoyé. La méthode de reprise de l’objet de connexion ne provoque pas l’envoi d’un message.

En plus de prendre des décisions de politique, cette méthode doit configurer l’objet de connexion. En particulier, en supposant que l’assistant décide d’accepter la connexion, il doit définir les propriétés suivantes sur la connexion :

  • exportedInterface – un objet d’interface qui décrit le protocole de l’objet que vous voulez exporter. (La création de cet objet a été décrite précédemment dans Connexion et utilisation d’une interface.)
  • exportedObject – l’objet local (généralement dans l’assistant) auquel les appels de méthode du client distant doivent être livrés. Chaque fois que l’extrémité opposée de la connexion (généralement dans l’application) appelle une méthode sur l’objet proxy de la connexion, la méthode correspondante est appelée sur l’objet spécifié par la propriété exportedObject.

Après avoir défini ces propriétés, il doit appeler la méthode resume de l’objet de connexion avant de retourner YES. Bien que le délégué puisse différer l’appel de resume, la connexion ne recevra aucun message jusqu’à ce qu’il le fasse.

Envoi de messages

Envoyer des messages avec NSXPC est aussi simple que de faire un appel de méthode. Par exemple, étant donné l’interface myCookieInterface (décrite dans les sections précédentes) sur l’objet de connexion XPC myConnection, vous pouvez appeler la méthode feedMeACookie comme suit :

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

Lorsque vous appelez cette méthode, la méthode correspondante dans l’aide XPC est appelée automatiquement. Cette méthode, à son tour, pourrait utiliser l’objet de connexion de l’aide XPC de manière similaire pour appeler une méthode sur l’objet exporté par l’application principale.

Gestion des erreurs

En plus de toute méthode de gestion des erreurs spécifique à la tâche d’une aide donnée, le service XPC et l’application principale devraient également fournir les blocs de gestion des erreurs XPC suivants :

  • Gestion des interruptions – appelé lorsque le processus à l’autre bout de la connexion s’est écrasé ou a autrement fermé sa connexion. L’objet de connexion local est typiquement toujours valide – tout appel futur spawna automatiquement une nouvelle instance d’aide à moins qu’il soit impossible de le faire – mais vous pouvez avoir besoin de réinitialiser tout état que l’aide aurait autrement conservé.

Le gestionnaire est invoqué sur la même file d’attente que les messages de réponse et les autres gestionnaires, et il est toujours exécuté après tout autre message ou gestionnaire de bloc de réponse (à l’exception du gestionnaire d’invalidation). Il est sûr de faire de nouvelles demandes sur la connexion à partir d’un gestionnaire d’interruption.

  • Manipulateur d’invalidation – appelé lorsque la méthode d’invalidation est appelée ou lorsqu’un assistant XPC n’a pas pu être lancé. Lorsque ce gestionnaire est appelé, l’objet de connexion local n’est plus valide et doit être recréé. C’est toujours le dernier gestionnaire appelé sur un objet de connexion. Lorsque ce bloc est appelé, l’objet de connexion a été détruit. Il n’est pas possible d’envoyer d’autres messages sur la connexion à ce moment-là, que ce soit à l’intérieur du gestionnaire ou ailleurs dans votre code.

Dans les deux cas, vous devriez utiliser des variables à portée de bloc pour fournir suffisamment d’informations contextuelles – peut-être une file d’attente d’opérations en attente et l’objet de connexion lui-même – afin que votre code de gestionnaire puisse faire quelque chose de sensé, comme réessayer les opérations en attente, déchirer la connexion, afficher un dialogue d’erreur, ou toute autre action qui a du sens dans votre application particulière.