Ein AIDL-Back-End ist ein Ziel für die Generierung von Stub-Code. Verwenden Sie immer AIDL-Dateien in einer bestimmten Sprache mit einer bestimmten Laufzeit. 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 Back-Ends:
Back-End | Sprache | API-Oberfläche | Build-Systeme |
---|---|---|---|
Java | Java | SDK oder 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 unter Android Interface Definition Language (AIDL).
- 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-Anwendungen verwenden das Binder-Crate auf Systemebene auf standardmäßige Weise. Der Rust-Teil wird in einem APEX-Archiv verpackt und versendet. Dieser Teil hängt von derlibbinder_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 jeder cc_
- oder java_
-Android.bp module
(oder in den entsprechenden Android.mk
-Dateien) können Sie AIDL-Dateien (.aidl
) als Quelldateien angeben. In diesem Fall werden die Java- oder CPP-Back-Ends von AIDL verwendet (nicht das NDK-Back-End) und die Klassen zur Verwendung der entsprechenden AIDL-Dateien werden dem Modul automatisch hinzugefügt. Sie können in diesen Modulen unter einer aidl:
-Gruppe Optionen wie local_include_dirs
angeben, die dem Build-System den Stammpfad zu den AIDL-Dateien in diesem Modul angeben.
Das Rust-Back-End darf nur mit Rust verwendet werden. rust_
-Module werden anders behandelt, da AIDL-Dateien nicht als Quelldateien angegeben werden. Stattdessen generiert das aidl_interface
-Modul einen rustlib
mit dem Namen aidl_interface_name-rust
, mit dem eine Verknüpfung hergestellt werden kann. Weitere Informationen finden Sie im Beispiel für Rust-AIDL.
aidl_interface
Typen, die mit dem aidl_interface
-Buildsystem verwendet werden, müssen strukturiert sein. Damit sie strukturiert sind, müssen Parcelable-Objekte direkt Felder enthalten und keine Deklarationen von Typen sein, die direkt in Zielsprachen definiert sind. Weitere Informationen dazu, wie sich strukturierte AIDL in stabile AIDL einfügt, finden Sie unter Strukturiertes und stabiles AIDL im Vergleich.
Typen
Betrachten Sie den aidl
-Compiler als Referenzimplementierung für Typen.
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- oder AIDL-Typ | C++-Typ | NDK-Typ | Rosttyp |
---|---|---|---|
boolean |
bool |
bool |
bool |
byte 8 |
int8_t |
int8_t |
i8 |
char |
char16_t |
char16_t |
u16 |
int |
int32_t |
int32_t |
i32 |
long |
int64_t |
int64_t |
i64 |
float |
float |
float |
f32 |
double |
double |
double |
f64 |
String |
android::String16 |
std::string |
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 |
std::vector 1 |
Eingabe: &[u8] Ausgabe: Vec<u8> |
List<T> |
std::vector<T> 2 |
std::vector<T> 3 |
Eingabe: In: &[T] 4Ausgabe: Vec<T> |
FileDescriptor |
android::base::unique_fd |
– | – |
ParcelFileDescriptor |
android::os::ParcelFileDescriptor |
ndk::ScopedFileDescriptor |
binder::parcel::ParcelFileDescriptor |
Schnittstellentyp (T ) |
android::sp<T> |
std::shared_ptr<T> 7 |
binder::Strong |
Typ der unterteilbaren Elemente (T ) |
T |
T |
T |
Gewerkschaftstyp (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 oder höher kann T
jeder nicht primitive Typ sein (einschließlich Schnittstellentypen), 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 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 einem anderen Prozess gehört. Wenn Sie das Objekt auf andere Weise erstellen, führt das zu doppelten Eigentumsrechten.
8. Siehe auch Java- oder AIDL-Typ byte[]
.
Richtung (in, out und inout)
Wenn Sie die Typen der Argumente für Funktionen angeben, können Sie in
, out
oder inout
angeben. Damit wird die Richtung festgelegt, in die Informationen für einen IPC-Aufruf übergeben werden.
Die Argumentangabe
in
gibt an, dass Daten vom Aufrufer an den Aufrufbaren übergeben werden. Die Angabein
ist die Standardrichtung. Wenn Datentypen aber auchout
sein können, müssen Sie die Richtung angeben.Die Argumentangabe
out
bedeutet, dass Daten vom aufgerufenen Objekt an den Aufrufer übergeben werden.Der Argument-Spezifizierer
inout
ist eine Kombination aus beiden. Wir empfehlen jedoch, den Argumentbezeichnerinout
nicht zu verwenden. Wenn Sieinout
mit einer versionierten Schnittstelle und einem älteren Gerufenen verwenden, werden die zusätzlichen Felder, die nur im Aufrufer vorhanden sind, auf ihre Standardwerte zurückgesetzt. In Rust erhält ein normalerinout
-Typ&mut 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);
}
UTF-8 und UTF-16
Beim 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 zu konvertieren. Das NDK und die Rust-Backends verwenden immer UTF-8-Strings. Weitere Informationen zur Anmerkung utf8InCpp
finden Sie unter utf8InCpp.
Null-Zulässigkeit
Typen, die null sein können, können Sie mit @nullable
annotieren.
Weitere Informationen zur nullable
-Anmerkung finden Sie unter nullable.
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.
Hier ein Beispiel für eine AIDL-Deklaration für Parcelable:
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-Klasse dann als Typ in AIDL-Dateien verwenden, sie wird jedoch nicht von AIDL generiert. Bieten Sie <
- und ==
-Operatoren für benutzerdefinierte Parcelables des CPP- und NDK-Backends an, um sie in union
zu verwenden.
Standardwerte
Bei strukturierten Parcelable-Objekten können pro Feld Standardwerte für primitive Typen, String
-Felder und Arrays dieser Typen deklariert werden.
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 anhand des Standardwerts 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)
Rust-Beispiel
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 Bindern 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, nicht 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
können beispielsweise verwendet werden, 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 durch die integrierten Fehlertypen abgedeckt werden, kann der spezielle dienstspezifische integrierte Fehler verwendet werden, der die Aufnahme eines dienstspezifischen Fehlerwerts ermöglicht, der vom Nutzer definiert wird. 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 speziell 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 in 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, aber in den CPP- und NDK-Back-Ends müssen Sie 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 wird. 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 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. Sie können diese Vorgänge nur ausführen, wenn Sie für die Plattform Android kompilieren.
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 run on this thread.
std::future::pending().await
}
Ein wichtiger Unterschied zu den anderen Optionen besteht darin, dass Sie join_thread_pool
nicht aufrufen, wenn Sie 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. Im folgenden Beispiel dient der Hauptthread 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 Aufgaben nicht im Hauptthread ausgeführt. Daher ist es sinnvoller, join_thread_pool
im Hauptthread aufzurufen, damit der Hauptthread nicht inaktiv ist. Sie müssen 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 leckende 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 dannmy_binder.link_to_death(&mut my_death_recipient)
auf. DaDeathRecipient
der Inhaber des Callbacks 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 Prozess-ID (PID) bezieht sich auf die Linux-Prozess-ID des Prozesses, der eine Transaktion sendet. Die Nutzer-ID (UI) 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();
API für Fehlerberichte und Debugging 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 eine benutzerdefinierte Ausgabe hinzufügen möchten, überschreiben Sie die Methode dump
in Ihrem Serverobjekt, wie Sie es bei jeder anderen in einer AIDL-Datei definierten IPC-Methode tun würden. Beschränken Sie dabei das Dumping auf die App-Berechtigung android.permission.DUMP
oder auf bestimmte UIDs.
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 Verweisdaten 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));
Verwenden Sie im Rust-Backend 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 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 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. Dieser befindet sich auf einem separaten Binder-Knoten, sodass der Threadpool nicht gemeinsam genutzt wird.
Beim Java-Backend kann der Threadpool nur vergrößert werden, da er bereits gestartet wurde:
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 mithilfe der Syntax für Roh-IDs 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 bedenkenlos umbenennen, ohne die Protokollkompatibilität zu verlieren. Möglicherweise müssen Sie Ihren Code aktualisieren, um mit dem Erstellen fortzufahren. Bereits erstellte Programme funktionieren aber weiterhin.
Folgende Namen sollten Sie vermeiden: