Rust, un lenguaje de programación de sistemas conocido por su seguridad de memoria sin garbage collector, ha operado bajo la premisa fundamental de que todos los valores pueden ser movidos (reubicados en memoria) y olvidados (ignorando la ejecución de su destructor vía mem::forget). Estas asunciones, si bien simplifican el modelo de propiedad y préstamo, introducen complejidades significativas y bloquean patrones de diseño cruciales en sistemas distribuidos y de alto rendimiento, especialmente en el contexto de programación asíncrona y el desarrollo de kernels.
El problema fundamental que esta propuesta aborda es la tensión entre la ubicuidad de la movilidad y la necesidad de inmovilidad garantizada para tipos auto-referenciales, así como la tensión entre la seguridad de mem::forget y la necesidad de ejecución garantizada de destructores. La solución actual para la inmovilidad, Pin, es una propiedad del 'lugar' en memoria donde reside un valor, no del 'tipo' en sí, lo que lleva a una ergonomía compleja y a la duplicación de traits. Para la garantía de destructores, la seguridad de mem::forget impide patrones como el 'scoped spawn' seguro, donde una tarea asíncrona toma prestado de un ámbito padre y debe ser unida antes de que el ámbito termine.
Arquitectura del Sistema
La propuesta introduce dos nuevos 'auto-traits' en el sistema de tipos de Rust: Move y Forget. Estos traits son 'opt-out', lo que significa que, por defecto, todos los tipos implementan Move y Forget, manteniendo la compatibilidad hacia atrás. Los tipos pueden optar por no implementar Move (!Move) o Forget (!Forget), declarando explícitamente sus capacidades.
El trait Move (#[lang = "move"] unsafe auto trait Move {}) codifica la movilidad como una propiedad del tipo. Un tipo que implementa !Move no puede ser reubicado en memoria y debe mantener una dirección estable durante toda su vida útil. Esto simplifica la gestión de tipos auto-referenciales, eliminando gran parte de la complejidad asociada con Pin. La construcción de tipos !Move dependerá de la capacidad de inicialización 'in-place'.
El trait Forget permite que los tipos opten por no ser 'olvidables'. Si un tipo implementa !Forget (ej. unsafe impl !Forget for ScopedTaskHandle {}), se garantiza que su destructor se ejecutará. Esto es crucial para patrones donde la limpieza o la finalización de recursos son obligatorias, como un ScopedTaskHandle cuyo destructor debe unirse a una tarea asíncrona para evitar referencias colgantes. Esta garantía habilita el 'safe scoped spawn' para async Rust, un patrón actualmente inviable de forma segura.
Flujo de Vida de un Tipo !Move
- 1 Definición Tipo Desarrollador define un tipo que implementa `!Move` (ej. para auto-referencia...
- 2 Inicialización In-Place El tipo se construye directamente en su ubicación final en memoria.
- 3 Uso del Tipo El tipo es utilizado, garantizando que su dirección de memoria es estable.
- 4 Fin de Ámbito El tipo sale de ámbito, su destructor se ejecuta (si no es `!Forget`).
Flujo de 'Safe Scoped Spawn' con !Forget
- 1 Spawn de Tarea Se crea una tarea asíncrona que toma prestado de un ámbito padre.
- 2 Retorno de Handle La función de spawn retorna un `ScopedTaskHandle` que implementa `!Forget`.
- 3 Uso de Handle El `ScopedTaskHandle` se mantiene en el ámbito padre.
- 4 Fin de Ámbito Padre El ámbito padre finaliza, el destructor del `ScopedTaskHandle` se invoca.
- 5 Join Garantizado El destructor del handle ejecuta un `join()` garantizado, evitando referencia...
| Capa | Tecnología | Justificación |
|---|---|---|
| compute | Rust Type System | La introducción de `Move` y `Forget` modifica el sistema de tipos para expresar nuevas propiedades de los valores, permitiendo un control más granular sobre su ciclo de vida y reubicación en memoria. Esto afecta directamente cómo se compila y ejecuta el código, especialmente en escenarios de concurrencia y estructuras de datos complejas. vs Extender `Pin` con más ergonomía (ej. `&pin x`, `fn drop(&pin mut self)`) |
Trade-offs
Ganancias
- ▲ Simplificación de tipos auto-referenciales
- ▲ Habilitación de 'safe scoped spawn' y otros patrones de concurrencia
- ▲ Mejora de la ergonomía para la inmovilidad
- ▲ Garantía de ejecución de destructores para tipos críticos
Costes
- △ Aumento de la complejidad del sistema de tipos de Rust
- △ Curva de aprendizaje inicial para nuevos traits
#[lang = "move"]
unsafe auto trait Move {}// Types implementing !Forget must have their destructors run
unsafe impl !Forget for ScopedTaskHandle {}Fundamentos Teóricos
Esta propuesta se conecta con principios fundamentales de la computación relacionados con la gestión de memoria y la seguridad de tipos. La idea de controlar la movilidad de objetos en memoria tiene paralelos con sistemas de tiempo de ejecución que gestionan la reubicación de objetos, como los garbage collectors compactadores, aunque en Rust la gestión es explícita y en tiempo de compilación. La necesidad de garantizar la ejecución de destructores se relaciona directamente con el patrón RAII (Resource Acquisition Is Initialization), un concepto central en C++ y Rust, donde la vida útil de un objeto está ligada a la gestión de un recurso. La capacidad de 'opt-out' de Forget eleva el RAII de una convención a una garantía de tipo.
El trabajo sobre la jerarquía Sized en Rust, que relajó la asunción de que todos los tipos tienen un tamaño conocido en tiempo de compilación, sirve como precedente directo para esta iniciativa. Ambos esfuerzos demuestran cómo la evolución del sistema de tipos de un lenguaje puede relajar asunciones universales para habilitar nuevos patrones de diseño y mejorar la ergonomía en casos de uso avanzados, sin sacrificar la seguridad. Conceptos como la 'inmovilidad' y la 'auto-referencialidad' han sido objeto de estudio en la teoría de tipos y lenguajes de programación, buscando equilibrar la expresividad con la seguridad de memoria.