Backend AIDL

Un backend AIDL è un obiettivo per la generazione del codice stub. Quando si utilizzano i file AIDL, li si utilizza sempre in una determinata lingua con un runtime specifico. A seconda del contesto, dovresti utilizzare diversi backend AIDL.

AIDL ha i seguenti backend:

Backend Lingua Superficie dell'API Costruisci sistemi
Giava Giava SDK/SystemApi (stabile*) tutti
NDK C++ libbinder_ndk (stabile*) interfaccia_aidl
CPP C++ libbinder (instabile) tutti
Ruggine Ruggine libbinder_rs (instabile) interfaccia_aidl
  • Queste superfici API sono stabili, ma molte delle API, ad esempio quelle per la gestione dei servizi, sono riservate all'uso interno della piattaforma e non sono disponibili per le app. Per ulteriori informazioni su come utilizzare AIDL nelle app, consulta la documentazione per gli sviluppatori .
  • Il backend Rust è stato introdotto in Android 12; il backend NDK è disponibile a partire da Android 10.
  • La cassa Rust è costruita su libbinder_ndk . Gli APEX utilizzano la cassa del raccoglitore allo stesso modo di chiunque altro sul lato del sistema. La porzione Rust è impacchettata in un APEX e spedita al suo interno. Dipende da libbinder_ndk.so sulla partizione di sistema.

Costruisci sistemi

A seconda del backend, ci sono due modi per compilare AIDL nel codice stub. Per ulteriori dettagli sui sistemi di compilazione, vedere la guida di riferimento del modulo Soong .

Sistema di costruzione di base

In qualsiasi cc_ o java_ Android.bp (o nei loro equivalenti Android.mk ), i file .aidl possono essere specificati come file sorgente. In questo caso, vengono utilizzati i backend Java/CPP di AIDL (non il backend NDK) e le classi per utilizzare i file AIDL corrispondenti vengono aggiunte automaticamente al modulo. Opzioni come local_include_dirs , che indica al sistema di compilazione il percorso root dei file AIDL in quel modulo, possono essere specificate in questi moduli sotto un gruppo aidl: Si noti che il backend di Rust è solo per l'uso con Rust. I moduli rust_ sono gestiti in modo diverso in quanto i file AIDL non sono specificati come file sorgente. Invece, il modulo aidl_interface produce una rustlib chiamata <aidl_interface name>-rust quale può essere collegata. Per ulteriori dettagli, vedere l' esempio Rust AIDL .

interfaccia_aidl

Vedere AIDL stabile . I tipi utilizzati con questo sistema di compilazione devono essere strutturati; cioè espresso direttamente in AIDL. Ciò significa che i parcelable personalizzati non possono essere utilizzati.

Tipi

Puoi considerare il compilatore aidl come un'implementazione di riferimento per i tipi. Quando crei un'interfaccia, invoca aidl --lang=<backend> ... per vedere il file di interfaccia risultante. Quando usi il modulo aidl_interface , puoi visualizzare l'output in out/soong/.intermediates/<path to module>/ .

Tipo Java/AIDL Tipo C++ Tipo NDK Tipo di ruggine
booleano bool bool bool
byte int8_t int8_t i8
char char16_t char16_t u16
int int32_t int32_t i32
lungo int64_t int64_t i64
galleggiante galleggiante galleggiante f32
Doppio Doppio Doppio f64
Corda android::String16 std::stringa Corda
android.os.Parcelable android::Parcellabile N / A N / A
Raccoglitore android::IBinder ndk::SpAIBinder binder::SpIBinder
T[] std::vettore<T> std::vettore<T> In: &[T]
Uscita: Vec<T>
byte[] std::vector<uint8_t> std::vector<int8_t> 1 In: &[u8]
Fuori: Vec<u8>
Lista<T> std::vector<T> 2 std::vector<T> 3 In: &[T] 4
Uscita: Vec<T>
FileDescriptor android::base::unique_fd N / A binder::parcel::ParcelFileDescriptor
ParcelFileDescriptor android::os::ParcelFileDescriptor ndk::ScopedFileDescriptor binder::parcel::ParcelFileDescriptor
tipo di interfaccia (T) android::sp<T> std::shared_ptr<T> raccoglitore::Forte
tipo parcellabile (T) T T T
tipo di raccordo (T) 5 T T T
T[N] 6 std::array<T, N> std::array<T, N> [T; N]

1. In Android 12 o versioni successive, gli array di byte utilizzano uint8_t anziché int8_t per motivi di compatibilità.

2. Il backend C++ supporta List<T> dove T è uno tra String , IBinder , ParcelFileDescriptor o parcelable. In Android 13 o versioni successive, T può essere qualsiasi tipo non primitivo (inclusi i tipi di interfaccia) ad eccezione degli array. AOSP consiglia di utilizzare tipi di array come T[] , poiché funzionano in tutti i backend.

3. Il backend NDK supporta List<T> dove T è uno tra String , ParcelFileDescriptor o parcelable. In Android 13 o versioni successive, T può essere qualsiasi tipo non primitivo tranne gli array.

4. I tipi vengono passati in modo diverso per il codice Rust a seconda che siano input (un argomento) o output (un valore restituito).

5. I tipi di unione sono supportati in Android 12 e versioni successive.

6. In Android 13 o versioni successive, sono supportati gli array di dimensioni fisse. Gli array a dimensione fissa possono avere più dimensioni (ad esempio int[3][4] ). Nel backend Java, gli array a dimensione fissa sono rappresentati come tipi di array.

Direzionalità (in/out/inout)

Quando si specificano i tipi degli argomenti alle funzioni, è possibile specificarli come in , out o inout . Questo controlla in quale direzione vengono passate le informazioni per una chiamata IPC. in è la direzione predefinita e indica che i dati vengono passati dal chiamante al chiamato. out significa che i dati vengono passati dal chiamato al chiamante. inout è la combinazione di entrambi. Tuttavia, il team di Android consiglia di evitare di utilizzare l'identificatore di argomento inout . Se utilizzi inout con un'interfaccia versionata e un chiamato meno recente, i campi aggiuntivi presenti solo nel chiamante vengono reimpostati sui valori predefiniti. Rispetto a Rust, un normale tipo inout riceve &mut Vec<T> , e un tipo list inout riceve &mut Vec<T> .

UTF8/UTF16

Con il backend CPP puoi scegliere se le stringhe sono utf-8 o utf-16. Dichiara le stringhe come @utf8InCpp String in AIDL per convertirle automaticamente in utf-8. I backend NDK e Rust utilizzano sempre stringhe utf-8. Per ulteriori informazioni sull'annotazione utf8InCpp , vedere Annotazioni in AIDL .

Annullabilità

Puoi annotare i tipi che possono essere null nel backend Java con @nullable per esporre valori null ai backend CPP e NDK. Nel backend Rust questi tipi @nullable sono esposti come Option<T> . I server nativi rifiutano i valori null per impostazione predefinita. Le uniche eccezioni a questo sono i tipi di interface e IBinder , che possono sempre essere nulli per le letture NDK e le scritture CPP/NDK. Per ulteriori informazioni sull'annotazione nullable , vedere Annotazioni in AIDL .

Parcelable personalizzati

Nei backend C++ e Java nel sistema di compilazione principale, puoi dichiarare un parcelable implementato manualmente in un backend di destinazione (in C++ o in Java).

    package my.package;
    parcelable Foo;

o con dichiarazione di intestazione C++:

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

Quindi puoi utilizzare questo parcelable come tipo nei file AIDL, ma non verrà generato da AIDL.

Rust non supporta i parcelable personalizzati.

Valori standard

I parcelable strutturati possono dichiarare valori predefiniti per campo per primitive, String e array di questi tipi.

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

Nel backend Java quando mancano i valori predefiniti, i campi vengono inizializzati come valori zero per i tipi primitivi e null per i tipi non primitivi.

In altri backend, i campi vengono inizializzati con valori inizializzati predefiniti quando i valori predefiniti non sono definiti. Ad esempio, nel back-end C++, i campi String vengono inizializzati come una stringa vuota e i campi List<T> vengono inizializzati come un vector<T> vuoto. I campi @nullable vengono inizializzati come campi con valore nullo.

Gestione degli errori

Il sistema operativo Android fornisce tipi di errore incorporati per i servizi da utilizzare durante la segnalazione degli errori. Questi vengono utilizzati dal raccoglitore e possono essere utilizzati da qualsiasi servizio che implementa un'interfaccia del raccoglitore. Il loro utilizzo è ben documentato nella definizione AIDL e non richiedono alcuno stato definito dall'utente o tipo restituito.

Parametri di output con errori

Quando una funzione AIDL segnala un errore, la funzione potrebbe non inizializzare o modificare i parametri di output. In particolare, i parametri di output possono essere modificati se l'errore si verifica durante l'unparceling invece che durante l'elaborazione della transazione stessa. In generale, quando si riceve un errore da una funzione AIDL, tutti i parametri inout e out così come il valore restituito (che agisce come un parametro out in alcuni backend) dovrebbero essere considerati in uno stato indefinito.

Quali valori di errore utilizzare

Molti dei valori di errore incorporati possono essere utilizzati in qualsiasi interfaccia AIDL, ma alcuni vengono trattati in modo speciale. Ad esempio, EX_UNSUPPORTED_OPERATION e EX_ILLEGAL_ARGUMENT possono essere utilizzati quando descrivono la condizione di errore, ma EX_TRANSACTION_FAILED non deve essere utilizzato perché viene trattato in modo speciale dall'infrastruttura sottostante. Controlla le definizioni specifiche del backend per ulteriori informazioni su questi valori incorporati.

Se l'interfaccia AIDL richiede valori di errore aggiuntivi che non sono coperti dai tipi di errore integrati, possono utilizzare l'errore integrato specifico del servizio che consente l'inclusione di un valore di errore specifico del servizio definito dall'utente . Questi errori specifici del servizio sono in genere definiti nell'interfaccia AIDL come const int o int -backed enum e non vengono analizzati dal binder.

In Java, gli errori vengono associati alle eccezioni, ad esempio android.os.RemoteException . Per le eccezioni specifiche del servizio, Java utilizza android.os.ServiceSpecificException insieme all'errore definito dall'utente.

Il codice nativo in Android non usa le eccezioni. Il backend CPP utilizza android::binder::Status . Il back-end NDK utilizza ndk::ScopedAStatus . Ogni metodo generato da AIDL restituisce uno di questi, che rappresenta lo stato del metodo. Il backend Rust utilizza gli stessi valori del codice di eccezione dell'NDK, ma li converte in errori Rust nativi ( StatusCode , ExceptionCode ) prima di consegnarli all'utente. Per gli errori specifici del servizio, lo Status restituito o ScopedAStatus utilizza EX_SERVICE_SPECIFIC insieme all'errore definito dall'utente.

I tipi di errore incorporati possono essere trovati nei seguenti file:

Backend Definizione
Giava android/os/Parcel.java
CPP binder/Status.h
NDK android/binder_status.h
Ruggine android/binder_status.h

Utilizzo di vari backend

Queste istruzioni sono specifiche per il codice della piattaforma Android. Questi esempi usano un tipo definito, my.package.IFoo . Per istruzioni su come utilizzare il backend Rust, vedere l' esempio Rust AIDL nella pagina Android Rust Patterns .

Tipi di importazione

Se il tipo definito è un'interfaccia, un parcelable o un'unione, puoi importarlo in Java:

import my.package.IFoo;

O nel backend CPP:

#include <my/package/IFoo.h>

O nel backend NDK (nota lo spazio dei nomi aggiuntivo di aidl ):

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

Oppure nel backend di Rust:

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

Sebbene sia possibile importare un tipo nidificato in Java, nei backend CPP/NDK è necessario includere l'intestazione per il relativo tipo root. Ad esempio, quando importi un tipo annidato Bar definito in my/package/IFoo.aidl ( IFoo è il tipo root del file) devi includere <my/package/IFoo.h> per il backend CPP (o <aidl/my/package/IFoo.h> per il backend NDK).

Servizi di implementazione

Per implementare un servizio, devi ereditare dalla classe stub nativa. Questa classe legge i comandi dal driver del raccoglitore ed esegue i metodi implementati. Immagina di avere un file AIDL come questo:

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

In Java, devi estendere da questa classe:

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

Nel back-end CPP:

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

Nel backend NDK (notare lo spazio dei nomi aggiuntivo di aidl ):

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

Nel backend di 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(())
        }
    }

Registrazione e acquisizione dei servizi

I servizi nella piattaforma Android sono solitamente registrati con il processo servicemanager . Oltre alle API seguenti, alcune API controllano il servizio (il che significa che ritornano immediatamente se il servizio non è disponibile). Controllare l'interfaccia di servicemanager corrispondente per i dettagli esatti. Queste operazioni possono essere eseguite solo durante la compilazione su piattaforma Android.

In Giava:

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

Nel back-end 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"));

Nel backend NDK (notare lo spazio dei nomi aggiuntivo di aidl ):

    #include <android/binder_manager.h>
    // registering
    status_t err = AServiceManager_addService(myService->asBinder().get(), "service-name");
    // return if service is started now
    myService = IFoo::fromBinder(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(SpAIBinder(AServiceManager_waitForService("service-name")));

Nel backend di 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()
}

Puoi richiedere di ricevere una notifica per quando un servizio che ospita un raccoglitore muore. Questo può aiutare a evitare la perdita di proxy di callback o assistere nel ripristino degli errori. Eseguire queste chiamate sugli oggetti proxy del raccoglitore.

  • In Java, usa android.os.IBinder::linkToDeath .
  • Nel backend CPP, usa android::IBinder::linkToDeath .
  • Nel backend NDK, usa AIBinder_linkToDeath .
  • Nel backend Rust, crea un oggetto DeathRecipient , quindi chiama my_binder.link_to_death(&mut my_death_recipient) . Si noti che poiché il DeathRecipient possiede il callback, è necessario mantenere attivo tale oggetto finché si desidera ricevere notifiche.

Informazioni sul chiamante

Quando si riceve una chiamata al binder del kernel, le informazioni sul chiamante sono disponibili in diverse API. Il PID (o Process ID) si riferisce all'ID del processo Linux del processo che sta inviando una transazione. L'UID (o ID utente) si riferisce all'ID utente di Linux. Quando si riceve una chiamata unidirezionale, il PID chiamante è 0. Quando si è al di fuori di un contesto di transazione del raccoglitore, queste funzioni restituiscono il PID e l'UID del processo corrente.

Nel backend Java:

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

Nel back-end CPP:

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

Nel back-end NDK:

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

Nel backend di Rust, quando si implementa l'interfaccia, specificare quanto segue (invece di consentirne l'impostazione predefinita):

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

Segnalazioni di bug e API di debug per i servizi

Quando vengono eseguiti i bugreport (ad esempio, con adb bugreport ), raccolgono informazioni da tutto il sistema per facilitare il debug di vari problemi. Per i servizi AIDL, i bugreport utilizzano i dumpsys binari su tutti i servizi registrati con il gestore del servizio per scaricare le loro informazioni nel bugreport. Puoi anche utilizzare dumpsys sulla riga di comando per ottenere informazioni da un servizio con dumpsys SERVICE [ARGS] . Nei backend C++ e Java, puoi controllare l'ordine in cui i servizi vengono scaricati utilizzando argomenti aggiuntivi per addService . Puoi anche utilizzare dumpsys --pid SERVICE per ottenere il PID di un servizio durante il debug.

Per aggiungere un output personalizzato al tuo servizio, puoi sovrascrivere il metodo dump nel tuo oggetto server come se stessi implementando qualsiasi altro metodo IPC definito in un file AIDL. Quando si esegue questa operazione, è necessario limitare il dumping all'autorizzazione dell'app android.permission.DUMP o limitare il dumping a UID specifici.

Nel backend Java:

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

Nel back-end CPP:

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

Nel back-end NDK:

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

Nel backend di Rust, quando si implementa l'interfaccia, specificare quanto segue (invece di consentirne l'impostazione predefinita):

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

Recupero dinamico del descrittore di interfaccia

Il descrittore di interfaccia identifica il tipo di un'interfaccia. Ciò è utile durante il debug o quando si dispone di un raccoglitore sconosciuto.

In Java, puoi ottenere il descrittore dell'interfaccia con codice come:

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

Nel back-end CPP:

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

I backend NDK e Rust non supportano questa funzionalità.

Recupero statico del descrittore dell'interfaccia

A volte (come quando si registrano i servizi @VintfStability ), è necessario sapere qual è staticamente il descrittore dell'interfaccia. In Java, puoi ottenere il descrittore aggiungendo codice come:

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

Nel back-end CPP:

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

Nel backend NDK (notare lo spazio dei nomi aggiuntivo di aidl ):

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

Nel backend di Rust:

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

Intervallo enumerazione

Nei backend nativi, puoi iterare sui possibili valori che un enum può assumere. A causa di considerazioni sulla dimensione del codice, attualmente non è supportato in Java.

Per un enum MyEnum definito in AIDL, l'iterazione viene fornita come segue.

Nel back-end CPP:

    ::android::enum_range<MyEnum>()

Nel back-end NDK:

   ::ndk::enum_range<MyEnum>()

Nel backend di Rust:

    MyEnum::enum_range()

Gestione dei thread

Ogni istanza di libbinder in un processo mantiene un pool di thread. Per la maggior parte dei casi d'uso, questo dovrebbe essere esattamente un threadpool, condiviso tra tutti i backend. L'unica eccezione a questo è quando il codice del fornitore potrebbe caricare un'altra copia di libbinder per parlare con /dev/vndbinder . Poiché si trova su un nodo binder separato, il threadpool non è condiviso.

Per il backend Java, il threadpool può solo aumentare di dimensioni (poiché è già avviato):

    BinderInternal.setMaxThreads(<new larger value>);

Per il backend CPP, sono disponibili le seguenti operazioni:

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

Allo stesso modo, nel backend NDK:

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

Nel backend di Rust:

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

Nomi riservati

C++, Java e Rust riservano alcuni nomi come parole chiave o per uso specifico del linguaggio. Sebbene AIDL non imponga restrizioni basate sulle regole del linguaggio, l'utilizzo di nomi di campi o tipi che corrispondono a un nome riservato potrebbe comportare un errore di compilazione per C++ o Java. Per Rust, il campo o il tipo viene rinominato usando la sintassi "raw identifier", accessibile usando il prefisso r# .

Si consiglia di evitare di utilizzare nomi riservati nelle definizioni AIDL, ove possibile, per evitare associazioni non ergonomiche o errori di compilazione.

Se hai già nomi riservati nelle tue definizioni AIDL, puoi tranquillamente rinominare i campi rimanendo compatibile con il protocollo; potrebbe essere necessario aggiornare il codice per continuare a creare, ma tutti i programmi già creati continueranno a interagire.

Nomi da evitare: * Parole chiave C++ * Parole chiave Java * Parole chiave Rust