Servidores AIDL

Un backend AIDL es un objetivo para la generación de código auxiliar. Cuando usa archivos AIDL, siempre los usa en un idioma particular con un tiempo de ejecución específico. Dependiendo del contexto, debe usar diferentes backends AIDL.

AIDL tiene los siguientes backends:

back-end Idioma superficie API Construir sistemas
Java Java SDK/SystemApi (estable*) todos
NDK C++ libbinder_ndk (estable*) interfaz_aidl
CPP C++ libbinder (inestable) todos
Óxido Óxido libbinder_rs (inestable) interfaz_aidl
  • Estas superficies de API son estables, pero muchas de las API, como las de administración de servicios, están reservadas para uso interno de la plataforma y no están disponibles para las aplicaciones. Para obtener más información sobre cómo usar AIDL en aplicaciones, consulte la documentación para desarrolladores .
  • El backend de Rust se introdujo en Android 12; el backend NDK ha estado disponible a partir de Android 10.
  • La caja Rust está construida sobre libbinder_ndk . Los APEX usan la caja de carpetas de la misma manera que cualquier otra persona en el lado del sistema. La parte de Rust se empaqueta en un APEX y se envía dentro de él. Depende de libbinder_ndk.so en la partición del sistema.

Construir sistemas

Según el backend, hay dos formas de compilar AIDL en código auxiliar. Para obtener más detalles sobre los sistemas de compilación, consulte la Referencia del módulo Soong .

Sistema de construcción central

En cualquier módulo Android.bp cc_ o java_ (o en sus equivalentes Android.mk ), los archivos .aidl se pueden especificar como archivos fuente. En este caso, se usan los backends Java/CPP de AIDL (no el backend NDK), y las clases para usar los archivos AIDL correspondientes se agregan al módulo automáticamente. Las opciones como local_include_dirs , que le indica al sistema de compilación la ruta raíz a los archivos AIDL en ese módulo, se pueden especificar en estos módulos en un grupo aidl: Tenga en cuenta que el backend de Rust solo se puede usar con Rust. Los módulos rust_ se manejan de manera diferente en el sentido de que los archivos AIDL no se especifican como archivos fuente. En su lugar, el módulo aidl_interface produce una rustlib llamada <aidl_interface name>-rust la que se puede vincular. Para obtener más detalles, consulte el ejemplo de Rust AIDL .

interfaz_aidl

Ver AIDL estable . Los tipos utilizados con este sistema de compilación deben estar estructurados; es decir, expresado en AIDL directamente. Esto significa que no se pueden usar parcelables personalizados.

Tipos

Puede considerar el compilador aidl como una implementación de referencia para los tipos. Cuando cree una interfaz, invoque aidl --lang=<backend> ... para ver el archivo de interfaz resultante. Cuando usa el módulo aidl_interface , puede ver el resultado en out/soong/.intermediates/<path to module>/ .

Tipo Java/AIDL Tipo C++ Tipo de NDK Tipo de óxido
booleano bool bool bool
byte int8_t int8_t i8
carbonizarse char16_t char16_t u16
En t int32_t int32_t i32
largo int64_t int64_t i64
flotar flotar flotar f32
doble doble doble f64
Cuerda androide::String16 estándar::cadena Cuerda
android.os.Parcelable android::Parcelable N / A N / A
IBinder android::IBinder ndk::SpAIBinder carpeta::SpIBinder
T[] estándar::vector<T> estándar::vector<T> En t
Fuera: Vec<T>
byte[] estándar::vector<uint8_t> estándar::vector<int8_t> 1 En: &[u8]
Fuera: Vec<u8>
Lista<T> estándar::vector<T> 2 estándar::vector<T> 3 En: &[T] 4
Fuera: Vec<T>
Descriptor de archivo android::base::unique_fd N / A carpeta::parcela::ParcelFileDescriptor
ParcelFileDescriptorParcelFileDescriptor android::os::ParcelFileDescriptor ndk::ScopedFileDescriptor carpeta::parcela::ParcelFileDescriptor
tipo de interfaz (T) android::sp<T> std::shared_ptr<T> aglutinante::Fuerte
tipo parcelable (T) T T T
unión tipo (T) 5 T T T
TN[N] 6 std::arreglo<T, N> std::arreglo<T, N> [T; NORTE]

1. En Android 12 o superior, las matrices de bytes usan uint8_t en lugar de int8_t por motivos de compatibilidad.

2. El backend de C++ es compatible con List<T> , donde T es uno de String , IBinder , ParcelFileDescriptor o parcelable. En Android T (AOSP experimental) o superior, T puede ser cualquier tipo no primitivo (incluidos los tipos de interfaz), excepto matrices. AOSP recomienda que utilice tipos de matrices como T[] , ya que funcionan en todos los backends.

3. El backend del NDK admite List<T> , donde T es uno de String , ParcelFileDescriptor o parcelable. En Android T (AOSP experimental) o superior, T puede ser cualquier tipo no primitivo excepto matrices.

4. Los tipos se pasan de manera diferente para el código de Rust dependiendo de si son de entrada (un argumento) o de salida (un valor devuelto).

5. Los tipos de unión son compatibles con Android 12 y versiones posteriores.

6. En Android T (AOSP experimental) o superior, se admiten matrices de tamaño fijo. Las matrices de tamaño fijo pueden tener múltiples dimensiones (por ejemplo int[3][4] ). En el backend de Java, las matrices de tamaño fijo se representan como tipos de matriz.

Direccionalidad (entrada/salida/entrada fuera)

Al especificar los tipos de argumentos de las funciones, puede especificarlos como in , out o inout . Esto controla en qué dirección se pasa la información para una llamada IPC. in es la dirección predeterminada e indica que los datos pasan de la persona que llama al destinatario de la llamada. out significa que los datos pasan del destinatario de la llamada al llamante. inout es la combinación de ambos. Sin embargo, el equipo de Android recomienda que evite usar el especificador de argumento inout . Si usa inout con una interfaz versionada y una persona que llama más antigua, los campos adicionales que están presentes solo en la persona que llama se restablecen a sus valores predeterminados. Con respecto a Rust, un tipo de inout normal recibe &mut Vec<T> , y un tipo de inout de lista recibe &mut Vec<T> .

UTF8/UTF16

Con el backend de CPP, puede elegir si las cadenas son utf-8 o utf-16. Declare cadenas como @utf8InCpp String en AIDL para convertirlas automáticamente a utf-8. Los backends de NDK y Rust siempre usan cadenas utf-8. Para obtener más información sobre la anotación utf8InCpp , consulte Anotaciones en AIDL .

nulabilidad

Puede anotar tipos que pueden ser nulos en el backend de Java con @nullable para exponer valores nulos a los backends de CPP y NDK. En el backend de Rust, estos tipos @nullable se exponen como Option<T> . Los servidores nativos rechazan los valores nulos por defecto. Las únicas excepciones a esto son los tipos de interface e IBinder , que siempre pueden ser nulos para lecturas de NDK y escrituras de CPP/NDK. Para obtener más información sobre la anotación nullable , consulte Anotaciones en AIDL .

Paquetes personalizados

En los backends de C++ y Java en el sistema de compilación central, puede declarar un paquete que se implementa manualmente en un backend de destino (en C++ o en Java).

    package my.package;
    parcelable Foo;

o con declaración de encabezado C++:

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

Luego, puede usar este paquete como un tipo en los archivos AIDL, pero AIDL no lo generará.

Rust no admite parcelables personalizados.

Valores predeterminados

Los parcelables estructurados pueden declarar valores predeterminados por campo para primitivas, String y matrices de estos tipos.

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

En el backend de Java, cuando faltan los valores predeterminados, los campos se inicializan como valores cero para los tipos primitivos y null para los tipos no primitivos.

En otros backends, los campos se inicializan con valores inicializados predeterminados cuando los valores predeterminados no están definidos. Por ejemplo, en el backend de C++, los campos String se inicializan como una cadena vacía y los campos List<T> se inicializan como un vector<T> vacío. Los campos @nullable se inicializan como campos de valor nulo.

Manejo de errores

El sistema operativo Android proporciona tipos de error incorporados para que los servicios los usen al informar errores. Estos son utilizados por binder y pueden ser utilizados por cualquier servicio que implemente una interfaz de binder. Su uso está bien documentado en la definición de AIDL y no requieren ningún estado definido por el usuario ni tipo de retorno.

Cuando una función AIDL informa un error, es posible que la función no inicialice ni modifique los parámetros de salida. Específicamente, los parámetros de salida pueden modificarse si el error ocurre durante el desparcelado en lugar de ocurrir durante el procesamiento de la transacción en sí. En general, cuando se obtiene un error de una función inout , todos los parámetros de entrada y out , así como el valor de retorno (que actúa como un parámetro de out en algunos backends) deben considerarse en un estado indefinido.

Si la interfaz AIDL requiere valores de error adicionales que no están cubiertos por los tipos de error incorporados, entonces pueden usar el error incorporado específico del servicio especial que permite la inclusión de un valor de error específico del servicio definido por el usuario. . Estos errores específicos del servicio generalmente se definen en la interfaz enum como una enumeración respaldada por int const int o int y no son analizados por binder.

En Java, los errores se asignan a excepciones, como android.os.RemoteException . Para las excepciones específicas del servicio, Java usa android.os.ServiceSpecificException junto con el error definido por el usuario.

El código nativo en Android no usa excepciones. El backend de CPP usa android::binder::Status . El backend de NDK usa ndk::ScopedAStatus . Cada método generado por AIDL devuelve uno de estos, que representa el estado del método. El backend de Rust usa los mismos valores de código de excepción que el NDK, pero los convierte en errores nativos de Rust ( StatusCode , ExceptionCode ) antes de entregarlos al usuario. Para errores específicos del servicio, el Status devuelto o ScopedAStatus usa EX_SERVICE_SPECIFIC junto con el error definido por el usuario.

Los tipos de error incorporados se pueden encontrar en los siguientes archivos:

back-end Definición
Java android/os/Parcel.java
CPP binder/Status.h
NDK android/binder_status.h
Óxido android/binder_status.h

Usando varios backends

Estas instrucciones son específicas para el código de la plataforma Android. Estos ejemplos usan un tipo definido, my.package.IFoo . Para obtener instrucciones sobre cómo usar el backend de Rust, consulte el ejemplo de Rust AIDL en la página de patrones de Android Rust .

Importación de tipos

Ya sea que el tipo definido sea una interfaz, un paquete o una unión, puede importarlo en Java:

    import my.package.IFoo;

O en el backend de CPP:

    #include <my/package/IFoo.h>

O en el backend del NDK (observa el espacio de nombres adicional aidl ):

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

O en el backend de Rust:

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

Aunque puede importar un tipo anidado en Java, en los backends de CPP/NDK debe incluir el encabezado para su tipo raíz. Por ejemplo, al importar una Bar de tipo anidado definida en my/package/IFoo.aidl ( IFoo es el tipo raíz del archivo), debe incluir <my/package/IFoo.h> para el backend de CPP (o <aidl/my/package/IFoo.h> para el backend NDK).

Servicios de implementación

Para implementar un servicio, debe heredar de la clase auxiliar nativa. Esta clase lee los comandos del controlador de la carpeta y ejecuta los métodos que implementa. Imagina que tienes un archivo AIDL como este:

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

En Java, debe extender desde esta clase:

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

En el back-end de CPP:

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

En el backend del NDK (observa el espacio de nombres adicional aidl ):

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

En el backend de 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(())
        }
    }

Registrarse y obtener servicios

Los servicios en la plataforma Android generalmente se registran con el proceso servicemanager . Además de las API a continuación, algunas API verifican el servicio (lo que significa que regresan inmediatamente si el servicio no está disponible). Consulte la interfaz del administrador de servicemanager correspondiente para obtener detalles exactos. Estas operaciones solo se pueden realizar al compilar contra la plataforma Android.

En Java:

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

En el back-end de 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"));

En el backend del NDK (observa el espacio de nombres adicional 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")));

En el backend de 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()
}

Puede solicitar recibir una notificación cuando falle un servicio que aloja un archivador. Esto puede ayudar a evitar la fuga de proxies de devolución de llamadas o ayudar en la recuperación de errores. Realice estas llamadas en objetos de proxy de enlace.

  • En Java, use android.os.IBinder::linkToDeath .
  • En el backend de CPP, use android::IBinder::linkToDeath .
  • En el backend del NDK, use AIBinder_linkToDeath .
  • En el backend de Rust, cree un objeto DeathRecipient , luego llame a my_binder.link_to_death(&mut my_death_recipient) . Tenga en cuenta que debido a que DeathRecipient posee la devolución de llamada, debe mantener vivo ese objeto mientras desee recibir notificaciones.

Informes de errores y API de depuración para servicios

Cuando se ejecutan los informes de errores (por ejemplo, con adb bugreport ), recopilan información de todo el sistema para ayudar a depurar varios problemas. Para los servicios AIDL, los informes de errores utilizan el dumpsys binario en todos los servicios registrados con el administrador de servicios para volcar su información en el informe de errores. También puede usar dumpsys en la línea de comandos para obtener información de un servicio con dumpsys SERVICE [ARGS] . En los backends de C++ y Java, puede controlar el orden en que se vuelcan los servicios usando argumentos adicionales para addService . También puede usar dumpsys --pid SERVICE para obtener el PID de un servicio durante la depuración.

Para agregar una salida personalizada a su servicio, puede anular el método de dump en su objeto de servidor como si estuviera implementando cualquier otro método IPC definido en un archivo AIDL. Al hacer esto, debe restringir el volcado al permiso de la aplicación android.permission.DUMP o restringir el volcado a UID específicos.

En el back-end de Java:

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

En el back-end de CPP:

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

En el back-end del NDK:

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

En el backend de Rust, al implementar la interfaz, especifique lo siguiente (en lugar de permitir que se establezca de forma predeterminada):

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

Obtener dinámicamente el descriptor de la interfaz

El descriptor de interfaz identifica el tipo de una interfaz. Esto es útil durante la depuración o cuando tiene un enlazador desconocido.

En Java, puede obtener el descriptor de interfaz con código como:

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

En el back-end de CPP:

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

Los backends de NDK y Rust no admiten esta funcionalidad.

Obtención estática del descriptor de la interfaz

A veces (como cuando se registran los servicios de @VintfStability ), necesita saber cuál es el descriptor de la interfaz de forma estática. En Java, puede obtener el descriptor agregando código como:

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

En el back-end de CPP:

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

En el backend del NDK (observa el espacio de nombres adicional aidl ):

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

En el backend de Rust:

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

Rango de enumeración

En los backends nativos, puede iterar sobre los posibles valores que puede tomar una enumeración. Debido a consideraciones de tamaño de código, esto no es compatible con Java actualmente.

Para una enumeración MyEnum definida en AIDL, la iteración se proporciona de la siguiente manera.

En el back-end de CPP:

    ::android::enum_range<MyEnum>()

En el back-end del NDK:

   ::ndk::enum_range<MyEnum>()

En el backend de Rust:

    MyEnum::enum_range()

Gestión de subprocesos

Cada instancia de libbinder en un proceso mantiene un subproceso. Para la mayoría de los casos de uso, debería ser exactamente un grupo de subprocesos, compartido entre todos los backends. La única excepción a esto es cuando el código del proveedor puede cargar otra copia de libbinder para comunicarse con /dev/vndbinder . Dado que se encuentra en un nodo de enlace independiente, el grupo de subprocesos no se comparte.

Para el backend de Java, el grupo de subprocesos solo puede aumentar de tamaño (ya que ya se inició):

    BinderInternal.setMaxThreads(<new larger value>);

Para el backend de CPP, están disponibles las siguientes operaciones:

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

Del mismo modo, en el backend del NDK:

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

En el backend de Rust:

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