Backends AIDL

Un backend AIDL est une cible pour la génération de code bouchon. Utilisez toujours des fichiers AIDL dans une langue spécifique avec un environnement d'exécution spécifique. Selon le contexte, vous devez utiliser différents backends AIDL.

Dans le tableau suivant, la stabilité de la surface de l'API fait référence à la possibilité de compiler du code avec cette surface d'API de manière à ce que le code puisse être fourni indépendamment du binaire libbinder.so system.img.

AIDL dispose des backends suivants:

Backend Langue Surface d'API Systèmes de compilation
Java Java SDK ou SystemApi (stable*) Tous
NDK C++ libbinder_ndk (stable*) aidl_interface
CPP C++ libbinder (instable) Tous
Rust Rust libbinder_rs (stable*) aidl_interface
  • Ces surfaces d'API sont stables, mais de nombreuses API, telles que celles de gestion des services, sont réservées à l'utilisation interne de la plate-forme et ne sont pas disponibles pour les applications. Pour en savoir plus sur l'utilisation d'AIDL dans les applications, consultez la section Langage de définition d'interface Android (AIDL).
  • Le backend Rust a été introduit dans Android 12. Le backend NDK est disponible depuis Android 10.
  • La crate Rust est basée sur libbinder_ndk, ce qui la rend stable et portable. Les APEX utilisent la crate de liaison de manière standard côté système. La partie Rust est groupée dans un APEX et expédiée à l'intérieur. Cette partie dépend de libbinder_ndk.so sur la partition système.

Systèmes de compilation

Selon le backend, il existe deux façons de compiler AIDL en code bouchon. Pour en savoir plus sur les systèmes de compilation, consultez la documentation de référence sur les modules Soong.

Système de compilation de base

Dans un Android.bp module cc_ ou java_ (ou dans leurs équivalents Android.mk), vous pouvez spécifier des fichiers AIDL (.aidl) comme fichiers sources. Dans ce cas, les backends Java ou CPP d'AIDL sont utilisés (et non le backend NDK), et les classes permettant d'utiliser les fichiers AIDL correspondants sont ajoutées automatiquement au module. Vous pouvez spécifier des options telles que local_include_dirs (qui indique au système de compilation le chemin d'accès racine aux fichiers AIDL de ce module) dans ces modules sous un groupe aidl:.

Le backend Rust ne doit être utilisé qu'avec Rust. Les modules rust_ sont gérés différemment, car les fichiers AIDL ne sont pas spécifiés comme fichiers sources. À la place, le module aidl_interface produit un rustlib appelé aidl_interface_name-rust, avec lequel il peut être associé. Pour en savoir plus, consultez l'exemple AIDL Rust.

aidl_interface

Les types utilisés avec le système de compilation aidl_interface doivent être structurés. Pour être structurés, les éléments parcellables doivent contenir des champs directement et ne pas être des déclarations de types définis directement dans les langues cibles. Pour savoir comment l'AIDL structuré s'intègre à l'AIDL stable, consultez la section AIDL structuré par rapport à l'AIDL stable.

Types

Considérez le compilateur aidl comme une implémentation de référence pour les types. Lorsque vous créez une interface, appelez aidl --lang=<backend> ... pour afficher le fichier d'interface généré. Lorsque vous utilisez le module aidl_interface, vous pouvez afficher la sortie dans out/soong/.intermediates/<path to module>/.

Type Java ou AIDL Type C++ Type de NDK Type de rouille
boolean bool bool bool
byte8 int8_t int8_t i8
char char16_t char16_t u16
int int32_t int32_t i32
long int64_t int64_t i64
float float float f32
double double double f64
String android::String16 std::string Entrée: &str
Sortie: String
android.os.Parcelable android::Parcelable N/A N/A
IBinder android::IBinder ndk::SpAIBinder binder::SpIBinder
T[] std::vector<T> std::vector<T> Entrée: &[T]
Sortie: Vec<T>
byte[] std::vector std::vector1 Entrée: &[u8]
Sortie: Vec<u8>
List<T> std::vector<T>2 std::vector<T>3 Entrée: In: &[T]4
Sortie: Vec<T>
FileDescriptor android::base::unique_fd N/A N/A
ParcelFileDescriptor android::os::ParcelFileDescriptor ndk::ScopedFileDescriptor binder::parcel::ParcelFileDescriptor
Type d'interface (T) android::sp<T> std::shared_ptr<T>7 binder::Strong
Type de parcelable (T) T T T
Type d'union (T)5 T T T
T[N]6 std::array<T, N> std::array<T, N> [T; N]

 1. Dans Android 12 ou version ultérieure, les tableaux d'octets utilisent uint8_t au lieu de int8_t pour des raisons de compatibilité.

2. Le backend C++ est compatible avec List<T>, où T est l'un des éléments suivants : String, IBinder, ParcelFileDescriptor ou parcelable. Dans Android 13 ou version ultérieure, T peut être n'importe quel type non primitif (y compris les types d'interface), à l'exception des tableaux. AOSP recommande d'utiliser des types de tableaux tels que T[], car ils fonctionnent dans tous les backends.

3. Le backend du NDK est compatible avec List<T>, où T est l'un des éléments suivants : String, ParcelFileDescriptor ou parcelable. Sous Android 13 ou version ultérieure, T peut être n'importe quel type non primitif, à l'exception des tableaux.

4. Les types sont transmis différemment pour le code Rust selon qu'ils sont une entrée (un argument) ou une sortie (une valeur renvoyée).

5. Les types d'union sont compatibles avec Android 12 ou version ultérieure.

6. Sous Android 13 ou version ultérieure, les tableaux de taille fixe sont compatibles. Les tableaux de taille fixe peuvent avoir plusieurs dimensions (par exemple, int[3][4]). Dans le backend Java, les tableaux de taille fixe sont représentés sous la forme de types de tableaux.

7. Pour instancier un objet SharedRefBase de liaison, utilisez SharedRefBase::make\<My\>(... args ...). Cette fonction crée un objet std::shared_ptr\<T\>, qui est également géré en interne, au cas où le liaisonneur appartient à un autre processus. Créer l'objet d'une autre manière entraîne une double propriété.

8. Consultez également le type Java ou AIDL byte[].

Sens de circulation (avant, arrière et avant/arrière)

Lorsque vous spécifiez les types des arguments des fonctions, vous pouvez les spécifier comme in, out ou inout. Il contrôle la direction dans laquelle les informations sont transmises pour un appel IPC.

  • Le spécificateur d'argument in indique que des données sont transmises de l'appelant à l'appelé. Le spécificateur in est la direction par défaut, mais si les types de données peuvent également être out, vous devez spécifier la direction.

  • Le spécificateur d'argument out signifie que des données sont transmises de l'appelant à l'appelant.

  • Le spécificateur d'argument inout combine ces deux éléments. Toutefois, nous vous recommandons d'éviter d'utiliser le spécificateur d'argument inout. Si vous utilisez inout avec une interface versionnée et un appelant plus ancien, les champs supplémentaires qui ne sont présents que dans l'appelant sont rétablis à leurs valeurs par défaut. En ce qui concerne Rust, un type inout normal reçoit &mut T, et un type inout de liste reçoit &mut Vec<T>.

interface IRepeatExamples {
    MyParcelable RepeatParcelable(MyParcelable token); // implicitly 'in'
    MyParcelable RepeatParcelableWithIn(in MyParcelable token);
    void RepeatParcelableWithInAndOut(in MyParcelable param, out MyParcelable result);
    void RepeatParcelableWithInOut(inout MyParcelable param);
}

UTF-8 et UTF-16

Avec le backend CPP, vous pouvez choisir si les chaînes sont en UTF-8 ou en UTF-16. Déclarez les chaînes en tant que @utf8InCpp String dans AIDL pour les convertir automatiquement en UTF-8. Les backends du NDK et de Rust utilisent toujours des chaînes UTF-8. Pour en savoir plus sur l'annotation utf8InCpp, consultez utf8InCpp.

Possibilité de valeur nulle

Vous pouvez annoter les types pouvant être nuls avec @nullable. Pour en savoir plus sur l'annotation nullable, consultez la section nullable.

Parcelables personnalisés

Un parcelable personnalisé est un parcelable implémenté manuellement dans un backend cible. N'utilisez des parcelables personnalisés que lorsque vous essayez d'ajouter la prise en charge d'autres langues pour un parcelable personnalisé existant qui ne peut pas être modifié.

Voici un exemple de déclaration parcelable AIDL:

    package my.pack.age;
    parcelable Foo;

Par défaut, cela déclare un parcelable Java où my.pack.age.Foo est une classe Java implémentant l'interface Parcelable.

Pour déclarer un backend CPP personnalisé parcelable dans AIDL, utilisez cpp_header:

    package my.pack.age;
    parcelable Foo cpp_header "my/pack/age/Foo.h";

L'implémentation C++ dans my/pack/age/Foo.h se présente comme suit:

    #include <binder/Parcelable.h>

    class MyCustomParcelable : public android::Parcelable {
    public:
        status_t writeToParcel(Parcel* parcel) const override;
        status_t readFromParcel(const Parcel* parcel) override;

        std::string toString() const;
        friend bool operator==(const MyCustomParcelable& lhs, const MyCustomParcelable& rhs);
        friend bool operator!=(const MyCustomParcelable& lhs, const MyCustomParcelable& rhs);
    };

Pour déclarer un parcelable NDK personnalisé dans AIDL, utilisez ndk_header:

    package my.pack.age;
    parcelable Foo ndk_header "android/pack/age/Foo.h";

L'implémentation du NDK dans android/pack/age/Foo.h se présente comme suit:

    #include <android/binder_parcel.h>

    class MyCustomParcelable {
    public:

        binder_status_t writeToParcel(AParcel* _Nonnull parcel) const;
        binder_status_t readFromParcel(const AParcel* _Nonnull parcel);

        std::string toString() const;

        friend bool operator==(const MyCustomParcelable& lhs, const MyCustomParcelable& rhs);
        friend bool operator!=(const MyCustomParcelable& lhs, const MyCustomParcelable& rhs);
    };

Dans Android 15, pour déclarer un parcelable Rust personnalisé dans AIDL, utilisez rust_type:

package my.pack.age;
@RustOnlyStableParcelable parcelable Foo rust_type "rust_crate::Foo";

L'implémentation Rust dans rust_crate/src/lib.rs se présente comme suit:

use binder::{
    binder_impl::{BorrowedParcel, UnstructuredParcelable},
    impl_deserialize_for_unstructured_parcelable, impl_serialize_for_unstructured_parcelable,
    StatusCode,
};

#[derive(Clone, Debug, Eq, PartialEq)]
struct Foo {
    pub bar: String,
}

impl UnstructuredParcelable for Foo {
    fn write_to_parcel(&self, parcel: &mut BorrowedParcel) -> Result<(), StatusCode> {
        parcel.write(&self.bar)?;
        Ok(())
    }

    fn from_parcel(parcel: &BorrowedParcel) -> Result<Self, StatusCode> {
        let bar = parcel.read()?;
        Ok(Self { bar })
    }
}

impl_deserialize_for_unstructured_parcelable!(Foo);
impl_serialize_for_unstructured_parcelable!(Foo);

Vous pouvez ensuite utiliser ce parcelable comme type dans les fichiers AIDL, mais il ne sera pas généré par AIDL. Fournissez des opérateurs < et == pour les parcelables personnalisés du backend CPP et NDK afin de les utiliser dans union.

Valeurs par défaut

Les parcelables structurés peuvent déclarer des valeurs par défaut par champ pour les primitives, les champs String et les tableaux de ces types.

    parcelable Foo {
      int numField = 42;
      String stringField = "string value";
      char charValue = 'a';
      ...
    }

Dans le backend Java, lorsque des valeurs par défaut sont manquantes, les champs sont initialisés avec des valeurs nulles pour les types primitifs et null pour les types non primitifs.

Dans d'autres backends, les champs sont initialisés avec des valeurs initialisées par défaut lorsque les valeurs par défaut ne sont pas définies. Par exemple, dans le backend C++, les champs String sont initialisés en tant que chaîne vide et les champs List<T> sont initialisés en tant que vector<T> vide. Les champs @nullable sont initialisés en tant que champs à valeur nulle.

Unions

Les unions AIDL sont taguées et leurs fonctionnalités sont similaires dans tous les backends. Ils sont construits en fonction de la valeur par défaut du premier champ et ils disposent d'un moyen d'interagir avec eux propre à la langue:

    union Foo {
      int intField;
      long longField;
      String stringField;
      MyParcelable parcelableField;
      ...
    }

Exemple Java

    Foo u = Foo.intField(42);              // construct

    if (u.getTag() == Foo.intField) {      // tag query
      // use u.getIntField()               // getter
    }

    u.setSringField("abc");                // setter

Exemple C++ et NDK

    Foo u;                                            // default constructor

    assert (u.getTag() == Foo::intField);             // tag query
    assert (u.get<Foo::intField>() == 0);             // getter

    u.set<Foo::stringField>("abc");                   // setter

    assert (u == Foo::make<Foo::stringField>("abc")); // make<tag>(value)

Exemple Rust

En Rust, les unions sont implémentées en tant qu'énumérations et ne comportent pas de getters et de setters explicites.

    let mut u = Foo::Default();              // default constructor
    match u {                                // tag match + get
      Foo::IntField(x) => assert!(x == 0);
      Foo::LongField(x) => panic!("Default constructed to first field");
      Foo::StringField(x) => panic!("Default constructed to first field");
      Foo::ParcelableField(x) => panic!("Default constructed to first field");
      ...
    }
    u = Foo::StringField("abc".to_string()); // set

Gestion des exceptions

L'OS Android fournit des types d'erreur intégrés que les services peuvent utiliser pour signaler les erreurs. Ils sont utilisés par les liaisons et peuvent être utilisés par tous les services implémentant une interface de liaison. Leur utilisation est bien documentée dans la définition AIDL, et ils ne nécessitent aucun état ni type de retour défini par l'utilisateur.

Paramètres de sortie avec erreurs

Lorsqu'une fonction AIDL signale une erreur, il est possible qu'elle n'initialise ni ne modifie pas les paramètres de sortie. Plus précisément, les paramètres de sortie peuvent être modifiés si l'erreur se produit lors du désassemblage, plutôt que lors du traitement de la transaction elle-même. En général, lorsqu'une erreur est renvoyée par une fonction AIDL, tous les paramètres inout et out, ainsi que la valeur de retour (qui agit comme un paramètre out dans certains backends) doivent être considérés comme étant dans un état indéfini.

Valeurs d'erreur à utiliser

De nombreuses valeurs d'erreur intégrées peuvent être utilisées dans n'importe quelle interface AIDL, mais certaines sont traitées de manière spéciale. Par exemple, EX_UNSUPPORTED_OPERATION et EX_ILLEGAL_ARGUMENT peuvent être utilisés lorsqu'ils décrivent la condition d'erreur, mais EX_TRANSACTION_FAILED ne doit pas être utilisé, car il est traité de manière spéciale par l'infrastructure sous-jacente. Consultez les définitions spécifiques au backend pour en savoir plus sur ces valeurs intégrées.

Si l'interface AIDL nécessite des valeurs d'erreur supplémentaires qui ne sont pas couvertes par les types d'erreur intégrés, elle peut utiliser l'erreur intégrée spéciale spécifique au service qui permet d'inclure une valeur d'erreur spécifique au service définie par l'utilisateur. Ces erreurs spécifiques au service sont généralement définies dans l'interface AIDL en tant que enum compatible avec const int ou int et ne sont pas analysées par le liaisonneur.

En Java, les erreurs sont mappées à des exceptions, telles que android.os.RemoteException. Pour les exceptions spécifiques au service, Java utilise android.os.ServiceSpecificException avec l'erreur définie par l'utilisateur.

Le code natif d'Android n'utilise pas d'exceptions. Le backend CPP utilise android::binder::Status. Le backend du NDK utilise ndk::ScopedAStatus. Chaque méthode générée par AIDL renvoie l'un de ces éléments, qui représente l'état de la méthode. Le backend Rust utilise les mêmes valeurs de code d'exception que le NDK, mais les convertit en erreurs Rust natives (StatusCode, ExceptionCode) avant de les transmettre à l'utilisateur. Pour les erreurs spécifiques au service, Status ou ScopedAStatus renvoyés utilisent EX_SERVICE_SPECIFIC avec l'erreur définie par l'utilisateur.

Les types d'erreurs intégrés se trouvent dans les fichiers suivants:

Backend Définition
Java android/os/Parcel.java
CPP binder/Status.h
NDK android/binder_status.h
Rust android/binder_status.h

Utiliser différents backends

Ces instructions sont spécifiques au code de la plate-forme Android. Ces exemples utilisent un type défini, my.package.IFoo. Pour savoir comment utiliser le backend Rust, consultez l'exemple de Rust AIDL dans les modèles Rust Android.

Types d'importation

Que le type défini soit une interface, un parcelable ou une union, vous pouvez l'importer en Java:

import my.package.IFoo;

Ou dans le backend CPP:

#include <my/package/IFoo.h>

Ou dans le backend du NDK (notez l'espace de noms aidl supplémentaire):

#include <aidl/my/package/IFoo.h>

Ou dans le backend Rust:

use my_package::aidl::my::package::IFoo;

Bien que vous puissiez importer un type imbriqué en Java, vous devez inclure l'en-tête de son type racine dans les backends CPP et NDK. Par exemple, lorsque vous importez un type Bar imbriqué défini dans my/package/IFoo.aidl (IFoo est le type racine du fichier), vous devez inclure <my/package/IFoo.h> pour le backend CPP (ou <aidl/my/package/IFoo.h> pour le backend NDK).

Implémenter une interface

Pour implémenter une interface, vous devez hériter de la classe de bouchon natif. Une implémentation d'une interface est souvent appelée service lorsqu'elle est enregistrée auprès du gestionnaire de services ou de android.app.ActivityManager, et appelée appel de rappel lorsqu'elle est enregistrée par un client d'un service. Toutefois, différents noms sont utilisés pour décrire les implémentations d'interface en fonction de l'utilisation exacte. La classe stub lit les commandes du pilote de liaison et exécute les méthodes que vous implémentez. Imaginons que vous disposiez d'un fichier AIDL comme suit:

    package my.package;
    interface IFoo {
        int doFoo();
    }

En Java, vous devez étendre la classe Stub générée:

    import my.package.IFoo;
    public class MyFoo extends IFoo.Stub {
        @Override
        int doFoo() { ... }
    }

Dans le backend du CPP:

    #include <my/package/BnFoo.h>
    class MyFoo : public my::package::BnFoo {
        android::binder::Status doFoo(int32_t* out) override;
    }

Dans le backend du NDK (notez l'espace de noms aidl supplémentaire):

    #include <aidl/my/package/BnFoo.h>
    class MyFoo : public aidl::my::package::BnFoo {
        ndk::ScopedAStatus doFoo(int32_t* out) override;
    }

Dans le backend Rust:

    use aidl_interface_name::aidl::my::package::IFoo::{BnFoo, IFoo};
    use binder;

    /// This struct is defined to implement IRemoteService AIDL interface.
    pub struct MyFoo;

    impl Interface for MyFoo {}

    impl IFoo for MyFoo {
        fn doFoo(&self) -> binder::Result<()> {
           ...
           Ok(())
        }
    }

Ou avec Rust asynchrone:

    use aidl_interface_name::aidl::my::package::IFoo::{BnFoo, IFooAsyncServer};
    use binder;

    /// This struct is defined to implement IRemoteService AIDL interface.
    pub struct MyFoo;

    impl Interface for MyFoo {}

    #[async_trait]
    impl IFooAsyncServer for MyFoo {
        async fn doFoo(&self) -> binder::Result<()> {
           ...
           Ok(())
        }
    }

S'inscrire et obtenir des services

Les services de la plate-forme Android sont généralement enregistrés avec le processus servicemanager. En plus des API suivantes, certaines API vérifient le service (c'est-à-dire qu'elles renvoient immédiatement une réponse si le service n'est pas disponible). Pour en savoir plus, consultez l'interface servicemanager correspondante. Vous ne pouvez effectuer ces opérations que lors de la compilation pour la plate-forme Android.

En Java:

    import android.os.ServiceManager;
    // registering
    ServiceManager.addService("service-name", myService);
    // return if service is started now
    myService = IFoo.Stub.asInterface(ServiceManager.checkService("service-name"));
    // waiting until service comes up (new in Android 11)
    myService = IFoo.Stub.asInterface(ServiceManager.waitForService("service-name"));
    // waiting for declared (VINTF) service to come up (new in Android 11)
    myService = IFoo.Stub.asInterface(ServiceManager.waitForDeclaredService("service-name"));

Dans le backend du CPP:

    #include <binder/IServiceManager.h>
    // registering
    defaultServiceManager()->addService(String16("service-name"), myService);
    // return if service is started now
    status_t err = checkService<IFoo>(String16("service-name"), &myService);
    // waiting until service comes up (new in Android 11)
    myService = waitForService<IFoo>(String16("service-name"));
    // waiting for declared (VINTF) service to come up (new in Android 11)
    myService = waitForDeclaredService<IFoo>(String16("service-name"));

Dans le backend du NDK (notez l'espace de noms aidl supplémentaire):

    #include <android/binder_manager.h>
    // registering
    binder_exception_t err = AServiceManager_addService(myService->asBinder().get(), "service-name");
    // return if service is started now
    myService = IFoo::fromBinder(ndk::SpAIBinder(AServiceManager_checkService("service-name")));
    // is a service declared in the VINTF manifest
    // VINTF services have the type in the interface instance name.
    bool isDeclared = AServiceManager_isDeclared("android.hardware.light.ILights/default");
    // wait until a service is available (if isDeclared or you know it's available)
    myService = IFoo::fromBinder(ndk::SpAIBinder(AServiceManager_waitForService("service-name")));

Dans le backend Rust:

use myfoo::MyFoo;
use binder;
use aidl_interface_name::aidl::my::package::IFoo::BnFoo;

fn main() {
    binder::ProcessState::start_thread_pool();
    // [...]
    let my_service = MyFoo;
    let my_service_binder = BnFoo::new_binder(
        my_service,
        BinderFeatures::default(),
    );
    binder::add_service("myservice", my_service_binder).expect("Failed to register service?");
    // Does not return - spawn or perform any work you mean to do before this call.
    binder::ProcessState::join_thread_pool()
}

Dans le backend Rust asynchrone, avec un environnement d'exécution à un seul thread:

use myfoo::MyFoo;
use binder;
use binder_tokio::TokioRuntime;
use aidl_interface_name::aidl::my::package::IFoo::BnFoo;

#[tokio::main(flavor = "current_thread")]
async fn main() {
    binder::ProcessState::start_thread_pool();
    // [...]
    let my_service = MyFoo;
    let my_service_binder = BnFoo::new_async_binder(
        my_service,
        TokioRuntime(Handle::current()),
        BinderFeatures::default(),
    );

    binder::add_service("myservice", my_service_binder).expect("Failed to register service?");

    // Sleeps forever, but does not join the binder threadpool.
    // Spawned tasks run on this thread.
    std::future::pending().await
}

Une différence importante par rapport aux autres options est que vous n'appelez pas join_thread_pool lorsque vous utilisez Rust asynchrone et un environnement d'exécution à un seul thread. En effet, vous devez fournir à Tokio un thread sur lequel il peut exécuter les tâches générées. Dans l'exemple suivant, le thread principal sert à cet effet. Toutes les tâches créées à l'aide de tokio::spawn s'exécutent sur le thread principal.

Dans le backend Rust asynchrone, avec un environnement d'exécution multithread:

use myfoo::MyFoo;
use binder;
use binder_tokio::TokioRuntime;
use aidl_interface_name::aidl::my::package::IFoo::BnFoo;

#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() {
    binder::ProcessState::start_thread_pool();
    // [...]
    let my_service = MyFoo;
    let my_service_binder = BnFoo::new_async_binder(
        my_service,
        TokioRuntime(Handle::current()),
        BinderFeatures::default(),
    );

    binder::add_service("myservice", my_service_binder).expect("Failed to register service?");

    // Sleep forever.
    tokio::task::block_in_place(|| {
        binder::ProcessState::join_thread_pool();
    });
}

Avec l'environnement d'exécution multithread Tokio, les tâches créées ne s'exécutent pas sur le thread principal. Il est donc plus logique d'appeler join_thread_pool sur le thread principal afin qu'il ne soit pas inactif. Vous devez encapsuler l'appel dans block_in_place pour quitter le contexte asynchrone.

Vous pouvez demander à recevoir une notification lorsqu'un service hébergeant un liaisonneur est arrêté. Cela peut vous aider à éviter les fuites de proxys de rappel ou à faciliter la récupération d'erreurs. Effectuez ces appels sur des objets proxy de liaison.

  • En Java, utilisez android.os.IBinder::linkToDeath.
  • Dans le backend CPP, utilisez android::IBinder::linkToDeath.
  • Dans le backend du NDK, utilisez AIBinder_linkToDeath.
  • Dans le backend Rust, créez un objet DeathRecipient, puis appelez my_binder.link_to_death(&mut my_death_recipient). Notez que, comme DeathRecipient est propriétaire du rappel, vous devez conserver cet objet tant que vous souhaitez recevoir des notifications.

Informations sur l'appelant

Lorsque vous recevez un appel de liaison de kernel, les informations sur l'appelant sont disponibles dans plusieurs API. L'ID de processus (PID) fait référence à l'ID de processus Linux du processus qui envoie une transaction. L'ID utilisateur (UI) fait référence à l'ID utilisateur Linux. Lorsque vous recevez un appel à sens unique, le PID appelant est 0. En dehors d'un contexte de transaction de liaison, ces fonctions renvoient le PID et l'UID du processus en cours.

Dans le backend Java:

    ... = Binder.getCallingPid();
    ... = Binder.getCallingUid();

Dans le backend du CPP:

    ... = IPCThreadState::self()->getCallingPid();
    ... = IPCThreadState::self()->getCallingUid();

Dans le backend du NDK:

    ... = AIBinder_getCallingPid();
    ... = AIBinder_getCallingUid();

Dans le backend Rust, lors de l'implémentation de l'interface, spécifiez les éléments suivants (au lieu de les laisser par défaut):

    ... = ThreadState::get_calling_pid();
    ... = ThreadState::get_calling_uid();

Rapports de bugs et API de débogage pour les services

Lorsque les rapports de bugs s'exécutent (par exemple, avec adb bugreport), ils collectent des informations sur l'ensemble du système pour faciliter le débogage de divers problèmes. Pour les services AIDL, les rapports de bugs utilisent le binaire dumpsys sur tous les services enregistrés auprès du gestionnaire de services pour extraire leurs informations dans le rapport de bug. Vous pouvez également utiliser dumpsys sur la ligne de commande pour obtenir des informations à partir d'un service avec dumpsys SERVICE [ARGS]. Dans les backends C++ et Java, vous pouvez contrôler l'ordre dans lequel les services sont mis en décharge à l'aide d'arguments supplémentaires pour addService. Vous pouvez également utiliser dumpsys --pid SERVICE pour obtenir le PID d'un service lors du débogage.

Pour ajouter une sortie personnalisée à votre service, remplacez la méthode dump dans votre objet serveur comme vous implémentez toute autre méthode IPC définie dans un fichier AIDL. Dans ce cas, limitez le vidage à l'autorisation d'application android.permission.DUMP ou à des UID spécifiques.

Dans le backend Java:

    @Override
    protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter fout,
        @Nullable String[] args) {...}

Dans le backend du CPP:

    status_t dump(int, const android::android::Vector<android::String16>&) override;

Dans le backend du NDK:

    binder_status_t dump(int fd, const char** args, uint32_t numArgs) override;

Dans le backend Rust, lors de l'implémentation de l'interface, spécifiez les éléments suivants (au lieu de les laisser par défaut):

    fn dump(&self, mut file: &File, args: &[&CStr]) -> binder::Result<()>

Utiliser des pointeurs faibles

Vous pouvez conserver une référence faible à un objet de liaison.

Bien que Java prenne en charge WeakReference, il n'est pas compatible avec les références de liaison faibles au niveau de la couche native.

Dans le backend CPP, le type faible est wp<IFoo>.

Dans le backend du NDK, utilisez ScopedAIBinder_Weak:

#include <android/binder_auto_utils.h>

AIBinder* binder = ...;
ScopedAIBinder_Weak myWeakReference = ScopedAIBinder_Weak(AIBinder_Weak_new(binder));

Dans le backend Rust, utilisez WpIBinder ou Weak<IFoo>:

let weak_interface = myIface.downgrade();
let weak_binder = myIface.as_binder().downgrade();

Obtenir dynamiquement le descripteur d'interface

Le descripteur d'interface identifie le type d'interface. Cela est utile lors du débogage ou lorsque vous disposez d'un liant inconnu.

En Java, vous pouvez obtenir le descripteur d'interface avec du code tel que:

    service = /* get ahold of service object */
    ... = service.asBinder().getInterfaceDescriptor();

Dans le backend du CPP:

    service = /* get ahold of service object */
    ... = IInterface::asBinder(service)->getInterfaceDescriptor();

Les backends NDK et Rust ne sont pas compatibles avec cette fonctionnalité.

Obtenir le descripteur d'interface de manière statique

Parfois (par exemple, lors de l'enregistrement de services @VintfStability), vous devez savoir ce qu'est le descripteur d'interface de manière statique. En Java, vous pouvez obtenir le descripteur en ajoutant du code tel que:

    import my.package.IFoo;
    ... IFoo.DESCRIPTOR

Dans le backend du CPP:

    #include <my/package/BnFoo.h>
    ... my::package::BnFoo::descriptor

Dans le backend du NDK (notez l'espace de noms aidl supplémentaire):

    #include <aidl/my/package/BnFoo.h>
    ... aidl::my::package::BnFoo::descriptor

Dans le backend Rust:

    aidl::my::package::BnFoo::get_descriptor()

Plage d'énumération

Dans les backends natifs, vous pouvez itérer sur les valeurs possibles d'un énumérateur. Pour des raisons de taille de code, cette fonctionnalité n'est pas disponible en Java.

Pour une énumération MyEnum définie dans AIDL, l'itération est fournie comme suit.

Dans le backend du CPP:

    ::android::enum_range<MyEnum>()

Dans le backend du NDK:

   ::ndk::enum_range<MyEnum>()

Dans le backend Rust:

    MyEnum::enum_values()

Gestion des threads

Chaque instance de libbinder dans un processus gère un pool de threads. Pour la plupart des cas d'utilisation, il doit s'agir d'un seul pool de threads, partagé entre tous les backends. La seule exception est si le code du fournisseur charge une autre copie de libbinder pour communiquer avec /dev/vndbinder. Il s'agit d'un nœud de liaison distinct. Le pool de threads n'est donc pas partagé.

Pour le backend Java, la taille du pool de threads ne peut qu'augmenter (car il est déjà démarré):

    BinderInternal.setMaxThreads(<new larger value>);

Pour le backend CPP, les opérations suivantes sont disponibles:

    // set max threadpool count (default is 15)
    status_t err = ProcessState::self()->setThreadPoolMaxThreadCount(numThreads);
    // create threadpool
    ProcessState::self()->startThreadPool();
    // add current thread to threadpool (adds thread to max thread count)
    IPCThreadState::self()->joinThreadPool();

De même, dans le backend du NDK:

    bool success = ABinderProcess_setThreadPoolMaxThreadCount(numThreads);
    ABinderProcess_startThreadPool();
    ABinderProcess_joinThreadPool();

Dans le backend Rust:

    binder::ProcessState::start_thread_pool();
    binder::add_service("myservice", my_service_binder).expect("Failed to register service?");
    binder::ProcessState::join_thread_pool();

Avec le backend Rust asynchrone, vous avez besoin de deux pools de threads: le binder et Tokio. Cela signifie que les applications utilisant Rust asynchrone nécessitent des considérations particulières, en particulier en ce qui concerne l'utilisation de join_thread_pool. Pour en savoir plus, consultez la section sur l'enregistrement des services.

Noms réservés

C++, Java et Rust réservent certains noms en tant que mots clés ou pour une utilisation spécifique à la langue. Bien qu'AIDL n'applique pas de restrictions basées sur des règles de langage, l'utilisation de noms de champ ou de type correspondant à un nom réservé peut entraîner un échec de compilation pour C++ ou Java. Pour Rust, le champ ou le type est renommé à l'aide de la syntaxe d'identifiant brut, accessible à l'aide du préfixe r#.

Dans la mesure du possible, nous vous recommandons d'éviter d'utiliser des noms réservés dans vos définitions AIDL pour éviter les liaisons peu ergonomiques ou les échecs de compilation.

Si vous avez déjà des noms réservés dans vos définitions AIDL, vous pouvez renommer des champs en toute sécurité tout en restant compatible avec le protocole. Vous devrez peut-être mettre à jour votre code pour continuer à compiler, mais les programmes déjà compilés continuent à interagir.

Noms à éviter: