Backendy AIDL

Backend AIDL jest miejscem docelowym generowania kodu pośredniego. Korzystając z plików AIDL, zawsze używa się ich w określonym języku w określonym środowisku wykonawczym. W zależności od kontekstu należy używać różnych backendów AIDL.

W poniższej tabeli stabilność platformy interfejsu API odnosi się do możliwości skompilowania kodu na tej platformie w taki sposób, że kod może być dostarczony niezależnie od pliku binarnego system.img libbinder.so.

AIDL ma następujące backendy:

Backend Język Interfejs API Systemy kompilacji
Java Java SDK/SystemApi (stabilny*) wszystkie
NDK C++ libbinder_ndk (stabilny*) interfejs AIDl
Wartość CPP C++ libbinder (niestabilny) wszystkie
Rust Rust libbinder_rs (stabilny*) interfejs AIDl
  • Te interfejsy API są stabilne, ale wiele interfejsów API, na przykład do zarządzania usługami, jest zarezerwowanych do użytku wewnętrznego na platformie i nie jest dostępnych dla aplikacji. Więcej informacji o używaniu AIDL w aplikacjach znajdziesz w dokumentacji dla programistów.
  • Backend Rust został wprowadzony w Androidzie 12, a backend NDK jest dostępny od wersji 10.
  • Skrzynka Rust opiera się na libbinder_ndk, co sprawia, że jest stabilna i przenośna. APEX używa segregatorów w taki sam sposób, jak inni użytkownicy systemu. Część Rust jest pakowana w APEX i wysyłana w niej. Zależy to od elementu libbinder_ndk.so na partycji systemowej.

Systemy kompilacji

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

Podstawowy system kompilacji

W dowolnym module Android.bp cc_ lub java_ (lub w ich odpowiednikach Android.mk) pliki .aidl można określić jako pliki źródłowe. W takim przypadku używane są backendy AIDL w języku Java/CPP (a nie backend NDK), a klasy korzystające z odpowiadających im plików AIDL są automatycznie dodawane do modułu. Opcje takie jak local_include_dirs, które informują system kompilacji o ścieżce głównej do plików AIDL w tym module, które można określić w tych modułach w ramach grupy aidl:. Pamiętaj, że backend Rust jest do użytku tylko z Rust. Moduły rust_ są obsługiwane w inny sposób, ponieważ pliki AIDL nie są określone jako pliki źródłowe. Zamiast tego moduł aidl_interface tworzy element rustlib o nazwie <aidl_interface name>-rust, który można powiązać. Więcej informacji znajdziesz w przykładzie Rust AIDL.

interfejs AIDl

Typy używane w tym systemie kompilacji muszą być uporządkowane. Aby zachować strukturę, obiekty działkowe muszą zawierać pola bezpośrednio i nie mogą być deklaracjami typów zdefiniowanych bezpośrednio w językach docelowych. Aby sprawdzić, jak ustrukturyzowany AIDL pasuje do stabilnej wersji AIDL, zapoznaj się z sekcją Uporządkowane a stabilne AIDL.

Rodzaje

Kompilator aidl możesz traktować jako referencyjną implementację typów. Po utworzeniu interfejsu wywołaj aidl --lang=<backend> ..., by zobaczyć wynikowy plik interfejsu. Gdy korzystasz z modułu aidl_interface, dane wyjściowe możesz wyświetlić w narzędziu out/soong/.intermediates/<path to module>/.

Typ Java/AIDL Typ C++ Typ NDK Rodzaj rdzy
Wartość logiczna wartość logiczna wartość logiczna wartość logiczna
bajt int8_t, int8_t, i8
znak znak16_t znak16_t U16
int int32_t, int32_t, i32
długi int64_t, int64_t, i64
liczba zmiennoprzecinkowa liczba zmiennoprzecinkowa liczba zmiennoprzecinkowa f32
podwójny podwójny podwójny F64
Ciąg znaków android::Ciąg16 std::string Ciąg znaków
android.os.Parcelable, android::Z możliwością pakietu Nie dotyczy Nie dotyczy
IBinder android::IBinder, ndk::SpAIBinder binder::SpIBinder
K[] std::vector<T>, std::vector<T>, Za: &[T]
Wyjście: Vec<T>
bajt[] std::vector<uint8_t> std::vector<int8_t>1 Przy: &[u8]
Wyjście: Vec<u8>
Lista<T> std::vector<T>2 std::vector<T>3 Przy: &[T]4
Wyjście: Vec<T>
Deskryptor pliku android::base::Unique_fd Nie dotyczy binder::parcel::ParcelFileDescriptor
Deskryptor pliku ParcelFileDescriptor android::os::ParcelFileDescriptor, ndk::ScopedFileDescriptor binder::parcel::ParcelFileDescriptor
typ interfejsu (T) android::sp<T> std::shared_ptr<T>7 binder::Silne
typ parcelable (T) T T T
typ sumy (T)5 T T T
T[N] 6 std::array<T, N> std::array<T, N> [T; N]

1. Ze względu na zgodność z Androidem 12 lub nowszym tablice bajtów używają parametru uint8_t zamiast int8_t.

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

3. Backend NDK obsługuje List<T>, gdzie T to jedna z wartości String, ParcelFileDescriptor lub parcelable. W Androidzie 13 lub nowszym T może być dowolnym typem niepodstawowym, z wyjątkiem tablic.

4. Typy są przekazywane w różny sposób w przypadku kodu Rust w zależności od tego, czy są to dane wejściowe (argument), czy dane wyjściowe (wartość zwrócona).

5. Typy połączeń są obsługiwane w Androidzie 12 i nowszych.

6. Na Androidzie 13 i nowszych 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 tablic.

7. Aby utworzyć instancję wiązania SharedRefBase, użyj SharedRefBase::make\<My\>(... args ...). Ta funkcja tworzy obiekt std::shared_ptr\<T\>, którym jest również zarządzany wewnętrznie, na wypadek gdyby powiązanie należy do innego procesu. Utworzenie obiektu w inny sposób powoduje podwójne prawa własności.

Kierunek (wejście/wyjście/wyjście)

Określając typy argumentów funkcji, możesz podać je jako in, out lub inout. To ustawienie określa, w jakim kierunku są przekazywane informacje w przypadku wywołania IPC. Domyślnym kierunkiem jest in, który wskazuje, że dane są przekazywane od rozmówcy do rozmówcy. out oznacza, że dane są przekazywane od osoby wywołującej do elementu wywołującego. inout to połączenie obu tych elementów. Zespół Androida odradza jednak korzystanie ze specyfikatora argumentów inout. Jeśli używasz inout z interfejsem w wersji i starszą wywołaniem, 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 odbiera &mut Vec<T>, a typ listy inout&mut Vec<T>.

interface IRepeatExamples {
    MyParcelable RepeatParcelable(MyParcelable token); // implicitly 'in'
    MyParcelable RepeatParcelableWithIn(in MyParcelable token);
    void RepeatParcelableWithInAndOut(in MyParcelable param, out MyParcelable result);
    void RepeatParcelableWithInOut(inout MyParcelable param);
}

UTF8/UTF16

W backendzie CPP możesz wybrać, czy ciągi tekstowe to utf-8 czy utf-16. Zadeklaruj w AIDL ciągi tekstowe jako @utf8InCpp String, aby automatycznie konwertować je na format UTF-8. Backendy NDK i Rust zawsze używają ciągów UTF-8. Więcej informacji o adnotacjach utf8InCpp znajdziesz w artykule Adnotacje w AIDL.

Dopuszczalność wartości null

Za pomocą funkcji @nullable możesz dodawać adnotacje do typów, które mogą mieć wartość null w backendzie Javy, aby udostępniać wartości puste dla backendów CPP i NDK. W backendzie Rust te typy @nullable są udostępniane jako Option<T>. Serwery natywne domyślnie odrzucają puste wartości. Jedynymi wyjątkami od tej reguły są typy interface i IBinder, które zawsze mogą mieć wartość null w przypadku odczytów NDK i zapisów CPP/NDK. Więcej informacji o adnotacjach nullable znajdziesz w artykule Adnotacje w AIDL.

Działki niestandardowe

Atrybut parcelable [do_sprzedaży niestandardowej] to pakiet parcelowy, który jest wdrażany ręcznie w docelowym backendzie. Korzystaj z niestandardowych pakietów tylko wtedy, gdy chcesz dodać obsługę innych języków do istniejącego obszaru niestandardowego, którego nie można zmienić.

W celu zadeklarowania działki niestandardowej, aby powiadamiano AIDL o tym, deklaracja AIDL wygląda tak:

    package my.pack.age;
    parcelable Foo;

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

Aby zadeklarować obsługę niestandardowego backendu CPP w AIDL, użyj cpp_header:

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

Implementacja języka C++ w języku 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 zgłosić deklarację niestandardowego pakietu NDK 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 (eksperymentalny AOSP) dla deklaracji niestandardowej klasy Rust w AIDL użyj właściwości 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 pakietu jako typu w plikach AIDL, ale nie zostanie on wygenerowany przez AIDL. Podaj operatory < i == dla niestandardowych pakietów SDK CPP/NDK backendu, aby używać ich w union.

Wartości domyślne

Obiekty uporządkowane w praktyce mogą deklarować domyślne wartości pól dla elementów podstawowych, obiektów String i tablic tego typu.

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

Gdy brakuje wartości domyślnych, w backendzie Java pola są inicjowane jako zero w przypadku typów podstawowych, a null w przypadku typów innych niż podstawowe.

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

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 usługę Binder i mogą być używane przez wszystkie usługi implementujące interfejs Binder. Ich użycie jest dobrze udokumentowane w definicji AIDL i nie wymaga zdefiniowanego przez użytkownika stanu ani typu zwrotu.

Parametry wyjściowe z błędami

Gdy funkcja AIDL zgłasza błąd, może nie zainicjować ani zmodyfikować parametrów wyjściowych. W szczególności parametry wyjściowe mogą być modyfikowane, jeśli błąd wystąpi podczas wyodrębniania paczki, a nie podczas przetwarzania samej transakcji. Ogólnie w przypadku błędu z funkcji AIDL należy uznać, że wszystkie parametry inout i out oraz wartość zwracana (w niektórych backendach działają jak parametr out) mają stan nieokreślony.

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

Wiele wbudowanych wartości błędów można wykorzystać w dowolnym interfejsie AIDL, ale niektóre są traktowane w specjalny sposób. Na przykład EX_UNSUPPORTED_OPERATION i EX_ILLEGAL_ARGUMENT mogą być używane do opisu stanu błędu, ale EX_TRANSACTION_FAILED nie można używać, ponieważ infrastruktura bazowa jest traktowana specjalnie. Aby dowiedzieć się więcej o tych wartościach wbudowanych, zapoznaj się z definicjami backendu.

Jeśli interfejs AIDL wymaga dodatkowych wartości błędów, których nie obejmują wbudowane typy błędów, może on skorzystać ze specjalnego wbudowanego błędu usługi, który umożliwia dołączenie wartości błędu określonej przez użytkownika. Te błędy dotyczące konkretnej usługi są zwykle zdefiniowane w interfejsie AIDL jako enum wspierany przez const int lub int i nie są analizowane przez powiązanie.

W Javie błędy są mapowane na wyjątki, takie jak android.os.RemoteException. W przypadku wyjątków dotyczących konkretnych usług Java używa parametru android.os.ServiceSpecificException, a także błędu zdefiniowanego przez użytkownika.

Kod natywny na 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 jeden z nich, reprezentujący stan metody. Backend Rust używa tych samych wartości kodu wyjątku co NDK, ale przed przekazaniem ich użytkownikowi jest konwertowany na natywne błędy Rusta (StatusCode, ExceptionCode). W przypadku błędów związanych z konkretnymi usługami zwracana zasada Status lub ScopedAStatus używa parametru EX_SERVICE_SPECIFIC oraz błędu zdefiniowanego przez użytkownika.

Typy błędów wbudowanych znajdziesz w tych plikach:

Backend Definicja
Java android/os/Parcel.java
Wartość 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 Androida. W tych przykładach używamy zdefiniowanego typu: my.package.IFoo. Instrukcje korzystania z backendu Rust znajdziesz w przykładzie Rust AIDL na stronie Wzorce Rust na Androidzie.

Typy importu

Niezależnie od tego, czy zdefiniowany typ jest interfejsem, parcelable lub Union, możesz go zaimportować w Javie:

import my.package.IFoo;

Lub w backendzie 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ż można zaimportować typ zagnieżdżony w Javie, w backendach CPP/NDK musisz uwzględnić nagłówek dla jego typu głównego. Na przykład, gdy importujesz zagnieżdżony typ Bar zdefiniowany w my/package/IFoo.aidl (IFoo to typ główny pliku), musisz dodać <my/package/IFoo.h> dla backendu CPP (lub <aidl/my/package/IFoo.h> dla backendu NDK).

Wdrażanie usług

Aby wdrożyć usługę, musisz odziedziczyć ją z natywnej klasy wersji pośredniej. Ta klasa odczytuje polecenia ze sterownika powiązania i wykonuje wdrożone metody. Wyobraź sobie, że masz plik AIDL podobny do tego:

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

W Javie musisz wychodzić z tej klasy:

    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 za pomocą asynchronicznego środowiska 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(())
        }
    }

Zarejestruj się i pobierz usługi

Usługi na platformie Android są zwykle rejestrowane w procesie servicemanager. Oprócz wymienionych poniżej interfejsów API niektóre interfejsy API sprawdzają usługę (czyli zwracają natychmiast, gdy usługa jest niedostępna). Szczegółowe informacje znajdziesz w odpowiednim interfejsie servicemanager. Te operacje można wykonać 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 Rust ze środowiskiem wykonawczym jednowątkowym:

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

#[tokio::main(flavor = "current_thread")]
async fn main() {
    binder::ProcessState::start_thread_pool();
    // [...]
    let my_service = MyFoo;
    let my_service_binder = BnFoo::new_async_binder(
        my_service,
        TokioRuntime(Handle::current()),
        BinderFeatures::default(),
    );

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

    // Sleeps forever, but does not join the binder threadpool.
    // Spawned tasks will run on this thread.
    std::future::pending().await
}

Ważną różnicą od pozostałych opcji jest to, że nie wywołujemy join_thread_pool w przypadku korzystania z asynchronicznego środowiska Rust i jednowątkowego środowiska wykonawczego. Dzieje się tak, ponieważ musisz utworzyć wątek, w którym Tokio może wykonywać wygenerowane zadania. W tym przykładzie służy do tego wątek główny. Wszystkie zadania generowane za pomocą tokio::spawn będą wykonywane w wątku głównym.

W asynchronicznym backendzie Rust 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 wygenerowane zadania nie są wykonywane w wątku głównym. Dlatego lepiej wywoływać metodę join_thread_pool w wątku głównym, aby wątek główny nie był tylko nieaktywny. Aby pozostawić kontekst asynchroniczny, musisz opakować wywołanie w block_in_place.

Możesz poprosić o otrzymanie powiadomienia, gdy usługa hostująca segregator wygaśnie. Pomaga to uniknąć wycieku serwerów proxy wywołań zwrotnych i ułatwia usuwanie błędów. Wykonuj te wywołania na obiektach serwera proxy powiązań.

  • W Javie 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 potem wywołaj my_binder.link_to_death(&mut my_death_recipient). Pamiętaj, że ponieważ wywołanie zwrotne jest właścicielem obiektu DeathRecipient, musisz go przechowywać tak długo, jak chcesz otrzymywać powiadomienia.

Informacje o rozmówcy

Po otrzymaniu wywołania Binder jądra informacje wywołujące są dostępne w kilku interfejsach API. Identyfikator PID (inaczej identyfikator procesu) odnosi się do identyfikatora procesu w systemie Linux procesu, który wysyła transakcję. Identyfikator UID (inaczej User ID) odnosi się do identyfikatora użytkownika systemu Linux. W przypadku połączenia jednokierunkowego identyfikator PID wywołania ma wartość 0. Funkcje te nie znajdują się w kontekście transakcji powiązania, zwracają identyfikatory 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();

Podczas implementowania interfejsu w backendzie Rust podaj te informacje (zamiast zezwalać na ustawienie domyślne):

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

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

Po uruchomieniu raportów o błędach (np. za pomocą funkcji adb bugreport) zbierane są informacje z całego systemu, które ułatwiają debugowanie różnych problemów. W przypadku usług AIDL raporty o błędach używają pliku binarnego dumpsys we wszystkich usługach zarejestrowanych przez menedżera usługi, aby skopiować ich dane do raportu o błędzie. Aby uzyskać informacje z usługi za pomocą dumpsys SERVICE [ARGS], możesz też użyć w wierszu poleceń dumpsys. W backendach C++ i Java możesz kontrolować kolejność pobierania usług za pomocą dodatkowych argumentów funkcji addService. Możesz też użyć dumpsys --pid SERVICE, aby uzyskać identyfikator PID usługi podczas debugowania.

Aby dodać do usługi niestandardowe dane wyjściowe, możesz zastąpić metodę dump w obiekcie serwera tak, jak implementujesz dowolną inną metodę IPC zdefiniowaną w pliku AIDL. Aby to zrobić, ogranicz zapisywanie do uprawnień aplikacji android.permission.DUMP lub do konkretnych 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;

Podczas implementowania interfejsu w backendzie Rust podaj te informacje (zamiast zezwalać na ustawienie domyślne):

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

Dynamiczne pobieranie deskryptora interfejsu

Deskryptor interfejsu określa typ interfejsu. Jest to przydatne podczas debugowania lub tworzenia nieznanego powiązania.

W Javie możesz uzyskać deskryptor interfejsu zawierający kod podobny do tego:

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

W backendzie CPP:

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

Backendy NDK i Rust nie obsługują tej funkcji.

Statycznie pobieranie deskryptora interfejsu

Czasami (na przykład podczas rejestrowania usług @VintfStability) musisz wiedzieć, jaki deskryptor interfejsu jest statyczny. W języku Java można uzyskać deskryptor, dodając taki kod:

    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 backendach natywnych możesz iterować możliwe wartości wyliczeniowe. Ze względu na rozmiar kodu ta funkcja nie jest obsługiwana w Javie.

Oto iteracja MyEnum zdefiniowanej w AIDL.

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żde wystąpienie elementu libbinder w procesie utrzymuje 1 pulę wątków. W większości przypadków powinna to być dokładnie 1 pula wątków współdzielona przez wszystkie backendy. Jedynym wyjątkiem jest sytuacja, w której kod dostawcy może wczytać kolejną kopię libbinder w celu komunikacji z /dev/vndbinder. Ponieważ znajduje się on w osobnym węźle powiązania, pula wątków nie jest współdzielona.

W backendzie Java pula wątków może się tylko zwiększać (ponieważ jest już uruchomiona):

    BinderInternal.setMaxThreads(<new larger value>);

W backendzie 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 backendzie 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 asynchronicznym backendzie Rust potrzebujesz 2 pul wątków: Binder i Tokio. Oznacza to, że aplikacje korzystające z asynchronicznej wersji Rust wymagają szczególnych uwagi, zwłaszcza w przypadku korzystania z join_thread_pool. Więcej informacji na ten temat znajdziesz w sekcji dotyczącej rejestrowania usług.

Zarezerwowane nazwy

C++, Java i Rust rezerwują niektóre nazwy jako słowa kluczowe lub do użycia w różnych językach. Chociaż AIDL nie wymusza ograniczeń na podstawie reguł językowych, używanie nazw pól lub typów pasujących do zarezerwowanej nazwy może spowodować niepowodzenie kompilacji w języku C++ lub Java. W przypadku języka Rust nazwa pola lub typu jest zmieniana przy użyciu składni „nieprzetworzonego identyfikatora”, dostępnej za pomocą prefiksu r#.

Zalecamy unikanie używania nazw zarezerwowanych w definicjach AIDL tam, gdzie jest to możliwe, aby uniknąć nieergonomicznego wiązania lub całkowitego błędu kompilacji.

Jeśli w definicjach AIDL masz już zarezerwowane nazwy, możesz bezpiecznie zmienić nazwy pól, zachowując zgodność z protokołem. Aby kontynuować tworzenie, konieczne może być zaktualizowanie kodu, ale wszystkie utworzone wcześniej programy będą nadal ze sobą współpracować.

Nazwy, których należy unikać: