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-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 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 Plattformnutzung 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. APEXe verwenden den Binder-Container genauso wie alle anderen auf der Systemseite. Der Rust-Teil wird in einem APEX-Archiv verpackt und so ausgeliefert. Das hängt von derlibbinder_ndk.so
auf der Systempartition ab.
Build-Systeme
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.
Core 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 kann nur mit Rust verwendet werden. rust_
-Module werden anders behandelt, da AIDL-Dateien nicht als Quelldateien angegeben werden.
Stattdessen generiert das aidl_interface
-Modul eine rustlib
mit dem Namen <aidl_interface name>-rust
, mit der eine Verknüpfung hergestellt werden kann. Weitere Informationen finden Sie im Rust-AIDL-Beispiel.
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> |
Liste<T> | std::vector<T>2 | std::vector<T>3 | Eingabe: &[T]4 Ausgabe: Vec<T> |
FileDescriptor | android::base::unique_fd | – | – |
ParcelFileDescriptor | android::os::ParcelFileDescriptor | ndk::ScopedFileDescriptor | binder::parcel::ParcelFileDescriptor |
interface type (T) | android::sp<T> | std::shared_ptr<T>7 | binder::Strong |
parcelable-Typ (T) | T | T | T |
Uniontyp (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 in Byte-Arrays aus Kompatibilitätsgründen uint8_t anstelle von int8_t verwendet.
2. Das C++-Back-End unterstützt List<T>
, wobei T
eine der folgenden Optionen sein kann: String
, IBinder
, ParcelFileDescriptor
oder parcelable. Unter Android 13 oder 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 Backends 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 eine Eingabe (ein Argument) oder eine Ausgabe (einen zurückgegebenen Wert) handelt.
5. Union-Typen werden ab Android 12 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 doppelter Inhaberschaft.
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-Backend 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 benutzerdefinierter 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 in AIDL bekannt ist, 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 Parcelable
-Schnittstelle implementiert.
Verwenden Sie cpp_header
, um ein benutzerdefiniertes CPP-Back-End 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-Klasse 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
Bei strukturierten Parcelable-Objekten können pro Feld Standardwerte für primitive Typen, String
s und Arrays dieser Typen deklariert werden.
parcelable Foo {
int numField = 42;
String stringField = "string value";
char charValue = 'a';
...
}
Wenn im Java-Backend Standardwerte fehlen, 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 Nullwertfelder 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 alle inout
- und out
-Parameter sowie der Rückgabewert (der in einigen Back-Ends wie ein out
-Parameter funktioniert) als in einem unbestimmten Zustand betrachtet werden.
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 in Ordnung, 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 vordefinierten Fehlertypen abgedeckt werden, kann der spezielle dienstspezifische vordefinierte Fehler verwendet werden, der die Aufnahme eines vom Nutzer definierten dienstspezifischen Fehlerwerts ermöglicht. Diese dienstspezifischen Fehler werden in der AIDL-Schnittstelle in der Regel 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 dienstspezifische Ausnahmen verwendet Java android.os.ServiceSpecificException
zusammen mit dem benutzerdefinierten Fehler.
In nativem Code in Android werden keine Ausnahmen verwendet. Das CPP-Backend verwendet android::binder::Status
. Das NDK-Back-End 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
EX_SERVICE_SPECIFIC
zusammen mit dem benutzerdefinierten Fehler verwendet.
Die vordefinierten 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 gilt speziell für 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 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.
Schnittstelle implementieren
Wenn Sie eine Schnittstelle implementieren möchten, müssen Sie von der nativen Stub-Klasse ableiten. Eine Implementierung einer Schnittstelle wird oft als Dienst bezeichnet, wenn sie beim Dienstmanager oder android.app.ActivityManager
registriert ist, und als Callback, wenn sie von einem Client eines Dienstes registriert ist. Je nach Verwendung werden jedoch verschiedene Namen für die Benutzeroberflächenimplementierung verwendet. Die Stub-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 die generierte Stub
-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 asynchronem 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(())
}
}
Dienste registrieren und abrufen
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-Back-End mit einer einstufigen 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 join_thread_pool
nicht aufgerufen wird, wenn wir async Rust und eine einzeilige Laufzeit verwenden. Das liegt daran, dass Sie Tokio einen Thread geben müssen, in dem die erzeugten Aufgaben ausgeführt werden können. In diesem Beispiel dient der Haupt-Thread diesem Zweck. Alle Aufgaben, die mit tokio::spawn
gestartet werden, werden im Hauptthread ausgeführt.
Im asynchronen Rust-Back-End 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 die gestarteten Aufgaben nicht im Hauptthread ausgeführt. Daher ist es sinnvoller, join_thread_pool
im Hauptthread aufzurufen, damit der Hauptthread nicht nur inaktiv ist. Du musst den Aufruf in block_in_place
einschließen, um den asynchronen Kontext zu verlassen.
Link zum Todesfall
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 für Binder-Proxyobjekte aus.
- Verwenden Sie in Java
android.os.IBinder::linkToDeath
. - Verwenden Sie im CPP-Backend
android::IBinder::linkToDeath
. - Verwenden Sie im NDK-Backend
AIBinder_linkToDeath
. - Erstelle im Rust-Backend ein
DeathRecipient
-Objekt und rufe dannmy_binder.link_to_death(&mut my_death_recipient)
auf. Da der Callback zuDeathRecipient
gehört, muss dieses Objekt so lange aktiv bleiben, wie du Benachrichtigungen erhalten möchtest.
Anruferinformationen
Wenn ein Kernel-Binder-Aufruf empfangen wird, sind Informationen zum Aufrufer 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. Wenn ein einseitiger Anruf empfangen wird, 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 Methode dump
in Ihrem Serverobjekt überschreiben, genau 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 beim Debuggen oder bei einem unbekannten Binder hilfreich.
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.
Interface-Deskriptor statisch abrufen
Manchmal (z. B. bei der Registrierung von @VintfStability
-Diensten) müssen Sie den Interface-Beschreibungsblock statisch kennen. 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 die möglichen Werte eines enums durchgehen. 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 von allen Back-Ends gemeinsam genutzt wird.
Die einzige Ausnahme ist, wenn der Anbietercode möglicherweise 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();
Ähnlich im 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 „Raw Identifier“ 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: