- El sistema de gestión de dependencias de Rust hace que el desarrollo sea cómodo, pero la cantidad y la calidad de las dependencias son motivo de preocupación
- Incluso un crate bien utilizado puede no estar actualizado, así que a veces conviene más implementarlo uno mismo
- Después de agregar crates conocidos como Axum y Tokio, el número total de líneas de código incluyendo dependencias llegó a 3.6 millones, algo difícil de manejar
- El código que realmente escribí es de apenas unas 1,000 líneas, pero en la práctica es imposible revisar y auditar todo el código de alrededor
- No hay una solución clara sobre si se debe ampliar la biblioteca estándar de Rust ni sobre cómo implementar la infraestructura central, y toda la comunidad debe pensar junta en el equilibrio entre rendimiento, seguridad y mantenibilidad
Panorama general del problema de dependencias en Rust
- Rust es mi lenguaje favorito, y tanto la comunidad como la usabilidad del lenguaje son excelentes
- La productividad de desarrollo es alta, pero últimamente me han surgido preocupaciones en cuanto a la gestión de dependencias
Ventajas de los crates de Rust y Cargo
- Con Cargo es posible gestionar paquetes y automatizar tareas de compilación, lo que mejora mucho la productividad
- Es fácil moverse entre varios sistemas operativos y arquitecturas, y no hace falta preocuparse por administrar archivos manualmente ni configurar herramientas de build
- Se puede empezar a escribir código de inmediato sin preocuparse aparte por la gestión de paquetes
Desventajas de la gestión de crates en Rust
- Como se presta menos atención a la gestión de paquetes, se termina descuidando la estabilidad
- Por ejemplo, usé el crate dotenv y me enteré mediante un Security Advisory de que ya no tenía mantenimiento
- Consideré un crate alternativo (
dotenvy), pero terminé implementando por mi cuenta solo lo necesario en unas 35 líneas
- Como los problemas de paquetes sin mantenimiento ocurren con frecuencia en muchos lenguajes, el fondo del problema es que hay situaciones donde las dependencias son inevitables
El aumento explosivo del volumen de código causado por las dependencias
- Estoy usando paquetes importantes y de buena calidad del ecosistema Rust como Tokio y Axum
- Como dependencias agregué Axum, Reqwest, ripunzip, serde, serde_json, tokio, tower-http, tracing y tracing-subscriber
- El objetivo principal es un servidor web, descompresión de archivos y logging, así que el proyecto en sí es simple
- Usé la función
cargo vendor para descargar localmente todos los crates dependientes
- Al analizar las líneas de código con
tokei, el total incluyendo dependencias llegó a aproximadamente 3.6 millones de líneas (sin contar los crates vendorized, unas 11,136 líneas)
- Como referencia, se dice que todo el kernel de Linux tiene unas 27.8 millones de líneas, así que mi pequeño proyecto equivale a una séptima parte de eso
- El código real que escribí es de apenas unas 1,000 líneas
- Supervisar y auditar esa enorme cantidad de código dependiente es, en la práctica, imposible
Reflexiones sobre una solución
- Por ahora no hay una solución clara
- Algunos proponen ampliar la biblioteca estándar como en Go, pero eso también genera nuevos problemas, como una mayor carga de mantenimiento
- Rust busca alto rendimiento, seguridad y modularidad, y como apunta a competir en embebidos y con C++, hay que ser prudentes al ampliar la biblioteca estándar
- Por ejemplo, incluso un runtime tan sofisticado como Tokio se mantiene de forma muy activa en GitHub y Discord
- En la práctica, implementar por cuenta propia infraestructura clave como un runtime asíncrono o un servidor web es demasiado para un desarrollador individual
- Incluso un servicio grande como Cloudflare usa directamente dependencias de tokio y crates.io, y no está claro con qué frecuencia las auditan
- Clickhouse también ha mencionado problemas relacionados con el tamaño de los binarios y la cantidad de crates
- Con Cargo es difícil identificar con precisión las líneas de código que terminan incluidas en el binario final, y también existe la limitación de que se incluya código innecesario según la plataforma
- Al final, la realidad es que no queda más que preguntarle a toda la comunidad por una respuesta
3 comentarios
Si corres Trivy, hay muchos menos hallazgos high o critical y es más seguro que js NPM o Java Maven; entonces, ¿qué es lo que este artículo quiere afirmar sobre Rust?
Opiniones de Hacker News
import fooliben una línea y a nadie le importa qué hay dentro. En cada nivel solo necesitas como 5% de la funcionalidad, pero mientras más profundo es el árbol, más código inútil se acumula. Al final un binario simple termina pesando 500 MiB, y acabas trayendo una dependencia solo para formatear un número. Go y Rust fomentan meterlo todo en un solo archivo, así que si quieres usar solo una parte terminas en una situación incómoda. A largo plazo, la solución real sería un rastreo ultrafino de símbolos/dependencias, donde cada función/tipo declare exactamente qué elementos necesita, para usar solo el código exacto y desechar el resto. Personalmente no me encanta la idea, pero no se me ocurre otra forma de resolver el sistema actual, que raspa el universo entero desde el árbol de dependenciasvulkan, decodificación PNG, unicode shaping, etc.). Las dependencias innecesarias eran sobre todo muy pequeñas, y solo pude quitarserde_jsoncon una modificación menor. Las dependencias más grandes (winit/wgpu, etc.) requieren cambios estructurales, así que no se pueden sacar fácilmente.opor función y se agrupaban en un archivo.a, y el linker extraía solo las funciones necesarias. También se hacía namespacing con cosas comofoolib_do_thing(). Hoy, con algo parecido al patrón god object, todas las funciones viven en un objeto de nivel superior, así que al importarfoolibte traes todo. En ese estado, al linker le resulta difícil determinar qué funciones son realmente imprescindibles. En cambio, Go tiene una eliminación de código muerto excelente, así que si no se usa, se corta del binario finalmin-sized-rustisEven,isOddoleftpaden npm, una gran biblioteca de propósito general mantenida por un equipo federado ofrece mucho más futuro y continuidad--gc-sectionsglobdebería ser solo una función sencilla de globbing, pero el autor también empaqueta una herramienta de línea de comandos y mete un parser grande como dependencia. Eso provoca advertencias frecuentes de "dependency out-of-date". También se discute cuál debería ser el alcance de responsabilidad de una bibliotecaglob. Hacer solo pattern matching sobre strings da un diseño más flexible, por ejemplo para tests o para abstraer el sistema de archivos. Mucha gente quiere bibliotecas todopoderosas que "hagan de todo", pero mientras más hacen, mayores son los efectos secundarios. Sospecho que Rust no será tan diferentestdlib::data_structures::automata::weighted_finite_state_transducer). Como el lenguaje ya incorpora control de versiones y compatibilidad hacia atrás, espero que siga mejorandoglobde POSIX en realidad recorre el sistema de archivos. Para matching de strings estáfnmatch. Lo ideal sería quefnmatchestuviera en un módulo separado y fuera una dependencia deglob. Intentar implementarglobdirectamente es bastante difícil; hay requisitos complejos como la estructura de directorios, brace expansion y otros, así que hace falta una combinación bien diseñada de funcionesglobHello worldy no tiene tipo string. En cambio, está especializado en parsear formatos de archivo no confiables. Necesitamos más lenguajes de propósito especial así. Son rápidos y no presentan riesgos, por lo que también se pueden reducir chequeos innecesarios#![deny(unsafe_code)]puedes hacer que el uso de códigounsafecause error de compilación y notificarlo al usuario. No es una verificación coercitiva total, porque si se permite explícitamente, se puede seguir usando códigounsafe. Uno podría imaginar un capability system que controle de forma transitiva funciones de la biblioteca estándar, como una especie de feature flagpanic, y haría falta el esfuerzo de escribir/distribuir un capability profile por biblioteca. Algo parecido ya se ha demostrado en TypeScripttls,x509,base64, etc.) implican sufrimiento a la hora de elegir y gestionar bibliotecasimporty hacer todo mediante inyección de dependencias. Si no inyectas algo como un subsistema de IO, el código de terceros jamás puede acceder a eso. Si quieres dar solo capacidad de lectura, puedes inyectar solo un wrapper de lectura. Aun así, en programación de sistemas hay límites (por ejemplo porunsafe code)dom0, cada biblioteca en una template VM separada, y usar namespaces de red para la comunicación. En industrias sensibles, eso sí sería prácticoblessed.rsrecomienda una lista de bibliotecas útiles que son difíciles de meter en la biblioteca estándar. Me gusta porque, gracias a ese sistema, la mayoría de los paquetes quedan limitados a propósitos específicos y se pueden gestionar mejorcargo-vet. Permite rastrear y definir paquetes confiables, desde aquellos que requieren una auditoría experta antes de importarse, hasta políticas semi-YOLO como "confiemos nomás en los paquetes mantenidos por los maintainers de tokio". Es un poco más formal queblessed.rsy sirve bien para compartir dentro del equipo una lista oficial o cuasi estándarleftpadquedó una percepción negativa de los package managers. Pero algo comotokioen la práctica es casi una función a nivel de lenguaje, así que si el OP cree que también habría que auditar por cuenta propia todo Go entero o hasta el V8 de Node, eso no es realistatokiode forma constante. No es mucha gente, pero alguien lo hacecargoincluya ambas versiones cuando dos dependencias usan versiones distintas es algo quecargosoporta de manera bastante particularcargoson realmente una gran ventaja. Yo mismo suelo enviar PRs para esconder dependencias innecesarias detrás de esas flags. Concargo treepuedes ver fácilmente el árbol de dependencias. Una vista de líneas de código que realmente terminan en el binario no tiene mucho sentido. Muchas funciones terminan inlineadas y casi todo se fusiona dentro demainasyncpuede implementarse directamente del modo que quieras. No quedas atado a una implementación específicaNo es un problema exclusivo de Rust.
Es una ventaja compartida y a la vez un problema potencial de todos los lenguajes que tienen repositorios públicos de paquetes y gestores de paquetes con soporte para dependencias transitivas.
Al final, quienes las usan tienen que usarlas bien…
A pesar del incidente de
leftpaden Node&npm, no ha cambiado nada.