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 unterschiedliche AIDL-Backends 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 Backends:

Back-End Sprache API-Oberfläche Build-Systeme
Java Java SDK/SystemApi (stabil*) alle
NDK C++ libbinder_ndk (stabil*) aidl_interface
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-Chrom ist auf libbinder_ndk aufgebaut, 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.

Build-Systeme

Je nach Backend 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 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
Doppelt Doppelt Doppelt f64
String android::String16 std::string Eingabe: &str
Ausgabe: String
android.os.Parcelable android::Parcelable
IBinder android::IBinder ndk::SpAIBinder binder::SpIBinder
T[] 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 In: &[T]4
Aus: Vec<T>
FileDescriptor android::base::unique_fd binder::parcel::ParcelFileDescriptor
ParcelFileDescriptor android::os::ParcelFileDescriptor ndk::ScopedFileDescriptor binder::parcel::ParcelFileDescriptor
interface type (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 werden 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-Backend unterstützt List<T>, wobei T eine der folgenden Optionen sein kann: String, ParcelFileDescriptor oder parcelable. Unter Android 13 oder höher kann T jeder nicht primitive 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 auch intern verwaltet wird, falls der Binder 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)

Wenn Sie die Typen der Argumente für Funktionen angeben, können Sie in, out oder inout angeben. 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 in UTF-8 oder UTF-16 vorliegen. Deklarieren Sie Strings in AIDL als @utf8InCpp String, um sie automatisch in UTF-8 umzuwandeln. Die NDK- und Rust-Backends verwenden immer UTF-8-Strings. Weitere Informationen zur utf8InCpp-Anmerkung finden Sie unter Anmerkungen in AIDL.

Null-Zulässigkeit

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

Benutzerdefinierte Pakete

Ein benutzerdefiniertes Parcelable ist ein Parcelable, das manuell in einem Ziel-Backend implementiert wird. Verwenden Sie benutzerdefinierte Parcelable-Objekte nur, wenn Sie für ein vorhandenes benutzerdefiniertes Parcelable-Objekt, das nicht geändert werden kann, Unterstützung für andere Sprachen hinzufügen möchten.

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, um ein benutzerdefiniertes CPP-Backend in AIDL zu deklarieren:

    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 dann als Typ in AIDL-Dateien verwenden, sie wird jedoch nicht von AIDL generiert. Biete <- und ==-Operatoren für benutzerdefinierte Parcelables des CPP/NDK-Backends an, um sie in union zu verwenden.

Standardwerte

Strukturierte Parcelable-Objekte können pro Feld Standardwerte für primitive Typen, Strings und Arrays dieser Typen angeben.

    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 Status zurück. 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;

Sie können einen verschachtelten Typ zwar in Java importieren, in den CPP/NDK-Backends müssen Sie jedoch den Header für den Stammtyp einschließen. 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 ausgeführt werden, wenn die Kompilierung für die Plattform Android erfolgt.

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-Backend:

    #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 angeben müssen, in dem die gestarteten Aufgaben ausgeführt werden können. 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 mehrstufigen 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ühre diese Aufrufe auf Binder-Proxy-Objekten aus.

  • In Java verwenden Sie android.os.IBinder::linkToDeath.
  • Verwende im CPP-Backend android::IBinder::linkToDeath.
  • Verwenden Sie im NDK-Backend 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.

Anruferinformationen

Wenn ein Kernel-Binder-Aufruf empfangen wird, sind Informationen zum Anrufer in mehreren APIs verfügbar. Die PID (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 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 wird in Fehlerberichten das Binärformat dumpsys für alle beim Dienstmanager registrierten Dienste verwendet, um die 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, so wie Sie jede andere in einer AIDL-Datei definierte IPC-Methode implementieren. Dabei 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, aber keine schwachen Binderreferenzen in der nativen Schicht.

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

Verwenden Sie im NDK-Backend ScopedAIBinder_Weak:

#include <android/binder_auto_utils.h>

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

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

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

Interface-Deskriptor dynamisch abrufen

Der Interface-Deskriptor gibt den Typ einer Schnittstelle an. Das ist hilfreich beim Debuggen oder wenn Sie einen unbekannten Binder haben.

In Java können Sie den Interface-Descriptor 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 Descriptor mit folgendem Code abrufen:

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

Im CPP-Backend:

    #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. Für die meisten Anwendungsfälle sollte dies genau ein Threadpool sein, der für alle Back-Ends freigegeben ist. Die einzige Ausnahme ist, wenn der Anbietercode eine weitere Kopie von libbinder lädt, 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. In AIDL werden keine Einschränkungen auf der Grundlage von Sprachregeln erzwungen. Die Verwendung von Feld- oder Typennamen, die mit einem reservierten Namen übereinstimmen, kann jedoch zu einem Kompilierungsfehler in C++ oder Java führen. In Rust wird das Feld oder der Typ mit 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 Sie in Ihren AIDL-Definitionen bereits reservierte Namen haben, können Sie Felder gefahrlos umbenennen und gleichzeitig protokollkompatibel bleiben. Möglicherweise müssen Sie Ihren Code aktualisieren, um mit dem Erstellen fortzufahren. Bereits erstellte Programme sind jedoch weiterhin interoperabel.

Zu vermeidende Namen: