Interoperabilidad Rust / C ++ en la plataforma Android


Uno de los principales desafíos de evaluar Rust para su uso dentro de la plataforma Android fue garantizar que pudiéramos proporcionar suficiente interoperabilidad con nuestra base de código existente. Para que Rust cumpla con sus objetivos de mejorar la seguridad, la estabilidad y la calidad en todo Android, debemos poder usar Rust en cualquier lugar de la base de código donde se requiera el código nativo. Para lograr esto, debemos proporcionar la mayoría de las funcionalidades que utilizan los desarrolladores de plataformas. Como discutimos previamente, tenemos demasiado C ++ como para considerar ignorarlo, reescribirlo todo es inviable y reescribir el código antiguo probablemente sería contraproducente, ya que los errores en ese código se han corregido en gran medida. Esto significa que la interoperabilidad es la forma más práctica de avanzar.

Antes de introducir Rust en el Proyecto de código abierto de Android (AOSP), necesitábamos demostrar que la interoperabilidad de Rust con C y C ++ es suficiente para un uso práctico, conveniente y seguro dentro de Android. Agregar un nuevo idioma tiene costos; necesitábamos demostrar que Rust podría escalar a través del código base y alcanzar su potencial para justificar esos costos. Esta publicación cubrirá el análisis que hicimos hace más de un año mientras evaluamos Rust para su uso en Android. También presentamos un análisis de seguimiento con algunas ideas sobre cómo se ha mantenido el análisis original a medida que los proyectos de Android han adoptado Rust.

La interoperabilidad de los lenguajes existentes en Android se centra en los límites de la interfaz de función extranjera (FFI) bien definidos, que es donde el código escrito en un lenguaje de programación llama al código escrito en un lenguaje diferente. El soporte de Rust también se centrará en el límite de FFI, ya que esto es coherente con la forma en que se desarrollan los proyectos AOSP, cómo se comparte el código y cómo se gestionan las dependencias. Para la interoperabilidad de Rust con C, la interfaz binaria de la aplicación C (ABI) ya es suficiente.

La interoperabilidad con C ++ es más desafiante y es el tema central de esta publicación. Si bien tanto Rust como C ++ admiten el uso de C ABI, no es suficiente para el uso idiomático de ninguno de los dos lenguajes. La simple enumeración de las características de cada idioma da como resultado una conclusión nada sorprendente: muchos conceptos no son fácilmente traducibles, ni queremos necesariamente que lo sean. Después de todo, presentamos Rust porque muchas funciones y características de C ++ dificultan la escritura de código seguro y correcto. Por lo tanto, nuestro objetivo no es considerar todas las características del lenguaje, sino analizar cómo Android usa C ++ y asegurarnos de que la interoperabilidad sea conveniente para la gran mayoría de nuestros casos de uso.

Analizamos el código y las interfaces en la plataforma Android específicamente, no las bases de código en general. Si bien esto significa que nuestras conclusiones específicas pueden no ser precisas para otras bases de código, esperamos que la metodología pueda ayudar a otros a tomar una decisión más informada sobre la introducción de Rust en su gran base de código. Nuestros colegas del equipo del navegador Chrome han realizado un análisis similar, que puede encontrar aquí.

Este análisis no estaba destinado originalmente a publicarse fuera de Google: nuestro objetivo era tomar una decisión basada en datos sobre si Rust era o no una buena opción para el desarrollo de sistemas en Android. Si bien se pretende que el análisis sea preciso y práctico, nunca se pretendió que fuera exhaustivo, y hemos señalado un par de áreas en las que podría ser más completo. Sin embargo, también observamos que las investigaciones iniciales en estas áreas mostraron que no afectarían significativamente los resultados, por lo que decidimos no invertir el esfuerzo adicional.

Las funciones exportadas de las bibliotecas Rust y C ++ son aquellas en las que consideramos que la interoperabilidad es esencial. Nuestros objetivos son simples:

  • Rust debe poder llamar a funciones desde bibliotecas C ++ y viceversa.
  • FFI debe exigir un mínimo de caldera.
  • FFI no debería requerir una gran experiencia.

Si bien un objetivo es hacer que las funciones de Rust se puedan llamar desde C ++, este análisis se centra en hacer que las funciones de C ++ estén disponibles para Rust para que se pueda agregar nuevo código de Rust mientras se aprovechan las implementaciones existentes en C ++. Con ese fin, analizamos las funciones de C ++ exportadas y consideramos la compatibilidad existente y planificada con Rust a través de las bibliotecas de compatibilidad y ABI de C. Los tipos se extraen ejecutando objdump en bibliotecas compartidas para encontrar funciones externas de C ++ que utilizan1 y corriendo c++filt para analizar los tipos de C ++. Esto da funciones y sus argumentos. No considera valores de retorno, sino un análisis preliminar.2 de los que revelaron que no afectarían significativamente los resultados.

Luego clasificamos cada uno de estos tipos en uno de los siguientes grupos:

Apoyado por bindgen

Estos son generalmente tipos simples que involucran primitivas (incluidos punteros y referencias a ellos). Para estos tipos, la FFI existente de Rust los manejará correctamente y el sistema de compilación de Android autogenerar los enlaces.

Compatible con cxx compat crate

Estos son manejados por el cxx caja. Esto incluye actualmente std::string, std::vector, y métodos C ++ (incluidos punteros / referencias a estos tipos). Los usuarios simplemente tienen que definir los tipos y funciones que quieren compartir entre lenguajes y cxx generará el código para hacerlo de forma segura.

Soporte nativo

Estos tipos no son compatibles directamente, pero las interfaces que los utilizan se han modificado manualmente para agregar compatibilidad con Rust. Específicamente, esto incluye los tipos utilizados por AIDL y protobufs.

También hemos implementado una interfaz nativa para EstadísticasD ya que la interfaz de C ++ existente se basa en la sobrecarga de métodos, que no es compatible con bindgen y cxx3. El uso de este sistema no se muestra en el análisis porque la API de C ++ no usa tipos únicos.

Adición potencial a cxx

Actualmente se trata de estructuras de datos comunes como std::optional y std::chrono::duration e implementaciones personalizadas de cadenas y vectores.

Estos pueden ser compatibles de forma nativa mediante una contribución futura a cxx o mediante el uso de sus funciones ExternType. Solo hemos incluido tipos en esta categoría que creemos que son relativamente sencillos de implementar y tienen una probabilidad razonable de ser aceptados en el proyecto cxx.

No necesitamos / tenemos la intención de apoyar

Algunos tipos se exponen en las API de C ++ actuales que son una parte implícita de la API, no una API que esperamos usar de Rust, o son específicos del lenguaje. Ejemplos de tipos que no tenemos la intención de admitir incluyen:

  • Mutex: esperamos que el bloqueo se lleve a cabo en un idioma u otro, en lugar de tener que pasar mutex entre idiomas, según nuestra filosofía de grano grueso.
  • native_handle – este es un tipo de interfaz JNI, por lo que no es apropiado para su uso en la comunicación Rust / C ++.
  • std::locale& – Android utiliza un sistema de configuración regional independiente de las configuraciones regionales de C ++. Este tipo aparece principalmente en la salida debido a, por ejemplo, cout uso, que sería inapropiado para usar en Rust.

En general, esta categoría representa tipos que no creemos que deba utilizar un desarrollador de Rust.

HIDL

Android está en proceso de desaprobación HIDL y migrando a AIDL para HAL para nuevos servicios. También estamos migrando algunas implementaciones existentes a AIDL estable. Nuestro plan actual es no admitir HIDL, prefiriendo migrar a AIDL estable. Por lo tanto, estos tipos se encuentran actualmente en el grupo "No necesitamos / no tenemos la intención de brindar soporte" anterior, pero los desglosamos para ser más específicos. Si hay suficiente demanda de soporte de HIDL, es posible que revisemos esta decisión más adelante.

Otro

Contiene todos los tipos que no encajan en ninguno de los grupos anteriores. Actualmente es mayormente std::string se pasa por valor, que no es compatible con cxx.

Una de las principales razones para admitir la interoperabilidad es permitir la reutilización del código existente. Teniendo esto en cuenta, determinamos las bibliotecas de C ++ más utilizadas en Android: liblog, libbase, libutils, libcutils, libhidlbase, libbinder, libhardware, libz, libcrypto, y libui. Luego analizamos todas las funciones externas de C ++ utilizadas por estas bibliotecas y sus argumentos para determinar qué tan bien interoperarían con Rust.

En general, el 81% de los tipos se encuentran en las primeras tres categorías (que actualmente apoyamos por completo) y el 87% están en las primeras cuatro categorías (que incluyen aquellas que creemos que podemos admitir fácilmente). Casi todos los tipos restantes son aquellos que creemos que no necesitamos admitir.

Además de analizar las bibliotecas populares de C ++, también examinamos Módulos de línea principal. Apoyar este contexto es fundamental ya que Android es migrando algunas de sus funciones principales a Mainline, incluyendo gran parte del código nativo que esperamos aumentar con Rust. Además, su modularidad presenta una oportunidad para el soporte de interoperabilidad.

Analizamos 64 binarios y bibliotecas en 21 módulos. Para cada biblioteca analizada, examinamos sus funciones de C ++ utilizadas y analizamos los tipos de sus argumentos para determinar qué tan bien interoperarían con Rust de la misma manera que lo hicimos anteriormente para las 10 bibliotecas principales.

Aquí el 88% de los tipos están en las primeras tres categorías y el 90% en las primeras cuatro, y casi todos los restantes son tipos que no necesitamos manejar.

Con casi un año de desarrollo de Rust en AOSP detrás de nosotros, y más de cien mil líneas de código escritas en Rust, ahora podemos examinar cómo se ha mantenido nuestro análisis original basado en cómo se llama actualmente el código C / C ++ desde Rust en AOSP. .4

Los resultados coinciden en gran medida con lo que esperábamos de nuestro análisis con bindgen manejando la mayoría de las necesidades de interoperabilidad. El uso extensivo de AIDL por parte del nuevo servicio Keystore2 da como resultado la principal diferencia entre nuestro análisis original y el uso real de Rust en la categoría "Soporte nativo".

Algunos ejemplos actuales de interoperabilidad son:

  • Cxx en Bluetooth – Si bien Rust está destinado a ser el idioma principal para Bluetooth, la migración desde la implementación de C / C ++ existente se realizará por etapas. El uso de cxx permite que el equipo de Bluetooth sirva más fácilmente protocolos heredados como HIDL hasta que se eliminen gradualmente mediante el uso del soporte de C ++ existente para migrar gradualmente su servicio.
  • AIDL en el almacén de claves – Keystore implementa servicios AIDL e interactúa con aplicaciones y otros servicios a través de AIDL. Proporcionar esta funcionalidad sería difícil de respaldar con herramientas como cxx o bindgen, pero el soporte nativo de AIDL es simple y ergonómico de usar.
  • Contenedores escritos manualmente en profcollectd – Si bien nuestro objetivo es proporcionar una interoperabilidad perfecta para la mayoría de los casos de uso, también queremos demostrar que, incluso cuando las soluciones de interoperabilidad generadas automáticamente no son una opción, crearlas manualmente puede ser simple y directo. Profcollectd es un pequeño demonio que solo existe en compilaciones de ingeniería que no son de producción. En lugar de usar cxx, usa algunos envoltorios de C escritos manualmente alrededor de las bibliotecas de C ++ que luego pasa a bindgen.

Bindgen y cxx proporcionan la gran mayoría de la interoperabilidad de Rust / C ++ que necesita Android. Para algunas de las excepciones, como AIDL, la versión nativa proporciona una conveniente interoperabilidad entre Rust y otros lenguajes. Los contenedores escritos manualmente se pueden usar para manejar los pocos tipos y funciones restantes que no son compatibles con otras opciones, así como para crear API de Rust ergonómicas. En general, creemos que la interoperabilidad entre Rust y C ++ ya es en gran medida suficiente para el uso conveniente de Rust dentro de Android.

Si está considerando cómo Rust podría integrarse en su proyecto de C ++, le recomendamos que haga un análisis similar de su base de código. Al abordar las brechas de interoperabilidad, le recomendamos que considere la posibilidad de incorporar compatibilidad con bibliotecas de compatibilidad existentes, como cxx.

Nuestro primer intento de cuantificar la interoperabilidad de Rust / C ++ implicó el análisis de los posibles desajustes entre los lenguajes. Esto condujo a una gran cantidad de información interesante, pero fue difícil sacar conclusiones procesables. En lugar de enumerar todos los lugares potenciales donde podría ocurrir la interoperabilidad, Stephen Hines sugirió que consideremos cómo se comparte actualmente el código entre los proyectos de C / C ++ como un proxy razonable de dónde probablemente también querremos interoperabilidad para Rust. Esto nos brindó información procesable que fue fácil de priorizar e implementar. Mirando hacia atrás, los datos de nuestro uso de Rust en el mundo real han reforzado que la metodología inicial era sólida. ¡Gracias Stephen!

Además, gracias a:

  • Andrei Homescu y Stephen Crane por contribuir con el apoyo de AIDL a AOSP.
  • Ivan Lozano por contribuir con el apoyo de protobuf a AOSP.
  • David Tolnoy por publicar cxx y aceptar nuestras contribuciones.
  • Los numerosos autores y colaboradores de bindgen.
  • Jeff Vander Stoep y Adrian Taylor por sus contribuciones a esta publicación.



Enlace a la noticia original