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 skompilowania kodu w odniesieniu do tego interfejsu w taki sposób, aby kod można było dostarczać niezależnie od pliku binarnego system.img libbinder.so.

AIDL ma te interfejsy backendu:

Backend Język Powierzchnia interfejsu API Systemy kompilacji
Java Java SDK lub SystemApi (stabilna*) Wszystko
NDK C++ libbinder_ndk (stabilna*) aidl_interface
CPP C++ libbinder (niestabilny) Wszystko
Rust Rust 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 Android Interface Definition Language (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. Moduły APEX korzystają z kontenera binder w standardowy sposób po stronie systemu. Część napisana w 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 kod AIDL można skompilować do kodu stub na 2 sposoby. Więcej informacji o systemach kompilacji znajdziesz w dokumentacji modułów Soong.

Podstawowy system kompilacji

W dowolnym pliku 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 utworzyć połączenie. Więcej informacji znajdziesz w przykładzie Rust AIDL.

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

aidl_interface

Typy używane w systemie kompilacji aidl_interface muszą być strukturalne. 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 sekcji Uporządkowany a stabilny AIDL.

Typy

Jako implementację referencyjną typów możesz traktować kompilator aidl. Gdy utworzysz interfejs, wywołaj aidl --lang=<backend> ..., aby wyświetlić 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::SpIBinder
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 W: In: &[T]4
Wyjście: Vec<T>
FileDescriptor android::base::unique_fd Nie dotyczy Nie dotyczy
ParcelFileDescriptor android::os::ParcelFileDescriptor ndk::ScopedFileDescriptor binder::parcel::ParcelFileDescriptor
Typ interfejsu (T) android::sp<T> std::shared_ptr<T>7 binder::Strong
Typ obiektu Parcelable (T) T T T
Typ unii (T)5 T T T
T[N]6 std::array<T, N> std::array<T, N> [T; N]

1. W Androidzie 12 lub nowszym tablice bajtów używają znaku 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. Na 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 tego kodu: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. Utworzenie obiektu w inny sposób spowoduje podwójne prawo własności.

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

Kierunek (wejściowy, wyjściowy i wejściowo-wyjściowy)

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 to połączenie obu tych elementów. Zalecamy jednak unikanie specyfikatora argumentu inout. Jeśli używasz inout z interfejsem z wersją i starszym wywoływanym interfejsem, dodatkowe pola, które występują tylko w wywołującym interfejsie, zostaną zresetowane do wartości domyślnych. W przypadku języka Rust normalny typ inout otrzymuje &mut T, a lista typu 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 przekonwertować 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 sekcji 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 do 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, w którym my.pack.age.Foo jest klasą Java implementującą interfejs Parcelable.

Aby zadeklarować niestandardowy pakiet backendu CPP 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 parcelable w języku Rust w AIDL użyj rust_type:

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

Implementacja w 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 <== dla niestandardowych obiektów parcelable w 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,String pól i tablic tych typów.

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

W przypadku braku wartości domyślnych w backendzie Java 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ą otagowane, a ich funkcje są podobne we wszystkich backendach. Są one tworzone na podstawie domyślnej wartości 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 w 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 języku Rust

W języku Rust unie 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ć podczas 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 typu zwracanego.

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 inoutout, a także wartość zwracana (która w niektórych backendach 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_OPERATIONEX_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 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 specyficznych dla usługi 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
Rust 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 AIDLwzorcach Androida 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 dla jego typu głównego. Jeśli na przykład importujesz zagnieżdżony typ Bar zdefiniowany 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 pośrednicząca odczytuje polecenia z sterownika bindera i wykonuje zaimplementowane przez Ciebie metody. Załóżmy, że masz taki plik AIDL:

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

    #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 kodu w 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 kompilowania na platformę 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 Rust 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 przekazać 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ą funkcji 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 wątku głównym. Dlatego lepiej jest wywołać join_thread_pool w wątku głównym, aby nie był on bezczynny. Aby opuścić kontekst asynchroniczny, musisz zamknąć 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. Wywołuj te wywołania na obiektach proxy bindera.

  • W języku Java użyj android.os.IBinder::linkToDeath.
  • W backendzie CPP użyj android::IBinder::linkToDeath.
  • W backendzie NDK użyj AIBinder_linkToDeath.
  • 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 jest własnością obiektu DeathRecipient, musisz utrzymywać ten obiekt przy życiu tak długo, jak chcesz otrzymywać powiadomienia.

Informacje o rozmówcy

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 Binder te funkcje zwracają identyfikator PID i UID bieżącego procesu.

W backendzie Java:

    ... = 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 ich domyślne ustawienie):

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

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

Gdy raporty o błędach 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 zrzut do uprawnień aplikacjiandroid.permission.DUMP lub do określonych identyfikatorów UID.

W backendzie Java:

    @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 ich domyślne ustawienie):

    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 powiązań 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 binder.

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

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

    #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 nie jest to obsługiwane w przypadku Javy.

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

W backendzie CPP:

    ::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 bindera, 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 Rust potrzebujesz 2 puli 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ęzykowych, ale używanie nazw pól lub typów, które pasują do nazwy zastrzeżonej, może spowodować błąd kompilacji w przypadku języka C++ lub Java. W przypadku języka Rust nazwa pola lub typu jest zmieniana 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 zapobiec nieergonomicznym powiązaniom lub całkowitemu niepowodzeniu 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, może być konieczne zaktualizowanie kodu, ale wszystkie utworzone już programy będą nadal ze sobą współpracować.

Nazwy, których należy unikać: