Backendy AIDL

Backend AIDL jest celem generowania kodu stub. Zawsze używaj plików AIDL w określonym języku z konkretnym środowiskiem wykonawczym. W zależności od kontekstu należy używać różnych backendów AIDL.

W tabeli poniżej stabilność interfejsu API odnosi się do możliwości kompilowania kodu w odniesieniu do tego interfejsu w taki sposób, aby kod mógł być dostarczany niezależnie od pliku binarnego system.img libbinder.so.

AIDL ma te back-endy:

Backend Język Powierzchnia interfejsu API Systemy kompilacji
Java Java Pakiet SDK lub SystemApi (stabilna*) Wszystkie
NDK C++ libbinder_ndk (stabilna*) aidl_interface
CPP C++ libbinder (niestabilny) Wszystkie
Rdza Rdza libbinder_rs (stabilna*) aidl_interface
  • Te interfejsy API są stabilne, ale wiele z nich, np. interfejsy API do zarządzania usługami, jest zarezerwowanych do użytku wewnętrznego na platformie i nie są dostępne dla aplikacji. Więcej informacji o używaniu AIDL w aplikacjach znajdziesz w artykule Język definiowania interfejsu Androida (AIDL).
  • Backend w Rust został wprowadzony w Androidzie 12, a backend NDK jest dostępny od Androida 10.
  • Pakiet Rust jest oparty na libbinder_ndk, dzięki czemu jest stabilny i przenośny. APEX-y używają kontenera binder w standardowy sposób po stronie systemu. Część napisana w języku Rust jest dołączana do pakietu APEX i w nim dostarczana. Ta część zależy od libbinder_ndk.so na partycji systemowej.

Systemy kompilacji

W zależności od backendu istnieją 2 sposoby kompilowania AIDL do kodu stub. Więcej informacji o systemach kompilacji znajdziesz w dokumentacji modułów Soong.

Podstawowy system kompilacji

W dowolnym cc_ lub java_ Android.bp module (lub w ich odpowiednikach Android.mk) możesz określić pliki AIDL (.aidl) jako pliki źródłowe. W tym przypadku używane są backendy AIDL w Javie lub CPP (nie backend NDK), a klasy do używania odpowiednich plików AIDL są automatycznie dodawane do modułu. W tych modułach możesz określić opcje takie jak local_include_dirs (która informuje system kompilacji o ścieżce głównej do plików AIDL w tym module) w grupie aidl:.

Backend Rust jest przeznaczony tylko do używania z językiem Rust. Moduły rust_ są obsługiwane inaczej, ponieważ pliki AIDL nie są określane jako pliki źródłowe. Zamiast tego moduł aidl_interface generuje rustlib o nazwie aidl_interface_name-rust, z którym można połączyć kod. Więcej informacji znajdziesz w przykładzie Rust AIDL.

zawierają narzędzia C++ wymagane do używania z kompilatorem AIDL i są dostarczane razem z nim w pakiecie SDK w folderze platforms/$PLATFORM_NAME/optional/libbinder_ndk_cpp/.

aidl_interface

Typy używane w systemie kompilacji aidl_interface muszą mieć strukturę. Aby można było je strukturyzować, obiekty Parcelable muszą zawierać pola bezpośrednio, a nie deklaracje typów zdefiniowanych bezpośrednio w językach docelowych. Informacje o tym, jak uporządkowany AIDL pasuje do stabilnego AIDL, znajdziesz w artykule Uporządkowany a stabilny AIDL.

Typy

Rozważ użycie kompilatora aidl jako implementacji referencyjnej typów. Gdy utworzysz interfejs, wywołaj aidl --lang=<backend> ..., aby zobaczyć wynikowy plik interfejsu. Gdy używasz modułu aidl_interface, możesz wyświetlić dane wyjściowe w out/soong/.intermediates/<path to module>/.

Typ Java lub AIDL Typ C++ Typ NDK Rodzaj rdzy
boolean bool bool bool
byte8 int8_t int8_t i8
char char16_t char16_t u16
int int32_t int32_t i32
long int64_t int64_t i64
float float float f32
double double double f64
String android::String16 std::string Wejście: &str
Wyjście: String
android.os.Parcelable android::Parcelable Nie dotyczy Nie dotyczy
IBinder android::IBinder ndk::SpAIBinder binder::SpIBinder9
T[] std::vector<T> std::vector<T> Wejście: &[T]
Wyjście: Vec<T>
byte[] std::vector std::vector1 Wejście: &[u8]
Wyjście: Vec<u8>
List<T> std::vector<T>2 std::vector<T>3 Wejście: In: &[T]4
Wyjście: Vec<T>
FileDescriptor android::base::unique_fd Nie dotyczy Nie dotyczy
ParcelFileDescriptor android::os::ParcelFileDescriptor ndk::ScopedFileDescriptor binder::parcel::ParcelFileDescriptor9
Typ interfejsu (T) android::sp<T> std::shared_ptr<T>7 binder::Strong<dyn T>9
Typ obiektu Parcelable (T) T T T9
Typ unii (T)5 T T T
T[N]6 std::array<T, N> std::array<T, N> [T; N]

1. W Androidzie 12 i nowszych wersjach tablice bajtów używają uint8_t zamiast int8_t ze względu na zgodność.

2. Backend C++ obsługuje List<T>, gdzie T to jeden z tych typów:String, IBinder, ParcelFileDescriptor lub parcelable. W Androidzie 13 lub nowszym T może być dowolnym typem niepierwotnym (w tym typem interfejsu) z wyjątkiem tablic. AOSP zaleca używanie typów tablicowych, takich jak T[], ponieważ działają one we wszystkich backendach.

3. Backend NDK obsługuje List<T>, gdzie T to jeden z tych typów: String, ParcelFileDescriptor lub parcelable. W Androidzie 13 lub nowszym T może być dowolnym typem niepierwotnym z wyjątkiem tablic.

4. Typy są przekazywane w kodzie Rust w różny sposób w zależności od tego, czy są danymi wejściowymi (argumentem) czy wyjściowymi (wartością zwracaną).

5. Typy unii są obsługiwane na urządzeniach z Androidem 12 i nowszym.

6. W Androidzie 13 lub nowszym obsługiwane są tablice o stałym rozmiarze. Tablice o stałym rozmiarze mogą mieć wiele wymiarów (np. int[3][4]). W backendzie Javy tablice o stałym rozmiarze są reprezentowane jako typy tablicowe.

7. Aby utworzyć instancję obiektu SharedRefBase, użyj SharedRefBase::make\<My\>(... args ...). Ta funkcja tworzy obiekt std::shared_ptr\<T\>, który jest też zarządzany wewnętrznie, jeśli binder należy do innego procesu. Tworzenie obiektu w inny sposób powoduje podwójne prawo własności.

8. Zobacz też typ Java lub AIDL byte[].

9. W Rust typy, które nie implementują Default (IBinder, ParcelFileDescriptor, typy interfejsów i niestrukturalne obiekty Parcelable), są mapowane na Option<T>, gdy wymagana jest wartość domyślna (np. w polach Parcelable, parametrach out i tablicach o stałym rozmiarze w tych kontekstach), nawet bez adnotacji @nullable. Wymóg niepustych wartości jest nadal egzekwowany w czasie działania programu podczas przekazywania danych, a jeśli wartość to None, zwracana jest wartość StatusCode::UNEXPECTED_NULL. W przypadku wartości zwracanych przez metody, parametrów in (przekazywanych jako &T) i parametrów inout (przekazywanych jako &mut T) te typy nie są opakowywane w Option<T>, chyba że są oznaczone adnotacją @nullable.

Kierunkowość (in, out i inout)

Podczas określania typów argumentów funkcji możesz podać je jako in, out lub inout. Określa kierunek przekazywania informacji w przypadku wywołania IPC.

  • Specyfikator argumentu in wskazuje, że dane są przekazywane z funkcji wywołującej do funkcji wywoływanej. Specyfikator in jest domyślnym kierunkiem, ale jeśli typy danych mogą być też out, musisz określić kierunek.

  • Specyfikator argumentu out oznacza, że dane są przekazywane z funkcji wywoływanej do funkcji wywołującej.

  • Specyfikator argumentu inout jest połączeniem obu tych elementów. Zalecamy jednak unikanie specyfikatora argumentu inout. Jeśli używasz inout z interfejsem z obsługą wersji i starszym wywoływanym, dodatkowe pola, które występują tylko w elemencie wywołującym, zostaną zresetowane do wartości domyślnych. W przypadku języka Rust normalny typ inout otrzymuje &mut T, a typ listy inout otrzymuje &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 i UTF-16

W przypadku backendu CPP możesz wybrać, czy ciągi znaków mają być w formacie UTF-8 czy UTF-16. Deklaruj ciągi znaków jako @utf8InCpp String w AIDL, aby automatycznie konwertować je na UTF-8. Backendy NDK i Rust zawsze używają ciągów znaków UTF-8. Więcej informacji o adnotacji utf8InCpp znajdziesz w artykule utf8InCpp.

Dopuszczalność wartości null

Typy, które mogą mieć wartość null, możesz oznaczyć symbolem @nullable. Więcej informacji o adnotacji nullable znajdziesz w sekcji nullable.

Niestandardowe obiekty Parcelable

Niestandardowy obiekt Parcelable to obiekt Parcelable, który jest implementowany ręcznie w docelowym backendzie. Używaj niestandardowych obiektów Parcelable tylko wtedy, gdy chcesz dodać obsługę innych języków w przypadku istniejącego niestandardowego obiektu Parcelable, którego nie można zmienić.

Oto przykład deklaracji typu parcelable w AIDL:

    package my.pack.age;
    parcelable Foo;

Domyślnie deklaruje to obiekt Java Parcelable, gdzie my.pack.age.Foo to klasa Java implementująca interfejs Parcelable.

Aby zadeklarować niestandardowy pakiet CPP backendu z możliwością przekształcenia w AIDL, użyj cpp_header:

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

Implementacja w C++ w my/pack/age/Foo.h wygląda tak:

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

Aby zadeklarować niestandardowy obiekt NDK parcelable w AIDL, użyj ndk_header:

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

Implementacja NDK w android/pack/age/Foo.h wygląda tak:

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

W Androidzie 15 w przypadku deklaracji niestandardowego obiektu Rust parcelable w AIDL używaj rust_type:

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

Implementacja w języku Rust w rust_crate/src/lib.rs wygląda tak:

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

Następnie możesz użyć tego obiektu Parcelable jako typu w plikach AIDL, ale nie będzie on generowany przez AIDL. Udostępnij operatory < i == dla niestandardowych obiektów Parcelable w backendzie CPP i NDK, aby można było ich używać w union.

Wartości domyślne

Uporządkowane obiekty Parcelable mogą deklarować domyślne wartości poszczególnych pól dla typów prostych, pól String i tablic tych typów.

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

W przypadku backendu Java, gdy brakuje wartości domyślnych, pola są inicjowane jako zera w przypadku typów prostych i null w przypadku typów złożonych.

W innych backendach pola są inicjowane domyślnymi wartościami, gdy nie są zdefiniowane wartości domyślne. Na przykład w backendzie C++ pola String są inicjowane jako pusty ciąg znaków, a pola List<T> są inicjowane jako pusta vector<T>. Pola @nullable są inicjowane jako pola o wartości null.

Związki zawodowe

Unie AIDL są oznaczane tagami, a ich funkcje są podobne we wszystkich backendach. Są one tworzone na podstawie wartości domyślnej pierwszego pola i mają specyficzny dla danego języka sposób interakcji:

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

Przykład w Javie

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

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

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

Przykład C++ i 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)

Przykład w Rust

W Rust związki są implementowane jako wyliczenia i nie mają jawnych funkcji pobierających ani ustawiających.

    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

Obsługa błędów

System operacyjny Android udostępnia wbudowane typy błędów, których usługi mogą używać do zgłaszania błędów. Są one używane przez bindery i mogą być używane przez wszystkie usługi implementujące interfejs bindera. Ich użycie jest dobrze udokumentowane w definicji AIDL i nie wymagają one żadnego zdefiniowanego przez użytkownika stanu ani zwracanego typu.

Parametry wyjściowe z błędami

Gdy funkcja AIDL zgłosi błąd, może nie zainicjować lub zmodyfikować parametrów wyjściowych. W szczególności parametry wyjściowe mogą zostać zmodyfikowane, jeśli błąd wystąpi podczas rozpakowywania, a nie podczas przetwarzania samej transakcji. Ogólnie rzecz biorąc, gdy funkcja AIDL zwraca błąd, wszystkie parametry inout i out, a także wartość zwracana (która w przypadku niektórych backendów działa jak parametr out) powinny być traktowane jako znajdujące się w nieokreślonym stanie.

Jakich wartości błędów używać

Wiele wbudowanych wartości błędów można stosować w dowolnych interfejsach AIDL, ale niektóre z nich są traktowane w specjalny sposób. Na przykład symbole EX_UNSUPPORTED_OPERATION i EX_ILLEGAL_ARGUMENT można stosować, gdy opisują stan błędu, ale nie wolno używać symbolu EX_TRANSACTION_FAILED, ponieważ jest on traktowany w specjalny sposób przez infrastrukturę bazową. Więcej informacji o tych wbudowanych wartościach znajdziesz w definicjach dotyczących backendu.

Jeśli interfejs AIDL wymaga dodatkowych wartości błędów, które nie są objęte wbudowanymi typami błędów, może użyć specjalnego wbudowanego błędu specyficznego dla usługi, który umożliwia uwzględnienie wartości błędu specyficznego dla usługi zdefiniowanej przez użytkownika. Te błędy specyficzne dla usługi są zwykle definiowane w interfejsie AIDL jako const int lub int-backed enum i nie są analizowane przez binder.

W Javie błędy są mapowane na wyjątki, np. android.os.RemoteException. W przypadku wyjątków dotyczących konkretnych usług Java używa android.os.ServiceSpecificException wraz z błędem zdefiniowanym przez użytkownika.

Kod natywny w Androidzie nie używa wyjątków. Backend CPP używa android::binder::Status. Backend NDK używa ndk::ScopedAStatus. Każda metoda wygenerowana przez AIDL zwraca jedną z tych wartości, która reprezentuje stan metody. Backend w Rust używa tych samych wartości kodów wyjątków co NDK, ale przed przekazaniem ich użytkownikowi przekształca je w natywne błędy Rusta (StatusCode, ExceptionCode). W przypadku błędów specyficznych dla usługi zwracany kod Status lub ScopedAStatus używa kodu EX_SERVICE_SPECIFIC wraz z błędem zdefiniowanym przez użytkownika.

Wbudowane typy błędów znajdziesz w tych plikach:

Backend Definicja
Java android/os/Parcel.java
CPP binder/Status.h
NDK android/binder_status.h
Rdza android/binder_status.h

Używanie różnych backendów

Te instrukcje dotyczą kodu platformy Android. W tych przykładach użyto zdefiniowanego typu my.package.IFoo. Instrukcje korzystania z backendu Rust znajdziesz w przykładzie Rust AIDL w wzorcach Android Rust.

Typy importu

Niezależnie od tego, czy zdefiniowany typ jest interfejsem, obiektem z możliwością przekazywania, czy unią, możesz go zaimportować w języku Java:

import my.package.IFoo;

lub w backendzie platformy CPP:

#include <my/package/IFoo.h>

lub w backendzie NDK (zwróć uwagę na dodatkową przestrzeń nazw aidl):

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

Lub w backendzie Rust:

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

Chociaż w Java możesz importować typ zagnieżdżony, w przypadku backendów CPP i NDK musisz uwzględnić nagłówek jego typu głównego. Na przykład podczas importowania zagnieżdżonego typu Bar zdefiniowanego w my/package/IFoo.aidl (IFoo to typ główny pliku) musisz uwzględnić <my/package/IFoo.h> w przypadku backendu CPP (lub <aidl/my/package/IFoo.h> w przypadku backendu NDK).

Implementowanie interfejsu

Aby zaimplementować interfejs, musisz dziedziczyć po natywnej klasie stub. Implementacja interfejsu jest często nazywana usługą, gdy jest zarejestrowana w menedżerze usług lub android.app.ActivityManager, a wywołaniem zwrotnym, gdy jest zarejestrowana przez klienta usługi. Jednak w zależności od konkretnego zastosowania do opisywania implementacji interfejsu używa się różnych nazw. Klasa zastępcza odczytuje polecenia z sterownika bindera i wykonuje zaimplementowane przez Ciebie metody. Załóżmy, że masz plik AIDL w tym formacie:

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

W języku Java musisz rozszerzyć wygenerowaną klasę Stub:

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

W backendzie platformy zgody użytkowników:

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

W backendzie NDK (zwróć uwagę na dodatkową przestrzeń nazw aidl):

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

W backendzie Rust:

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

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

    impl Interface for MyFoo {}

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

lub w przypadku asynchronicznego języka 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(())
        }
    }

Rejestracja i korzystanie z usług

Usługi na platformie Android są zwykle rejestrowane w procesie servicemanager. Oprócz tych interfejsów API niektóre interfejsy API sprawdzają usługę (co oznacza, że jeśli usługa jest niedostępna, natychmiast zwracają wynik). Szczegółowe informacje znajdziesz w odpowiednim interfejsie servicemanager. Te operacje możesz wykonywać tylko podczas kompilacji na platformie Android.

W Javie:

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

W backendzie CPP:

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

W backendzie NDK (zwróć uwagę na dodatkową przestrzeń nazw 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")));

W backendzie Rust:

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

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

W asynchronicznym backendzie Rusta z jednowątkowym środowiskiem wykonawczym:

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
}

Ważną różnicą w porównaniu z innymi opcjami jest to, że w przypadku asynchronicznego języka Rust i środowiska wykonawczego z jednym wątkiem nie wywołujesz funkcji join_thread_pool. Dzieje się tak, ponieważ musisz udostępnić Tokio wątek, w którym może wykonywać utworzone zadania. W poniższym przykładzie główny wątek służy do tego celu. Wszystkie zadania utworzone za pomocą tokio::spawn są wykonywane w wątku głównym.

W asynchronicznym backendzie Rusta z wielowątkowym środowiskiem wykonawczym:

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

W wielowątkowym środowisku wykonawczym Tokio utworzone zadania nie są wykonywane w głównym wątku. Dlatego lepiej jest wywoływać funkcję join_thread_pool w głównym wątku, aby nie był on bezczynny. Aby opuścić kontekst asynchroniczny, musisz umieścić wywołanie w tagach block_in_place.

Możesz poprosić o powiadomienie, gdy usługa hostująca binder przestanie działać. Może to pomóc w uniknięciu wycieku serwerów proxy wywołania zwrotnego lub w przywróceniu działania po błędzie. Wykonuj te wywołania na obiektach proxy bindera.

  • W przypadku Javy użyj android.os.IBinder::linkToDeath.
  • W backendzie CPP użyj android::IBinder::linkToDeath.
  • W backendzie NDK użyj AIBinder_linkToDeath. Zawsze używaj parametru AIBinder_DeathRecipient_setOnUnlinked, aby kontrolować okres ważności pliku cookie odbiorcy informacji o śmierci.
  • W backendzie Rust utwórz obiekt DeathRecipient, a następnie wywołaj my_binder.link_to_death(&mut my_death_recipient). Pamiętaj, że ponieważ wywołanie zwrotne należy do obiektu DeathRecipient, musisz utrzymywać ten obiekt przy życiu tak długo, jak chcesz otrzymywać powiadomienia.

Informacje o dzwoniącym

Podczas odbierania wywołania bindera jądra informacje o wywołującym są dostępne w kilku interfejsach API. Identyfikator procesu (PID) to identyfikator procesu Linux, który wysyła transakcję. Identyfikator użytkownika (UI) to identyfikator użytkownika systemu Linux. W przypadku połączenia jednokierunkowego identyfikator PID połączenia to 0. Poza kontekstem transakcji powiązania te funkcje zwracają identyfikator procesu i identyfikator użytkownika bieżącego procesu.

W backendzie Javy:

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

W backendzie CPP:

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

W backendzie NDK:

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

W przypadku implementowania interfejsu w backendzie Rust określ te wartości (zamiast zezwalać na wartości domyślne):

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

Raporty o błędach i interfejs API debugowania usług

Gdy zgłoszenia błędów są uruchamiane (np. za pomocą polecenia adb bugreport), zbierają informacje z całego systemu, aby ułatwić debugowanie różnych problemów. W przypadku usług AIDL raporty o błędach używają pliku binarnego dumpsys we wszystkich usługach zarejestrowanych w menedżerze usług, aby zrzucać informacje do raportu o błędach. Możesz też użyć dumpsys w wierszu poleceń, aby uzyskać informacje z usługi za pomocą dumpsys SERVICE [ARGS]. W przypadku backendów C++ i Java możesz kontrolować kolejność zrzucania usług, używając dodatkowych argumentów w funkcji addService. Możesz też użyć dumpsys --pid SERVICE, aby podczas debugowania uzyskać identyfikator PID usługi.

Aby dodać niestandardowe dane wyjściowe do usługi, zastąp metodę dump w obiekcie serwera, tak jak w przypadku implementowania dowolnej innej metody IPC zdefiniowanej w pliku AIDL. W tym celu ogranicz zrzucanie do uprawnień aplikacjiandroid.permission.DUMP lub do określonych identyfikatorów UID.

W backendzie Javy:

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

W backendzie CPP:

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

W backendzie NDK:

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

W przypadku implementowania interfejsu w backendzie Rust określ te wartości (zamiast zezwalać na wartości domyślne):

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

Używanie słabych wskaźników

Możesz przechowywać słabe odwołanie do obiektu bindera.

Java obsługuje WeakReference, ale nie obsługuje słabych odwołań do komponentów w warstwie natywnej.

W backendzie CPP słaby typ to wp<IFoo>.

W backendzie NDK użyj ScopedAIBinder_Weak:

#include <android/binder_auto_utils.h>

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

W backendzie Rust użyj WpIBinder lub Weak<IFoo>:

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

Dynamiczne pobieranie deskryptora interfejsu

Deskryptor interfejsu określa typ interfejsu. Jest to przydatne podczas debugowania lub gdy masz nieznany moduł.

W języku Java możesz uzyskać deskryptor interfejsu za pomocą kodu takiego jak ten:

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

W backendzie platformy zgody użytkowników:

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

Środowiska NDK i Rust nie obsługują tej funkcji.

Statyczne pobieranie deskryptora interfejsu

Czasami (np. podczas rejestrowania usług @VintfStability) musisz znać statyczny deskryptor interfejsu. W Javie możesz uzyskać deskryptor, dodając kod taki jak ten:

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

W backendzie platformy zgody użytkowników:

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

W backendzie NDK (zwróć uwagę na dodatkową przestrzeń nazw aidl):

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

W backendzie Rust:

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

Zakres wyliczenia

W natywnych backendach możesz iterować po możliwych wartościach, jakie może przyjmować wyliczenie. Ze względu na rozmiar kodu ta funkcja nie jest obsługiwana w Javie.

W przypadku wyliczenia MyEnum zdefiniowanego w AIDL iteracja jest zapewniana w ten sposób.

W backendzie platformy zgody użytkowników:

    ::android::enum_range<MyEnum>()

W backendzie NDK:

   ::ndk::enum_range<MyEnum>()

W backendzie Rust:

    MyEnum::enum_values()

Zarządzanie wątkami

Każda instancja libbinder w procesie utrzymuje jedną pulę wątków. W większości przypadków użycia powinien to być dokładnie 1 pula wątków, współdzielona przez wszystkie interfejsy backendu. Jedynym wyjątkiem jest sytuacja, w której kod dostawcy wczytuje inną kopię libbinder, aby komunikować się z /dev/vndbinder. Dzieje się to w osobnym węźle usługi Binder, więc pula wątków nie jest udostępniana.

W przypadku backendu Java rozmiar puli wątków może się tylko zwiększać (ponieważ jest już uruchomiona):

    BinderInternal.setMaxThreads(<new larger value>);

W przypadku backendu CPP dostępne są te operacje:

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

Podobnie w przypadku backendu NDK:

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

W backendzie Rust:

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

W przypadku asynchronicznego backendu Rusta potrzebne są 2 pule wątków: binder i Tokio. Oznacza to, że aplikacje korzystające z asynchronicznego języka Rust wymagają szczególnej uwagi, zwłaszcza jeśli chodzi o użycie join_thread_pool. Więcej informacji znajdziesz w sekcji dotyczącej rejestrowania usług.

Zarezerwowane nazwy

W językach C++, Java i Rust niektóre nazwy są zarezerwowane jako słowa kluczowe lub do użytku w ramach danego języka. AIDL nie wymusza ograniczeń na podstawie reguł języka, ale używanie nazw pól lub typów, które pasują do nazwy zastrzeżonej, może spowodować błąd kompilacji w przypadku C++ lub Javy. W przypadku języka Rust pole lub typ jest zmieniany przy użyciu składni surowego identyfikatora, do której można uzyskać dostęp za pomocą prefiksu r#.

Zalecamy unikanie w definicjach AIDL nazw zastrzeżonych, aby uniknąć niewygodnych powiązań lub całkowitego niepowodzenia kompilacji.

Jeśli w definicjach AIDL masz już zarezerwowane nazwy, możesz bezpiecznie zmienić nazwy pól, zachowując zgodność protokołu. Aby kontynuować tworzenie, być może trzeba będzie zaktualizować kod, ale wszystkie utworzone już programy będą nadal ze sobą współpracować.

Nazwy, których należy unikać: