Integración de Rust en el proyecto de código abierto de Android


El equipo de Android ha estado trabajando en la introducción del lenguaje de programación Rust en el Proyecto de código abierto de Android (AOSP) desde 2019 como una alternativa segura para la memoria para el desarrollo de código nativo de la plataforma. Al igual que con cualquier proyecto grande, la introducción de un nuevo idioma requiere una consideración cuidadosa. Para Android, un área importante fue evaluar cómo encajar mejor Rust en el sistema de compilación de Android. Actualmente esto significa el Soong sistema de construcción (donde reside el soporte de Rust), pero estas decisiones y consideraciones de diseño son igualmente aplicables para Bazel cuando AOSP migra a ese sistema de compilación. Esta publicación analiza algunas de las consideraciones de diseño clave y las decisiones resultantes que tomamos al integrar la compatibilidad con Rust en el sistema de compilación de Android.

Integración de Rust en grandes proyectos

Una reunión de la RustConf 2019 sobre Uso de óxido dentro de grandes organizaciones destacó varios desafíos, como el riesgo de que evitar Cargo en favor del uso del compilador Rust, rustc, directamente (consulte la siguiente sección) puede eliminar organizaciones de la comunidad de Rust en general. Compartimos esta misma preocupación. Cuando los cambios en las cajas importadas de terceros pueden ser beneficiosos para la comunidad en general, nuestro objetivo es impulsar esos cambios. Del mismo modo, cuando las cajas desarrolladas para Android puedan beneficiar a la comunidad de Rust en general, esperamos lanzarlas como cajas independientes. Creemos que el éxito de Rust en Android depende de minimizar cualquier divergencia entre Android y la comunidad de Rust en general, y esperamos que la comunidad de Rust se beneficie de la participación de Android.

Sin sistemas de construcción anidados

Rust proporciona Cargo como predeterminado sistema de compilación y administrador de paquetes, recolectando dependencias e invocando rustc (el compilador de Rust) para construir la caja de destino (paquete Rust). Soong toma este rol en su lugar en Android y llama rustc directamente por varias razones:

  • En Cargo, las dependencias de C se manejan de forma independiente de manera ad-hoc a través de scripts build.rs. Soong ya proporciona un mecanismo para construir bibliotecas C y definirlas como dependencias, y Android controla cuidadosamente la versión del compilador y los indicadores de compilación global para garantizar que las bibliotecas se construyan de una manera particular. Confiar en Cargo introduciría un segundo mecanismo distinto de Soong para definir / construir bibliotecas C que no estaría limitado por los controles de compilación cuidadosamente seleccionados implementados en Soong. Esto también podría conducir a múltiples versiones diferentes de la misma biblioteca, lo que afectaría negativamente el uso de memoria / disco.
  • Llamar a los compiladores directamente a través de Soong proporciona la estabilidad y el control que Android requiere para la variedad de configuraciones de compilación que admite (por ejemplo, especificar dónde están las dependencias específicas del destino y qué indicadores de compilación usar). Si bien técnicamente sería posible lograr el nivel necesario de control sobre rustc indirectamente a través de Cargo, Soong no entendería cómo el Cargo.toml (el archivo de compilación de Cargo) influiría en los comandos que Cargo emite a rustc. Junto con el hecho de que Cargo evoluciona de forma independiente, esto restringiría severamente la capacidad de Soong para controlar con precisión cómo se crean los artefactos de construcción.
  • Compilaciones autónomas e insensibles a la configuración del host, conocidas como construcciones herméticas, son necesarios para que Android produzca compilaciones reproducibles. Cargo, que depende de build.rs scripts, aún no ofrece garantías de hermeticidad.
  • Las construcciones incrementales son importantes para mantener la productividad de la ingeniería; La construcción de Android requiere una cantidad considerable de recursos. Cargo no fue diseñado para integrarse en sistemas de construcción existentes y no expone sus unidades de compilación. Cada invocación de carga se construye todo el gráfico de dependencia de la caja para una dada Cargo.toml, reconstruyendo cajas varias veces en todos los proyectos1. Esto es demasiado burdo para la integración en el soporte de compilación incremental de Soong, que espera unidades de compilación más pequeñas. Este soporte es necesario para ampliar el uso de Rust en Android.

    El uso del compilador de Rust directamente nos permite evitar estos problemas y es coherente con la forma en que compilamos el resto del código en AOSP. Proporciona el mayor control sobre el proceso de compilación y facilita la integración en el sistema de compilación existente de Android. Desafortunadamente, evitarlo presenta varios desafíos e influye en muchas otras decisiones del sistema de construcción porque el uso de Cargo está muy arraigado en el ecosistema de cajas de Rust.

    Sin scripts de build.rs

    A build.rs El script se compila en un binario de Rust que Cargo construye y ejecuta durante una compilación para manejar tareas previas a la compilación, comúnmente configurando el entorno de compilación o compilando bibliotecas en otros lenguajes (por ejemplo, C / C ++). Esto es análogo a configurar los scripts utilizados para otros idiomas.

    Evitando build.rs Los scripts fluyen de alguna manera de forma natural por no depender de Cargo, ya que respaldarlos requeriría replicar el comportamiento y los supuestos de Cargo. Sin embargo, más allá de esto, hay buenas razones para que AOSP evite también la creación de scripts:

    • build.rs Los scripts pueden ejecutar código arbitrario en el host de compilación. Desde una perspectiva de seguridad, esto introduce una carga adicional al agregar o actualizar código de terceros como build.rs El guión necesita un escrutinio cuidadoso.
    • Tercero build.rs Los guiones pueden no ser herméticos o reproducibles en formas potencialmente sutiles. También es común para build.rs archivos para acceder a archivos fuera del directorio de compilación (como /usr/lib). Cuando no son herméticos, necesitaríamos llevar un parche local o trabajar con upstream para resolver el problema.
    • La tarea más común para build.rs es construir bibliotecas C de las que depende el código Rust. Ya apoyamos esto a través de Soong.
    • Android también evita ejecutar scripts de compilación mientras compila para otros lenguajes, en su lugar, simplemente los usa para informar la estructura del Android.bp expediente.

Para instancias en código de terceros donde un script de compilación se usa solo para compilar dependencias de C, usamos existentes cc_library Definiciones tan largas (como boringssl por Quiche) o crear nuevas definiciones para el código específico de la caja.

Cuando el build.rs se usa para generar fuente, intentamos replicar la funcionalidad principal en un Soong rust_binary módulo para usar como generador de fuente personalizado. En otros casos en los que Soong puede proporcionar la información sin la generación de la fuente, podemos llevar un pequeño parche que aprovecha esta información.

¿Por qué proc_macro pero no build.rs?

¿Por qué apoyamos proc_macros, que son complementos del compilador que ejecutan código en el host dentro del contexto del compilador, pero no build.rs ¿guiones?

Tiempo build.rs el código está escrito como código único para manejar la construcción de una sola caja, proc_macros definir la funcionalidad reutilizable dentro del compilador que puede ser ampliamente utilizada en la comunidad de Rust. Como resultado popular proc_macros por lo general, se mantienen mejor y se controlan mejor en sentido ascendente, lo que hace que el proceso de revisión del código sea más manejable. También se colocan más fácilmente en un espacio aislado como parte del proceso de compilación, ya que es menos probable que tengan dependencias externas al compilador.

proc_macros también son una característica del lenguaje en lugar de un método para crear código. Estos se basan en el código fuente, son inevitables para las dependencias de terceros y son lo suficientemente útiles como para definirlos y usarlos dentro del código de nuestra plataforma. Si bien podemos evitar build.rs aprovechando nuestro sistema de compilación, no se puede decir lo mismo de proc_macros.

También existe prioridad para la compatibilidad con complementos del compilador dentro del sistema de compilación de Android. Por ejemplo, vea Soong's java_plugin módulos.

Fuente generada como cajas

A diferencia de los compiladores de C / C ++, rustc solo acepta un único archivo fuente que representa un punto de entrada a un binario o biblioteca. Espera que el árbol de origen esté estructurado de manera que todos los archivos de origen necesarios se puedan descubrir automáticamente. Esto significa que la fuente generada debe colocarse en el árbol de fuentes o proporcionarse a través de una directiva de inclusión en la fuente:

include!("/path/to/hello.rs");

La comunidad de Rust depende de build.rs scripts junto con suposiciones sobre el entorno de compilación de Cargo para superar esta limitación. Al construir, el cargo comando establece un Variable de entorno OUT_DIR en los que se espera que los scripts de build.rs coloquen el código fuente generado. Esta fuente se puede incluir a través de:

include!(concat!(env!("OUT_DIR"), "/hello.rs"));

Esto presenta un desafío para Soong, ya que las salidas de cada módulo se colocan en su propio out/ directorio2; no hay un solo OUT_DIR donde las dependencias dan salida a su fuente generada.

Para el código de plataforma, preferimos empaquetar la fuente generada en una caja que se pueda importar. Hay algunas razones para favorecer este enfoque:

  • Evite que los nombres de archivos de origen generados colisionen.
  • Reducir código repetitivo registrado en todo el árbol y que necesita ser mantenido. Cualquier calderería necesaria para hacer el compilación fuente generada en una caja se puede mantener de forma centralizada.
  • Evite lo implícito3 interacciones entre el código generado y la caja circundante.
  • Reduzca la presión sobre la memoria y el disco haciendo que le gusten dinámicamente las fuentes generadas de uso común.

    Como resultado, todos los tipos de módulos de generación de fuentes Rust de Android producen código que se puede compilar y usar. como una caja.

    Seguimos admitiendo cajas de terceros sin modificaciones al copiar todas las dependencias de origen generadas para un módulo en un único directorio por módulo similar a Cargo. Soong luego establece el OUT_DIR variable de entorno a ese directorio al compilar el módulo para que se pueda encontrar la fuente generada. Sin embargo, desaconsejamos el uso de este mecanismo en el código de la plataforma a menos que sea absolutamente necesario por las razones descritas anteriormente.

    Enlace dinámico por defecto

    Por defecto, el ecosistema Rust asume que las cajas serán vinculado estáticamente a binarios. Los beneficios habituales de las bibliotecas dinámicas son las actualizaciones (ya sea por seguridad o funcionalidad) y menor uso de memoria. La falta de Rust de una interfaz binaria estable y el uso del flujo de información entre cajas impide actualizar las bibliotecas sin actualizar todo el código dependiente. Incluso cuando dos programas diferentes del sistema utilizan la misma caja, es poco probable que la proporcione el mismo objeto compartido4 debido a la precisión con la que Rust identifica sus cajas. Esto hace que los binarios de Rust sean más portátiles, pero también da como resultado un espacio de memoria y disco más grande.

    Esto es problemático para los dispositivos Android donde los recursos como la memoria y el uso del disco deben administrarse con cuidado porque la vinculación estática de todas las cajas en los binarios de Rust daría como resultado una duplicación excesiva de código (especialmente en la biblioteca estándar). Sin embargo, nuestra situación también es diferente del entorno de host estándar: creamos Android utilizando decisiones globales sobre dependencias. Esto significa que casi todas las cajas se pueden compartir entre todos los usuarios de esa caja. Por lo tanto, optamos por vincular cajas dinámicamente de forma predeterminada para los objetivos del dispositivo. Esto reduce la huella de memoria general de Rust en Android al permitir que las cajas se reutilicen en múltiples binarios que dependen de ellos.

    Dado que esto es inusual en la comunidad de Rust, no todas las cajas de terceros admiten la compilación dinámica. A veces debemos llevar pequeños parches mientras nosotros trabajar con mantenedores aguas arriba para agregar apoyo.

    Estado actual del soporte de compilación

    Apoyamos la construcción de todos los tipos de salida compatibles con rustc (rlibs, dylibs, proc_macros, cdylibs, staticlibs y ejecutables). Los módulos de Rust pueden solicitar automáticamente el enlace de caja apropiado para una dependencia determinada (rlib vs dylib). Los módulos C y C ++ pueden depender de Rust cdylib o staticlib produciendo módulos de la misma manera que lo harían para una biblioteca C o C ++.

    Además de poder compilar código Rust, el sistema de compilación de Android también brinda soporte para protobuf y gRPC y AIDL cajas generadas. Primera clase bindgen El soporte hace que la interfaz con el código C existente sea simple y tenemos soporte modulos utilizando cxx para una integración más estrecha con el código C ++.

    La comunidad de Rust produce excelentes herramientas para desarrolladores, como el servidor de idiomas analizador de herrumbre. Tenemos soporte integrado para el analizador de óxido en el sistema de compilación para que cualquier IDE que lo admita pueda proporcionar finalización de código e ir a definiciones para módulos de Android.

    Cobertura de código basado en fuente Las compilaciones son compatibles para proporcionar a los desarrolladores de plataformas señales de alto nivel sobre qué tan bien su código está cubierto por las pruebas. Los puntos de referencia se admiten como su propio tipo de módulo, aprovechando la caja de criterio para proporcionar métricas de rendimiento. Para mantener un estilo y un nivel de calidad de código consistentes, un conjunto predeterminado de clippy pelusas y rustc las pelusas son habilitado por defecto. Además, los fuzzers HWASAN / ASAN son soportado, con el HWASAN rustc apoyo añadido a aguas arriba.

    En un futuro próximo, planeamos agregar documentación a source.android.com sobre cómo definir y usar módulos Rust en Soong. Esperamos que el soporte de Android para Rust continúe evolucionando junto con el ecosistema de Rust y esperamos seguir participando en los debates sobre cómo se puede integrar Rust en los sistemas de compilación existentes.

    Gracias a Matthew Maurer, Jeff Vander Stoep, Joel Galenson, Manish Goregaokar y Tyler Mandry por sus contribuciones a esta publicación.

    Notas



Enlace a la noticia original