Zaplecze AIDL

Backend AIDL jest celem generowania kodu pośredniczącego. Korzystając z plików AIDL, zawsze używasz ich w określonym języku i określonym środowisku 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 Buduj systemy
Jawa Jawa SDK/SystemApi (stabilny*) Wszystko
NDK C++ libbinder_ndk (stabilny*) interfejs_pomocniczy
CPP C++ libbinder (niestabilny) Wszystko
Rdza Rdza libbinder_rs (niestabilny) interfejs_pomocniczy
  • Te powierzchnie API są stabilne, ale wiele interfejsów API, takich jak te do zarządzania usługami, jest zarezerwowanych do użytku na platformie wewnętrznej i nie jest dostępnych dla aplikacji. Więcej informacji na temat korzystania z AIDL w aplikacjach można znaleźć w dokumentacji dla programistów .
  • Backend Rusta został wprowadzony w Androidzie 12; backend NDK jest dostępny od Androida 10.
  • Skrzynia Rust jest zbudowana na bazie libbinder_ndk . Urządzenia APEX korzystają ze skrzynki segregatorów w taki sam sposób, jak wszyscy inni po stronie systemu. Część rdzy jest pakowana w APEX i wysyłana w środku. Zależy to od pliku libbinder_ndk.so na partycji systemowej.

Buduj systemy

W zależności od backendu istnieją dwa sposoby kompilowania AIDL do kodu pośredniczącego. Aby uzyskać więcej informacji na temat systemów kompilacji, zobacz Dokumentacja modułu Soong .

Podstawowy system budowania

W dowolnym module cc_ lub java_ Android.bp (lub w ich odpowiednikach Android.mk ) jako pliki źródłowe można określić pliki .aidl . W tym przypadku używane jest zaplecze Java/CPP AIDL (a nie zaplecze NDK), a klasy korzystające z odpowiednich 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, można określić w tych modułach w grupie aidl: Pamiętaj, że backend Rusta jest przeznaczony wyłącznie 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 powiązać. Aby uzyskać więcej informacji, zobacz przykład Rust AIDL .

interfejs_pomocniczy

Typy używane w tym systemie kompilacji muszą mieć strukturę. Aby mieć strukturę, parcelable muszą zawierać bezpośrednio pola, a nie deklaracje typów zdefiniowanych bezpośrednio w językach docelowych. Aby dowiedzieć się, jak ustrukturyzowany AIDL pasuje do stabilnego AIDL, zobacz Strukturalny a stabilny AIDL .

Typy

Kompilator aidl można uznać za implementację referencyjną dla typów. Kiedy tworzysz 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/AIDL Typ C++ Typ NDK Typ rdzy
wartość logiczna bool bool bool
bajt int8_t int8_t i8
zwęglać char16_t char16_t u16
wew int32_t int32_t i32
długi int64_t int64_t i64
platforma platforma platforma f32
podwójnie podwójnie podwójnie f64
Strunowy android::String16 std::string Strunowy
android.os.Parcelable Android:: Możliwość wysyłki Nie dotyczy Nie dotyczy
IBinder android::IBinder ndk::SpAIBinder spoiwo::SpIBinder
T[] std::wektor<T> std::wektor<T> W: &[T]
Schodzi: Vec<T>
bajt[] std::vector<uint8_t> std::vector<int8_t> 1 W: &[u8]
Schodzi: Vec<u8>
Lista<T> std::vector<T> 2 std::vector<T> 3 W: &[T] 4
Schodzi: Vec<T>
Deskryptor pliku android::base::unique_fd Nie dotyczy binder::parcel::ParcelFileDescriptor
Deskryptor pliku działki android::os::ParcelFileDescriptor ndk::ScopedFileDescriptor binder::parcel::ParcelFileDescriptor
typ interfejsu (T) android::sp<T> std::shared_ptr<T> spoiwo::Mocne
typ z możliwością wysyłki (T) T T T
typ złącza (T) 5 T T T
T[N] 6 std::array<T, N> std::array<T, N> [T; N]

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

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

4. Typy są przekazywane w różny sposób w kodzie Rusta, w zależności od tego, czy są wprowadzane (argument), czy też wyjściowe (zwracana wartość).

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

6. W systemie Android 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 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 je określić jako in , out lub inout . Kontroluje to, w jakim kierunku przekazywane są informacje dla połączenia IPC. in jest kierunkiem domyślnym i wskazuje, że dane są przekazywane od osoby wywołującej do odbiorcy. out oznacza, że ​​dane są przekazywane od odbiorcy do dzwoniącego. inout jest kombinacją obu. Zespół Androida zaleca jednak unikanie używania specyfikatora argumentu inout . Jeśli użyjesz inout z wersjonowanym interfejsem i starszym obiektem wywoływanym, dodatkowe pola obecne tylko w obiekcie wywołującym zostaną zresetowane do wartości domyślnych. W odniesieniu do Rusta normalny typ inout otrzymuje &mut Vec<T> , a typ inout otrzymuje &mut Vec<T> .

UTF8/UTF16

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

Możliwość zerowania

Możesz dodawać adnotacje do typów, które mogą mieć wartość null w backendie Java, używając @nullable , aby udostępnić wartości null backendom CPP i NDK. W zapleczu Rusta te typy @nullable są widoczne jako Option<T> . Serwery natywne domyślnie odrzucają wartości null. Jedynymi wyjątkami 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 , zobacz Adnotacje w AIDL .

Przesyłki niestandardowe

Pakiet niestandardowy to pakiet, który jest zaimplementowany ręcznie w docelowym backendie. Używaj niestandardowych pakietów do pakowania tylko wtedy, gdy próbujesz dodać obsługę innych języków dla istniejącego niestandardowego pakietu do pakowania, którego nie można zmienić.

Aby zadeklarować przesyłkę niestandardową, aby AIDL o tym wiedziała, deklaracja przesyłki AIDL wygląda następująco:

    package my.pack.age;
    parcelable Foo;

Domyślnie deklaruje to możliwość parcelowania Java, gdzie my.pack.age.Foo jest klasą Java implementującą interfejs Parcelable .

Aby zadeklarować niestandardowy backend CPP, który można podzielić na paczki w AIDL, użyj cpp_header :

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

Implementacja C++ w my/pack/age/Foo.h wygląda następująco:

    #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 pakiet NDK w formacie 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 następująco:

    #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), aby zadeklarować niestandardową wersję Rusta, którą można paczkować w AIDL, użyj rust_type :

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

Implementacja Rusta w rust_crate/src/lib.rs wygląda następująco:

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 CPP/NDK zaplecza, aby używać ich w union .

Wartości domyślne

Strukturyzowane pakiety mogą deklarować domyślne wartości poszczególnych pól dla elementów pierwotnych, String znaków i tablic tego typu.

    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 domyślnymi, zainicjowanymi wartościami, jeśli wartości domyślne nie są zdefiniowane. Na przykład w zapleczu języka C++ pola String są inicjowane jako pusty ciąg, 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 usługi wbudowane typy błędów, które można wykorzystać podczas raportowania błędów. Są one używane przez binder i mogą być używane przez dowolne usługi implementujące interfejs bindera. Ich użycie jest dobrze udokumentowane w definicji AIDL i nie wymagają żadnego zdefiniowanego przez użytkownika statusu ani typu zwrotu.

Parametry wyjściowe z błędami

Gdy funkcja AIDL zgłasza błąd, może nie zainicjować lub zmodyfikować parametrów wyjściowych. W szczególności parametry wyjściowe można modyfikować, jeśli błąd wystąpi podczas rozpakowywania, a nie podczas przetwarzania samej transakcji. Ogólnie rzecz biorąc, w przypadku otrzymania błędu z funkcji AIDL, wszystkie parametry inout i out , a także wartość zwracaną (która w niektórych backendach działa jak parametr out ) należy uważać za nieokreślone.

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

Wiele wbudowanych wartości błędów może być używanych w dowolnym interfejsie AIDL, ale niektóre są traktowane w specjalny sposób. Na przykład EX_UNSUPPORTED_OPERATION i EX_ILLEGAL_ARGUMENT można używać do opisu warunku błędu, ale EX_TRANSACTION_FAILED nie można używać, ponieważ jest on traktowany wyjątkowo przez podstawową infrastrukturę. Sprawdź definicje specyficzne dla zaplecza, aby uzyskać więcej informacji na temat tych wbudowanych wartości.

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 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 spoiwo.

W Javie błędy są mapowane na wyjątki, takie jak android.os.RemoteException . W przypadku wyjątków specyficznych dla 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 nich, reprezentującą 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 backendów

Te instrukcje dotyczą kodu platformy Android. W tych przykładach używany jest zdefiniowany typ 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, pakowalnym czy unią, możesz go zaimportować w Javie:

import my.package.IFoo;

Lub w backendie 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 zapleczu Rusta:

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 jego typu głównego. Na przykład, podczas importowania zagnieżdżonego typu Bar zdefiniowanego w my/package/IFoo.aidl ( IFoo jest typem głównym pliku) musisz dołączyć <my/package/IFoo.h> dla backendu CPP (lub <aidl/my/package/IFoo.h> dla zaplecza NDK).

Wdrażanie usług

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

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

W Javie musisz rozszerzyć tę klasę:

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

    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 z asynchronicznym Rustem:

    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 uzyskiwanie usług

Usługi na platformie Android rejestrowane są najczęściej w procesie servicemanager . Oprócz poniższych interfejsów API niektóre interfejsy API sprawdzają usługę (co oznacza, że ​​zwracają się natychmiast, jeśli usługa nie jest dostępna). Aby uzyskać dokładne informacje, sprawdź odpowiedni interfejs servicemanager . Te operacje można wykonać tylko podczas kompilacji 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 Rusta:

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 backendie 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 will run on this thread.
    std::future::pending().await
}

Ważną różnicą w stosunku do innych opcji jest to, że nie wywołujemy join_thread_pool podczas korzystania z asynchronicznego Rusta i jednowątkowego środowiska wykonawczego. Dzieje się tak, ponieważ musisz dać Tokio wątek, w którym może wykonywać zrodzone zadania. W tym przykładzie główny wątek będzie służył temu celowi. Wszelkie zadania utworzone za pomocą tokio::spawn zostaną wykonane w głównym wątku.

W asynchronicznym backendie 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 utworzone zadania nie są wykonywane w głównym wątku. Dlatego rozsądniej jest wywołać join_thread_pool w głównym wątku, aby główny wątek nie był po prostu bezczynny. Musisz zawinąć wywołanie w block_in_place , aby opuścić kontekst asynchroniczny.

Możesz poprosić o powiadomienie, gdy usługa hostująca segregator przestanie działać. Może to pomóc w uniknięciu wycieków serwerów proxy wywołania zwrotnego lub pomóc w odzyskiwaniu błędów. Wykonuj te wywołania na obiektach proxy segregatora.

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

Informacje o dzwoniącym

Podczas odbierania wywołania spoiwa jądra informacje o wywołującym są dostępne w kilku interfejsach API. PID (lub identyfikator procesu) odnosi się do identyfikatora procesu Linux, który wysyła transakcję. UID (lub identyfikator użytkownika) odnosi się do identyfikatora użytkownika systemu Linux. Podczas odbierania wywołania jednokierunkowego wywołujący PID wynosi 0. Gdy poza kontekstem transakcji segregatora, te funkcje zwracają PID i UID bieżącego procesu.

W backendzie Java:

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

W backendzie CPP:

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

W zapleczu NDK:

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

W backendzie Rusta, implementując interfejs, określ następujące elementy (zamiast zezwalać na ustawienie domyślne):

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

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

Kiedy uruchamiane są raporty o błędach (na przykład z adb bugreport ), zbierają informacje z całego systemu, aby pomóc w debugowaniu różnych problemów. W przypadku usług AIDL raporty błędów wykorzystują plik binarny dumpsys we wszystkich usługach zarejestrowanych w menedżerze usług w celu zrzutu informacji do raportu o błędach. Możesz także 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ść zrzucania usług, używając dodatkowych argumentów do addService . Możesz także użyć dumpsys --pid SERVICE aby uzyskać PID usługi podczas debugowania.

Aby dodać niestandardowe dane wyjściowe do swojej 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, powinieneś ograniczyć zrzucanie do uprawnień aplikacji android.permission.DUMP lub ograniczyć zrzucanie 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 zapleczu NDK:

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

W backendzie Rusta, implementując interfejs, 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 segregator.

W Javie deskryptor interfejsu można uzyskać za pomocą kodu takiego 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 rejestracji 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 Rusta:

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

Zakres wyliczeniowy

W natywnych backendach możesz iterować po możliwych wartościach, jakie może przyjąć wyliczenie. Ze względu na rozmiar kodu nie jest to obecnie obsługiwane w Javie.

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

W backendzie CPP:

    ::android::enum_range<MyEnum>()

W zapleczu NDK:

   ::ndk::enum_range<MyEnum>()

W backendzie Rusta:

    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 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 oddzielnym węźle powiązania, pula wątków nie jest udostępniana.

W przypadku zaplecza Java pula wątków może jedynie zwiększyć rozmiar (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 Rusta:

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

Dzięki asynchronicznemu backendowi Rust potrzebujesz dwóch pul wątków: spoiwa i Tokio. Oznacza to, że aplikacje korzystające z asynchronicznego Rusta wymagają specjalnego rozważenia, zwłaszcza jeśli chodzi o użycie join_thread_pool . Więcej informacji na ten temat znajdziesz w części dotyczącej rejestracji usług .

Zarezerwowane nazwy

C++, Java i Rust rezerwują niektóre nazwy jako słowa kluczowe lub do użytku specyficznego dla języka. Chociaż AIDL nie wymusza ograniczeń opartych na regułach językowych, użycie nazw pól lub typów pasujących do zastrzeżonej nazwy może spowodować niepowodzenie kompilacji w przypadku C++ lub Java. W przypadku Rusta nazwa pola lub typu jest zmieniana przy użyciu składni „surowego identyfikatora”, dostępnej po przedrostku r# .

Zalecamy, aby tam, gdzie to możliwe, unikać używania nazw zastrzeżonych w definicjach AIDL, aby uniknąć nieergonomicznych powiązań lub całkowitego niepowodzenia kompilacji.

Jeśli masz już zastrzeżone nazwy w definicjach AIDL, możesz bezpiecznie zmieniać nazwy pól, zachowując jednocześnie zgodność z protokołem; być może będziesz musiał zaktualizować swój kod, aby kontynuować budowanie, ale wszystkie już zbudowane programy będą nadal współdziałać.

Nazwy, których należy unikać: * Słowa kluczowe C++ * Słowa kluczowe Java * Słowa kluczowe Rust