AIDL-Back-Ends

Ein AIDL-Back-End ist ein Ziel für die Generierung von Stub-Code. AIDL-Dateien werden immer in einer bestimmten Sprache mit einer bestimmten Laufzeit verwendet. Je nach Kontext sollten Sie verschiedene AIDL-Back-Ends verwenden.

In der folgenden Tabelle bezieht sich die Stabilität der API-Oberfläche auf die Möglichkeit, Code für diese API-Oberfläche so zu kompilieren, dass er unabhängig vom system.img-libbinder.so-Binärcode bereitgestellt werden kann.

AIDL hat die folgenden Back-Ends:

Back-End Sprache API-Oberfläche Build-Systeme
Java Java SDK/SystemApi (stabil*) alle
NDK C++ libbinder_ndk (stabil*) hilfe_schnittstelle
CPP C++ libbinder (instabil) alle
Rust Rust libbinder_rs (stabil*) aidl_interface
  • Diese API-Oberflächen sind stabil, aber viele der APIs, z. B. für die Dienstverwaltung, sind für die interne Nutzung der Plattform reserviert und nicht für Apps verfügbar. Weitere Informationen zur Verwendung von AIDL in Apps finden Sie in der Entwicklerdokumentation.
  • Das Rust-Backend wurde in Android 12 eingeführt. Das NDK-Backend ist seit Android 10 verfügbar.
  • Der Rust-Crust basiert auf libbinder_ndk, wodurch er stabil und portabel ist. APEX-Objekte verwenden den Binder-Container genauso wie alle anderen Nutzer auf der Systemseite. Der Rust-Teil wird in einem APEX-Archiv verpackt und versendet. Das hängt von der libbinder_ndk.so auf der Systempartition ab.

Systeme erstellen

Je nach Back-End gibt es zwei Möglichkeiten, AIDL in Stub-Code zu kompilieren. Weitere Informationen zu den Build-Systemen finden Sie in der Soong-Modulreferenz.

Kern-Build-System

In jedem cc_- oder java_-Android.bp-Modul (oder in den entsprechenden Android.mk-Dateien) können .aidl-Dateien als Quelldateien angegeben werden. In diesem Fall werden die Java/C++-Backends von AIDL verwendet (nicht das NDK-Backend) und die Klassen zur Verwendung der entsprechenden AIDL-Dateien werden dem Modul automatisch hinzugefügt. Optionen wie local_include_dirs, die dem Build-System den Stammpfad zu den AIDL-Dateien in diesem Modul angeben, können in diesen Modulen in einer aidl:-Gruppe angegeben werden. Das Rust-Backend ist nur für die Verwendung mit Rust vorgesehen. rust_-Module werden anders behandelt, da AIDL-Dateien nicht als Quelldateien angegeben werden. Stattdessen generiert das aidl_interface-Modul eine rustlib namens <aidl_interface name>-rust, mit der eine Verknüpfung hergestellt werden kann. Weitere Informationen finden Sie im Beispiel für Rust-AIDL.

aidl_interface

Die mit diesem Build-System verwendeten Typen müssen strukturiert sein. Damit sie strukturiert sind, müssen Parcelable-Objekte Felder direkt enthalten und keine Deklarationen von Typen sein, die direkt in Zielsprachen definiert sind. Weitere Informationen dazu, wie strukturiertes AIDL in stabiles AIDL passt, finden Sie unter Strukturiertes und stabiles AIDL im Vergleich.

Typen

Sie können den aidl-Compiler als Referenzimplementierung für Typen betrachten. Wenn Sie eine Benutzeroberfläche erstellen, rufen Sie aidl --lang=<backend> ... auf, um die resultierende Benutzeroberflächendatei aufzurufen. Wenn Sie das aidl_interface-Modul verwenden, können Sie die Ausgabe in out/soong/.intermediates/<path to module>/ ansehen.

Java-/AIDL-Typ C++-Typ NDK-Typ Rosttyp
Boolesch bool bool Boolescher Wert
Byte8 int8_t int8_t i8
char char16_t char16_t u16
int int32_t int32_t i32
long int64_t int64_t i64
schweben float float f32
Doppelt Doppelt Doppelt f64
String android::String16 std::string String
android.os.Parcelable android::Parcelable
Binder android::IBinder ndk::SpAIBinder binder::SpIBinder
D[] std::vector<T> std::vector<T> Eingabe: &[T]
Ausgabe: Vec<T>
byte[] std::vector<uint8_t> std::vector<int8_t>1 Eingabe: &[u8]
Ausgabe: Vec<u8>
List<T> std::vector<T>2 std::vector<T>3 Eingabe: &[T]4
Ausgabe: Vec<T>
FileDescriptor android::base::unique_fd binder::parcel::ParcelFileDescriptor
ParcelFileDescriptor android::os::ParcelFileDescriptor ndk::ScopedFileDescriptor, binder::parcel::ParcelFileDescriptor
Schnittstellentyp (T) android::sp<T> std::shared_ptr<T>7 binder::Strong
Typ „Parcelable“ (T) T T T
Union-Typ (T)5 T T T
T[N] 6 std::array<T, N> std::array<T, N> [T; N]

1. Unter Android 12 oder höher wird für Byte-Arrays aus Kompatibilitätsgründen uint8_t anstelle von int8_t verwendet.

2. Das C++-Backend unterstützt List<T>, wobei T eine der folgenden Optionen sein kann: String, IBinder, ParcelFileDescriptor oder parcelable. Unter Android 13 und höher kann T jeder nicht primitive Typ (einschließlich Schnittstellentypen) sein, mit Ausnahme von Arrays. AOSP empfiehlt die Verwendung von Arraytypen wie T[], da sie in allen Back-Ends funktionieren.

3. Das NDK-Back-End unterstützt List<T>, wobei T entweder String, ParcelFileDescriptor oder parzellenbar ist. In Android 13 oder höher kann T ein beliebiger nicht-primitiver Typ sein, mit Ausnahme von Arrays.

4. Typen werden in Rust-Code unterschiedlich übergeben, je nachdem, ob es sich um Eingabe (ein Argument) oder Ausgabe (einen zurückgegebenen Wert) handelt.

5. Union-Typen werden unter Android 12 und höher unterstützt.

6. Unter Android 13 oder höher werden Arrays mit fester Größe unterstützt. Arrays mit fester Größe können mehrere Dimensionen haben (z. B. int[3][4]). Im Java-Backend werden Arrays mit fester Größe als Arraytypen dargestellt.

7. Verwenden Sie SharedRefBase::make\<My\>(... args ...), um ein Binder-SharedRefBase-Objekt zu instanziieren. Diese Funktion erstellt ein std::shared_ptr\<T\>-Objekt, das ebenfalls intern verwaltet wird, falls der Binder zu einem anderen Prozess gehört. Wenn Sie das Objekt auf andere Weise erstellen, führt das zu doppelten Eigentumsrechten.

8. Siehe auch Java-/AIDL-Typ byte[].

Richtung (in/out/inout)

Die Typen der Argumente für Funktionen können als in, out oder inout angegeben werden. Damit wird festgelegt, in welche Richtung Informationen für einen IPC-Aufruf übergeben werden. in ist die Standardrichtung und gibt an, dass Daten vom Aufrufer an den Gerufenen übergeben werden. out bedeutet, dass Daten vom Angerufenen an den Anrufer übergeben werden. inout ist eine Kombination aus beiden. Das Android-Team empfiehlt jedoch, den Argument-Spezifizierer inout zu vermeiden. Wenn Sie inout mit einer versionierten Benutzeroberfläche und einem älteren Aufgerufenen verwenden, werden die zusätzlichen Felder, die nur im Aufrufer vorhanden sind, auf die Standardwerte zurückgesetzt. In Rust erhält ein normaler inout-Typ &mut Vec<T> und ein Listeninout-Typ &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);
}

UTF8/UTF16

Mit dem CPP-Back-End können Sie auswählen, ob Strings utf-8 oder utf-16 sind. Deklarieren Sie Strings in AIDL als @utf8InCpp String, um sie automatisch in UTF-8 umzuwandeln. Die Back-Ends NDK und Rust verwenden immer utf-8-Strings. Weitere Informationen zur Annotation utf8InCpp finden Sie unter Annotationen in AIDL.

Null-Zulässigkeit

Typen, die null sein können, können Sie mit @nullable annotieren. Weitere Informationen zur Annotation nullable finden Sie unter Annotationen in AIDL.

Benutzerdefinierte Pakete

Ein benutzerdefiniertes Parcelable ist ein Parcelable, das manuell in einem Ziel-Backend implementiert wird. Verwenden Sie benutzerdefinierte Parzellen nur dann, wenn Sie versuchen, andere Sprachen für eine vorhandene benutzerdefiniertes Paket zu unterstützen, das nicht geändert werden kann.

Um ein benutzerdefiniertes Parcelable zu deklarieren, damit es von AIDL erkannt wird, sieht die Parcelable-Deklaration in AIDL so aus:

    package my.pack.age;
    parcelable Foo;

Standardmäßig wird dadurch ein Java-Parcelable deklariert, bei dem my.pack.age.Foo eine Java-Klasse ist, die die Schnittstelle Parcelable implementiert.

Verwenden Sie cpp_header zum Deklarieren eines benutzerdefinierten CPP-Back-End-Parzellenformats in AIDL:

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

Die C++-Implementierung in my/pack/age/Foo.h sieht so aus:

    #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);
    };

Verwenden Sie ndk_header, um eine benutzerdefinierte NDK-Parcelable-Klasse in AIDL zu deklarieren:

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

Die NDK-Implementierung in android/pack/age/Foo.h sieht so aus:

    #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);
    };

In Android 15 verwenden Sie für die Deklaration einer benutzerdefinierten Rust-Parcelable in AIDL rust_type:

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

Die Rust-Implementierung in rust_crate/src/lib.rs sieht so aus:

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);

Sie können diese Parcelable-Klasse dann als Typ in AIDL-Dateien verwenden, sie wird jedoch nicht von AIDL generiert. Geben Sie die Operatoren < und == für benutzerdefinierte CPP/NDK-Back-End-Parzellenables an, um sie in union zu verwenden.

Standardwerte

Strukturierte Parcelables können Standardwerte pro Feld für Primitive, Strings und Arrays dieser Typen deklarieren.

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

Wenn im Java-Backend keine Standardwerte vorhanden sind, werden Felder als Nullwerte für primitive Typen und als null für nicht primitive Typen initialisiert.

In anderen Back-Ends werden Felder mit Standardwerten initialisiert, wenn keine Standardwerte definiert sind. Im C++-Backend werden String-Felder beispielsweise als leerer String und List<T>-Felder als leere vector<T> initialisiert. @nullable-Felder werden als Felder mit Nullwert initialisiert.

Gewerkschaften

AIDL-Unions sind getaggt und ihre Funktionen sind in allen Back-Ends ähnlich. Sie werden standardmäßig mit dem Standardwert des ersten Felds erstellt und es gibt eine sprachspezifische Möglichkeit, mit ihnen zu interagieren.

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

Java-Beispiel

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

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

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

Beispiel für C++ und 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)

Beispiel für Rost

In Rust werden Unionen als enums implementiert und haben keine expliziten Getter und Setter.

    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

Fehlerbehandlung

Das Android-Betriebssystem bietet integrierte Fehlertypen für Dienste, die beim Melden von Fehlern verwendet werden können. Sie werden von Binder verwendet und können von allen Diensten verwendet werden, die eine Binder-Schnittstelle implementieren. Ihre Verwendung ist in der AIDL-Definition gut dokumentiert und sie erfordern keinen benutzerdefinierten Status oder Rückgabetyp.

Ausgabeparameter mit Fehlern

Wenn eine AIDL-Funktion einen Fehler meldet, werden die Ausgabeparameter möglicherweise nicht initialisiert oder geändert. Insbesondere können Ausgabeparameter geändert werden, wenn der Fehler beim Entpacken auftritt, anstatt bei der Verarbeitung der Transaktion selbst. Wenn Sie von einer AIDL-Funktion einen Fehler erhalten, sollten Sie davon ausgehen, dass alle inout- und out-Parameter sowie der Rückgabewert (der in einigen Back-Ends wie ein out-Parameter funktioniert) sich in einem unbestimmten Zustand befinden.

Welche Fehlerwerte zu verwenden sind

Viele der vordefinierten Fehlerwerte können in allen AIDL-Schnittstellen verwendet werden, einige werden jedoch auf besondere Weise behandelt. EX_UNSUPPORTED_OPERATION und EX_ILLEGAL_ARGUMENT sind beispielsweise zulässig, wenn sie die Fehlerbedingung beschreiben. EX_TRANSACTION_FAILED darf jedoch nicht verwendet werden, da es von der zugrunde liegenden Infrastruktur speziell behandelt wird. Weitere Informationen zu diesen vordefinierten Werten finden Sie in den backendspezifischen Definitionen.

Wenn für die AIDL-Schnittstelle zusätzliche Fehlerwerte erforderlich sind, die nicht von den integrierten Fehlertypen abgedeckt werden, kann der spezielle dienstspezifische integrierte Fehler verwendet werden, der die Aufnahme eines vom Nutzer definierten dienstspezifischen Fehlerwerts ermöglicht. Diese dienstspezifischen Fehler werden in der Regel in der AIDL-Schnittstelle als enum mit const int- oder int-Unterstützung definiert und nicht vom Binder geparst.

In Java werden Fehler Ausnahmen zugeordnet, z. B. android.os.RemoteException. Für servicespezifische Ausnahmen verwendet Java android.os.ServiceSpecificException zusammen mit dem benutzerdefinierten Fehler.

Für nativen Code in Android werden keine Ausnahmen verwendet. Das CPP-Backend verwendet android::binder::Status. Das NDK-Backend verwendet ndk::ScopedAStatus. Jede von AIDL generierte Methode gibt einen dieser Werte zurück, der den Status der Methode darstellt. Das Rust-Backend verwendet dieselben Ausnahmecodewerte wie das NDK, wandelt sie jedoch in native Rust-Fehler (StatusCode, ExceptionCode) um, bevor sie an den Nutzer gesendet werden. Bei dienstspezifischen Fehlern wird für die zurückgegebene Status oder ScopedAStatus die EX_SERVICE_SPECIFIC zusammen mit dem benutzerdefinierten Fehler verwendet.

Die integrierten Fehlertypen finden Sie in den folgenden Dateien:

Back-End Definition
Java android/os/Parcel.java
CPP binder/Status.h
NDK android/binder_status.h
Rust android/binder_status.h

Verschiedene Back-Ends verwenden

Diese Anleitung bezieht sich auf Android-Plattformcode. In diesen Beispielen wird der definierte Typ my.package.IFoo verwendet. Eine Anleitung zur Verwendung des Rust-Backends finden Sie im Rust-AIDL-Beispiel auf der Seite Android Rust-Muster.

Importtypen

Unabhängig davon, ob der definierte Typ eine Schnittstelle, ein Parcelable oder eine Union ist, können Sie ihn in Java importieren:

import my.package.IFoo;

Oder im CPP-Backend:

#include <my/package/IFoo.h>

Oder im NDK-Backend (beachten Sie den zusätzlichen Namespace aidl):

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

Oder im Rust-Backend:

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

Obwohl Sie einen verschachtelten Typ in Java importieren können, müssen Sie in den CPP/NDK-Back-Ends den Header für seinen Stammtyp angeben. Wenn Sie beispielsweise einen verschachtelten Typ Bar importieren, der in my/package/IFoo.aidl definiert ist (IFoo ist der Stammtyp der Datei), müssen Sie <my/package/IFoo.h> für das CPP-Backend (oder <aidl/my/package/IFoo.h> für das NDK-Backend) angeben.

Dienste implementieren

Wenn Sie einen Dienst implementieren möchten, müssen Sie von der nativen Stub-Klasse ableiten. Diese Klasse liest Befehle vom Binder-Treiber und führt die von Ihnen implementierten Methoden aus. Angenommen, Sie haben eine AIDL-Datei wie diese:

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

In Java müssen Sie diese Klasse erweitern:

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

Im CPP-Backend:

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

Im NDK-Backend (beachten Sie den zusätzlichen Namespace aidl):

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

Im Rust-Backend:

    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(())
        }
    }

Oder mit async Rust:

    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(())
        }
    }

Registrieren und Dienste nutzen

Dienste in der Android-Plattform werden in der Regel mit dem Prozess servicemanager registriert. Zusätzlich zu den folgenden APIs prüfen einige APIs den Dienst. Das bedeutet, dass sie sofort zurückgegeben werden, wenn der Dienst nicht verfügbar ist. Genaue Informationen findest du in der entsprechenden servicemanager-Benutzeroberfläche. Diese Vorgänge können nur bei der Kompilierung auf der Android-Plattform ausgeführt werden.

In 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"));

Im CPP-Back-End:

    #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"));

Im NDK-Backend (beachten Sie den zusätzlichen Namespace aidl):

    #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")));

Im Rust-Backend:

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()
}

Im asynchronen Rust-Backend mit einer Single-Threaded-Laufzeit:

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 will run on this thread.
    std::future::pending().await
}

Ein wichtiger Unterschied zu den anderen Optionen besteht darin, dass wir join_thread_pool nicht aufrufen, wenn wir async Rust und eine einzeilige Laufzeit verwenden. Das liegt daran, dass Sie Tokio einen Thread geben müssen, in dem es die gestarteten Aufgaben ausführen kann. In diesem Beispiel dient dazu der Haupt-Thread. Alle Aufgaben, die mit tokio::spawn gestartet werden, werden im Haupt-Thread ausgeführt.

Im asynchronen Rust-Backend mit einer mehrstufigen Laufzeit:

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();
    });
}

Bei der Multithread-Tokio-Laufzeit werden erzeugte Aufgaben nicht im Hauptthread ausgeführt. Daher ist es sinnvoller, join_thread_pool im Hauptthread aufzurufen, damit der Hauptthread nicht nur inaktiv ist. Sie müssen den Aufruf in block_in_place einschließen, um den asynchronen Kontext zu verlassen.

Sie können eine Benachrichtigung erhalten, wenn ein Dienst, der einen Binder hostet, ausfällt. So lassen sich Lecks von Rückruf-Proxys vermeiden oder Fehler leichter beheben. Führen Sie diese Aufrufe für Binder-Proxy-Objekte aus.

  • In Java verwenden Sie android.os.IBinder::linkToDeath.
  • Verwende im CPP-Backend android::IBinder::linkToDeath.
  • Verwenden Sie im NDK-Back-End AIBinder_linkToDeath.
  • Erstelle im Rust-Backend ein DeathRecipient-Objekt und rufe dann my_binder.link_to_death(&mut my_death_recipient) auf. Da der DeathRecipient-Callback dem DeathRecipient-Objekt zugewiesen ist, muss dieses Objekt so lange aktiv bleiben, wie du Benachrichtigungen erhalten möchtest.

Informationen zum Anrufer

Wenn ein Kernel-Binder-Aufruf empfangen wird, sind Informationen zum Anrufer in mehreren APIs verfügbar. Die PID (oder Prozess-ID) bezieht sich auf die Linux-Prozess-ID des Prozesses, der eine Transaktion sendet. Die UID (Nutzer-ID) bezieht sich auf die Linux-Nutzer-ID. Beim Empfangen eines einseitigen Anrufs ist die anrufende PID 0. Außerhalb eines Binder-Transaktionskontexts geben diese Funktionen die PID und die UID des aktuellen Prozesses zurück.

Im Java-Backend:

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

Im CPP-Backend:

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

Im NDK-Backend:

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

Geben Sie beim Implementieren der Schnittstelle im Rust-Backend Folgendes an (anstatt den Standardwert zuzulassen):

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

Fehlerberichte und Debugging-API für Dienste

Wenn Fehlerberichte ausgeführt werden (z. B. mit adb bugreport), werden Informationen aus dem gesamten System erfasst, um verschiedene Probleme zu beheben. Bei AIDL-Diensten verwenden Fehlerberichte das binäre dumpsys für alle Dienste, die beim Dienstmanager registriert sind, um ihre Informationen in den Fehlerbericht zu übertragen. Sie können auch dumpsys in der Befehlszeile verwenden, um Informationen von einem Dienst mit dumpsys SERVICE [ARGS] abzurufen. In den C++- und Java-Backends können Sie die Reihenfolge, in der Dienste gedumpt werden, mithilfe zusätzlicher Argumente für addService steuern. Sie können dumpsys --pid SERVICE auch verwenden, um während des Debuggens die PID eines Dienstes abzurufen.

Wenn Sie Ihrem Dienst benutzerdefinierte Ausgabe hinzufügen möchten, können Sie die dump-Methode in Ihrem Serverobjekt überschreiben, genau wie Sie jede andere in einer AIDL-Datei definierte IPC-Methode implementieren. In diesem Fall sollten Sie das Dumping auf die App-Berechtigung android.permission.DUMP oder auf bestimmte UIDs beschränken.

Im Java-Backend:

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

Im CPP-Backend:

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

Im NDK-Backend:

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

Geben Sie beim Implementieren der Schnittstelle im Rust-Backend Folgendes an (anstatt den Standardwert zuzulassen):

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

Schwache Zeiger verwenden

Sie können einen schwachen Verweis auf ein Binder-Objekt halten.

Java unterstützt zwar WeakReference, schwache Binderreferenzen auf der nativen Ebene jedoch nicht.

Im CPP-Backend ist der schwache Typ wp<IFoo>.

Verwenden Sie im NDK-Back-End ScopedAIBinder_Weak:

#include <android/binder_auto_utils.h>

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

Im Rust-Back-End verwenden Sie WpIBinder oder Weak<IFoo>:

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

Interface-Deskriptor dynamisch abrufen

Der Schnittstellendeskriptor identifiziert den Typ einer Schnittstelle. Das ist hilfreich beim Debuggen oder wenn Sie einen unbekannten Binder haben.

In Java können Sie den Schnittstellendeskriptor mit folgendem Code abrufen:

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

Im CPP-Backend:

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

Das NDK- und das Rust-Backend unterstützen diese Funktion nicht.

Schnittstellendeskriptor statisch abrufen

Manchmal (z. B. bei der Registrierung von @VintfStability-Diensten) müssen Sie wissen, was der Interface-Beschreibungsblock statisch ist. In Java können Sie den Deskriptor abrufen, indem Sie Code wie den folgenden hinzufügen:

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

Im CPP-Back-End:

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

Im NDK-Backend (beachten Sie den zusätzlichen Namespace aidl):

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

Im Rust-Backend:

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

Enum-Bereich

In nativen Back-Ends können Sie über die möglichen Werte eines Enumerationstyps iterieren. Aus Gründen der Codegröße wird dies in Java nicht unterstützt.

Für ein in AIDL definiertes Enum MyEnum wird die Iteration so bereitgestellt:

Im CPP-Backend:

    ::android::enum_range<MyEnum>()

Im NDK-Backend:

   ::ndk::enum_range<MyEnum>()

Im Rust-Backend:

    MyEnum::enum_values()

Threadverwaltung

Jede Instanz von libbinder in einem Prozess verwaltet einen Threadpool. In den meisten Anwendungsfällen sollte dies genau ein Threadpool sein, der von allen Back-Ends gemeinsam genutzt wird. Die einzige Ausnahme ist, wenn vom Anbietercode eine weitere Kopie von libbinder geladen wird, um mit /dev/vndbinder zu kommunizieren. Da sich dieser Vorgang auf einem separaten Binderknoten befindet, wird der Threadpool nicht gemeinsam genutzt.

Beim Java-Backend kann der Threadpool nur vergrößert werden, da er bereits gestartet ist:

    BinderInternal.setMaxThreads(<new larger value>);

Für das CPP-Backend sind die folgenden Vorgänge verfügbar:

    // 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();

Ähnliches gilt für das NDK-Backend:

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

Im Rust-Backend:

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

Für das asynchrone Rust-Backend benötigen Sie zwei Threadpools: binder und Tokio. Das bedeutet, dass bei Apps mit async Rust besondere Überlegungen angestellt werden müssen, insbesondere bei der Verwendung von join_thread_pool. Weitere Informationen finden Sie im Abschnitt zum Registrieren von Diensten.

Reservierte Namen

In C++, Java und Rust sind einige Namen als Keywords oder für die sprachspezifische Verwendung reserviert. AIDL erzwingt zwar keine Einschränkungen auf Basis von Sprachregeln, die Verwendung von Feld- oder Typnamen, die mit einem reservierten Namen übereinstimmen, kann jedoch zu einem Kompilierungsfehler für C++ oder Java führen. In Rust wird das Feld oder der Typ mithilfe der Syntax „rohe Kennung“ umbenannt, auf die über das Präfix r# zugegriffen werden kann.

Wir empfehlen, nach Möglichkeit keine reservierten Namen in Ihren AIDL-Definitionen zu verwenden, um unergonomische Bindungen oder einen vollständigen Kompilierungsfehler zu vermeiden.

Wenn Ihre AIDL-Definitionen bereits reservierte Namen enthalten, können Sie Felder problemlos umbenennen und gleichzeitig protokollkompatibel bleiben. Möglicherweise müssen Sie Ihren Code aktualisieren, um mit der Erstellung fortzufahren. Alle bereits erstellten Programme funktionieren jedoch weiterhin.

Zu vermeidende Namen: