RustProgrammationDéveloppeur Rust

Élucidez la rationale architecturale derrière l'interdiction de **Rust** d'implémenter des types à la fois **Copy** et **Drop**, et identifiez la violation spécifique de la sécurité mémoire que cette restriction empêche.

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse à la question

Histoire de la question

Le trait Copy est né dans les premiers designs de Rust comme un marqueur pour les types pouvant être dupliqués par un simple copie binaire sans soucis de gestion des ressources. Drop a été introduit pour gérer le nettoyage déterministe des ressources pour des types gérant des ressources externes comme des descripteurs de fichiers ou de la mémoire sur le tas. Le conflit entre la duplication implicite et la propriété unique est devenu évident lorsque les concepteurs ont réalisé que les copies binaires partageraient des handles de ressources non partageables. En conséquence, le compilateur a été conçu pour rejeter tout type qui tente d'implémenter les deux traits simultanément.

Le problème

Si un type implémentant Drop (par exemple, gérant un descripteur de fichier) était également Copy, assigner la valeur à une nouvelle variable créerait deux copies binaires identiques. Lorsque les deux copies sortent de leur portée, l'implémentation personnalisée de Drop s'exécute deux fois sur la même ressource sous-jacente. Cela conduit à une vulnérabilité de double libération ou utilisation après libération si la ressource est invalidée par le premier drop mais accessible par le second, compromettant ainsi la sécurité mémoire.

La solution

Le compilateur Rust inclut un contrôle de cohérence dans le système de traits qui interdit explicitement à un type d'implémenter à la fois Copy et Drop. Cette contrainte oblige les développeurs à utiliser Clone (duplication explicite) pour les types nécessitant une destruction personnalisée, permettant ainsi d'incrémenter correctement les comptes de référence ou d'effectuer des copies profondes. En s'assurant que chaque entité logique a un drop unique correspondant, le système de types maintient des abstractions à coût nul sans sacrifier les garanties de sécurité.

Situation de la vie réelle

Considérons une structure DatabaseHandle englobant un pointeur brut vers un objet de connexion dans une bibliothèque C externe. L'application nécessite de passer des handles par valeur dans plusieurs fermetures pour le logging, pourtant chaque handle doit fermer sa connexion unique via un appel FFI lors du drop. Si le handle était Copy, la duplication implicite créerait plusieurs handles revendiquant la propriété de la même ressource C sous-jacente, causant inévitablement des double-closes ou des utilisations après libération lorsque la portée se termine.

Une approche consistait à autoriser Copy et à implémenter Drop avec un comptage de référence interne en utilisant Arc. Cela ajouterait un coût de synchronisation pour chaque handle, augmentant la taille binaire et le coût d'exécution pour toutes les opérations. Cela complique également la frontière FFI où le pointeur brut doit être extrait de manière atomique de l'Arc, introduisant des blocages potentiels si la logique de drop rappelle également du code Rust.

Une autre approche impliquait d'utiliser Copy mais de documenter que les utilisateurs doivent appeler une méthode close manuellement avant que la valeur ne soit libérée. Cela place la responsabilité de la sécurité mémoire entièrement sur le programmeur, violant le principe fondamental de Rust qui est de prévenir les erreurs à la compilation. Cela conduit inévitablement à des fuites de ressources lorsque les développeurs oublient d'appeler close, ou à des double-closes lorsque ceux-ci copient le handle par inadvertance et tentent de fermer les deux copies.

La solution choisie était de supprimer Copy et d'implémenter Clone manuellement, ainsi que Drop. Clone effectue une copie profonde en ouvrant une nouvelle connexion de base de données, garantissant que chaque instance possède sa propre ressource distincte et empêchant l'aliasing du pointeur C sous-jacent. Drop ferme uniquement sa propre connexion, tandis que le compilateur empêche les copies binaires accidentelles, maintenant la sécurité sans frais d'exécution.

Le système de types empêche maintenant les copies accidentelles à la compilation, obligeant les développeurs à appeler explicitement clone et rendant l'acquisition des ressources visible dans le code source. Le programme évite les erreurs de double libération lorsque les handles sont passés dans des threads ou des fermetures, et les garanties de destruction déterministes restent intactes sans nécessiter d'opérations atomiques ou de gestion de mémoire manuelle.

Ce que les candidats manquent souvent

Pourquoi ne puis-je pas dériver Copy pour une structure contenant un Vec ?

Un Vec possède une mémoire allouée sur le tas et implémente Drop pour libérer cette mémoire lorsque le vecteur sort de portée. Si une structure contenant un Vec était Copy, la duplication binaire créerait deux structures pointant vers le même tampon de tas sur la pile, mais toutes deux contiendraient le même pointeur vers le tas. Lorsque la première structure est libérée, la mémoire est libérée ; lorsque la seconde est libérée, elle tente de libérer la même mémoire à nouveau, causant un comportement indéfini. Rust empêche cela en exigeant que tous les champs d'un type Copy soient également Copy, garantissant récursivement qu'aucune implémentation Drop imbriquée n'existe.

Est-ce que mem::forget prévient les problèmes liés à Copy et Drop ?

std::mem::forget consomme une valeur sans exécuter son destructeur, mais cela n'affecte qu'une valeur possédée spécifique, pas toutes ses copies. Si Copy et Drop étaient autorisés, oublier une copie ne ferait pas en sorte que d'autres copies binaires n'exécutent leurs implémentations Drop lorsqu'elles sortent de la portée. Ces drops restants tenteraient toujours de libérer la même ressource sous-jacente, conduisant à une utilisation après libération ou une double libération, peu importe l'instance oubliée.

Puis-je utiliser ManuallyDrop pour implémenter Copy en toute sécurité ?

Enveloppant un champ dans ManuallyDrop empêche l'invocation automatique de Drop, ce qui permet techniquement à la structure extérieure de dériver Copy. Cependant, cela déplace la responsabilité d'appeler ManuallyDrop::drop à l'utilisateur pour chaque copie créée, créant effectivement un scénario de gestion manuelle de la mémoire. Si l'utilisateur oublie de libérer ne serait-ce qu'une seule copie, la ressource fuit de façon permanente ; Rust interdit ce modèle pour les types possédant des ressources parce qu'il compromet la garantie de sécurité d'un nettoyage déterministe et automatique.