La variación en los sistemas de tipos determina cómo las relaciones de subtipado entre los parámetros genéricos afectan al tipo global. El enfoque de Rust fue fuertemente influenciado por la investigación sobre la gestión de memoria basada en regiones y la necesidad de prevenir vulnerabilidades de uso después de liberar. Cuando Rust introdujo referencias mutables (&mut T), los diseñadores tuvieron que decidir si debían ser covariantes (como &T), contravariantes o invariantes. La elección de la invariancia para &mut T sobre T fue crítica para mantener la seguridad de memoria sin requerir comprobaciones en tiempo de ejecución.
Si &mut T fuera covariante sobre T, podrías sustituir &mut U donde se espera &mut V si U es un subtipo de V. En términos de tiempo de vida, dado que 'long es un subtipo de 'short (porque 'long vive más que 'short), esto significaría que podrías asignar &mut &'long str a &mut &'short str. Esto parece inofensivo pero crea un agujero de sonoridad.
&mut T es invariante sobre T. Esto significa que &mut &'a str y &mut &'b str son tipos no relacionados a menos que 'a sea exactamente igual a 'b, sin importar la relación de subtipado entre las duraciones de vida. El compilador rechaza el código que intenta forzar la conversión entre ellos, impidiendo la asignación de datos de corta duración a posiciones que esperan referencias de más larga duración a través de una indirecta mutable.
Ejemplo de Código:
fn demonstrate_invariance() { let mut long_lived: &'static str = "cadena estática"; // Esto compilaría si &mut T fuera covariante: // let short_ref: &mut &'short str = &mut long_lived; // Pero como &mut T es invariante, esto falla: // error: desajuste de duración // let short_ref: &mut &'_ str = &mut long_lived; let local = String::from("temporal"); // Si lo anterior se permitiera, podríamos hacer: // *short_ref = &local; // Ahora long_lived apunta a datos eliminados (¡UAF!) } // local eliminado aquí
Un equipo estaba construyendo un gestor de configuración para una pila de redes de alto rendimiento. La estructura principal necesitaba contener una referencia mutable a una configuración de protocolo que pudiera ser intercambiada en tiempo de ejecución sin tomar posesión.
El Problema: El diseño inicial de la API utilizó &mut &'a Config donde 'a era la duración de la sesión de red. Los desarrolladores intentaron inicializar esto con &mut &'static Config (para configuraciones globales por defecto) y luego pasarlo a funciones que esperaban &mut &'session Config. El compilador rechazó esto, causando confusión porque las referencias inmutables (& &'static Config) funcionaban bien.
Soluciones Consideradas:
1. Transmutación insegura para forzar la conversión El equipo consideró usar std::mem::transmute para convertir &mut &'static Config a &mut &'session Config. Esto eludiría las comprobaciones de variación del compilador. Sin embargo, esto permitiría escribir una referencia de configuración de corta duración en un lugar que podría vivir más allá del alcance actual, lo que llevaría a un comportamiento indefinido inmediato si la configuración se accediera después de ser eliminada. El riesgo de uso después de liberar en el código de producción hizo que esto fuera inaceptable.
2. Cambiar a Referencias Inmutables Consideraron cambiar la API para usar & &'a Config en lugar de &mut &'a Config. Dado que las referencias compartidas son covariantes, & &'static Config podría forzarse a & &'session Config. Sin embargo, esto eliminó la capacidad de intercambiar configuraciones atómicamente durante las actualizaciones en tiempo de ejecución, que era un requisito fundamental para la recarga en caliente de configuraciones sin reiniciar las conexiones.
3. Usar Cell<&'a Config> para Mutabilidad Interna Esta opción permitiría la mutación a través de una referencia compartida. Sin embargo, Cell<T> también es invariante sobre T por las mismas razones de seguridad, por lo que no resolvió el problema de variación. Además, Cell no proporciona sincronización para el acceso multihilo, y el costo de la comprobación de préstamos en tiempo de ejecución con RefCell se consideró demasiado caro para la ruta crítica.
4. Rediseñar con Tipos Propios e Indirección La solución elegida eliminó por completo el patrón de referencia a referencia. En lugar de almacenar &mut &'a Config, la estructura almacenaba &'a mut ConfigHolder, donde ConfigHolder era un envoltorio propietario. Esto trasladó la mutabilidad al nivel del contenedor en lugar del nivel de referencia, evitando la trampa de variabilidad mientras se mantenía la capacidad de intercambiar configuraciones. La API se volvió más ergonómica porque los usuarios ya no tenían que gestionar dobles referencias.
El Resultado: El rediseño produjo una API más segura que compiló sin código inseguro. La naturaleza invariante de &mut T obligó al equipo a reconocer un posible defecto arquitectónico donde las suposiciones sobre las duraciones de vida podían ser violadas. El sistema final previno una categoría de errores donde los punteros de configuración obsoletos podían persistir más allá de su periodo de validez.
¿Por qué es Cell<T> invariante sobre T, y cómo se relaciona esto con la variación de &mut T?
Cell<T> proporciona mutabilidad interna, permitiendo la mutación a través de referencias compartidas. Si Cell<T> fuera covariante sobre T, podrías subir de clase Cell<&'short str> a Cell<&'static str>. Luego, podrías almacenar una referencia de cadena de corta duración dentro y más tarde leerla a través del tipo Cell<&'static str>, tratando los datos temporales como estáticos. Esto sería una vulnerabilidad de uso después de liberar. Por lo tanto, al igual que &mut T, Cell<T> (y UnsafeCell<T>) deben ser invariantes sobre T para prevenir la escritura de datos de corta duración en un espacio que afirma contener datos de más larga duración. Esta invariancia se propaga a RefCell, Mutex, y otros tipos de mutabilidad interna.
¿Cómo afecta PhantomData<T> la variación de una estructura que no contiene ningún T real, y por qué usarías PhantomData<fn(T)> para lograr contravariación?
PhantomData<T> le dice al compilador que trate la estructura como si poseyera un T para propósitos de variación y comprobación de eliminación. Por defecto, PhantomData<T> le da a la estructura la misma variación que T. Sin embargo, los punteros de función tienen una variación especial: fn(A) -> B es contravariante en A (el argumento) y covariante en B (el retorno). Si necesitas que una estructura sea contravariante sobre una duración de vida (lo que significa que Struct<'long> es un subtipo de Struct<'short> cuando 'long vive más que 'short), usas PhantomData<fn(T)>. Esto es crucial para construir callbacks o comparadores seguros en tipos donde la relación entre las duraciones de vida debe ser revertida.
En código inseguro, al implementar una estructura autorreferencial utilizando punteros en bruto, ¿por qué debe marcarse la estructura como invariante sobre sus parámetros de duración?
Cuando una estructura contiene un puntero en bruto que apunta a otros datos dentro de la misma estructura (autorreferencial), la duración de esa estructura determina la validez del puntero. Si la estructura fuera covariante sobre su duración 'a, podrías reducir 'a a una duración más corta 'b, reclamando efectivamente que la estructura vive solo durante 'b. Sin embargo, el puntero en bruto dentro fue creado cuando la estructura vivía más tiempo, y podría apuntar a datos que ya no son válidos en el alcance más corto. La invariancia asegura que la estructura no pueda ser forzada a una duración más corta, preservando la invariante de seguridad de que la autorreferencia siga siendo válida durante toda la duración codificada en el sistema de tipos. Por eso, Pin a menudo se combina con marcadores de variabilidad explícitos en implementaciones inseguras autorreferenciales.