Articles

XPC-diensten op macOS-app

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

Follow

4 sep, 2020 – 9 min read

Vóór XPC pakten we vroeger Sockets en Mach Messages (Mach Ports).

Het XPC-mechanisme biedt een alternatief voor sockets (of Mach Services met MIG) voor IPC. We zouden bijvoorbeeld een proces kunnen hebben dat fungeert als een “server” die wacht op clients om toegang te krijgen tot zijn API en een of andere dienst te verlenen.

XPC Services op applicaties

Wanneer we het hebben over XPC Services (hoofdletter ‘S’), verwijzen we naar de bundel genaamd XPC Service. Bundels in het Apple ecosysteem verwijzen naar entiteiten die worden vertegenwoordigd door een specifieke mappenstructuur. De meest voorkomende bundel die u tegenkomt zijn Application Bundles. Als u met de rechtermuisknop klikt op een toepassing (bijvoorbeeld Chess.app) en u selecteert Inhoud weergeven, dan ziet u een mappenstructuur. Terug naar XPC, applicaties kunnen meerdere XPC Service bundels hebben. U vindt ze in de Contents/XPCServices/ directory in de applicatiebundel. U kunt zoeken in uw /Applications directory en zien hoeveel van de toepassingen vertrouwen op XPC Services.

U kunt ook XPC Services binnen Frameworks (die een ander type Bundle).

Aanvullende voordelen van XPC Services

Het gebruik van XPC Services in onze apps stelt ons in staat om sommige functionaliteit op te splitsen in afzonderlijke modules (de XPC Service). We kunnen een XPC Service maken die belast kan worden met het uitvoeren van dure maar weinig voorkomende taken. Bijvoorbeeld een crypto-taak om willekeurige getallen te genereren.

Een ander bijkomend voordeel is dat de XPC Service op zijn eigen proces draait. Als dat proces crasht of wordt gedood, heeft dat geen invloed op onze hoofdapplicatie. Stel dat uw applicatie door de gebruiker gedefinieerde plugins ondersteunt. En de plugins zijn gebouwd met behulp van XPC Services. Als ze slecht zijn gecodeerd en crashen, hebben ze geen invloed op de integriteit van uw hoofdapplicatie.

Een bijkomend voordeel van de XPC Service is dat ze hun eigen rechten kunnen hebben. De applicatie heeft het recht alleen nodig als het gebruik maakt van een service die wordt geleverd door XPC Service die het recht vereist. Stel u voor dat u een app heeft die locatie gebruikt, maar alleen voor specifieke functies. U zou die functies kunnen verplaatsen naar een XPC Service en de locatie entitlement alleen aan die XPC Service kunnen toevoegen. Als uw gebruiker de functie die gebruik maakt van de locatie nooit nodig heeft, zal hij niet om rechten worden gevraagd, waardoor het gebruik van uw app betrouwbaarder wordt.

XPC en onze vriend launchd

launchd is het eerste proces dat op ons systeem draait. Het is verantwoordelijk voor het starten en beheren van andere processen, services en daemons. launchd is ook verantwoordelijk voor het plannen van taken. Het is dus logisch dat launchd ook verantwoordelijk is voor het beheer van XPC Services.

XPC Services kunnen worden gestopt als ze lange tijd inactief zijn geweest, of op verzoek worden gespawned. Al het beheer wordt gedaan door launchd, en we hoeven niets te doen om het te laten werken.

launchd heeft informatie over systeembrede beschikbaarheid van bronnen en geheugendruk, wie kan het beste beslissingen nemen over hoe we de bronnen van ons systeem het meest effectief kunnen gebruiken dan launchd

Implementeer XPC Services

Een XPC service is een bundel in de Contents/XPCServices directory van de hoofd applicatie bundel; de XPC service bundel bevat een Info.plist bestand, een executable, en alle bronnen die de service nodig heeft. De XPC-service geeft aan welke functie moet worden aangeroepen wanneer de service berichten ontvangt door xpc_main(3) Mac OS X Developer Tools Manual Page aan te roepen vanuit zijn hoofdfunctie.

Om een XPC-service in Xcode te maken, doet u het volgende:

  1. Een nieuw doel aan uw project toevoegen, met behulp van het XPC Service-sjabloon.
  2. Voeg een Copy Files-fase toe aan de build-instellingen van uw toepassing, die de XPC-service kopieert naar de Contents/XPCServices-directory van de hoofdapplicatiebundel.
  3. Voeg een dependency toe aan de build-instellingen van uw toepassing, om aan te geven dat deze afhankelijk is van de XPC-servicebundel.
  4. Als u een low-level (op C gebaseerde) XPC-service schrijft, implementeert u een minimale main-functie om uw event-handler te registreren, zoals in de volgende code-vermelding wordt getoond. Vervang my_event_handler door de naam van uw event handler-functie.
int main(int argc, const char *argv) {
xpc_main(my_event_handler);
// The xpc_main() function never returns.
exit(EXIT_FAILURE);
}

Als u een service op hoog niveau (gebaseerd op Objective-C) schrijft met NSXPCConnection, maakt u eerst een verbindingsdelegate-klasse die voldoet aan het NSXPCListenerDelegate-protocol. Implementeer vervolgens een minimale hoofdfunctie die een luisterobject maakt en configureert, zoals in de volgende code listing.

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

De service gebruiken

De manier waarop u een XPC service gebruikt, hangt af van of u werkt met de C API (XPC Services) of de Objective-C API (NSXPCConnection).

De Objective-C NSXPCConnection API gebruiken De Objective-C NSXPCConnection API biedt een interface op hoog niveau voor het op afstand aanroepen van procedures, waarmee u methoden op objecten in het ene proces vanuit een ander proces kunt aanroepen (meestal een toepassing die een methode in een XPC-service aanroept). De NSXPCConnection API serialiseert automatisch gegevensstructuren en objecten voor overdracht en deserialiseert ze aan de andere kant. Als gevolg hiervan gedraagt het aanroepen van een methode op een object op afstand zich als het aanroepen van een methode op een lokaal object.

Om de NSXPCConnection API te gebruiken, moet u het volgende maken:

  • Een interface. Deze bestaat voornamelijk uit een protocol dat beschrijft welke methoden vanuit het remote proces moeten kunnen worden aangeroepen. Dit is beschreven in Ontwerpen van een interface
  • Een verbindingsobject aan beide kanten. Aan de service kant, is dit eerder beschreven in De service maken. Aan de kant van de client is dit beschreven in Verbinding maken met en gebruik maken van een Interface.
  • Een luisteraar. Deze code in de XPC service accepteert verbindingen. Dit wordt beschreven in Een verbinding accepteren in de Helper. Berichten.

Algemene architectuur

Algemene architectuur

Wanneer u werkt met helper-apps die zijn gebaseerd op NSXPCConnection, hebben zowel de hoofdtoepassing als de helper een instantie van NSXPCConnection. De hoofdtoepassing maakt zelf een verbindingsobject aan, waardoor de helper wordt gestart. Een delegatiemethode in de helper krijgt zijn verbindingsobject doorgegeven wanneer de verbinding tot stand wordt gebracht. Dit wordt geïllustreerd in Figuur 4-1.

Elk NSXPCConnection-object biedt drie belangrijke kenmerken:

  • Een geëxporteerdeInterface eigenschap die de methoden beschrijft die beschikbaar moeten worden gesteld aan de andere kant van de verbinding.
  • Een geëxporteerdObject eigenschap die een lokaal object bevat om methodeaanroepen af te handelen die van de andere kant van de verbinding binnenkomen.
  • De mogelijkheid om een proxy-object te verkrijgen voor het aanroepen van methoden aan de andere kant van de verbinding.

Wanneer de hoofdtoepassing een methode op een proxy-object aanroept, roept het NSXPCConnection-object van de XPC-service die methode aan op het object dat in zijn eigenschap exportedObject is opgeslagen.

Ook als de XPC-service een proxy-object verkrijgt en een methode op dat object aanroept, roept het NSXPCConnection-object van de hoofdtoepassing die methode aan op het object dat is opgeslagen in de eigenschap exportedObject

Ontwerpen van een interface

De NSXPCConnection API maakt gebruik van Objective-C-protocollen om de programmatische interface tussen de aanroepende toepassing en de service te definiëren. Elke instance-methode die u wilt aanroepen vanaf de andere kant van een verbinding moet expliciet worden gedefinieerd in een formeel protocol. Bijvoorbeeld:

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

Omdat communicatie over XPC asynchroon is, moeten alle methoden in het protocol een terugkeertype van void hebben. Als u gegevens moet retourneren, kunt u een antwoordblok als volgt definiëren:

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

Een methode kan slechts één antwoordblok hebben. Omdat verbindingen bidirectioneel zijn, kan de XPC service helper echter ook antwoorden door methoden aan te roepen in de interface die door de hoofdtoepassing wordt verschaft, indien gewenst.

Elke methode moet een terugkeertype van void hebben, en alle parameters voor methoden of antwoordblokken moeten zijn:

  • Arithmetische typen (int, char, float, double, uint64_t, NSUInteger, enzovoort)
  • BOOL
  • C strings
  • C structuren en arrays die alleen de hierboven genoemde typen bevatten
  • Objective-C objecten die het NSSecureCoding protocol implementeren.

Belangrijk: Als een methode (of het antwoordblok) parameters heeft die Objective-C verzamelingsklassen zijn (NSDictionary, NSArray, enzovoort), en als u uw eigen aangepaste objecten binnen een verzameling moet doorgeven, moet u XPC expliciet vertellen om die klasse als lid van die verzamelingsparameter toe te staan.

Verbinden met en gebruiken van een interface

Nadat u het protocol hebt gedefinieerd, moet u een interface-object maken dat het beschrijft. Roep hiertoe de interfaceWithProtocol: methode op de NSXPCInterface klasse aan. Bijvoorbeeld:

NSXPCInterface *myCookieInterface =
;

Nadat u het interface-object hebt gemaakt, moet u binnen de hoofd-app een verbinding ermee configureren door de methode initWithServiceName: aan te roepen. Bijvoorbeeld:

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

Note: Voor communicatie met XPC services buiten je app bundle, kun je ook een XPC verbinding configureren met de initWithMachServiceName: methode.

Op dit punt kan de hoofdapplicatie de methoden remoteObjectProxy of remoteObjectProxyWithErrorHandler: aanroepen op het object myConnection om een proxy-object te verkrijgen.

Dit object fungeert als een proxy voor het object dat de XPC-service heeft ingesteld als zijn geëxporteerde object (door de eigenschap exportedObject in te stellen). Dit object moet voldoen aan het protocol dat is gedefinieerd door de eigenschap remoteObjectInterface.

Wanneer uw applicatie een methode op het proxy-object aanroept, wordt de corresponderende methode op het geëxporteerde object binnen de XPC-service aangeroepen. Wanneer de methode van de service het antwoordblok aanroept, worden de parameterwaarden geserialiseerd en teruggestuurd naar de toepassing, waar de parameterwaarden worden gedeserialiseerd en doorgegeven aan het antwoordblok. (Het antwoordblok wordt uitgevoerd binnen de adresruimte van de toepassing.)

Note: Als u wilt toestaan dat het helperproces methoden aanroept op een object in uw toepassing, moet u de eigenschappen exportedInterface en exportedObject instellen voordat u resume aanroept. Deze eigenschappen worden verder beschreven in de volgende sectie.

Een verbinding accepteren in de helper

Wanneer een op NSXPCConnection gebaseerde helper het eerste bericht van een verbinding ontvangt, wordt de methode listener delegate’s listener:shouldAcceptNewConnection: aangeroepen met een listener-object en een verbindingsobject. Deze methode laat u beslissen of u de verbinding accepteert of niet; het moet YES teruggeven om de verbinding te accepteren of NO om de verbinding te weigeren.

Note: De helper ontvangt een verbindingsverzoek wanneer het eerste eigenlijke bericht wordt verzonden. De resume-methode van het verbindingsobject veroorzaakt niet dat een bericht wordt verzonden.

Naast het nemen van beleidsbeslissingen, moet deze methode het verbindingsobject configureren. In het bijzonder, ervan uitgaande dat de helper besluit om de verbinding te accepteren, moet het de volgende eigenschappen op de verbinding instellen:

  • exportedInterface – een interface-object dat het protocol beschrijft voor het object dat u wilt exporteren. (Het maken van dit object is eerder beschreven in Verbinding maken met en gebruik maken van een interface.)
  • exportedObject – het lokale object (gewoonlijk in de helper) waaraan de methode-aanroepen van de client op afstand moeten worden afgeleverd. Telkens wanneer de andere kant van de verbinding (gewoonlijk in de toepassing) een methode op het proxy-object van de verbinding aanroept, wordt de overeenkomstige methode aangeroepen op het object dat door de eigenschap exportedObject wordt gespecificeerd.

Na het instellen van deze eigenschappen, moet het de resume-methode van het verbindingsobject aanroepen alvorens YES terug te geven. Hoewel de delegate het oproepen van resume kan uitstellen, zal de verbinding geen berichten ontvangen totdat hij dat doet.

Berichten

Het verzenden van berichten met NSXPC is net zo eenvoudig als het maken van een methode-aanroep. Bijvoorbeeld, gegeven de interface myCookieInterface (beschreven in vorige secties) op het XPC-verbindingsobject myConnection, kunt u de methode feedMeACookie als volgt aanroepen:

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

Wanneer u die methode aanroept, wordt de overeenkomstige methode in de XPC-helper automatisch aangeroepen. Die methode kan op haar beurt het verbindingsobject van de XPC-helper op dezelfde manier gebruiken om een methode op het door de hoofdapplicatie geëxporteerde object aan te roepen.

Handling Errors

Naast eventuele methoden voor foutafhandeling die specifiek zijn voor een bepaalde taak van een helper, moeten zowel de XPC-service als de hoofdapplicatie ook de volgende XPC-foutafhandelingsblokken verschaffen:

  • Interruptiehandler – wordt aangeroepen wanneer het proces aan de andere kant van de verbinding is gecrasht of anderszins de verbinding heeft gesloten. Het lokale verbindingsobject is doorgaans nog steeds geldig – bij elke toekomstige oproep wordt automatisch een nieuwe helperinstantie gespawnd, tenzij dit onmogelijk is – maar het is mogelijk dat u de status die de helper anders zou hebben behouden, opnieuw moet instellen.

De handler wordt aangeroepen op dezelfde wachtrij als antwoordberichten en andere handlers, en wordt altijd uitgevoerd na alle andere berichten of handlers voor antwoordblokken (behalve de handler voor ongeldig maken). Het is veilig om nieuwe verzoeken te doen op de verbinding vanuit een onderbrekingshandler.

  • Invalidatiehandler – wordt aangeroepen wanneer de invalidate-methode wordt aangeroepen of wanneer een XPC-helper niet kon worden gestart. Wanneer deze handler wordt aangeroepen, is het lokale verbindingsobject niet langer geldig en moet het opnieuw worden aangemaakt. Dit is altijd de laatste handler die wordt aangeroepen voor een verbindingsobject. Wanneer dit blok wordt aangeroepen, is het verbindingsobject afgebroken. Op dat moment is het niet mogelijk om verdere berichten over de verbinding te verzenden, noch binnen de handler, noch elders in uw code.

In beide gevallen moet u block-scoped variabelen gebruiken om voldoende contextuele informatie te verschaffen – misschien een wachtrij voor in behandeling zijnde bewerkingen en het verbindingsobject zelf – zodat uw handler-code iets zinnigs kan doen, zoals in behandeling zijnde bewerkingen opnieuw proberen, de verbinding afbreken, een foutendialoogvenster weergeven of welke andere actie dan ook die zinvol is in uw specifieke toepassing.