Historique : Avant la stabilisation de PhantomData dans Rust 1.0, les développeurs peinaient à exprimer les relations de type pour des structures qui possédaient conceptuellement des données génériques mais ne stockaient que des pointeurs bruts, comme lorsqu'ils enveloppaient des handles de bibliothèques C. Le compilateur se basait uniquement sur des champs concrets pour inférer la variance et la propriété, ce qui conduisait soit à des erreurs de durée de vie trop restrictives, soit à des violations silencieuses de la sécurité mémoire lorsque le vérificateur de prêts supposait qu'un type n'était pas lié à son contenu. PhantomData a été introduit comme un marqueur de taille zéro pour communiquer explicitement la variance, la propriété et les implications de trait sans coût d'exécution.
Le Problème : Considérons un pointeur intelligent personnalisé struct RawBox<T> { ptr: *const T }. Bien que *const T soit covariant par rapport à T, le compilateur manque de confirmation explicite que RawBox possède logiquement la valeur T, surtout en ce qui concerne la Vérification de Drop (dropck). Sans PhantomData, le compilateur traite T comme un paramètre de type purement synthétique que la structure mentionne simplement mais ne possède pas, permettant potentiellement à T d'être abandonné tandis que la structure maintient encore un pointeur brut vers sa mémoire. Cette omission empêche également la structure d'implémenter correctement des auto-traits comme Send et Sync en fonction des propriétés de T.
La Solution : En ajoutant un champ PhantomData<T>, vous marquez explicitement RawBox comme covariant par rapport à T et indiquez une propriété logique. Cela garantit que le compilateur impose que T vive plus longtemps que la structure et applique les règles de variance correctes pour le sous-typage. Pour les cas nécessitant une variance différente, PhantomData accepte divers constructeurs de type : PhantomData<fn(T)> crée une contravariance, tandis que PhantomData<*mut T> ou PhantomData<Cell<T>> imposent l'invariance. Ce mécanisme permet une abstraction sûre sur les pointeurs bruts tout en maintenant les garanties de coût zéro de Rust.
Lors du développement d'une bibliothèque de traitement audio haute performance, j'avais besoin d'envelopper un handle d'API C *mut AudioContext qui était en réalité typé comme une structure Rust AudioBuffer<T> où T pouvait être f32 ou i16. Le wrapper AudioHandle<T> stockait uniquement le pointeur brut et un pointeur vers le vtable, mais je devais le faire se comporter comme Box<AudioBuffer<T>> en ce qui concerne les durées de vie et la sécurité des threads. Spécifiquement, le handle devait être Send lorsque T était Send, et covariant par rapport à T pour permettre une substitution transparente des types d'échantillons audio.
La première approche consistait à omettre tout marqueur et à s'appuyer uniquement sur le champ *mut c_void. Cette stratégie maintenait une taille de structure minimale et évitait toute surcharge, qui étaient ses principaux avantages. Cependant, le compilateur supposait que AudioHandle<T> était invariant par rapport à T et refusait d'implémenter Send même lorsque T était Send, car il ne pouvait pas vérifier la propriété, brisant finalement le contrat API qui nécessitait le déplacement de handle entre threads.
La deuxième approche considérait le stockage d'un Option<Box<T> uniquement pour guider le système de types. Cette méthode établissait correctement la variance et la dérivation Send/Sync, résolvant les problèmes d'implémentation de trait. Malheureusement, elle doublait la taille de la structure et introduisait une logique de suppression complexe qui risquait de paniquer si le champ fictif n'était pas correctement synchronisé avec le pointeur C, ce qui contredisait l'objectif d'abstraction à coût zéro.
La solution choisie a été d'ajouter marker: PhantomData<AudioBuffer<T>> à la structure. Ce marqueur de taille zéro a instantanément accordé une sémantique covariante par rapport à T, a permis aux auto-traits de dériver correctement en fonction de T, et a garanti que la Vérification de Drop vérifiait que AudioBuffer<T> n'était pas abandonné avant le handle. Par conséquent, le wrapper FFI a compilé sans erreurs, n'a imposé aucun surcoût d'exécution et a permis en toute sécurité le mouvement inter-thread des handles audio lorsque T était Send, satisfaisant parfaitement les exigences de la bibliothèque.
Pourquoi PhantomData<T> déclenche-t-il spécifiquement la règle de Vérification de Drop (dropck) qui empêche une valeur d'être abandonnée pendant que les données référencées sont encore actives, et quelle insécurité se produirait sans elle?
Sans PhantomData<T>, le compilateur suppose que la structure ne possède pas T, permettant au code utilisateur d'abandonner T tandis que l'implémentation Drop de la structure maintient toujours un pointeur brut vers la mémoire de T. Cela entraîne une utilisation après libération lorsque le destructeur s'exécute, car la mémoire peut avoir été réallouée ou empoisonnée. PhantomData signale à dropck que la structure contient conceptuellement T, forçant le compilateur à vérifier que T vit strictement plus longtemps que la structure et empêchant cette insécurité même si T n'occupe aucun octet dans la disposition.
Comment PhantomData peut-il être utilisé pour imposer la contravariance sur un paramètre de type, et dans quel type de conception d'API cela est-il essentiel?
La contravariance est réalisée en utilisant PhantomData<fn(T)>. Cela est essentiel pour des types de stockage de rappel comme struct Comparator<T> { compare: fn(T, T) -> Ordering, _marker: PhantomData<fn(T)> }. Puisque fn(T) est contravariant par rapport à T, la structure modélise correctement qu'un comparateur acceptant &'static str peut être utilisé partout où un comparateur &'short str est attendu, ce qui est la relation opposée à la covariance et critique pour le sous-typage des pointeurs de fonction.
Qu'est-ce qui distingue les implications de variance de PhantomData<Cell<T>> de PhantomData<T>, et pourquoi une structure enveloppant un primitif de mutabilité intérieure non sécurisée pourrait-elle nécessiter le premier?
PhantomData<T> implique la covariance, tandis que PhantomData<Cell<T>> implique l'invariance parce que Cell est invariant par rapport à son contenu. Lors de la construction d'un conteneur personnalisé basé sur UnsafeCell comme MyRefCell<T>, l'invariance est obligatoire pour éviter de forcer MyRefCell<&'long str> à MyRefCell<&'short str>. Une telle coercition permettrait de stocker une référence de courte durée là où une référence de longue durée était attendue, violant les règles d'aliasing et causant des pointeurs pendants lors des opérations d'écriture, ce que le marqueur invariant empêche.