La variance dans les systèmes de types détermine comment les relations de sous-typage entre les paramètres génériques affectent le type global. L'approche de Rust a été fortement influencée par des recherches sur la gestion de la mémoire basée sur des régions et le besoin de prévenir les vulnérabilités d'utilisation après libération. Lorsque Rust a introduit des références mutables (&mut T), les concepteurs devaient décider s'ils devaient être covariants (comme &T), contravariants ou invariants. Le choix de l'invariance pour &mut T par rapport à T était crucial pour maintenir la sécurité mémoire sans nécessiter de vérifications d'exécution.
Si &mut T était covariant par rapport à T, vous pourriez substituer &mut U là où &mut V est attendu si U est un sous-type de V. En termes de durée de vie, puisque 'long est un sous-type de 'short (parce que 'long vit plus longtemps que 'short), cela signifierait que vous pourriez assigner &mut &'long str à &mut &'short str. Cela semble inoffensif mais crée un trou de validité.
&mut T est invariant par rapport à T. Cela signifie que &mut &'a str et &mut &'b str sont des types non liés à moins que 'a soit exactement égal à 'b, indépendamment de la relation de sous-typage entre les durées de vie. Le compilateur rejette le code qui tente de se contraindre entre eux, empêchant l'assignation de données à courte durée de vie à des emplacements s'attendant à des références à plus longue durée de vie via une indirection mutable.
Exemple de Code :
fn demonstrate_invariance() { let mut long_lived: &'static str = "chaine statique"; // Cela compilerait si &mut T était covariant : // let short_ref: &mut &'short str = &mut long_lived; // Mais parce que &mut T est invariant, cela échoue : // erreur : conflit de durée de vie // let short_ref: &mut &'_ str = &mut long_lived; let local = String::from("temporaire"); // Si ce qui précède était autorisé, nous pourrions faire : // *short_ref = &local; // Maintenant long_lived pointe vers des données supprimées (UAF !) } // local supprimé ici
Une équipe construisait un gestionnaire de configuration pour une pile réseau haute performance. La structure principale devait contenir une référence mutable à une configuration de protocole qui pouvait être échangée à l'exécution sans prendre possession.
Le Problème : La conception initiale de l'API utilisait &mut &'a Config où 'a était la durée de vie de la session réseau. Les développeurs ont tenté d'initialiser cela avec &mut &'static Config (pour des configurations par défaut globales) puis de le passer à des fonctions s'attendant à &mut &'session Config. Le compilateur a rejeté cela, provoquant de la confusion car les références immuables (& &'static Config) fonctionnaient très bien.
Solutions Envisagées :
1. Transmute Non Sûr pour Forcer la Conversion L'équipe a envisagé d'utiliser std::mem::transmute pour convertir &mut &'static Config en &mut &'session Config. Cela contournerait les vérifications de variance du compilateur. Cependant, cela permettrait d'écrire une référence de configuration à courte durée de vie dans un emplacement qui pourrait survivre à la portée actuelle, entraînant un comportement indéfini immédiat si la configuration était accédée après avoir été supprimée. Le risque d'utilisation après libération dans le code de production rendait cela inacceptable.
2. Changer pour des Références Immutables Ils ont envisagé de changer l'API pour utiliser & &'a Config au lieu de &mut &'a Config. Étant donné que les références partagées sont covariantes, & &'static Config pourrait se contraindre à & &'session Config. Cependant, cela supprimait la capacité d'échanger des configurations de manière atomique pendant les mises à jour d'exécution, ce qui était un besoin fondamental pour le rechargement à chaud des paramètres sans redémarrer les connexions.
3. Utiliser Cell<&'a Config> pour la Mutabilité Interne Cette option permettrait la mutation via une référence partagée. Cependant, Cell<T> est également invariant par rapport à T pour les mêmes raisons de sécurité, donc cela n'a pas résolu le problème de variance. De plus, Cell ne fournit pas de synchronisation pour l'accès multi-thread, et le surcoût des vérifications d'emprunt d'exécution avec RefCell a été considéré comme trop coûteux pour le chemin chaud.
4. Redesign avec des Types Possédés et Indirection La solution choisie a totalement éliminé le modèle de référence à référence. Au lieu de stocker &mut &'a Config, la structure stockait &'a mut ConfigHolder, où ConfigHolder était un wrapper possesseur. Cela a déplacé la mutabilité au niveau du holder plutôt qu'au niveau de la référence, évitant le piège de variance tout en maintenant la capacité d'échanger des configurations. L'API est devenue plus ergonomique car les utilisateurs n'avaient plus à gérer des double références.
Le Résultat : La redéfinition a produit une API plus sûre qui a été compilée sans code non sûr. La nature invariant de &mut T a contraint l'équipe à reconnaître un défaut architectural potentiel où les hypothèses de durée de vie pouvaient être violées. Le système final a empêché une catégorie de bugs où des pointeurs de configuration obsolètes pouvaient persister au-delà de leur période de validité.
Pourquoi Cell<T> est-il invariant par rapport à T, et comment cela est-il lié à la variance de &mut T ?
Cell<T> offre une mutabilité interne, permettant la mutation via des références partagées. Si Cell<T> était covariant par rapport à T, vous pourriez lever un Cell<&'short str> en Cell<&'static str>. Ensuite, vous pourriez stocker une référence de chaîne à courte durée de vie à l'intérieur et plus tard la lire via le type Cell<&'static str>, traitant les données temporaires comme statiques. Cela constituerait une vulnérabilité d'utilisation après libération. Par conséquent, tout comme &mut T, Cell<T> (et UnsafeCell<T>) doit être invariant par rapport à T pour éviter d'écrire des données à courte durée de vie dans un emplacement qui prétend contenir des données à plus longue durée de vie. Cette invariance se propage à RefCell, Mutex, et d'autres types de mutabilité interne.
Comment PhantomData<T> affecte-t-il la variance d'une structure qui ne contient pas de T réel, et pourquoi utiliseriez-vous PhantomData<fn(T)> pour atteindre la contravariance ?
PhantomData<T> indique au compilateur de traiter la structure comme si elle possédait un T pour les besoins de la variance et de la vérification de suppression. Par défaut, PhantomData<T> donne à la structure la même variance que T. Cependant, les pointeurs de fonction ont une variance spéciale : fn(A) -> B est contravariant en A (l'argument) et covariant en B (le retour). Si vous avez besoin qu'une structure soit contravariante par rapport à une durée de vie (ce qui signifie que Struct<'long> est un sous-type de Struct<'short> lorsque 'long vit plus longtemps que 'short), vous utilisez PhantomData<fn(T)>. Cela est crucial pour construire des rappels ou des comparateurs sûrs types où la relation entre les durées de vie doit être inversée.
Dans le code non sûr, lors de l'implémentation d'une structure autoreférentielle à l'aide de pointeurs bruts, pourquoi la structure doit-elle être marquée comme invariante par rapport à ses paramètres de durée de vie ?
Lorsqu'une structure contient un pointeur brut qui pointe vers d'autres données dans la même structure (autoreférentielle), la durée de vie de cette structure détermine la validité du pointeur. Si la structure était covariante par rapport à sa durée de vie 'a, vous pourriez réduire 'a à une durée de vie plus courte 'b, affirmant effectivement que la structure vit uniquement pour 'b. Cependant, le pointeur brut à l'intérieur a été créé lorsque la structure vivait plus longtemps, et pourrait pointer vers des données qui ne sont plus valides dans la portée plus courte. L'invariance garantit que la structure ne peut pas être contrainte à une durée de vie plus courte, préservant l'invariant de sécurité que la référence autocontrante reste valide pour toute la durée de vie encodée dans le système de types. C'est pourquoi Pin est souvent associé à des marqueurs de variance explicites dans les implémentations autoreférentielles non sûres.