Un backend de AIDL es un destino para la generación de código de stub. Cuando usas archivos AIDL, siempre los usas en un lenguaje en particular con un entorno de ejecución específico. Según el contexto, debes usar diferentes backends de AIDL.
En la siguiente tabla, la estabilidad de la plataforma de la API se refiere a la capacidad de compilar código con esta plataforma de una manera en que el código se pueda entregar independientemente del objeto binario system.img
libbinder.so
.
El AIDL tiene los siguientes backends:
Backend | Idioma | Plataforma de API | Sistemas de compilación |
---|---|---|---|
Java | Java | SDK/SystemApi (estable*) | todos |
NDK | C++ | libbinder_ndk (estable*) | interfaz_aidl |
CPP | C++ | libbinder (inestable) | todos |
Rust | Rust | libbinder_rs (estable*) | interfaz_aidl |
- Estas plataformas de API son estables, pero muchas de las APIs, como las de administración de servicios, están reservadas para el uso interno de la plataforma y no están disponibles para las apps. Si deseas obtener más información para usar el AIDL en las apps, consulta la documentación para desarrolladores.
- El backend de Rust se introdujo en Android 12 y el backend del NDK está disponible a partir de Android 10.
- El contenedor de Rust se compila sobre
libbinder_ndk
, lo que le permite ser estable y portátil. Los APEX usan el contenedor de Binder de la misma manera que cualquier otro usuario del sistema. La parte de Rust se agrupa en un APEX y se envía dentro de él. Depende dellibbinder_ndk.so
en la partición del sistema.
Sistemas de compilación
Según el backend, hay dos formas de compilar AIDL en código de stub. Para obtener más detalles sobre los sistemas de compilación, consulta la Referencia del módulo de Soong.
Sistema de compilación principal
En cualquier módulo de Android.bp cc_
o java_
(o en sus equivalentes Android.mk
), se pueden especificar los archivos .aidl
como archivos de origen. En este caso, se usan los backends Java/CPP del AIDL (no el backend del NDK), y las clases para usar los archivos AIDL correspondientes se agregan automáticamente al módulo. Opciones como local_include_dirs
, que le indica al sistema de compilación que la ruta raíz a los archivos AIDL en ese módulo se puede especificar en estos módulos en un grupo aidl:
. Ten en cuenta que el backend de Rust solo se usa con Rust. Los módulos rust_
se manejan de manera diferente en el sentido de que los archivos AIDL no se especifican como archivos de origen.
En su lugar, el módulo aidl_interface
produce un rustlib
llamado <aidl_interface name>-rust
con el que se puede vincular. Para obtener más detalles, consulta el ejemplo del AIDL de Rust.
interfaz_aidl
Los tipos que se usan con este sistema de compilación deben estar estructurados. Para poder estructurarse, los objetos parcelables deben contener campos directamente y no ser declaraciones de tipos definidos directamente en los idiomas objetivo. Para saber cómo se adapta el AIDL estructurado al AIDL estable, consulta AIDL estructurado versus estable.
Tipos
Puedes considerar el compilador aidl
como una implementación de referencia para los tipos.
Cuando crees una interfaz, invoca a aidl --lang=<backend> ...
para ver el archivo de interfaz resultante. Cuando usas el módulo aidl_interface
, puedes ver el resultado en out/soong/.intermediates/<path to module>/
.
Tipo de Java/AIDL | Tipo de C++ | Tipo de NDK | Tipo de Rust |
---|---|---|---|
boolean | booleano | booleano | booleano |
byte | int8_t | int8_t | i8 |
char | char16_t | char16_t | U16 |
int | int32_t | int32_t | i32 |
long | int64_t | int64_t | i64 |
float | float | float | F32 |
double | double | double | F64 |
Cadena | android::String16 | std::cadena | Cadena |
android.os.Parcelable | android::Parcelable | N/A | N/A |
IBinder | android::IBinder | ndk::SpAIBinder | Binder::SpIBinder, |
M | std::vector<T> | std::vector<T> | Entrada: &[T] Salida: Vec<T> |
byte | std::vector<uint8_t> | std::vector<int8_t>1 | In: &[u8] Salida: Vec<u8> |
Lista<T> | std::vector<T>2 | std::vector<T>3 | In: &[T]4 Salida: Vec<T> |
FileDescriptor | android::base::unique_fd | N/A | binder::parcel::ParcelFileDescriptor |
ParcelFileDescriptor | android::os::ParcelFileDescriptor | ndk::ScopedFileDescriptor | binder::parcel::ParcelFileDescriptor |
tipo de interfaz (T) | android::sp<T> | std::shared_ptr<T>7 | Binder::Sólida |
tipo parcelable (T) | T | T | T |
tipo de unión (T)5 | T | T | T |
V[N] 6 | std::matriz<T, N> | std::matriz<T, N> | [L; N] |
1. En Android 12 o versiones posteriores, los arrays de bytes usan uint8_t en lugar de int8_t por razones de compatibilidad.
2. El backend de C++ admite List<T>
, en el que T
es String
, IBinder
, ParcelFileDescriptor
o parcelable. En Android 13 o versiones posteriores, T
puede ser cualquier tipo no primitivo (incluidos los tipos de interfaz), excepto los arrays. AOSP recomienda que uses tipos de array como T[]
, ya que funcionan en todos los backends.
3. El backend del NDK admite List<T>
, en el que T
es String
, ParcelFileDescriptor
o parcelable. En Android 13 o versiones posteriores, T
puede ser cualquier tipo no primitivo, excepto arrays.
4. En el código de Rust, los tipos se pasan de manera diferente según si son una entrada (un argumento) o un resultado (un valor que se muestra).
5. Los tipos de unión son compatibles con Android 12 y versiones posteriores.
6. En Android 13 o versiones posteriores, se admiten arrays de tamaño fijo. Los arrays de tamaño fijo pueden tener varias dimensiones (p.ej., int[3][4]
).
En el backend de Java, los arrays de tamaño fijo se representan como tipos de array.
7. Para crear una instancia de un objeto SharedRefBase
de Binder, usa SharedRefBase::make\<My\>(... args ...)
. Esta función crea un objeto std::shared_ptr\<T\>
que también se administra de forma interna, en caso de que el Binder pertenezca a otro proceso. Crear el objeto de otras maneras provoca la doble propiedad.
Direccionalidad (entrada/salida/entrada)
Cuando especificas los tipos de argumentos de las funciones, puedes 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 y también indica que se pasan datos del emisor al destinatario. out
significa que los datos se pasan del destinatario al llamador. inout
es la combinación de ambos. Sin embargo, el equipo de Android recomienda que evites usar el especificador de argumentos inout
.
Si usas inout
con una interfaz con versión y un destinatario anterior, los campos adicionales que están presentes solo en el emisor se restablecen a sus valores predeterminados. Con respecto a Rust, un tipo inout
normal recibe &mut Vec<T>
y un tipo inout
de lista recibe &mut Vec<T>
.
interface IRepeatExamples {
MyParcelable RepeatParcelable(MyParcelable token); // implicitly 'in'
MyParcelable RepeatParcelableWithIn(in MyParcelable token);
void RepeatParcelableWithInAndOut(in MyParcelable param, out MyParcelable result);
void RepeatParcelableWithInOut(inout MyParcelable param);
}
UTF8/UTF16
Con el backend de la CPP, puedes elegir si las cadenas son utf-8 o utf-16. Declara cadenas como @utf8InCpp String
en el AIDL para convertirlas automáticamente a utf-8.
Los backends del NDK y Rust siempre usan cadenas utf-8. Para obtener más información sobre la anotación utf8InCpp
, consulta Anotaciones en AIDL.
Nulabilidad
Puedes 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 de @nullable
se exponen como Option<T>
. Los servidores nativos rechazan los valores nulos de forma predeterminada. Las únicas excepciones son los tipos interface
y IBinder
, que siempre pueden ser nulos para lecturas del NDK y escrituras de CPP/NDK. Para obtener más información sobre la anotación nullable
, consulta Anotaciones en AIDL.
Objetos parcelables personalizados
Un parcelable personalizado es un objeto parcelable que se implementa de forma manual en un backend de destino. Usa objetos parcelables personalizados solo cuando intentes agregar compatibilidad a otros lenguajes para un elemento parcelable personalizado existente que no se puede cambiar.
Para declarar un elemento parcelable personalizado de modo que el AIDL esté al tanto de él, la declaración parcelable del AIDL se ve de la siguiente manera:
package my.pack.age;
parcelable Foo;
De forma predeterminada, esto declara un Java parcelable en el que my.pack.age.Foo
es una clase de Java que implementa la interfaz Parcelable
.
Para declarar un backend de CPP personalizado parcelable en AIDL, usa cpp_header
:
package my.pack.age;
parcelable Foo cpp_header "my/pack/age/Foo.h";
La implementación de C++ en my/pack/age/Foo.h
se ve de la siguiente manera:
#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);
};
Para declarar un NDK personalizado parcelable en AIDL, usa ndk_header
:
package my.pack.age;
parcelable Foo ndk_header "android/pack/age/Foo.h";
La implementación del NDK en android/pack/age/Foo.h
se ve de la siguiente manera:
#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);
};
En Android 15 (experimental de AOSP), para declarar un elemento Parcelable personalizado de Rust en AIDL, usa rust_type
:
package my.pack.age;
@RustOnlyStableParcelable parcelable Foo rust_type "rust_crate::Foo";
La implementación de Rust en rust_crate/src/lib.rs
se ve de la siguiente manera:
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);
Luego, puedes usar este parcelable como un tipo en los archivos del AIDL, pero el AIDL no lo generará. Proporciona los operadores <
y ==
para los objetos parcelables personalizados del backend de CPP o NDK para usarlos en union
.
Valores predeterminados
Los objetos parcelables estructurados pueden declarar valores predeterminados por campo para primitivas, String
y arrays de estos tipos.
parcelable Foo {
int numField = 42;
String stringField = "string value";
char charValue = 'a';
...
}
En el backend de Java, cuando faltan 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 de forma predeterminada cuando estos valores no están definidos. Por ejemplo, en el backend de C++, los campos String
se inicializan como una string 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 SO Android proporciona tipos de errores integrados para que los servicios los usen cuando informen errores. Binder los utiliza, y cualquier servicio que implemente una interfaz de Binder puede usarlos. Su uso está bien documentado en la definición del AIDL y no requieren ningún estado definido por el usuario ni tipo de datos que se muestra.
Parámetros de salida con errores
Cuando una función de AIDL informa un error, esta no puede inicializar ni modificar los parámetros de salida. Específicamente, los parámetros de salida pueden modificarse si el error se produce durante la desempaquetado, en lugar de ocurrir durante el procesamiento de la transacción en sí. En general, cuando se recibe un error de una función del AIDL, todos los parámetros inout
y out
, así como el valor que se muestra (que actúa como un parámetro out
en algunos backends), deben considerarse en estado indefinido.
Qué valores de error usar
Muchos de los valores de error integrados se pueden usar en cualquier interfaz de AIDL, pero algunos se tratan de una manera especial. Por ejemplo, se pueden usar EX_UNSUPPORTED_OPERATION
y EX_ILLEGAL_ARGUMENT
cuando describen la condición de error, pero no se debe usar EX_TRANSACTION_FAILED
porque la infraestructura subyacente la trata de manera especial. Consulta las definiciones específicas del backend para obtener más información sobre estos valores integrados.
Si la interfaz del AIDL requiere valores de error adicionales que no están cubiertos por los tipos de error integrados, pueden usar el error integrado específico del servicio que permite la inclusión de un valor de error específico del servicio definido por el usuario. Por lo general, estos errores específicos del servicio se definen en la interfaz del AIDL como un enum
respaldado por const int
o int
, y no se analizan mediante el enlace.
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 la CPP usa android::binder::Status
. El backend del NDK usa ndk::ScopedAStatus
. Cada método generado por el AIDL muestra 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
o ScopedAStatus
que se muestra usa EX_SERVICE_SPECIFIC
junto con el error definido por el usuario.
Los tipos de errores integrados se pueden encontrar en los siguientes archivos:
Backend | Definición |
---|---|
Java | android/os/Parcel.java |
CPP | binder/Status.h |
NDK | android/binder_status.h |
Rust | android/binder_status.h |
Usa varios backends
Estas instrucciones son específicas para el código de la plataforma de Android. En estos ejemplos, se usa un tipo definido, my.package.IFoo
. Si quieres obtener instrucciones para usar el backend de Rust, consulta el ejemplo del AIDL de Rust en la página Patrones de Rust para Android.
Tipos de importación
Si el tipo definido es una interfaz, parcelable o unión, puedes importarlo en Java:
import my.package.IFoo;
O en el backend de CPP:
#include <my/package/IFoo.h>
También puedes hacerlo en el backend del NDK (observa el espacio de nombres aidl
adicional):
#include <aidl/my/package/IFoo.h>
También puedes hacer lo siguiente en el backend de Rust:
use my_package::aidl::my::package::IFoo;
Si bien puedes importar un tipo anidado en Java, debes incluir el encabezado para su tipo raíz en los backends de CPP o NDK. Por ejemplo, cuando importes un tipo anidado Bar
definido en my/package/IFoo.aidl
(IFoo
es el tipo raíz del archivo), debes incluir <my/package/IFoo.h>
para el backend de CPP (o <aidl/my/package/IFoo.h>
para el backend del NDK).
Implementa servicios
Para implementar un servicio, debes heredar la clase de stub nativo. Esta clase lee comandos del controlador de Binder y ejecuta los métodos que implementas. Imagina que tienes un archivo AIDL como el siguiente:
package my.package;
interface IFoo {
int doFoo();
}
En Java, debes extender desde esta clase:
import my.package.IFoo;
public class MyFoo extends IFoo.Stub {
@Override
int doFoo() { ... }
}
En el backend 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(())
}
}
O con Rust asíncrono:
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(())
}
}
Regístrate y obtén servicios
Por lo general, los servicios de la plataforma de Android se registran con el proceso servicemanager
. Además de las siguientes APIs, algunas verifican el servicio (lo que significa que regresan de inmediato si el servicio no está disponible).
Consulta la interfaz servicemanager
correspondiente para obtener los detalles exactos. Estas operaciones solo se pueden realizar cuando se compilan en la plataforma de Android.
En Java:
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"));
En el backend de 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"));
En el backend del NDK (observa el espacio de nombres adicional 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")));
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()
}
En el backend asíncrono de Rust con un entorno de ejecución de un solo subproceso, haz lo siguiente:
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
}
Una diferencia importante con respecto a las otras opciones es que no llamamos a join_thread_pool
cuando usamos Rust asíncrono y un entorno de ejecución de un solo subproceso. Esto se debe a que debes otorgarle a Tokio un subproceso en el que pueda ejecutar las tareas generadas. En este ejemplo, el subproceso principal cumplirá con ese propósito. Todas las tareas generadas con tokio::spawn
se ejecutarán en el subproceso principal.
En el backend asíncrono de Rust con un entorno de ejecución de varios subprocesos, haz lo siguiente:
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();
});
}
Con el tiempo de ejecución de varios subprocesos de Tokio, las tareas generadas no se ejecutan en el subproceso principal. Por lo tanto, tiene más sentido llamar a join_thread_pool
en el subproceso principal para que este no solo esté inactivo. Debes unir la llamada en block_in_place
para dejar el contexto asíncrono.
Vínculo a la muerte
Puedes solicitar recibir una notificación para cuando se cierre un servicio que aloja un Binder. Esto puede ayudar a evitar la filtración de proxies de devolución de llamada o ayudar en la recuperación de errores. Realiza estas llamadas en objetos del proxy de Binder.
- En Java, usa
android.os.IBinder::linkToDeath
. - En el backend de la CPP, usa
android::IBinder::linkToDeath
. - En el backend del NDK, usa
AIBinder_linkToDeath
. - En el backend de Rust, crea un objeto
DeathRecipient
y, luego, llama amy_binder.link_to_death(&mut my_death_recipient)
. Ten en cuenta que, como elDeathRecipient
es propietario de la devolución de llamada, debes mantener ese objeto activo mientras desees recibir notificaciones.
Información de la persona que llama
Cuando se recibe una llamada a Binder de kernel, la información del emisor está disponible en varias APIs. El PID (o ID de proceso) se refiere al ID de proceso de Linux del proceso que envía una transacción. El UID (o ID de usuario) se refiere al ID de usuario de Linux. Cuando se recibe una llamada unidireccional, el PID de llamada es 0. Cuando están fuera del contexto de transacción de Binder, estas funciones muestran el PID y el UID del proceso actual.
En el backend de Java:
... = Binder.getCallingPid();
... = Binder.getCallingUid();
En el backend de CPP:
... = IPCThreadState::self()->getCallingPid();
... = IPCThreadState::self()->getCallingUid();
En el backend del NDK:
... = AIBinder_getCallingPid();
... = AIBinder_getCallingUid();
En el backend de Rust, cuando implementes la interfaz, especifica lo siguiente (en lugar de permitir que sea la predeterminada):
... = ThreadState::get_calling_pid();
... = ThreadState::get_calling_uid();
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 facilitar la depuración de varios problemas.
En el caso de los servicios de AIDL, los informes de errores usan el objeto binario dumpsys
en todos los servicios registrados en el administrador de servicios para volcar su información en el informe de errores. También puedes 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, puedes controlar el orden en que se vuelcan los servicios mediante argumentos adicionales para addService
. También puedes usar dumpsys --pid SERVICE
para obtener el PID de un servicio durante la depuración.
Para agregar una salida personalizada a tu servicio, puedes anular el método dump
en tu objeto de servidor como si estuvieras implementando cualquier otro método de IPC definido en un archivo AIDL. Cuando lo hagas, debes restringir el volcado al permiso de la app android.permission.DUMP
o restringirlo a UID específicos.
En el backend de Java:
@Override
protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter fout,
@Nullable String[] args) {...}
En el backend de CPP:
status_t dump(int, const android::android::Vector<android::String16>&) override;
En el backend del NDK:
binder_status_t dump(int fd, const char** args, uint32_t numArgs) override;
En el backend de Rust, cuando implementes la interfaz, especifica lo siguiente (en lugar de permitir que sea la predeterminada):
fn dump(&self, mut file: &File, args: &[&CStr]) -> binder::Result<()>
Obtén un descriptor de interfaz de forma dinámica
El descriptor de interfaz identifica el tipo de interfaz. Esto es útil cuando realizas depuraciones o cuando tienes un Binder desconocido.
En Java, puedes obtener el descriptor de la interfaz con código como el siguiente:
service = /* get ahold of service object */
... = service.asBinder().getInterfaceDescriptor();
En el backend de CPP:
service = /* get ahold of service object */
... = IInterface::asBinder(service)->getInterfaceDescriptor();
Los backends de NDK y Rust no admiten esta capacidad.
Obtén el descriptor de la interfaz de forma estática
A veces (por ejemplo, cuando registras servicios @VintfStability
), necesitas saber cuál es el descriptor de interfaz de forma estática. En Java, puedes obtener el descriptor si agregas código como el siguiente:
import my.package.IFoo;
... IFoo.DESCRIPTOR
En el backend 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, puedes iterar sobre los valores posibles que puede asumir una enumeración. Debido a consideraciones del tamaño del código, esto no se admite en Java.
Para una enumeración MyEnum
definida en el AIDL, la iteración se proporciona de la siguiente manera.
En el backend de CPP:
::android::enum_range<MyEnum>()
En el backend del NDK:
::ndk::enum_range<MyEnum>()
En el backend de Rust:
MyEnum::enum_values()
Administración de subprocesos
Cada instancia de libbinder
en un proceso mantiene un conjunto de subprocesos. En la mayoría de los casos prácticos, debería ser exactamente un conjunto de subprocesos, compartido entre todos los backends.
La única excepción es cuando el código del proveedor podría cargar otra copia de libbinder
para comunicarse con /dev/vndbinder
. Como se encuentra en un nodo de Binder independiente, el grupo de subprocesos no se comparte.
En el backend de Java, el conjunto de subprocesos solo puede aumentar de tamaño (debido a que ya se inició):
BinderInternal.setMaxThreads(<new larger value>);
Para el backend de la CPP, se encuentran 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();
De manera similar, 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();
Con el backend asíncrono de Rust, necesitas dos grupos de subprocesos: Binder y Tokio.
Esto significa que las apps que usan Rust asíncrono necesitan consideraciones especiales, en especial, cuando se trata del uso de join_thread_pool
. Consulta la sección sobre el registro de servicios para obtener más información.
Nombres reservados
C++, Java y Rust reservan algunos nombres como palabras clave o para usos específicos del lenguaje. Si bien el AIDL no aplica restricciones basadas en reglas de lenguaje, el uso de nombres de campo o de tipo que coincidan con un nombre reservado puede generar una falla de compilación para C++ o Java. En Rust, se cambia el nombre del campo o tipo con la sintaxis de "identificador sin procesar", a la que se puede acceder con el prefijo r#
.
Te recomendamos que evites el uso de nombres reservados en las definiciones de AIDL siempre que sea posible para evitar las vinculaciones unergonómicas o la falla total de compilación.
Si ya tienes nombres reservados en las definiciones del AIDL, puedes cambiar el nombre de los campos de forma segura sin dejar de ser compatible con el protocolo. Es posible que debas actualizar tu código para continuar con la compilación, pero cualquier programa ya compilado seguirá interoperando.
Nombres que debes evitar: