Zaplecze AIDL

Backend AIDL jest celem generowania kodu pośredniczącego. Używając plików AIDL, zawsze używasz ich w określonym języku z określonym środowiskiem wykonawczym. W zależności od kontekstu powinieneś używać różnych backendów AIDL.

AIDL ma następujące backendy:

Zaplecze Język Powierzchnia API Budowanie systemów
Jawa Jawa SDK/SystemApi (stabilny*) wszystko
NDK C++ libbinder_ndk (stabilny*) pomoc_interfejsu
CPP C++ libbinder (niestabilny) wszystko
Rdza Rdza libbinder_rs (niestabilny) pomoc_interfejsu
  • Te powierzchnie interfejsów API są stabilne, ale wiele interfejsów API, takich jak te służące do zarządzania usługami, jest zarezerwowanych do użytku wewnętrznego platformy i nie jest dostępnych dla aplikacji. Więcej informacji na temat korzystania z AIDL w aplikacjach znajdziesz w dokumentacji dla programistów .
  • Backend Rust został wprowadzony w Androidzie 12; backend NDK jest dostępny od Androida 10.
  • Skrzynia Rust jest zbudowana na bazie libbinder_ndk . APEX-y używają skrzynki segregatora w taki sam sposób, jak wszyscy inni po stronie systemu. Część Rust jest pakowana w APEX i dostarczana w środku. Zależy to od libbinder_ndk.so na partycji systemowej.

Budowanie systemów

W zależności od zaplecza istnieją dwa sposoby kompilacji AIDL do kodu pośredniczącego. Aby uzyskać więcej informacji na temat systemów kompilacji, zobacz Soong Module Reference .

System budowy rdzenia

W dowolnym module java_ cc_ (lub w ich odpowiednikach Android.mk ) pliki .aidl można określić jako pliki źródłowe. W tym przypadku używane są backendy AIDL Java/CPP (nie backend NDK), a klasy do używania odpowiednich plików AIDL są automatycznie dodawane do modułu. Opcje takie jak local_include_dirs , które informują system budowania o ścieżce głównej do plików AIDL w tym module, można określić w tych modułach w grupie aidl: Pamiętaj, że backend Rusta jest przeznaczony tylko do użytku z Rustem. Moduły rust_ są obsługiwane inaczej, ponieważ pliki AIDL nie są określone jako pliki źródłowe. Zamiast tego moduł aidl_interface tworzy rustlib o nazwie <aidl_interface name>-rust , z którą można się połączyć. Aby uzyskać więcej informacji, zobacz przykład Rust AIDL .

pomoc_interfejsu

Zobacz Stabilny AIDL . Typy używane w tym systemie kompilacji muszą mieć strukturę; to jest bezpośrednio wyrażone w AIDL. Oznacza to, że nie można używać przesyłek niestandardowych.

Rodzaje

Kompilator aidl można traktować jako implementację referencyjną dla typów. Kiedy tworzysz interfejs, aidl --lang=<backend> ... , aby zobaczyć wynikowy plik interfejsu. Kiedy używasz modułu aidl_interface , możesz wyświetlić dane wyjściowe w out/soong/.intermediates/<path to module>/ .

Typ Java/AIDL Typ C++ Typ NDK Rodzaj rdzy
logiczne głupota głupota głupota
bajt int8_t int8_t i8
zwęglać char16_t char16_t u16
int int32_t int32_t i32
długi int64_t int64_t i64
unosić się unosić się unosić się f32
podwójnie podwójnie podwójnie f64
Strunowy android::String16 std::ciąg Strunowy
android.os.Przesyłka android::Parcelable Nie dotyczy Nie dotyczy
IBinder android::Binder ndk::SpAIBinder spoiwo::SpIBinder
T[] std::wektor<T> std::wektor<T> W: &T
Wyjście: Vec<T>
bajt[] std::wektor<uint8_t> std::wektor<int8_t> 1 W: &[u8]
Wyjście: Vec<u8>
Lista<T> std::wektor<T> 2 std::wektor<T> 3 W: &[T] 4
Wyjście: Vec<T>
Deskryptor pliku android::base::unique_fd Nie dotyczy segregator::paczka::ParcelFileDescriptor
ParcelFileDescriptor android::os::ParcelFileDescriptor ndk::ScopedFileDescriptor segregator::paczka::ParcelFileDescriptor
typ interfejsu (T) android::sp<T> std::shared_ptr<T> spoiwo::Mocne
typ działkowy (T) T T T
typ złączki (T) 5 T T T
T[N] 6 std::tablica<T, N> std::tablica<T, N> [T; N]

1. W systemie Android 12 lub nowszym tablice bajtowe używają uint8_t zamiast int8_t ze względu na zgodność.

2. Backend C++ obsługuje List<T> , gdzie T jest jednym z String , IBinder , ParcelFileDescriptor lub parcelable. W systemie Android T (eksperymentalne AOSP) lub nowszym, T może być dowolnym typem nieprymitywnym (w tym typami interfejsów) 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 jest jednym z String , ParcelFileDescriptor lub parcelable. W systemie Android T (eksperymentalne AOSP) lub nowszym, T może być dowolnym typem nieprymitywnym z wyjątkiem tablic.

4. Typy są przekazywane w różny sposób dla kodu Rusta w zależności od tego, czy są one danymi wejściowymi (argument), czy wyjściowymi (wartość zwracana).

5. Typy Unii są obsługiwane w systemie Android 12 i nowszych.

6. W systemie Android T (eksperymentalne AOSP) lub nowszym obsługiwane są macierze o stałym rozmiarze. Tablice o stałym rozmiarze mogą mieć wiele wymiarów (np. int[3][4] ). W zapleczu Java tablice o stałym rozmiarze są reprezentowane jako typy tablic.

Kierunkowość (wejście/wyjście/wejście)

Określając typy argumentów funkcji, możesz określić je jako in , out lub inout . Kontroluje to, w jakim kierunku przekazywane są informacje dla połączenia IPC. in jest domyślnym kierunkiem i wskazuje, że dane są przekazywane od wywołującego do wywoływanego. out oznacza, że ​​dane są przekazywane od wywoływanego do wywołującego. inout jest kombinacją obu tych elementów. Jednak zespół Androida zaleca unikanie używania specyfikatora argumentu inout . Jeśli używasz inout z wersjonowanym interfejsem i starszym wywoływanym, dodatkowe pola, które są obecne tylko w wywołującym, zostaną zresetowane do wartości domyślnych. W odniesieniu do Rust, normalny typ inout odbiera &mut Vec<T> , a typ listy inout odbiera &mut Vec<T> .

UTF8/UTF16

Dzięki backendowi CPP możesz wybrać, czy ciągi są utf-8 czy utf-16. Zadeklaruj ciągi 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. Aby uzyskać więcej informacji na temat adnotacji utf8InCpp , zobacz Adnotacje w AIDL .

Nieważność

Możesz opisywać typy, które mogą mieć wartość null w backendzie Java za pomocą @nullable , aby uwidocznić wartości null dla backendów CPP i NDK. W zapleczu Rust te @nullable typy są uwidaczniane jako Option<T> . Serwery natywne domyślnie odrzucają wartości null. Jedynymi wyjątkami od tego są typy interface i IBinder , które zawsze mogą mieć wartość null dla odczytów NDK i zapisów CPP/NDK. Aby uzyskać więcej informacji na temat adnotacji nullable wartość null, zobacz Adnotacje w AIDL .

Paczki niestandardowe

W backendach C++ i Java w systemie budowania rdzenia można zadeklarować element parcelable, który jest zaimplementowany ręcznie w docelowym backendzie (w C++ lub w Javie).

    package my.package;
    parcelable Foo;

lub z deklaracją nagłówka C++:

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

Następnie możesz użyć tego parcelable jako typu w plikach AIDL, ale nie będzie on generowany przez AIDL.

Rust nie obsługuje przesyłek niestandardowych.

Wartości domyślne

Strukturyzowane parcelables mogą deklarować wartości domyślne dla poszczególnych pól dla prymitywów, String i tablic tych typów.

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

W zapleczu Java, gdy brakuje wartości domyślnych, pola są inicjowane jako wartości zerowe dla typów pierwotnych i null dla typów innych niż pierwotne.

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

Obsługa błędów

System operacyjny Android zapewnia wbudowane typy błędów, których można używać podczas zgłaszania błędów przez usługi. Są one używane przez spinacz i mogą być używane przez dowolne usługi implementujące interfejs spinacza. Ich użycie jest dobrze udokumentowane w definicji AIDL i nie wymagają żadnego statusu zdefiniowanego przez użytkownika ani typu zwracanego.

Gdy funkcja AIDL zgłosi błąd, funkcja 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, podczas uzyskiwania błędu z funkcji AIDL, wszystkie parametry inout i out , a także wartość zwracana (która działa jak parametr out w niektórych backendach) powinny być uważane za w stanie nieokreślonym.

Jeśli interfejs AIDL wymaga dodatkowych wartości błędów, które nie są objęte wbudowanymi typami błędów, mogą użyć specjalnego wbudowanego błędu specyficznego dla usługi, który umożliwia włączenie wartości błędu specyficznego dla usługi, która jest zdefiniowana 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 spinacz.

W Javie błędy są mapowane na wyjątki, takie jak 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 generowana przez AIDL zwraca jeden z nich, reprezentujący status metody. Backend Rusta używa tych samych wartości kodów wyjątków co NDK, ale konwertuje je na natywne błędy Rusta ( StatusCode , ExceptionCode ) przed dostarczeniem ich użytkownikowi. W przypadku błędów specyficznych dla usługi zwrócony Status lub ScopedAStatus używa EX_SERVICE_SPECIFIC wraz z błędem zdefiniowanym przez użytkownika.

Wbudowane typy błędów można znaleźć w następujących plikach:

Zaplecze Definicja
Jawa android/os/Parcel.java
CPP binder/Status.h
NDK android/binder_status.h
Rdza android/binder_status.h

Korzystanie z różnych zapleczy

Te instrukcje dotyczą kodu platformy Android. W tych przykładach użyto zdefiniowanego typu my.package.IFoo . Aby uzyskać instrukcje dotyczące korzystania z zaplecza Rust, zobacz przykład Rust AIDL na stronie Android Rust Patterns .

Importowanie typów

Niezależnie od tego, czy zdefiniowany typ jest interfejsem, parcelable czy 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żesz zaimportować typ zagnieżdżony w Javie, w backendach CPP/NDK musisz dołączyć nagłówek dla jego typu głównego. Na przykład podczas importowania zagnieżdżonego typu Bar zdefiniowanego w my/package/IFoo.aidl ( IFoo jest głównym typem pliku) musisz dołączyć <my/package/IFoo.h> dla backendu CPP (lub <aidl/my/package/IFoo.h> dla backendu NDK).

Usługi wdrożeniowe

Aby zaimplementować usługę, musisz dziedziczyć z natywnej klasy pośredniczącej. Ta klasa odczytuje polecenia ze sterownika spinacza i wykonuje implementowane metody. Wyobraź sobie, że masz taki plik AIDL:

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

W Javie musisz rozszerzyć 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(())
        }
    }

Rejestracja i uzyskiwanie usług

Usługi na platformie Android są zwykle rejestrowane w procesie servicemanager . Oprócz poniższych interfejsów API niektóre interfejsy API sprawdzają usługę (co oznacza, że ​​natychmiast wracają, jeśli usługa jest niedostępna). Sprawdź odpowiedni interfejs servicemanager , aby uzyskać szczegółowe informacje. Te operacje można wykonać tylko podczas kompilacji na platformie Android.

W Javie:

    import android.os.ServiceManager;
    // registering
    ServiceManager.addService("service-name", myService);
    // getting
    myService = IFoo.Stub.asInterface(ServiceManager.getService("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);
    // getting
    status_t err = getService<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
    status_t err = AServiceManager_addService(myService->asBinder().get(), "service-name");
    // getting
    myService = IFoo::fromBinder(SpAIBinder(AServiceManager_getService("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(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()
}

Możesz poprosić o powiadomienie o śmierci usługi obsługującej segregator. Może to pomóc w uniknięciu wycieku serwerów proxy wywołań zwrotnych lub pomóc w odzyskiwaniu błędów. Wykonaj te wywołania obiektów proxy bindera.

  • 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 następnie wywołaj my_binder.link_to_death(&mut my_death_recipient) . Zauważ, że ponieważ DeathRecipient jest właścicielem wywołania zwrotnego, musisz utrzymywać ten obiekt przy życiu tak długo, jak chcesz otrzymywać powiadomienia.

Raporty o błędach i debugowanie API dla usług

Kiedy raporty błędów są uruchamiane (na przykład za pomocą adb bugreport ), zbierają informacje z całego systemu, aby pomóc w debugowaniu różnych problemów. W przypadku usług AIDL raporty o błędach używają binarnego dumpsys we wszystkich usługach zarejestrowanych przez menedżera usług, aby zrzucić swoje informacje do raportu błędów. Możesz również użyć dumpsys w wierszu poleceń, aby uzyskać informacje z usługi za pomocą dumpsys SERVICE [ARGS] . W backendach C++ i Java możesz kontrolować kolejność, w jakiej usługi są zrzucane, używając dodatkowych argumentów addService . Możesz również użyć dumpsys --pid SERVICE , aby uzyskać PID usługi podczas debugowania.

Aby dodać niestandardowe dane wyjściowe do usługi, możesz zastąpić metodę dump w obiekcie serwera, tak jak implementujesz dowolną inną metodę IPC zdefiniowaną w pliku AIDL. Robiąc to, należy ograniczyć zrzut do uprawnienia aplikacji android.permission.DUMP lub ograniczyć zrzut 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 backendzie Rusta, podczas implementacji interfejsu, określ następujące elementy (zamiast zezwalać na ustawienie domyślne):

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

Dynamiczne pobieranie deskryptora interfejsu

Deskryptor interfejsu identyfikuje typ interfejsu. Jest to przydatne podczas debugowania lub gdy masz nieznany spinacz.

W Javie można uzyskać deskryptor interfejsu z kodem takim jak:

    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 funkcjonalności.

Statyczne pobieranie deskryptora interfejsu

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

    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ć nad możliwymi wartościami, jakie może przyjąć enum. Ze względu na rozmiar kodu nie jest to obecnie obsługiwane w Javie.

W przypadku wyliczenia MyEnum zdefiniowanego w AIDL iteracja jest podana w następujący sposób.

W backendzie CPP:

    ::android::enum_range<MyEnum>()

W backendzie NDK:

   ::ndk::enum_range<MyEnum>()

W backendzie Rust:

    MyEnum::enum_range()

Zarządzanie wątkami

Każda instancja libbinder w procesie obsługuje jedną pulę wątków. W większości przypadków powinna to być dokładnie jedna pula wątków, współdzielona przez wszystkie backendy. Jedynym wyjątkiem jest sytuacja, gdy kod dostawcy może załadować kolejną kopię libbinder , aby porozmawiać z /dev/vndbinder . Ponieważ znajduje się to w osobnym węźle spinacza, pula wątków nie jest udostępniana.

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

    BinderInternal.setMaxThreads(<new larger value>);

Dla backendu CPP dostępne są następujące 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();