AIDL para HAL

Android 11 introduce la capacidad de usar AIDL para HALs en Android. Esto permite implementar partes de Android sin HIDL. Las HAL de transición deben usar el AIDL exclusivamente cuando sea posible (cuando las HAL upstream usan HIDL, se debe usar el HIDL).

Las HAL que usan AIDL para comunicarse entre componentes del framework, como los de system.img, y componentes de hardware, como los de vendor.img, deben usar AIDL estable. Sin embargo, para comunicarse dentro de una partición, por ejemplo, de una HAL a otra, no hay restricciones en cuanto al mecanismo de IPC.

Motivación

El AIDL existe desde hace más tiempo que el HIDL y se usa en muchos otros lugares, como entre los componentes del framework de Android o en las apps. Ahora que el AIDL tiene compatibilidad con la estabilidad, es posible implementar una pila completa con un solo entorno de ejecución de IPC. El AIDL también tiene un mejor sistema de control de versiones que el HIDL.

  • Usar un solo lenguaje de IPC significa tener solo una cosa que aprender, depurar, optimizar y proteger.
  • El AIDL admite el control de versiones local para los propietarios de una interfaz:
    • Los propietarios pueden agregar métodos al final de las interfaces o campos a los objetos parcelables. Esto significa que es más fácil modificar las versiones del código a lo largo de los años y, además, el costo interanual es menor (los tipos se pueden modificar in situ y no hay necesidad de bibliotecas adicionales para cada versión de la interfaz).
    • Las interfaces de extensión se pueden conectar durante el tiempo de ejecución en lugar del sistema de tipos, por lo que no es necesario reubicar las extensiones downstream en versiones más recientes de las interfaces.
  • Una interfaz de AIDL existente se puede usar directamente cuando su propietario decide estabilizarla. Antes, se debía crear una copia completa de la interfaz en HIDL.

Compila en el entorno de ejecución de AIDL

El AIDL tiene tres backends diferentes: Java, NDK y CPP. Para usar el AIDL estable, siempre debes usar la copia del sistema de libbinder en system/lib*/libbinder.so y hablar en /dev/binder. Para el código de la imagen del proveedor, significa que no se puede usar libbinder (del VNDK), ya que esta biblioteca tiene una API de C++ inestable y sus componentes internos inestables. En su lugar, el código nativo del proveedor debe usar el backend del NDK del AIDL, vincularse con libbinder_ndk (que está respaldado por el sistema libbinder.so) y vincularse con las bibliotecas del NDK creadas por entradas aidl_interface. Si quieres conocer los nombres exactos de los módulos, consulta las reglas de nomenclatura de los módulos.

Escribe una interfaz de la HAL del AIDL

Para que se use una interfaz del AIDL entre el sistema y el proveedor, debe realizar dos cambios:

  • Cada definición de tipo debe anotarse con @VintfStability.
  • La declaración aidl_interface debe incluir stability: "vintf",.

Solo el propietario de una interfaz puede realizar estos cambios.

Cuando realices estos cambios, la interfaz debe estar en el manifiesto de VINTF para funcionar. Prueba esto (y los requisitos relacionados, como verificar que las interfaces lanzadas estén bloqueadas) con la prueba vts_treble_vintf_vendor_test de VTS. Puedes usar una interfaz @VintfStability sin estos requisitos llamando a AIBinder_forceDowngradeToLocalStability en el backend del NDK, android::Stability::forceDowngradeToLocalStability en el backend de C++ o android.os.Binder#forceDowngradeToSystemStability en el backend de Java en un objeto vinculante antes de que se envíe a otro proceso. Cambiar un servicio al nivel inferior de estabilidad de proveedor no es compatible en Java porque todas las apps se ejecutan en un contexto del sistema.

Además, para obtener la máxima portabilidad del código y evitar posibles problemas, como bibliotecas adicionales innecesarias, inhabilita el backend de CPP.

Ten en cuenta que el uso de backends en el siguiente ejemplo de código es correcto, ya que hay tres backends (Java, NDK y CPP). En el siguiente código, se indica cómo seleccionar el backend de CPP específicamente para inhabilitarlo.

    aidl_interface: {
        ...
        backends: {
            cpp: {
                enabled: false,
            },
        },
    }

Cómo buscar interfaces de HAL del AIDL

Las interfaces estables del AIDL del AOSP para HAL están en los mismos directorios base que las interfaces HIDL, en carpetas aidl.

  • hardware/interfaces
  • frameworks/hardware/interfaces
  • sistema/hardware/interfaces

Debes colocar interfaces de extensión en otros subdirectorios hardware/interfaces, en vendor o hardware.

Interfaces de extensión

Android tiene un conjunto de interfaces oficiales del AOSP con cada versión. Cuando los socios de Android quieran agregar funciones a esas interfaces, no deberían cambiarlas directamente, ya que esto significaría que su tiempo de ejecución de Android no es compatible con el tiempo de ejecución de AOSP para Android. En el caso de los dispositivos con GMS, evitar cambiar estas interfaces también garantiza que la imagen de GSI pueda seguir funcionando.

Las extensiones se pueden registrar de dos maneras diferentes:

  • en el entorno de ejecución, consulta Extensiones adjuntas.
  • independientes, registrados globalmente y en VINTF.

Sin embargo, si se registra una extensión, cuando los componentes específicos del proveedor (es decir, no forman parte de los AOSP ascendentes) usan la interfaz, no hay posibilidad de que se produzca un conflicto de combinación. Sin embargo, cuando se realizan modificaciones descendentes en los componentes ascendentes del AOSP, pueden producirse conflictos de combinación y se recomiendan las siguientes estrategias:

  • Las incorporaciones de interfaz se pueden subir a AOSP en la próxima versión
  • Las adiciones de interfaz que permiten una mayor flexibilidad, sin conflictos de combinación, pueden subirse en la próxima versión

Extensiones parcelables: ParcelableHolder

ParcelableHolder es un Parcelable que puede contener otro Parcelable. El caso de uso principal de ParcelableHolder es hacer que un elemento Parcelable sea extensible. Por ejemplo, la imagen que los implementadores de dispositivos esperan poder extender una Parcelable definida por AOSP, AospDefinedParcelable, para incluir sus funciones de valor agregado.

Antes, sin ParcelableHolder, los implementadores de dispositivos no podían modificar una interfaz de AIDL estable definida por AOSP porque agregaría más campos sería un error:

parcelable AospDefinedParcelable {
  int a;
  String b;
  String x; // ERROR: added by a device implementer
  int[] y; // added by a device implementer
}

Como se ve en el código anterior, esta práctica no funciona porque los campos que agrega el implementador de dispositivos pueden generar un conflicto cuando se revisa el objeto Parcelable en las próximas versiones de Android.

Con ParcelableHolder, el propietario de un objeto parcelable puede definir un punto de extensión en un Parcelable.

parcelable AospDefinedParcelable {
  int a;
  String b;
  ParcelableHolder extension;
}

Luego, los implementadores de dispositivos pueden definir su propio Parcelable para su extensión.

parcelable OemDefinedParcelable {
  String x;
  int[] y;
}

Por último, el Parcelable nuevo se puede adjuntar a la Parcelable original con el campo ParcelableHolder.


// Java
AospDefinedParcelable ap = ...;
OemDefinedParcelable op = new OemDefinedParcelable();
op.x = ...;
op.y = ...;

ap.extension.setParcelable(op);

...

OemDefinedParcelable op = ap.extension.getParcelable(OemDefinedParcelable.class);

// C++
AospDefinedParcelable ap;
OemDefinedParcelable op;
std::shared_ptr<OemDefinedParcelable> op_ptr = make_shared<OemDefinedParcelable>();

ap.extension.setParcelable(op);
ap.extension.setParcelable(op_ptr);

...

std::shared_ptr<OemDefinedParcelable> op_ptr;

ap.extension.getParcelable(&op_ptr);

// NDK
AospDefinedParcelable ap;
OemDefinedParcelable op;
ap.extension.setParcelable(op);

...

std::optional<OemDefinedParcelable> op;
ap.extension.getParcelable(&op);

// Rust
let mut ap = AospDefinedParcelable { .. };
let op = Rc::new(OemDefinedParcelable { .. });

ap.extension.set_parcelable(Rc::clone(&op));

...

let op = ap.extension.get_parcelable::<OemDefinedParcelable>();

Nombres de las instancias del servidor de la HAL del AIDL

Por convención, los servicios de la HAL del AIDL tienen un nombre de instancia con el formato $package.$type/$instance. Por ejemplo, una instancia de la HAL del vibrador se registra como android.hardware.vibrator.IVibrator/default.

Escribe un servidor de la HAL del AIDL

Los servidores de AIDL @VintfStability deben declararse en el manifiesto de VINTF. Por ejemplo, de la siguiente manera:

    <hal format="aidl">
        <name>android.hardware.vibrator</name>
        <version>1</version>
        <fqname>IVibrator/default</fqname>
    </hal>

De lo contrario, deberían registrar un servicio de AIDL normalmente. Cuando se ejecutan pruebas de VTS, se espera que todas las HAL del AIDL declaradas estén disponibles.

Escribe un cliente de AIDL

Los clientes del AIDL deben declararse en la matriz de compatibilidad, por ejemplo, de la siguiente manera:

    <hal format="aidl" optional="true">
        <name>android.hardware.vibrator</name>
        <version>1-2</version>
        <interface>
            <name>IVibrator</name>
            <instance>default</instance>
        </interface>
    </hal>

Cómo convertir una HAL existente de HIDL en AIDL

Usa la herramienta hidl2aidl para convertir una interfaz HIDL en AIDL.

Funciones de hidl2aidl:

  • Crear archivos .aidl basados en los archivos .hal para el paquete determinado
  • Crear reglas de compilación para el paquete AIDL recién creado con todos los backends habilitados
  • Crear métodos de traducción en los backends de Java, CPP y NDK para traducir de los tipos HIDL a AIDL
  • Cómo crear reglas de compilación para bibliotecas de Traductor con las dependencias requeridas
  • Crea declaraciones estáticas para asegurarte de que los enumeradores HIDL y AIDL tengan los mismos valores en los backends de CPP y NDK

Sigue estos pasos para convertir un paquete de archivos .hal en archivos .aidl:

  1. Compila la herramienta ubicada en system/tools/hidl/hidl2aidl.

    Compilar esta herramienta a partir de la fuente más reciente proporciona la experiencia más completa. Puedes usar la versión más reciente para convertir interfaces en ramas más antiguas de versiones anteriores.

    m hidl2aidl
    
  2. Ejecuta la herramienta con un directorio de salida seguido del paquete que deseas convertir.

    De manera opcional, usa el argumento -l para agregar el contenido de un archivo de licencia nuevo en la parte superior de todos los archivos generados. Asegúrate de usar la licencia y la fecha correctas.

    hidl2aidl -o <output directory> -l <file with license> <package>
    

    Por ejemplo:

    hidl2aidl -o . -l my_license.txt android.hardware.nfc@1.2
    
  3. Lee los archivos generados y soluciona cualquier problema con la conversión.

    • conversion.log contiene problemas no controlados que se deben corregir primero.
    • Los archivos .aidl generados pueden tener advertencias y sugerencias que podrían necesitar alguna acción. Estos comentarios comienzan con //.
    • Aprovecha la oportunidad de realizar una limpieza y realizar mejoras en el paquete.
    • Consulta la anotación @JavaDerive para conocer las funciones que podrían ser necesarias, como toString o equals.
  4. Compila solo los objetivos que necesitas.

    • Inhabilita los backends que no se usarán. Prefiere el backend del NDK en lugar del backend de CPP. Consulta Cómo elegir el tiempo de ejecución.
    • Quita las bibliotecas de Traductor o cualquier código generado que no se utilice.
  5. Consulta Principales diferencias de AIDL y HIDL.

    • El uso del Status integrado del AIDL y las excepciones suelen mejorar la interfaz y quitar la necesidad de otro tipo de estado específico de la interfaz.
    • Los argumentos de la interfaz del AIDL en los métodos no son @nullable de forma predeterminada como si estuvieran en HIDL.

SEPolicy para HAL de AIDL

Un tipo de servicio de AIDL que sea visible para el código del proveedor debe tener el atributo hal_service_type. De lo contrario, la configuración de políticas es la misma que la de cualquier otro servicio de AIDL (aunque existen atributos especiales para las HAL). Esta es una definición de ejemplo de un contexto del servicio de HAL:

    type hal_foo_service, service_manager_type, hal_service_type;

Para la mayoría de los servicios definidos por la plataforma, ya se agrega un contexto de servicio con el tipo correcto (por ejemplo, android.hardware.foo.IFoo/default ya estaría marcado como hal_foo_service). Sin embargo, si un cliente de framework admite varios nombres de instancias, se deben agregar nombres de instancia adicionales en archivos service_contexts específicos del dispositivo.

    android.hardware.foo.IFoo/custom_instance u:object_r:hal_foo_service:s0

Los atributos de la HAL deben agregarse cuando creamos un tipo nuevo de HAL. Un atributo de HAL específico podría estar asociado con varios tipos de servicios (cada uno de los cuales puede tener varias instancias, como acabamos de mencionar). Para una HAL, foo, tenemos hal_attribute(foo). Esta macro define los atributos hal_foo_client y hal_foo_server. Para un dominio determinado, las macros hal_client_domain y hal_server_domain asocian un dominio con un atributo de HAL determinado. Por ejemplo, si el servidor del sistema es cliente de esta HAL, corresponde a la política hal_client_domain(system_server, hal_foo). De manera similar, un servidor de HAL incluye hal_server_domain(my_hal_domain, hal_foo). Por lo general, para un atributo HAL determinado, también creamos un dominio como hal_foo_default de HAL de referencia o de ejemplo. Sin embargo, algunos dispositivos usan estos dominios para sus propios servidores. La distinción entre dominios para varios servidores solo es importante si tenemos varios servidores que entregan la misma interfaz y necesitamos un conjunto de permisos diferente en sus implementaciones. En todas estas macros, hal_foo no es en realidad un objeto de política. En su lugar, estas macros usan este token para hacer referencia al grupo de atributos asociados con un par cliente-servidor.

Sin embargo, hasta ahora, no hemos asociado hal_foo_service y hal_foo (el par de atributos de hal_attribute(foo)). Un atributo de HAL se asocia con los servicios de HAL de AIDL mediante la macro hal_attribute_service (las HAL de HIDL usan la macro hal_attribute_hwservice). Por ejemplo, hal_attribute_service(hal_foo, hal_foo_service). Esto significa que los procesos hal_foo_client pueden obtener la HAL y los procesos hal_foo_server pueden registrar la HAL. El administrador de contexto (servicemanager) realiza la aplicación de estas reglas de registro. Ten en cuenta que los nombres de los servicios no siempre corresponden a atributos de HAL. Por ejemplo, podríamos ver hal_attribute_service(hal_foo, hal_foo2_service). Sin embargo, en general, debido a que esto implica que los servicios siempre se usan juntos, podríamos quitar hal_foo2_service y usar hal_foo_service para todos nuestros contextos de servicios. La mayoría de las HAL que establecen varios hal_attribute_service se deben a que el nombre del atributo de la HAL original no es lo suficientemente general y no se puede cambiar.

Con todo esto, una HAL de ejemplo se ve de la siguiente manera:

    public/attributes:
    // define hal_foo, hal_foo_client, hal_foo_server
    hal_attribute(foo)

    public/service.te
    // define hal_foo_service
    type hal_foo_service, hal_service_type, protected_service, service_manager_type

    public/hal_foo.te:
    // allow binder connection from client to server
    binder_call(hal_foo_client, hal_foo_server)
    // allow client to find the service, allow server to register the service
    hal_attribute_service(hal_foo, hal_foo_service)
    // allow binder communication from server to service_manager
    binder_use(hal_foo_server)

    private/service_contexts:
    // bind an AIDL service name to the selinux type
    android.hardware.foo.IFooXxxx/default u:object_r:hal_foo_service:s0

    private/<some_domain>.te:
    // let this domain use the hal service
    binder_use(some_domain)
    hal_client_domain(some_domain, hal_foo)

    vendor/<some_hal_server_domain>.te
    // let this domain serve the hal service
    hal_server_domain(some_hal_server_domain, hal_foo)

Interfaces de extensión conectadas

Se puede adjuntar una extensión a cualquier interfaz de Binder, ya sea una interfaz de nivel superior registrada directamente con el administrador de servicios o una subinterfaz. Cuando obtengas una extensión, debes confirmar que el tipo sea el esperado. Las extensiones solo se pueden configurar desde el proceso que publica un Binder.

Las extensiones adjuntas deben usarse siempre que una extensión modifica la funcionalidad de una HAL existente. Cuando se necesita una funcionalidad completamente nueva, no es necesario usar este mecanismo, y se puede registrar una interfaz de extensión directamente con el administrador de servicios. Las interfaces de extensión conectadas son más convenientes cuando se adjuntan a subinterfaces, ya que estas jerarquías pueden ser profundas o de instancias múltiples. El uso de una extensión global para duplicar la jerarquía de la interfaz de Binder de otro servicio requeriría una gran cantidad de registros para proporcionar una funcionalidad equivalente a las extensiones conectadas directamente.

Para configurar una extensión en Binder, usa las siguientes APIs:

  • En el backend del NDK: AIBinder_setExtension
  • En el backend de Java: android.os.Binder.setExtension
  • En el backend de la CPP: android::Binder::setExtension
  • En el backend de Rust: binder::Binder::set_extension

Para obtener una extensión en un Binder, usa las siguientes APIs:

  • En el backend del NDK: AIBinder_getExtension
  • En el backend de Java: android.os.IBinder.getExtension
  • En el backend de la CPP: android::IBinder::getExtension
  • En el backend de Rust: binder::Binder::get_extension

Puedes encontrar más información sobre estas APIs en la documentación de la función getExtension en el backend correspondiente. Se puede encontrar un ejemplo de cómo usar extensiones en hardware/interfaces/tests/extension/vibrator.

Principales diferencias de AIDL y HIDL

Cuando uses las HAL del AIDL o las interfaces de la HAL del AIDL, ten en cuenta las diferencias en comparación con la escritura de HAL del HIDL.

  • La sintaxis del lenguaje de AIDL es más cercana a Java. La sintaxis del HIDL es similar a la de C++.
  • Todas las interfaces de AIDL tienen estados de error integrados. En lugar de crear tipos de estado personalizados, crea instancias de estado constantes en los archivos de interfaz y usa EX_SERVICE_SPECIFIC en los backends de CPP o NDK, y ServiceSpecificException en el backend de Java. Consulta Administración de errores.
  • El AIDL no inicia automáticamente los grupos de subprocesos cuando se envían objetos Binder. Se deben iniciar de forma manual (consulta la administración de subprocesos).
  • El AIDL no se anula en errores de transporte sin verificar (Return de HIDL se anula en errores no verificados).
  • El AIDL solo puede declarar un tipo por archivo.
  • Los argumentos del AIDL se pueden especificar como entrada, salida o entrada, además del parámetro de salida (no hay "devoluciones de llamada síncronas").
  • El AIDL usa un fd como el tipo primitivo en lugar de un handle.
  • HIDL usa versiones principales para cambios incompatibles y versiones secundarias para cambios compatibles. En el AIDL, se realizan los cambios retrocompatibles. El AIDL no tiene un concepto explícito de versiones principales; en cambio, esto se incorpora en los nombres de los paquetes. Por ejemplo, el AIDL podría usar el nombre de paquete bluetooth2.
  • El AIDL no hereda la prioridad en tiempo real de forma predeterminada. La función setInheritRt se debe usar por vinculador para habilitar la herencia de prioridad en tiempo real.