Articles

Servizi XPC su app macOS

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

Follow

4 settembre, 2020 – 9 min read

Prima di XPC prendevamo Sockets e Mach Messages (Mach Ports).

Il meccanismo XPC offre un’alternativa ai socket (o ai servizi Mach usando MIG) per l’IPC. Potremmo avere, per esempio, un processo che agisce come “server” in attesa che i client accedano alle sue API e forniscano qualche servizio.

Servizi XPC sulle applicazioni

Quando si parla di Servizi XPC (S maiuscola), ci si riferisce al bundle chiamato Servizio XPC. I bundle nell’ecosistema Apple si riferiscono a entità rappresentate da una specifica struttura di directory. I bundle più comuni che si incontrano sono i bundle delle applicazioni. Se clicchi con il tasto destro del mouse su qualsiasi applicazione (per esempio Chess.app) e selezioni Show content, quello che troverai è una struttura di directory. Tornando a XPC, le applicazioni possono avere più bundle di servizi XPC. Li troverai all’interno della directory Contents/XPCServices/ all’interno del bundle dell’applicazione. Puoi cercare nella tua directory /Applications e vedere quante applicazioni si affidano ai servizi XPC.

Puoi anche avere servizi XPC all’interno di Frameworks (che sono un altro tipo di Bundle).

Benefici aggiuntivi dei servizi XPC

L’utilizzo dei servizi XPC nelle nostre applicazioni ci permette di spezzare alcune funzionalità in moduli separati (il servizio XPC). Potremmo creare un servizio XPC che può essere incaricato di eseguire alcuni compiti costosi ma poco frequenti. Per esempio, qualche compito di crittografia per generare numeri casuali.

Un altro vantaggio aggiuntivo è che il servizio XPC viene eseguito su un proprio processo. Se questo processo si blocca o viene ucciso, non influisce sulla nostra applicazione principale. Immaginate che la vostra applicazione supporti plugin definiti dall’utente. E i plugin sono costruiti usando i servizi XPC. Se sono codificati male e vanno in crash, non influiranno sull’integrità della vostra applicazione principale.

Un ulteriore vantaggio del servizio XPC è che possono avere i propri diritti. L’applicazione richiederà il diritto solo quando farà uso di un servizio fornito da XPC Service che richiede il diritto. Immaginate di avere un’applicazione che usa la localizzazione ma solo per funzioni specifiche. Potreste spostare queste funzioni in un servizio XPC e aggiungere il diritto di localizzazione solo a quel servizio XPC. Se il vostro utente non ha mai bisogno della funzione che usa la localizzazione, non gli verranno richieste le autorizzazioni, rendendo l’uso della vostra app più affidabile.

XPC e il nostro amico launchd

launchd è il primo processo a girare sul nostro sistema. Si occupa di lanciare e gestire altri processi, servizi e demoni. launchd si occupa anche di programmare i compiti. Quindi ha senso che launchd sia anche responsabile della gestione dei servizi XPC.

Il servizio XPC può essere fermato se è stato inattivo per molto tempo, o essere generato su richiesta. Tutta la gestione è fatta da launchd, e non abbiamo bisogno di fare nulla perché funzioni.

launchd ha informazioni sulla disponibilità di risorse a livello di sistema e sulla pressione della memoria, chi meglio di launchd può prendere decisioni su come utilizzare al meglio le risorse del nostro sistema

Implementare i servizi XPC

Un servizio XPC è un bundle nella directory Contents/XPCServices del bundle dell’applicazione principale; il bundle del servizio XPC contiene un file Info.plist, un eseguibile e qualsiasi risorsa necessaria al servizio. Il servizio XPC indica quale funzione chiamare quando il servizio riceve messaggi chiamando xpc_main(3) Mac OS X Developer Tools Manual Page dalla sua funzione principale.

Per creare un servizio XPC in Xcode, fai come segue:

  1. Aggiungi un nuovo obiettivo al tuo progetto, usando il modello XPC Service.
  2. Aggiungi una fase Copy Files alle impostazioni di compilazione della tua applicazione, che copia il servizio XPC nella directory Contents/XPCServices del bundle principale dell’applicazione.
  3. Aggiungi una dipendenza alle impostazioni di compilazione della tua applicazione, per indicare che dipende dal bundle del servizio XPC.
  4. Se stai scrivendo un servizio XPC di basso livello (basato su C), implementa una funzione principale minima per registrare il tuo gestore di eventi, come mostrato nel seguente codice. Sostituite my_event_handler con il nome della vostra funzione gestore di eventi.
int main(int argc, const char *argv) {
xpc_main(my_event_handler);
// The xpc_main() function never returns.
exit(EXIT_FAILURE);
}

Se state scrivendo un servizio di alto livello (basato su Objective-C) usando NSXPCConnection, create prima una classe delegata di connessione conforme al protocollo NSXPCListenerDelegate. Poi, implementa una funzione principale minima che crea e configura un oggetto ascoltatore, come mostrato nel seguente elenco di codice.

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

Usare il servizio

Il modo in cui usi un servizio XPC dipende dal fatto che tu stia lavorando con l’API C (XPC Services) o l’API Objective-C (NSXPCConnection).

Utilizzare l’API Objective-C NSXPCConnection L’API Objective-C NSXPCConnection fornisce un’interfaccia di chiamata di procedura remota di alto livello che permette di chiamare metodi su oggetti in un processo da un altro processo (di solito un’applicazione che chiama un metodo in un servizio XPC). L’API NSXPCConnection serializza automaticamente le strutture dati e gli oggetti per la trasmissione e li deserializza all’altro capo. Di conseguenza, chiamare un metodo su un oggetto remoto si comporta come chiamare un metodo su un oggetto locale.

Per utilizzare l’API NSXPCConnection, è necessario creare quanto segue:

  • Un’interfaccia. Questa consiste principalmente in un protocollo che descrive quali metodi dovrebbero essere richiamabili dal processo remoto. Questo è descritto in Progettare un’interfaccia
  • Un oggetto di connessione su entrambi i lati. Dal lato del servizio, questo è stato descritto in precedenza in Creare il servizio. Sul lato client, questo è descritto in Connecting to and Using an Interface.
  • Un ascoltatore. Questo codice nel servizio XPC accetta le connessioni. Questo è descritto in Accettare una connessione nell’Helper. Messages.

Architettura generale

Architettura generale

Quando si lavora con applicazioni helper basate su NSXPCConnection, sia l’applicazione principale che l’helper hanno un’istanza di NSXPCConnection. L’applicazione principale crea da sola il suo oggetto di connessione, che provoca il lancio dell’helper. Un metodo delegato nell’helper riceve il suo oggetto di connessione quando la connessione viene stabilita. Questo è illustrato nella Figura 4-1.

Ogni oggetto NSXPCConnection fornisce tre caratteristiche chiave:

  • Una proprietà exportedInterface che descrive i metodi che dovrebbero essere resi disponibili al lato opposto della connessione.
  • Una proprietà exportedObject che contiene un oggetto locale per gestire le chiamate ai metodi provenienti dall’altro lato della connessione.
  • La capacità di ottenere un oggetto proxy per chiamare metodi dall’altro lato della connessione.

Quando l’applicazione principale chiama un metodo su un oggetto proxy, l’oggetto NSXPCConnection del servizio XPC chiama quel metodo sull’oggetto memorizzato nella sua proprietà exportedObject.

Similmente, se il servizio XPC ottiene un oggetto proxy e chiama un metodo su quell’oggetto, l’oggetto NSXPCConnection dell’applicazione principale chiama quel metodo sull’oggetto memorizzato nella sua proprietà exportedObject

Progettazione di un’interfaccia

L’API NSXPCConnection sfrutta i protocolli Objective-C per definire l’interfaccia programmatica tra l’applicazione chiamante e il servizio. Qualsiasi metodo di istanza che si vuole chiamare dal lato opposto di una connessione deve essere esplicitamente definito in un protocollo formale. Per esempio:

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

Perché la comunicazione su XPC è asincrona, tutti i metodi nel protocollo devono avere un tipo di ritorno nullo. Se avete bisogno di restituire dati, potete definire un blocco di risposta come questo:

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

Un metodo può avere solo un blocco di risposta. Tuttavia, poiché le connessioni sono bidirezionali, l’helper del servizio XPC può anche rispondere chiamando metodi nell’interfaccia fornita dall’applicazione principale, se lo desiderate.

Ogni metodo deve avere un tipo di ritorno di void, e tutti i parametri ai metodi o ai blocchi di risposta devono essere o:

  • Tipi aritmetici (int, char, float, double, uint64_t, NSUInteger, e così via)
  • BOOL
  • C stringhe
  • C strutture e array contenenti solo i tipi elencati sopra
  • Oggetti Objective-C che implementano il protocollo NSSecureCoding.

Importante: se un metodo (o il suo blocco di risposta) ha parametri che sono classi di raccolta Objective-C (NSDictionary, NSArray, e così via), e se avete bisogno di passare i vostri oggetti personalizzati all’interno di una raccolta, dovete dire esplicitamente a XPC di permettere quella classe come membro del parametro della raccolta.

Collegamento e uso di un’interfaccia

Una volta definito il protocollo, dovete creare un oggetto interfaccia che lo descriva. Per farlo, chiamate il metodo interfaceWithProtocol: sulla classe NSXPCInterface. Per esempio:

NSXPCInterface *myCookieInterface =
;

Una volta creato l’oggetto interfaccia, all’interno dell’applicazione principale, è necessario configurare una connessione con esso chiamando il metodo initWithServiceName:. Per esempio:

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

Nota: Per comunicare con i servizi XPC fuori dal tuo bundle di app, puoi anche configurare una connessione XPC con il metodo initWithMachServiceName:.

A questo punto, l’applicazione principale può chiamare i metodi remoteObjectProxy o remoteObjectProxyWithErrorHandler: sull’oggetto myConnection per ottenere un oggetto proxy.

Questo oggetto funge da proxy per l’oggetto che il servizio XPC ha impostato come suo oggetto esportato (impostando la proprietà exportedObject). Questo oggetto deve essere conforme al protocollo definito dalla proprietà remoteObjectInterface.

Quando la vostra applicazione chiama un metodo sull’oggetto proxy, il metodo corrispondente viene chiamato sull’oggetto esportato all’interno del servizio XPC. Quando il metodo del servizio chiama il blocco di risposta, i valori dei parametri vengono serializzati e inviati all’applicazione, dove i valori dei parametri vengono deserializzati e passati al blocco di risposta. (Il blocco di risposta viene eseguito all’interno dello spazio degli indirizzi dell’applicazione.)

Nota: Se volete permettere al processo helper di chiamare metodi su un oggetto nella vostra applicazione, dovete impostare le proprietà exportedInterface ed exportedObject prima di chiamare resume. Queste proprietà sono descritte ulteriormente nella prossima sezione.

Accettare una connessione nell’helper

Quando un helper basato su NSXPCConnection riceve il primo messaggio da una connessione, il metodo listener:shouldAcceptNewConnection: del delegato ascoltatore viene chiamato con un oggetto ascoltatore e un oggetto connessione. Questo metodo permette di decidere se accettare o meno la connessione; dovrebbe restituire YES per accettare la connessione o NO per rifiutarla.

Nota: L’helper riceve una richiesta di connessione quando viene inviato il primo messaggio effettivo. Il metodo resume dell’oggetto connessione non causa l’invio di un messaggio.

Oltre a prendere decisioni di policy, questo metodo deve configurare l’oggetto connessione. In particolare, assumendo che l’helper decida di accettare la connessione, deve impostare le seguenti proprietà sulla connessione:

  • exportedInterface – un oggetto interfaccia che descrive il protocollo per l’oggetto da esportare. (La creazione di questo oggetto è stata descritta in precedenza in Connessione a e uso di un’interfaccia.)
  • exportedObject – l’oggetto locale (di solito nell’helper) a cui devono essere consegnate le chiamate al metodo del client remoto. Ogni volta che l’estremità opposta della connessione (di solito nell’applicazione) chiama un metodo sull’oggetto proxy della connessione, il metodo corrispondente viene chiamato sull’oggetto specificato dalla proprietà exportedObject.

Dopo aver impostato queste proprietà, dovrebbe chiamare il metodo resume dell’oggetto connection prima di restituire YES. Anche se il delegato può rimandare la chiamata di resume, la connessione non riceverà alcun messaggio finché non lo farà.

Inviare messaggi

Inviare messaggi con NSXPC è semplice come fare una chiamata di metodo. Per esempio, data l’interfaccia myCookieInterface (descritta nelle sezioni precedenti) sull’oggetto di connessione XPC myConnection, potete chiamare il metodo feedMeACookie in questo modo:

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

Quando chiamate questo metodo, il metodo corrispondente nell’helper XPC viene chiamato automaticamente. Questo metodo, a sua volta, potrebbe usare l’oggetto di connessione dell’helper XPC in modo simile per chiamare un metodo sull’oggetto esportato dall’applicazione principale.

Gestione degli errori

In aggiunta a qualsiasi metodo di gestione degli errori specifico per un dato compito dell’helper, sia il servizio XPC che l’applicazione principale dovrebbero anche fornire i seguenti blocchi di gestione degli errori XPC:

  • Gestore delle interruzioni – chiamato quando il processo all’altro capo della connessione è andato in crash o ha chiuso la connessione in altro modo. L’oggetto locale della connessione è tipicamente ancora valido – qualsiasi chiamata futura genera automaticamente una nuova istanza dell’helper a meno che non sia impossibile farlo – ma potrebbe essere necessario resettare qualsiasi stato che l’helper avrebbe altrimenti mantenuto.

Il gestore viene invocato sulla stessa coda dei messaggi di risposta e degli altri gestori, e viene sempre eseguito dopo qualsiasi altro messaggio o gestore del blocco di risposta (eccetto il gestore di invalidazione). È sicuro fare nuove richieste sulla connessione da un gestore di interruzione.

  • Gestore di invalidazione – chiamato quando viene chiamato il metodo invalidate o quando un helper XPC non può essere avviato. Quando questo gestore viene chiamato, l’oggetto locale della connessione non è più valido e deve essere ricreato. Questo è sempre l’ultimo gestore chiamato su un oggetto di connessione. Quando questo blocco viene chiamato, l’oggetto connessione è stato abbattuto. Non è possibile inviare ulteriori messaggi sulla connessione a quel punto, sia all’interno del gestore che altrove nel vostro codice.

In entrambi i casi, dovreste usare variabili block-scoped per fornire abbastanza informazioni contestuali – forse una coda di operazioni in sospeso e lo stesso oggetto di connessione – in modo che il vostro codice gestore possa fare qualcosa di sensato, come riprovare le operazioni in sospeso, abbattere la connessione, mostrare una finestra di dialogo di errore, o qualsiasi altra azione abbia senso nella vostra particolare applicazione.