Inter Process Call

Nous allons nous intéresser à une technologie peu exploité par les développeurs d'applications, et pourtant maitresse de tous le framework : la communication entre les processus.
Par Philippe PRADOS - 2011
www.prados.fr
Pour permettre la communication entre plusieurs processus ou entre une application et le cœur du framework (SystemServer), un mécanisme de communication est proposé (initialement conçu pour BeOS). Il permet de décrire une interface de communication entre deux processus. Cette interface est exprimée dans un fichier AIDL (Android Interface Description Langage), avec une syntaxe très proche de la déclaration d'une interface Java. À partir de ce fichier, l'utilitaire aidl permet de générer une interface java et deux implémentations. L'une d'elle permet de propager les invocations des méthodes vers un autre processus, avec sérialisation de tous les paramètres. L'autre implémentation joue le rôle inverse. Elle reçoit les invocations d'un autre processus et les transforme en invocations sur une classe java d'implémentation.
L'interface et toutes les classes périphériques sont générées dans un seul fichier source Java, en utilisant des classes internes. Ce code généré doit être présent dans les deux processus pour permettre une communication.
L'extension ADT d'Eclipse (Android Development Tools) sait traiter automatiquement les fichiers AIDL. La présence d'un fichier de ce type à côté des sources Java permet la génération de tout le code nécessaire.
Voici un exemple d'interface AIDL toute simple, dans le fichier MonService.aidl, utilisant des types primitifs.
// Fichier IMonService.aidl
package org.monpackage;
interface IMonService
{
long add(int a,int b);
}
Pour générer le code Java correspondant, il faut utiliser Eclipse ou invoquer la commande suivante :
aidl IMonService.aidl
Le code produit est schématisé comme dans L'illustration 1. Les classes Proxy et Stub sont des classes internes de l'interface. IMonService. Proxy se charge d'invoquer le service à distance et Stub de recevoir les sollicitations des autres processus pour invoquer l'implémentation du service.
Pour publier une API d'invocations distantes, il est nécessaire de proposer un service Android, servant de point d'entrée. Ce dernier doit surcharger la méthode onBind() pour retourner l'implémentation du service distant.
<!-- Fichier AndroidManifest.xml -->
<service android:name=".MonService">
<intent-filter>
<action android:name="org.monpackage.IMonService"/>
</intent-filter>
</service>
// Fichier MonService.java
public final class MonService extends Service
{
@Override
public IBinder onBind(Intent intent)
{
return _binder;
}
public static final IMonService.Stub _binder = new ImonService.Stub()
{
@Override
public long add(int a, int b) throws RemoteException
{
return a+b;
}
};
}
L'invocation s'effectue par la découverte du service et par l'invocation des méthodes.
ServiceConnection connection = new ServiceConnection()
{
public void onServiceConnected(ComponentName className,IBinder service)
{
try
{
IMonService monService = ImonService.Stub.asInterface(service);
monService.add(10, 20);
}
catch (RemoteException e)
{
e.printStackTrace();
}
}
public void onServiceDisconnected(ComponentName className)
{
}
};
bindService(new Intent(IMonService.class.getName()),
connection,
Context.BIND_AUTO_CREATE);
Lorsqu'une méthode distante est invoquée par le même processus, la même file d'exécution est utilisée. Par contre, si la méthode est invoquée par un autre processus, un des fils d'exécution d'un pool est utilisé. Il faut donc rédiger les services de telle sorte qu'ils soient compatibles à une utilisation en multitâches.
Les types Parcelables
Les choses se compliquent lorsque on souhaite utiliser des paramètres plus complexes que les types primitifs.
Tous les paramètres et les objets renvoyés par une instruction return doivent être Parcelable. C'est une interface équivalente à Serializable de Java, mais exploitant une API spécifique. Il est nécessaire de rédiger pour chaque classe Parcelable, le code de sérialisation et de dé-sérialisation. Un paramètre Parcel est disponible, proposant des API de bas niveau permettant l'ajout de types primitifs au flux. En réalité, le flux est une zone mémoire partagée entre les processus. Lorsque le flux est entièrement constitué, la zone mémoire est envoyée à l'autre processus pour que ce dernier puisse l'analyser et construire une copie de tous les paramètres. Ensuite, le code peut invoquer le traitement Java distant. L'objet retourné par le traitement suit un chemin inverse pour retourner au processus appelant. Les appels sont synchrones. L'invocation d'un service distant est bloquant tant que ce dernier n'est pas traité.
Voici un exemple d'objet Parcelable tout simple.
public class MonObjet implements Parcelable
{
String _monString;
int _monEntier;
public MonObjet(String aString,int aEntier)
{
_monString=aString;
_monEntier=aEntier;
}
// -- Parcel managment
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeString(_monString);
dest.writeInt(_monEntier);
}
@Override
public int describeContents()
{
return 0;
}
public static final Parcelable.Creator<MonObjet> CREATOR =
new Parcelable.Creator<MonObjet>()
{
@Override
public MonObjet createFromParcel(Parcel in)
{
return new MonObjet(in.readString(),in.readInt());
}
@Override
public MonObjet[] newArray(int size)
{
return new MonObjet[size];
}
};
}
L'interface de notre service est modifiée pour accepter un paramètre de ce type.
// Fichier IMonServer.aidl
package org.monpackage;
import org.monpackage.MonObjet;
interface IMonService
{
long add(int a,int b);
MonObjet get(in MonObjet obj);
}
Mais, pour pouvoir compiler l'interface, il faut indiquer ce qu'il faut faire de la classe MonObjet. Un autre fichier AIDL permet de décrire cela. Il n'est que déclaratif et permet de signaler au compilateur AIDL que MonObjet est parcelable.
// Fichier MonObjet.aidl
package org.monpackage;
parcelable MonObjet;
Maintenant, nous pouvons générer le code nécessaire. Rien ne sera généré pour la classe MonObjet.
aidl IMonServer.aidl
Adjectifs des paramètres
Les paramètres d'une interface AIDL peuvent être enrichis d'un adjectif indiquant si ce dernier va servir d'entrée (in), de sortie (out) ou des deux (inout). Cela a un impact sur le code généré. Ces adjectifs n'ont de sens que sur les objets. Ils n'ont aucun impact sur les types primitifs qui ne fonctionnent que par valeur.
// Fichier IMonService.aidl
interface IMonService
{
void maMethode(
in TypeUn input,
out TypeDeux output,
inout TypeTrois inputoutput);
}
Le code coté client est alors similaire à celui-ci :
parcelRequest.writeParcelable(in);
parcelRequest.writeParcelable(inputout);
parcelResponse=inkoqueRemote();
output=parcelResponse.readParcelable();
inputoutput=parcelResponse.readParcelable();
Le code coté serveur est similaire à celui-ci :
input=parcelRequest.readParcelable();
output=new TypeDeux(); // Nouvelle instance
inputoutput=parcelRequest.readParcelable();
_impl.maMathode(input,output,inputOutput);
parcelResponse.writeParcelable(output);
parcelResponse.writeParcelable(inputoutput);
commit();
Le code proposé ci-dessus est une grande simplification du code généré, mais il permet de présenter l'impact de ces adjectifs sans complexité excessive. Une instance est construite sur le serveur avant l'invocation de la méthode pour les paramètres de type out. Les paramètres de types out et inout sont renvoyés par valeur à l'appelant.
Invocation dans les deux sens
Il est également possible de recevoir en paramètre une interface d'un objet distant. Dans cette situation, le paramètre n'est pas envoyé par valeur mais par référence. Cela permet la mise en place d'une communication dans les deux sens entre deux processus.
Voici deux interfaces : L'une permet d'invoquer un service distant en lui indiquant comment invoquer le processus appelant en retour ; l'autre interface décrit l'appel de retour.
// Fichier ImonService.aidl
package org.monpackage;
import org.monpackage.IMaCallBack;
interface ImonService
{
void enregistreCallBack(IMaCallBack callback);
}
// Fichier IMaCallBack.aidl
package org.monpackage;
interface ImaCallBack
{
void callBack();
}
Nous générons le code nécessaire comme d'habitude ou nous laissons Eclipse faire le boulot. Pour l'invocation du service distant par l'application, il faut procéder en plusieurs étapes. La première consiste à proposer une instanciation de l'interface IMaCallBack, en héritant de IMaCallBack.Stub.
// Fichier MaCallBackImpl.java
public class MaCallBackImpl extends IMaCallBack.Stub
{
@Override
public void callBack() throws RemoteException
{
System.out.println("callback");
}
}
Il n'est pas nécessaire d'exposer la callback via un service. Il suffit de créer une instance MaCallBackImpl avant de la livrer en paramètre lors de l'invocation d'une méthode distante.
public void onServiceConnected(ComponentName className,IBinder service)
{
try
{
IMonService monService = ImonService.Stub.asInterface(service);
IMaCallBack callback=new MaCallBackImpl();
monService.enregistreCallBack(callback);
}
catch (RemoteException e)
{
e.printStackTrace();
}
}
Ainsi, les deux processus sont inter-connectés.
Exceptions
Comment sont gérées les exceptions ? Il n'est pas possible d'indiquer d'exception dans une interface AIDL. Cela complexifie souvent le code, car le retour d'une méthode doit indiquer deux informations simultanément : la valeur retournée et/ou l’éventuelle exception capturée par le traitement. Comme les exceptions ne sont pas Parcelable, il n'est pas possible de les envoyer tel quel à l'appelant. Il faut alors les convertir lors de l'invocation et les reconstruire à la main dans l'appelant.
Plusieurs approches sont possibles : Utiliser une Pair<?,?> Parcelable, pour le retour de la méthode, ou ajouter un paramètre de type TransportException, pour propager l'éventuelle exception via un entier par exemple.
La première approche s'effectue assez facilement, avec une classe générique.
class PairParcelable<F extends Parcelable,S extends Parcelable>
implements Parcelable
{
public F first;
public S second;
public PairParcelable()
{
}
public PairParcelable(F f,S s)
{
first=f;
second=s;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeParcelable(first, flags);
dest.writeParcelable(second, flags);
}
@Override
public int describeContents()
{
return 0;
}
}
Pour gérer la persistance et l'objet CREATOR, nous devons utiliser une sous-classe.
public class MyPair extends PairParcelable<MyObject,MyException>
{
public MyPair()
{
}
public MyPair(MyObject f, MyException s)
{
super(f,s);
}
public static final Parcelable.Creator<MyPair> CREATOR = new Parcelable.Creator<MyPair>()
{
public MyPair createFromParcel(Parcel in)
{
final ClassLoader loader=getClass().getClassLoader();
MyObject f=(MyObject)in.readParcelable(loader);
MyException s=(MyException)in.readParcelable(loader);
return new MyPair(f,s);
}
public MyPair[] newArray(int size)
{
return new MyPair[size];
}
};
}
La deuxième approche est plus élégante. Elle consiste à créer une classe servant de transport aux exceptions de l'application. Cette classe est Parcelable. Elle mémorise une exception par un simple entier et est capable de la propager sur le client à la demande. Les routines de sérialisation et de dé-sérialisation s'occupent de construire les bonnes instances suivant l'héritage des exceptions de l'application.
public final class TransportException implements Parcelable
{
private QueryException _e;
public void setException(QueryException e)
{
_e=e;
}
public void getException() throws QueryException
{
if (_e!=null) throw _e;
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
int type=0;
if (_e instanceof AuthQueryException) type=2;
if (_e instanceof QueryError) type=1;
dest.writeInt(type);
dest.writeInt(_e._msg);
}
public void readFromParcel(Parcel in)
{
QueryException vrc=null;
switch (in.readInt())
{
case 2 :
vrc=new AuthQueryException();
break;
case 1:
vrc=new QueryError();
break;
default:
vrc=new QueryWarning();
}
vrc._msg=in.readInt();
_e=vrc;
}
public static final Parcelable.Creator<TransportException> CREATOR =
new Parcelable.Creator<TransportException>()
{
@Override
public TransportException createFromParcel(Parcel in)
{
TransportException rc=new TransportException();
rc.readFromParcel(in);
return rc;
}
@Override
public TransportException[] newArray(int size)
{
return new TransportException[size];
}
};
}
Ainsi, l'interface AIDL est modifiée pour ajouter un paramètre de type out à notre méthode.
import TransportException;
interface IMonInterface
{
MonObjet get(out TransportException e);
}
L'implémentation devient :
class MonInterfaceImpl extends IMonInterface.Stub
{
MonObjet get(TransportException e)
{
try
{
...
return monObjet;
}
catch (MonException ee)
{
e.setException(ee);
return null;
}
}
}
Et l'utilisation devient relativement simple.
IMonInterface interface=...
TransportException e=new TransportException();
MonObjet obj=interface.get(e);
e.getException(); // Lance une exception si nécessaire.
...
Cette approche est similaire à ce que le code Android utilise pour certaines exceptions, propagées automatiquement lors de l'invocation. C'est le cas des exceptions SecurityException, BadParcelableException, IllegalArgumentException, NullPointerException et IllegalStateException. Cela se retrouve dans la méthode Parcel.readException().
public final void readException(int code, String msg)
{
switch (code)
{
case EX_SECURITY:
throw new SecurityException(msg);
case EX_BAD_PARCELABLE:
throw new BadParcelableException(msg);
case EX_ILLEGAL_ARGUMENT:
throw new IllegalArgumentException(msg);
case EX_NULL_POINTER:
throw new NullPointerException(msg);
case EX_ILLEGAL_STATE:
throw new IllegalStateException(msg);
}
throw new RuntimeException("Unknown exception code: "
+ code + " msg " + msg);
}
Le mécanisme d'IPC est souvent utilisé sans que le développeur en ait toujours conscience. Par exemple, lors de l'utilisation d'un ContentProvider, une communication est établie entre le processus producteur et le processus consommateur. Un curseur retourne la première page des données. Lorsque cela est nécessaire, une nouvelle requête distante est invoquée pour obtenir la page suivante.
La clef de voûte
Cette technologie est la clef de voûte de tout le framework Android. En effet, les processus communiquent à l'aide de messages pour demander le lancement d'une activité, signaler un clic sur un bouton, la perte du réseau, etc. Ces messages sont envoyés à l'aide d'une seule interface AIDL.
// Fichier IMessenger.aidl extrait
// des sources d'Android
package android.os;
import android.os.Message;
oneway interface IMessenger
{
void send(in Message msg);
}
Cette interface permet de déposer des messages dans la file d'un autre processus. Ce dernier est alors mémorisé dans une pile de messages, à disposition de l'application. Sans cette interface, il n'y a pas de distribution des messages, donc pas d'Android.
Cet article a expliqué une technologie fondamentale du framework Android, qui est pourtant généralement ignorée des développeurs. Nous vous invitons à vous y attarder quelques heures afin de bien comprendre et maîtriser le cœur du framework.
