AIDL stabile

Android 10 aggiunge il supporto per Android Interface Definition Language (AIDL) stabile, un nuovo modo per tenere traccia dell'API (Application Program Interface)/ABI (Application Binary Interface) fornita dalle interfacce AIDL. L'AIDL stabile presenta le seguenti differenze chiave rispetto all'AIDL:

  • Le interfacce sono definite nel sistema di compilazione con aidl_interfaces .
  • Le interfacce possono contenere solo dati strutturati. I parcellabili che rappresentano i tipi desiderati vengono creati automaticamente in base alla loro definizione AIDL e vengono automaticamente sottoposti a marshalling e non marshalling.
  • Le interfacce possono essere dichiarate stabili (retrocompatibili). Quando ciò accade, la loro API viene tracciata e verificata in un file accanto all'interfaccia AIDL.

Definizione di un'interfaccia AIDL

Una definizione di aidl_interface è simile a questa:

aidl_interface {
    name: "my-aidl",
    srcs: ["srcs/aidl/**/*.aidl"],
    local_include_dir: "srcs/aidl",
    imports: ["other-aidl"],
    versions_with_info: [
        {
            version: "1",
            imports: ["ohter-aidl-V1"],
        },
        {
            version: "2",
            imports: ["other-aidl-V3"],
        }
    ],
    stability: "vintf",
    backend: {
        java: {
            enabled: true,
            platform_apis: true,
        },
        cpp: {
            enabled: true,
        },
        ndk: {
            enabled: true,
        },
        rust: {
            enabled: true,
        },
    },

}
  • name : il nome del modulo di interfaccia AIDL che identifica in modo univoco un'interfaccia AIDL.
  • srcs : l'elenco dei file sorgente AIDL che compongono l'interfaccia. Il percorso per un tipo AIDL Foo definito in un pacchetto com.acme dovrebbe essere in <base_path>/com/acme/Foo.aidl , dove <base_path> potrebbe essere qualsiasi directory correlata alla directory in cui si trova Android.bp . Nell'esempio precedente, <base_path> è srcs/aidl .
  • local_include_dir : il percorso da cui inizia il nome del pacchetto. Corrisponde a <base_path> spiegato sopra.
  • imports : un elenco di moduli aidl_interface che utilizza. Se una delle tue interfacce AIDL usa un'interfaccia o un parcelable da un'altra aidl_interface , metti il ​​suo nome qui. Può essere il nome da solo, per fare riferimento all'ultima versione, o il nome con il suffisso della versione (come -V1 ) per fare riferimento a una versione specifica. La specifica di una versione è supportata da Android 12
  • versions : le versioni precedenti dell'interfaccia che sono bloccate in api_dir , a partire da Android 11, le versions sono bloccate in aidl_api/ name . Se non ci sono versioni bloccate di un'interfaccia, questo non dovrebbe essere specificato e non ci saranno controlli di compatibilità. Questo campo è stato sostituito conversions_with_info per 13 e versions_with_info successive.
  • versions_with_info : Elenco di tuple, ognuna delle quali contiene il nome di una versione bloccata e un elenco con le importazioni di versioni di altri moduli aidl_interface importati da questa versione di aidl_interface. La definizione della versione V di un'interfaccia AIDL IFACE si trova in aidl_api/ IFACE / V . Questo campo è stato introdotto in Android 13 e non dovrebbe essere modificato direttamente in Android.bp. Il campo viene aggiunto o aggiornato richiamando *-update-api o *-freeze-api . Inoltre, i campi delle versions vengono migrati automaticamente versions_with_info quando un utente richiama *-update-api o *-freeze-api .
  • stability : il flag facoltativo per la promessa di stabilità di questa interfaccia. Attualmente supporta solo "vintf" . Se questo non è impostato, corrisponde a un'interfaccia con stabilità all'interno di questo contesto di compilazione (quindi un'interfaccia caricata qui può essere utilizzata solo con elementi compilati insieme, ad esempio su system.img). Se questo è impostato su "vintf" , ciò corrisponde a una promessa di stabilità: l'interfaccia deve essere mantenuta stabile finché viene utilizzata.
  • gen_trace : il flag facoltativo per attivare o disattivare la traccia. L'impostazione predefinita è false .
  • host_supported : il flag facoltativo che, se impostato su true , rende le librerie generate disponibili per l'ambiente host.
  • unstable : il flag facoltativo utilizzato per contrassegnare che questa interfaccia non deve essere stabile. Quando questo è impostato su true , il sistema di compilazione non crea il dump dell'API per l'interfaccia né ne richiede l'aggiornamento.
  • backend.<type>.enabled : questi flag attivano o disattivano ciascuno dei backend per cui il compilatore AIDL genera il codice. Attualmente sono supportati quattro backend: Java, C++, NDK e Rust. I backend Java, C++ e NDK sono abilitati per impostazione predefinita. Se uno di questi tre backend non è necessario, deve essere disabilitato in modo esplicito. Rust è disabilitato per impostazione predefinita.
  • backend.<type>.apex_available : l'elenco di nomi APEX per cui è disponibile la libreria stub generata.
  • backend.[cpp|java].gen_log : il flag facoltativo che controlla se generare codice aggiuntivo per raccogliere informazioni sulla transazione.
  • backend.[cpp|java].vndk.enabled : il flag facoltativo per rendere questa interfaccia parte di VNDK. L'impostazione predefinita è false .
  • backend.java.platform_apis : il flag facoltativo che controlla se la libreria di stub Java viene creata rispetto alle API private dalla piattaforma. Questo dovrebbe essere impostato su "true" quando stability è impostata su "vintf" .
  • backend.java.sdk_version : il flag facoltativo per specificare la versione dell'SDK su cui è costruita la libreria stub Java. Il valore predefinito è "system_current" . Questo non dovrebbe essere impostato quando backend.java.platform_apis è vero.
  • backend.java.platform_apis : il flag facoltativo che deve essere impostato su true quando le librerie generate devono essere compilate in base all'API della piattaforma anziché all'SDK.

Per ogni combinazione delle versioni e dei backend abilitati, viene creata una libreria stub. Per informazioni su come fare riferimento alla versione specifica della libreria stub per un backend specifico, consulta Regole di denominazione dei moduli .

Scrivere file AIDL

Le interfacce in AIDL stabile sono simili alle interfacce tradizionali, con l'eccezione che non possono usare parcelable non strutturati (perché questi non sono stabili!). La differenza principale nell'AIDL stabile è il modo in cui vengono definiti i parcelables. In precedenza, i parcelables venivano dichiarati in avanti ; in AIDL stabile, i campi e le variabili parcelabili sono definiti in modo esplicito.

// in a file like 'some/package/Thing.aidl'
package some.package;

parcelable SubThing {
    String a = "foo";
    int b;
}

Un valore predefinito è attualmente supportato (ma non richiesto) per boolean , char , float , double , byte , int , long e String . In Android 12 sono supportate anche le impostazioni predefinite per le enumerazioni definite dall'utente. Quando non viene specificato un valore predefinito, viene utilizzato un valore simile a 0 o vuoto. Le enumerazioni senza un valore predefinito vengono inizializzate su 0 anche se non esiste un enumeratore zero.

Utilizzo delle librerie di stub

Dopo aver aggiunto le librerie stub come dipendenza al tuo modulo, puoi includerle nei tuoi file. Ecco alcuni esempi di librerie stub nel sistema di compilazione ( Android.mk può essere utilizzato anche per definizioni di moduli legacy):

cc_... {
    name: ...,
    shared_libs: ["my-module-name-cpp"],
    ...
}
# or
java_... {
    name: ...,
    // can also be shared_libs if desire is to load a library and share
    // it among multiple users or if you only need access to constants
    static_libs: ["my-module-name-java"],
    ...
}
# or
rust_... {
    name: ...,
    rust_libs: ["my-module-name-rust"],
    ...
}

Esempio in C++:

#include "some/package/IFoo.h"
#include "some/package/Thing.h"
...
    // use just like traditional AIDL

Esempio in Java:

import some.package.IFoo;
import some.package.Thing;
...
    // use just like traditional AIDL

Esempio in ruggine:

use aidl_interface_name::aidl::some::package::{IFoo, Thing};
...
    // use just like traditional AIDL

Interfacce di versionamento

La dichiarazione di un modulo con il nome foo crea anche una destinazione nel sistema di compilazione che puoi utilizzare per gestire l'API del modulo. Una volta compilato, foo-freeze-api aggiunge una nuova definizione API sotto api_dir o aidl_api/ name , a seconda della versione di Android, e aggiunge un file .hash , che rappresentano entrambi la versione appena bloccata dell'interfaccia. foo-freeze-api aggiorna anche la proprietàversions_with_info per riflettere la versions_with_info aggiuntiva e imports per la versione. Fondamentalmente, imports in versions_with_info vengono copiate dal campo delle imports . Ma l'ultima versione stabile è specificata nelle imports versions_with_info per l'importazione che non ha una versione esplicita. Una volta specificata la proprietàversions_with_info, il sistema di compilazione esegue i controlli di compatibilità tra le versions_with_info bloccate e anche tra Top of Tree (ToT) e l'ultima versione bloccata.

Inoltre, è necessario gestire la definizione dell'API della versione ToT. Ogni volta che un'API viene aggiornata, esegui foo-update-api per aggiornare aidl_api/ name /current che contiene la definizione dell'API della versione ToT.

Per mantenere la stabilità di un'interfaccia, i proprietari possono aggiungere nuovi:

  • Metodi alla fine di un'interfaccia (o metodi con nuovi serial definiti in modo esplicito)
  • Elementi alla fine di un particellabile (richiede l'aggiunta di un valore predefinito per ogni elemento)
  • Valori costanti
  • In Android 11, enumeratori
  • In Android 12, campi alla fine di un'unione

Non sono consentite altre azioni e nessun altro può modificare un'interfaccia (altrimenti rischiano di entrare in collisione con le modifiche apportate da un proprietario).

Per verificare che tutte le interfacce siano bloccate per il rilascio, puoi compilare con le seguenti variabili ambientali impostate:

  • AIDL_FROZEN_REL=true m ... - la build richiede il congelamento di tutte le interfacce AIDL stabili che non hanno owner: campo specificato.
  • AIDL_FROZEN_OWNERS="aosp test" - la build richiede che tutte le interfacce AIDL stabili siano bloccate con il owner: campo specificato come "aosp" o "test".

Stabilità delle importazioni

L'aggiornamento delle versioni delle importazioni per le versioni bloccate di un'interfaccia è compatibile con le versioni precedenti a livello AIDL stabile. Tuttavia, l'aggiornamento di questi richiede l'aggiornamento di tutti i server e client che utilizzano la vecchia versione dell'interfaccia e alcune applicazioni potrebbero essere confuse quando si mescolano diverse versioni di tipi. Generalmente, per i pacchetti di soli tipi o comuni, questo è sicuro perché il codice deve essere già scritto per gestire i tipi sconosciuti dalle transazioni IPC.

Nel codice della piattaforma Android android.hardware.graphics.common è il più grande esempio di questo tipo di aggiornamento della versione.

Utilizzo di interfacce con versione

Metodi di interfaccia

In fase di esecuzione, quando si tenta di chiamare nuovi metodi su un vecchio server, i nuovi client ricevono un errore o un'eccezione, a seconda del back-end.

  • cpp backend ottiene ::android::UNKNOWN_TRANSACTION .
  • Il backend ndk ottiene STATUS_UNKNOWN_TRANSACTION .
  • Il backend java ottiene android.os.RemoteException con un messaggio che dice che l'API non è implementata.

Per le strategie per gestire questo, vedere interrogare le versioni e utilizzare defaults .

Parcellabili

Quando vengono aggiunti nuovi campi ai parcelable, i vecchi client e server li eliminano. Quando nuovi client e server ricevono vecchi parcelable, i valori predefiniti per i nuovi campi vengono compilati automaticamente. Ciò significa che i valori predefiniti devono essere specificati per tutti i nuovi campi in un parcelable.

I client non dovrebbero aspettarsi che i server utilizzino i nuovi campi a meno che non sappiano che il server sta implementando la versione che ha il campo definito (vedi interrogare le versioni ).

Enum e costanti

Allo stesso modo, client e server dovrebbero rifiutare o ignorare i valori costanti e gli enumeratori non riconosciuti a seconda dei casi, poiché in futuro potrebbero essere aggiunti altri. Ad esempio, un server non dovrebbe interrompere quando riceve un enumeratore di cui non è a conoscenza. Dovrebbe ignorarlo o restituire qualcosa in modo che il client sappia che non è supportato in questa implementazione.

Sindacati

Il tentativo di inviare un'unione con un nuovo campo fallisce se il destinatario è vecchio e non conosce il campo. L'implementazione non vedrà mai l'unione con il nuovo campo. L'errore viene ignorato se si tratta di una transazione unidirezionale; altrimenti l'errore è BAD_VALUE (per il backend C++ o NDK) o IllegalArgumentException (per il backend Java). L'errore viene ricevuto se il client sta inviando un'unione impostata sul nuovo campo a un vecchio server o quando è un vecchio client che riceve l'unione da un nuovo server.

Regole di denominazione dei moduli

In Android 11, per ogni combinazione delle versioni e dei backend abilitati, viene creato automaticamente un modulo libreria stub. Per fare riferimento a uno specifico modulo libreria stub per il collegamento, non utilizzare il nome del modulo aidl_interface , ma il nome del modulo libreria stub, che è ifacename - version - backend , dove

  • ifacename : nome del modulo aidl_interface
  • version è uno dei
    • V version-number per le versioni congelate
    • V latest-frozen-version-number + 1 per la versione tip-of-tree (ancora da congelare)
  • backend è uno dei
    • java per il backend Java,
    • cpp per il backend C++,
    • ndk o ndk_platform per il backend NDK. Il primo è per le app e il secondo è per l'utilizzo della piattaforma,
    • rust per il backend Rust.

Supponiamo che esista un modulo con il nome foo e la sua ultima versione sia 2 e supporti sia NDK che C++. In questo caso, AIDL genera questi moduli:

  • Basato sulla versione 1
    • foo-V1-(java|cpp|ndk|ndk_platform|rust)
  • Basato sulla versione 2 (l'ultima versione stabile)
    • foo-V2-(java|cpp|ndk|ndk_platform|rust)
  • Basato sulla versione ToT
    • foo-V3-(java|cpp|ndk|ndk_platform|rust)

Rispetto ad Android 11,

  • foo- backend , che si riferiva all'ultima versione stabile diventa foo- V2 - backend
  • foo-unstable- backend , che riferito alla versione foo- V3 - backend

I nomi dei file di output sono sempre gli stessi dei nomi dei moduli.

  • Basato sulla versione 1: foo-V1-(cpp|ndk|ndk_platform|rust).so
  • Basato sulla versione 2: foo-V2-(cpp|ndk|ndk_platform|rust).so
  • Basato sulla versione ToT: foo-V3-(cpp|ndk|ndk_platform|rust).so

Si noti che il compilatore AIDL non crea né un modulo di versione unstable né un modulo senza versione per un'interfaccia AIDL stabile. A partire da Android 12, il nome del modulo generato da un'interfaccia AIDL stabile include sempre la sua versione.

Nuovi metodi di meta interfaccia

Android 10 aggiunge diversi metodi di meta interfaccia per l'AIDL stabile.

Interrogazione della versione dell'interfaccia dell'oggetto remoto

I client possono interrogare la versione e l'hash dell'interfaccia implementata dall'oggetto remoto e confrontare i valori restituiti con i valori dell'interfaccia utilizzata dal client.

Esempio con il backend cpp :

sp<IFoo> foo = ... // the remote object
int32_t my_ver = IFoo::VERSION;
int32_t remote_ver = foo->getInterfaceVersion();
if (remote_ver < my_ver) {
  // the remote side is using an older interface
}

std::string my_hash = IFoo::HASH;
std::string remote_hash = foo->getInterfaceHash();

Esempio con il ndk (e ndk_platform ):

IFoo* foo = ... // the remote object
int32_t my_ver = IFoo::version;
int32_t remote_ver = 0;
if (foo->getInterfaceVersion(&remote_ver).isOk() && remote_ver < my_ver) {
  // the remote side is using an older interface
}

std::string my_hash = IFoo::hash;
std::string remote_hash;
foo->getInterfaceHash(&remote_hash);

Esempio con il backend java :

IFoo foo = ... // the remote object
int myVer = IFoo.VERSION;
int remoteVer = foo.getInterfaceVersion();
if (remoteVer < myVer) {
  // the remote side is using an older interface
}

String myHash = IFoo.HASH;
String remoteHash = foo.getInterfaceHash();

Per il linguaggio Java, il lato remoto DEVE implementare getInterfaceVersion() e getInterfaceHash() come segue ( super viene utilizzato invece di IFoo per evitare errori di copia/incolla. L'annotazione @SuppressWarnings("static") potrebbe essere necessaria per disabilitare gli avvisi, a seconda la configurazione javac ):

class MyFoo extends IFoo.Stub {
    @Override
    public final int getInterfaceVersion() { return super.VERSION; }

    @Override
    public final String getInterfaceHash() { return super.HASH; }
}

Questo perché le classi generate ( IFoo , IFoo.Stub , ecc.) sono condivise tra il client e il server (ad esempio, le classi possono trovarsi nel classpath di avvio). Quando le classi sono condivise, il server è anche collegato alla versione più recente delle classi anche se potrebbe essere stato creato con una versione precedente dell'interfaccia. Se questa meta interfaccia è implementata nella classe condivisa, restituisce sempre la versione più recente. Tuttavia, implementando il metodo come sopra, il numero di versione dell'interfaccia è incorporato nel codice del server (poiché IFoo.VERSION è un static final int che viene inserito in linea quando viene fatto riferimento) e quindi il metodo può restituire la versione esatta in cui è stato creato il server insieme a.

Gestione delle interfacce precedenti

È possibile che un client sia aggiornato con la versione più recente di un'interfaccia AIDL ma il server stia utilizzando la vecchia interfaccia AIDL. In questi casi, chiamare un metodo su una vecchia interfaccia restituisce UNKNOWN_TRANSACTION .

Con AIDL stabile, i clienti hanno più controllo. Sul lato client, puoi impostare un'implementazione predefinita su un'interfaccia AIDL. Un metodo nell'implementazione predefinita viene richiamato solo quando il metodo non è implementato nel lato remoto (poiché è stato creato con una versione precedente dell'interfaccia). Poiché i valori predefiniti sono impostati a livello globale, non devono essere utilizzati da contesti potenzialmente condivisi.

Esempio in C++ in Android 13 e versioni successive:

class MyDefault : public IFooDefault {
  Status anAddedMethod(...) {
   // do something default
  }
};

// once per an interface in a process
IFoo::setDefaultImpl(::android::sp<MyDefault>::make());

foo->anAddedMethod(...); // MyDefault::anAddedMethod() will be called if the
                         // remote side is not implementing it

Esempio in Java:

IFoo.Stub.setDefaultImpl(new IFoo.Default() {
    @Override
    public xxx anAddedMethod(...)  throws RemoteException {
        // do something default
    }
}); // once per an interface in a process


foo.anAddedMethod(...);

Non è necessario fornire l'implementazione predefinita di tutti i metodi in un'interfaccia AIDL. I metodi che sono garantiti per essere implementati nel lato remoto (poiché si è certi che il remoto sia compilato quando i metodi erano nella descrizione dell'interfaccia AIDL) non devono essere sovrascritti nella classe impl predefinita.

Conversione dell'AIDL esistente in AIDL strutturato/stabile

Se disponi di un'interfaccia AIDL esistente e del codice che la utilizza, utilizza i seguenti passaggi per convertire l'interfaccia in un'interfaccia AIDL stabile.

  1. Identifica tutte le dipendenze della tua interfaccia. Per ogni pacchetto da cui dipende l'interfaccia, determinare se il pacchetto è definito in AIDL stabile. Se non definito, il pacchetto deve essere convertito.

  2. Converti tutti i parcelable nella tua interfaccia in parcelable stabili (i file di interfaccia stessi possono rimanere invariati). Fallo esprimendo la loro struttura direttamente nei file AIDL. Le classi di gestione devono essere riscritte per utilizzare questi nuovi tipi. Questo può essere fatto prima di creare un pacchetto aidl_interface (sotto).

  3. Crea un pacchetto aidl_interface (come descritto sopra) che contenga il nome del tuo modulo, le sue dipendenze e qualsiasi altra informazione di cui hai bisogno. Per renderlo stabilizzato (non solo strutturato), deve anche essere versionato. Per ulteriori informazioni, consulta Interfacce di controllo delle versioni .