Rust: Unions non balisées (problème de suivi pour RFC 1444)

Créé le 8 avr. 2016  ·  210Commentaires  ·  Source: rust-lang/rust

Problème de suivi pour rust-lang / rfcs # 1444.

Questions non résolues:

  • [x] L'affectation directe à un champ d'union déclenche-t-elle une suppression du contenu précédent?
  • [x] Lors du déménagement d'un domaine d'un syndicat, les autres sont-ils considérés comme invalides? ( 1 , 2 , 3 , 4 )
  • [] Dans quelles conditions pouvez-vous implémenter Copy pour un syndicat? Par exemple, que se passe-t-il si certaines variantes sont de type non-Copy? Toutes les variantes?
  • [] Quelle interaction y a-t-il entre les unions et les optimisations de mise en page enum? (https://github.com/rust-lang/rust/issues/36394)

Problèmes ouverts de forte importation:

B-RFC-approved B-unstable C-tracking-issue F-untagged_unions T-lang disposition-merge finished-final-comment-period

Commentaire le plus utile

@nrc
Eh bien, le sous-ensemble est plutôt évident - «unions FFI», ou «unions C», ou «unions pré-C ++ 11» - même s'il n'est pas syntaxique. Mon objectif initial était de stabiliser ce sous-ensemble dès que possible (ce cycle, idéalement) afin qu'il puisse être utilisé dans des bibliothèques comme winapi .
Il n'y a rien de particulièrement douteux à propos du sous-ensemble restant et de son implémentation, ce n'est tout simplement pas urgent et il faut attendre un laps de temps incertain jusqu'à ce que le processus de RFC "Unions 1.2" se termine. Mes attentes seraient de stabiliser les pièces restantes en 1, 2 ou 3 cycles après la stabilisation du sous-ensemble initial.

Tous les 210 commentaires

J'ai peut-être manqué cela dans la discussion sur la RFC, mais ai-je raison de penser que les destructeurs de variantes d'union ne sont jamais exécutés? Le destructeur du Box::new(1) s'exécuterait-il dans cet exemple?

union Foo {
    f: i32,
    g: Box<i32>,
}

let mut f = Foo { g: Box::new(1) };
f.g = Box::new(2);

@sfackler Ma compréhension actuelle est que f.g = Box::new(2) _ lancera le destructeur mais f = Foo { g: Box::new(2) } ne le ferait pas. Autrement dit, l'assignation à une Box<i32> lvalue provoquera une baisse comme toujours, mais l'affectation à une Foo lvalue ne le fera pas.

Donc, une affectation à une variante est comme une affirmation que le champ était auparavant "valide"?

@sfackler Pour les Drop , oui, c'est ce que je comprends. S'ils n'étaient pas valides auparavant, vous devez utiliser le formulaire constructeur Foo ou ptr::write . D'un grep rapide, il ne semble pas que la RFC soit explicite sur ce détail, cependant. Je le vois comme une instanciation de la règle générale selon laquelle l'écriture dans une Drop lvalue provoque un appel de destructeur.

Une union & mut avec des variantes Drop doit-elle être une charpie?

Le vendredi 8 avril 2016, Scott Olson [email protected] a écrit:

@sfackler https://github.com/sfackler Pour les types Drop, ouais, c'est mon
compréhension. S'ils n'étaient pas valides auparavant, vous devez utiliser le Foo
forme constructeur ou ptr :: write. D'un grep rapide, il ne semble pas
la RFC est cependant explicite sur ce détail.

-
Vous recevez ceci parce que vous êtes abonné à ce fil.
Répondez directement à cet e-mail ou affichez-le sur GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment -207634431

Le 8 avril 2016 15:36:22 PDT, Scott Olson [email protected] a écrit:

@sfackler Pour les Drop , oui, c'est ce que je comprends. Si ils
n'étaient pas valides auparavant, vous devez utiliser le formulaire constructeur Foo ou
ptr::write . D'un grep rapide, il ne semble pas que la RFC soit
explicite sur ce détail, cependant.

J'aurais dû couvrir ce cas explicitement. Je pense que les deux comportements sont défendables, mais je pense qu'il serait beaucoup moins surprenant de ne jamais abandonner implicitement un champ. La RFC recommande déjà un lint pour les champs d'union avec des types qui implémentent Drop. Je ne pense pas que l'attribution à un champ implique que ce champ était auparavant valide.

Ouais, cette approche me semble aussi un peu moins dangereuse.

Ne pas abandonner lors de l'affectation à un champ d'union ferait que f.g = Box::new(2) agirait différemment de let p = &mut f.g; *p = Box::new(2) , car vous ne pouvez pas faire tomber le dernier cas. Je pense que mon approche est moins surprenante.

Ce n'est pas non plus un problème nouveau; unsafe programmeurs doivent déjà faire face à d'autres situations où foo = bar est UB si foo n'est pas initialisé et Drop .

Personnellement, je ne prévois pas du tout d'utiliser les types Drop avec les syndicats. Je m'en remettrai donc entièrement aux personnes qui ont travaillé avec du code dangereux analogue sur la sémantique de le faire.

Je n'ai pas non plus l'intention d'utiliser les types Drop dans les syndicats, donc dans les deux cas, cela n'a pas d'importance pour moi tant qu'il est cohérent.

Je n'ai pas l'intention d'utiliser des références mutables aux syndicats, et probablement
juste ceux "bizarrement étiquetés" avec Into

Le vendredi 8 avril 2016, Peter Atashian [email protected] a écrit:

Je n'ai pas non plus l'intention d'utiliser les types Drop dans les syndicats, donc de toute façon pas
importe pour moi tant qu’elle est cohérente.

-
Vous recevez ceci parce que vous êtes abonné à ce fil.
Répondez directement à cet e-mail ou affichez-le sur GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment -207653168

Il semble que ce soit une bonne question à soulever comme une question non résolue. Je ne sais pas encore quelle approche je préfère.

@nikomatsakis Autant que je trouve gênant d'affecter à un champ union d'un type avec Drop d'exiger une validité préalable de ce champ, le cas de référence @tsion mentionné semble presque inévitable. Je pense que cela pourrait simplement être un piège associé au code qui désactive intentionnellement la charpie pour mettre un type avec Drop dans une union. (Et une brève explication de cela devrait être dans le texte explicatif de cette charpie.)

Et je voudrais réitérer que les programmeurs unsafe doivent déjà généralement savoir que a = b signifie drop_in_place(&mut a); ptr::write(&mut a, b) pour écrire du code sûr. Ne pas supprimer les champs d'union serait une exception de plus à apprendre, pas une de moins.

(NB: la baisse ne se produit pas lorsque a est _statiquement_ connu pour être déjà non initialisé, comme let a; a = b; .)

Mais je suis d'accord pour avoir un avertissement par défaut contre les variantes Drop dans les syndicats que les gens doivent #[allow(..)] car c'est un détail assez peu évident.

@tsion ce n'est pas vrai pour a = b et peut-être seulement parfois vrai pour a.x = b mais c'est certainement vrai pour *a = b . Cette incertitude est ce qui m'a fait hésiter à ce sujet. Par exemple, ceci compile:

fn main() {
  let mut x: (i32, i32);
  x.0 = 2;
  x.1 = 3;
}

(bien qu'essayer d'imprimer x plus tard échoue, mais je considère que c'est un bogue)

@nikomatsakis Cet exemple est nouveau pour moi. Je suppose que j'aurais considéré comme un bogue que cet exemple compile, compte tenu de mon expérience précédente.

Mais je ne suis pas sûr de voir la pertinence de cet exemple. Pourquoi ce que j'ai dit n'est-il pas vrai pour a = b et parfois seulement pour a.x = b ?

Disons que si x.0 avait un type avec un destructeur, ce destructeur est sûrement appelé:

fn main() {
    let mut x: (Box<i32>, i32);
    x.0 = Box::new(2); // x.0 statically know to be uninit, destructor not called
    x.0 = Box::new(3); // x.0 destructor is called before writing new value
}

Peut-être juste des peluches contre ce genre d'écriture?

Mon point est seulement que = n'exécute pas toujours le destructeur; il
utilise certaines connaissances pour savoir si la cible est connue
initialisé.

Le mar 12 avril 2016 à 16:10:39 -0700, Scott Olson a écrit:

@nikomatsakis Cet exemple est nouveau pour moi. Je suppose que j'aurais considéré comme un bogue que cet exemple compile, compte tenu de mon expérience précédente.

Mais je ne suis pas sûr de voir la pertinence de cet exemple. Pourquoi ce que j'ai dit n'est-il pas vrai pour a = b et parfois seulement pour 'ax = b'?

Disons que si x.0 avait un type avec un destructeur, ce destructeur est sûrement appelé:

fn main() {
    let mut x: (Box<i32>, i32);
    x.0 = Box::new(2); // x.0 statically know to be uninit, destructor not called
    x.0 = Box::new(3); // x.0 destructor is called
}

@nikomatsakis

Il exécute le destructeur si l'indicateur de suppression est défini.

Mais je pense que ce genre d'écriture est de toute façon déroutant, alors pourquoi ne pas l'interdire? Vous pouvez toujours faire *(&mut u.var) = val .

Mon point est seulement que = n'exécute pas toujours le destructeur; il utilise certaines connaissances pour savoir si la cible est connue pour être initialisée.

@nikomatsakis J'ai déjà mentionné que:

(NB: la suppression ne se produit pas quand a est statiquement connu pour être déjà non initialisé, comme let a; a = b ;.)

Mais je n'ai pas tenu compte de la vérification dynamique des drapeaux de dépôt, donc c'est certainement plus compliqué que je ne le pensais.

@tsion

Les indicateurs de suppression ne sont que semi-dynamiques - une fois la suppression de zéro terminée, ils font partie de codegen. Je dis que nous interdisons ce genre d'écriture parce que cela fait plus de confusion que de bien.

Les types Drop devraient-ils même être autorisés dans les syndicats? Si je comprends bien les choses, la principale raison d'avoir des unions dans Rust est de s'interfacer avec du code C qui a des unions, et C n'a même pas de destructeurs. Pour toutes les autres fins, il semble qu'il soit préférable d'utiliser simplement un enum dans le code Rust.

Il existe un cas d'utilisation valide pour l'utilisation d'une union pour implémenter un type NoDrop qui inhibe la chute.

En plus d'appeler manuellement ce code via drop_in_place ou similaire.

Pour moi, laisser tomber une valeur de champ lors de l'écriture est définitivement faux car le type d'option précédent n'est pas défini.

Serait-il possible d'interdire les installateurs sur le terrain mais d'exiger le remplacement complet du syndicat? Dans ce cas, si l'union implémente Drop, la suppression d'union complète sera appelée pour la valeur remplacée comme prévu.

Je ne pense pas qu'il soit logique d'interdire les poseurs sur le terrain; la plupart des utilisations des unions ne devraient avoir aucun problème à les utiliser, et les champs sans implémentation Drop resteront probablement le cas courant. Les syndicats avec des champs qui implémentent Drop produiront un avertissement par défaut, ce qui le rendra encore moins susceptible de toucher ce cas accidentellement.

Pour des raisons de discussion, j'ai l'intention d'exposer des références mutables à des champs dans des unions _et_ y mettre des types arbitraires (éventuellement Drop ). Fondamentalement, je voudrais utiliser des unions pour écrire des énumérations personnalisées à faible encombrement. Par exemple,

union SlotInner<V> {
    next_empty: usize, /* index of next empty slot */
    value: V,
}

struct Slot<V> {
    inner: SlotInner<V>,
    version: u64 /* even version -> is_empty */
}

@nikomatsakis Je voudrais proposer une réponse concrète à la question actuellement répertoriée comme non résolue ici.

Pour éviter une sémantique inutilement complexe, l'affectation à un champ d'union doit agir comme une affectation à un champ struct, ce qui signifie supprimer l'ancien contenu. Il est assez facile d'éviter cela si vous le savez, en attribuant plutôt à l'ensemble du syndicat. C'est encore un comportement un peu surprenant, mais avoir un champ union qui implémente Drop produira un avertissement, et le texte de cet avertissement peut le mentionner explicitement comme une mise en garde.

Serait-il judicieux de fournir une demande d'extraction RFC modifiant la RFC1444 pour documenter ce comportement?

@joshtriplett Puisque @nikomatsakis est en vacances, je vais répondre: je pense que c'est une bonne forme de déposer un amendement RFC pour résoudre des questions comme celle-ci. Nous accélérions souvent ces RFC PR le cas échéant.

@aturon Merci. J'ai déposé la nouvelle RFC PR https://github.com/rust-lang/rfcs/issues/1663 avec ces clarifications à RFC1444, pour résoudre ce problème.

( @aturon, vous pouvez maintenant cocher cette question non résolue.)

J'ai une implémentation préliminaire dans https://github.com/petrochenkov/rust/tree/union.

Statut: mis en œuvre (bogues modulo), PR soumis (https://github.com/rust-lang/rust/pull/36016).

@petrochenkov Génial! Ça a l'air génial jusqu'à présent.

Je ne sais pas trop comment traiter les syndicats avec des champs non- Copy dans le vérificateur de mouvements.
Supposons que u est une valeur initialisée de union U { a: A, b: B } et maintenant nous sortons de l'un des champs:

1) A: !Copy, B: !Copy, move_out_of(u.a)
C'est simple, u.b est également mis à l'état non initialisé.
Vérification de l'intégrité: union U { a: T, b: T } doit se comporter exactement comme struct S { a: T } + alias de champ.

2) A: Copy, B: !Copy, move_out_of(u.a)
On suppose que u.b devrait toujours être initialisé, car move_out_of(u.a) est simplement un memcpy et ne change en rien u.b .

2) A: !Copy, B: Copy, move_out_of(u.a)
C'est le cas le plus étrange; soi-disant u.b devrait également être mis à l'état non initialisé bien qu'il soit Copy . Copy valeurs let a: u8; ), mais changer leur état d'initialisé à non initialisé est quelque chose de nouveau, AFAIK.

@ retep998
Je sais que cela n'a aucun rapport avec les besoins de FFI :)
La bonne nouvelle est que ce n'est pas un bloqueur, je vais mettre en œuvre le comportement le plus simple et soumettre des relations publiques ce week-end.

@petrochenkov mon instinct est que les syndicats sont essentiellement un "bit-bucket". Vous êtes responsable de savoir si les données sont initialisées ou non et quel est leur vrai type. Ceci est très similaire au référent d'un pointeur brut.

C'est pourquoi nous ne pouvons pas supprimer les données à votre place, et aussi pourquoi tout accès aux champs est dangereux (même si, par exemple, il n'y a qu'une seule variante).

Selon ces règles, je m'attendrais à ce que les syndicats implémentent Copy si une copie est implémentée pour eux. Contrairement aux structs / enums, cependant, il n'y aurait pas de vérifications internes: vous pouvez toujours implémenter une copie pour un type d'union si vous le souhaitez.

Permettez-moi de donner quelques exemples pour clarifier:

union Foo { ... } // contents don't matter

Cette union est affine, car Copy n'a pas été implémentée.

union Bar { x: Rc<String> }
impl Copy for Bar { }
impl Clone for Bar { fn clone(&self) -> Self { *self } }

Ce type d'union Bar est une copie, car Copy a été implémenté.

Notez que si Bar était une structure, ce serait une erreur d'implémenter Copy cause du type du champ x .

Euh, je suppose que je ne réponds pas vraiment à votre question, maintenant que je l'ai relue. =)

OK, donc, je me rends compte que je ne répondais pas du tout à votre question. Alors laissez-moi réessayer. Suivant le principe du "bit bucket", je m'attendrais toujours à ce que nous puissions sortir d'un syndicat à volonté. Mais bien sûr, une autre option serait de le traiter comme nous traitons un *mut T , et vous demandons d'utiliser ptr::read pour déménager.

EDIT: Je ne sais pas vraiment pourquoi nous interdirions de tels mouvements. Cela aurait peut-être dû faire w / déplacement en baisse - ou peut-être simplement parce qu'il est facile de se tromper et qu'il semble préférable de rendre les «déménagements» plus explicites? J'ai du mal à me souvenir de l'histoire ici.

@nikomatsakis

mon instinct est que les syndicats sont essentiellement un «petit compartiment».

Ha, je voudrais, au contraire, donner autant de garanties que possible sur le contenu du syndicat pour une construction aussi dangereuse.

L'interprétation est que l'union est une énumération pour laquelle nous ne connaissons pas le discriminant, c'est-à-dire que nous pouvons garantir qu'à tout moment au moins une des variantes de l'union a une valeur valide (sauf si du code unsafe est impliqué).

Toutes les règles d'emprunt / déplacement de l'implémentation actuelle prennent en charge cette garantie, c'est en même temps l'interprétation la plus conservatrice, qui nous permet d'opter soit pour la voie «sûre» (par exemple, en permettant un accès sécurisé à des unions avec des champs également utile ) ou de la manière "bit bucket" à l'avenir, lorsque plus d'expérience avec les unions Rust sera acquise.

En fait, j'aimerais le rendre encore plus conservateur comme décrit dans https://github.com/rust-lang/rust/pull/36016#issuecomment -242810887

@petrochenkov

L'interprétation est que l'union est une énumération pour laquelle nous ne connaissons pas le discriminant, c'est-à-dire que nous pouvons garantir qu'à tout moment au moins une des variantes de l'union a une valeur valide (sauf si du code unsafe est impliqué).

Notez que le code unsafe est toujours impliqué, lorsque vous travaillez avec un syndicat, car chaque accès à un champ est dangereux.

La façon dont j'y pense est, je pense, similaire. Fondamentalement, une union est comme une énumération, mais elle peut être dans plus d'une variante simultanément. L'ensemble des variantes valides n'est connu du compilateur à aucun moment, même si parfois nous pouvons comprendre que l'ensemble est vide (c'est-à-dire que l'énumération n'est pas initialisée).

Donc, je vois toute utilisation de some_union.field comme une assertion implicite (et non sûre) selon laquelle l'ensemble des variantes valides comprend actuellement field . Cela semble compatible avec le fonctionnement de l'intégration du vérificateur d'emprunt; si vous empruntez le champ x puis essayez d'utiliser y , vous obtenez une erreur parce que vous dites essentiellement que les données sont simultanément x et y (et il est emprunté). (En revanche, avec une énumération régulière, il n'est pas possible d'habiter plus d'une variante à la fois, et vous pouvez le voir dans la façon dont les règles d'emprunt se déroulent ).

Quoi qu'il en soit, le fait est que, lorsque nous «nous déplaçons» d'un champ d'union, la question qui se pose est de savoir si nous pouvons en déduire que cela implique que l'interprétation de la valeur comme les autres variantes n'est plus valide. Je pense que ce ne serait pas si difficile de discuter de toute façon, cependant. Je considère cela comme une zone grise.

Le danger d'être conservateur est que nous pourrions bien écarter un code dangereux qui, autrement, aurait du sens et serait valide. Mais je suis d'accord pour commencer plus serré et décider de me détendre plus tard.

Nous devrions discuter de la question des conditions nécessaires pour mettre en œuvre Copy sur un syndicat - également, nous devons nous assurer que nous avons une liste complète de ces zones grises énumérées ci-dessus pour nous assurer que nous traitons et documentons avant la stabilisation!

Fondamentalement, une union est comme une énumération, mais elle peut être dans plus d'une variante simultanément.

Un argument contre l'interprétation «plus d'une variante» est le comportement des unions dans des expressions constantes - pour ces unions, nous connaissons toujours la seule variante active et ne pouvons pas non plus accéder aux variantes inactives car la transmutation au moment de la compilation est généralement mauvaise (sauf si nous essayons pour transformer le compilateur en une sorte d'émulateur cible partiel).
Mon interprétation est qu'au moment de l'exécution, les variantes inactives sont toujours inactives mais sont accessibles si elles sont compatibles avec la variante active de l'union (définition plus restrictive) ou plutôt avec l'historique d'affectation des fragments de l'union (plus vague, mais plus utile).

nous devons nous assurer d'avoir une liste complète de ces zones grises

Je vais modifier le RFC syndical dans un avenir pas si lointain! L'interprétation "enum" a des conséquences assez amusantes.

la transmutation au moment de la compilation est généralement mauvaise (sauf si nous essayons de transformer le compilateur en une sorte d'émulateur de cible partielle)

@petrochenkov C'est l'un des objectifs de mon projet Miri . Miri peut déjà faire des transmutes et diverses manigances de pointeurs bruts. Ce serait une petite quantité de travail pour que Miri gère les unions (rien de nouveau du côté de la gestion de la mémoire brute).

Et @eddyb fait pression pour remplacer l'évaluation constante rustc par une version de Miri.

@petrochenkov

Un argument contre l'interprétation «plus d'une variante» est la façon dont les unions se comportent dans des expressions constantes ...

Comment soutenir au mieux l'utilisation des unions dans les constantes est une question intéressante, mais je ne vois aucun problème à restreindre les expressions constantes à un sous-ensemble de comportement d'exécution (c'est ce que nous faisons toujours, de toute façon). C'est-à-dire que le fait que nous ne soyons peut-être pas en mesure de prendre entièrement en charge une transmutation particulière au moment de la compilation ne signifie pas qu'elle est illégale à l'exécution.

Mon interprétation est qu'au moment de l'exécution, les variantes inactives sont toujours inactives mais sont accessibles si elles sont compatibles avec la variante active de l'union.

Hmm, j'essaie de penser en quoi c'est différent de dire que le syndicat appartient à toutes ces variantes simultanément. Je ne vois pas encore vraiment de différence. :)

J'ai l'impression que cette interprétation a des interactions étranges avec les mouvements en général. Par exemple, si les données sont «vraiment» un X et que vous l'interprétez comme un Y, mais que Y est affine, est-ce toujours un X?

Quoi qu'il en soit, je pense qu'il est bien que le fait de déplacer n'importe quel domaine consomme l'ensemble du syndicat puisse être considéré comme conforme à l'une de ces interprétations. Par exemple, dans l'approche «ensemble de variantes», l'idée est simplement que le déplacement de la valeur désinitialise toutes les variantes existantes (et bien sûr, la variante que vous avez utilisée doit faire partie de l'ensemble valide). Dans votre version, il semblerait «se transmuter» dans cette variante (et consommer l'original).

Je vais modifier le RFC syndical dans un avenir pas si lointain! L'interprétation "enum" a des conséquences assez amusantes.

Une telle confiance! Vous allez essayer;)

Souhaitez-vous donner quelques détails supplémentaires sur les changements concrets que vous envisagez?

Souhaitez-vous donner quelques détails supplémentaires sur les changements concrets que vous envisagez?

Description plus détaillée de l'implémentation (c.-à-d. Meilleure documentation), quelques petites extensions (comme les unions vides et .. dans les modèles d'union), deux principales alternatives (contradictoires) de l'évolution des unions - un «espace de travail» plus dangereux et moins restrictif interprétation et interprétation plus sûre et plus restrictive de "enum with unknown discriminant" - et leurs conséquences pour le vérificateur de déplacement / initialisation, Copy impls, unsafe ty d'accès au champ, etc.

Il serait également utile de définir lors de l'accès à un champ union inactif est UB, par exemple

union U { a: u8, b: () }
let u = U { b: () };
let a = u.a; // most probably an UB, equivalent to reading from `mem::uninitialized()`

mais c'est un domaine infiniment délicat.

Cela semble probable, la sémantique cross-field est essentiellement un pointeur, non?
_ (_ () comme * u8)

Le jeudi 1er septembre 2016, Vadim Petrochenkov [email protected]
a écrit:

Il serait également utile de définir lors de l'accès à un champ union inactif
est UB, par exemple

union U {a: u8, b: ()}
soit u = U {b: ()};
soit a = ua; // très probablement un UB, équivalent à la lecture de mem::uninitialized()

mais c'est un domaine infiniment délicat.

-
Vous recevez ceci parce que vous êtes abonné à ce fil.
Répondez directement à cet e-mail, affichez-le sur GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment -244154751,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/ABxXhi68qRITTFW5iJn6omZQQBQgzweNks5qlw4qgaJpZM4IDXsj
.

L'accès sur le terrain n'est-il pas toujours dangereux?

Le jeudi 1er septembre 2016, Vadim Petrochenkov [email protected]
a écrit:

Souhaitez-vous donner quelques détails supplémentaires sur les changements concrets que vous envisagez?

Description plus détaillée de la mise en œuvre (c.-à-d.
documentation), quelques petites extensions (comme les unions vides et .. en union
modèles), deux alternatives principales (contradictoires) de l'évolution des syndicats - plus
interprétation non sécurisée et moins restrictive de «l'espace de travail» et plus sûre
et plus restrictive "énumération avec interprétation discriminante inconnue"
leurs conséquences pour le vérificateur de mouvement / initialisation, les impls de copie, la non-sécurité
d'accès sur le terrain, etc.

-
Vous recevez ceci parce que vous êtes abonné à ce fil.
Répondez directement à cet e-mail, affichez-le sur GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment -244151164,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/ABxXhuHStN8AFhR3KYDU27U29MiMpN5Bks5qlws9gaJpZM4IDXsj
.

L'accès sur le terrain n'est-il pas toujours dangereux?

Il peut être sécurisé parfois, par exemple

  • l'affectation à des champs d'union trivialement destructibles est sûre.
  • tout accès aux champs d'un union U { f1: T, f2: T, ..., fN: T } (c'est-à-dire que tous les champs ont le même type) est sûr dans l'interprétation "enum avec discriminant inconnu".

Il semble préférable de ne pas appliquer de conditions particulières à cela, du point de vue de l'utilisateur. Appelez ça comme dangereux, toujours.

Teste actuellement le support des syndicats dans le dernier rustc de git. Tout ce que j'ai essayé fonctionne parfaitement.

J'ai rencontré un cas intéressant dans le vérificateur de champ mort. Essayez le code suivant:

#![feature(untagged_unions)]

union U {
    i: i32,
    f: f32,
}

fn main() {
    println!("{}", std::mem::size_of::<U>());
    let u = U { f: 1.0 };
    println!("{:#x}", unsafe { u.i });
}

Vous obtiendrez cette erreur:

warning: struct field is never used: `f`, #[warn(dead_code)] on by default

On dirait que le vérificateur dead_code n'a pas remarqué l'initialisation.

(J'ai déjà déposé le PR # 36252 sur l'utilisation de "struct field", en le changeant simplement en "field".)

Les unions ne peuvent actuellement pas contenir de champs de taille dynamique, mais le RFC ne spécifie pas ce comportement dans les deux cas:

#![feature(untagged_unions)]

union Foo<T: ?Sized> {
  value: T,
}

Production:

error[E0277]: the trait bound `T: std::marker::Sized` is not satisfied
 --> <anon>:4:5
  |
4 |     value: T,
  |     ^^^^^^^^ trait `T: std::marker::Sized` not satisfied
  |
  = help: consider adding a `where T: std::marker::Sized` bound
  = note: only the last field of a struct or enum variant may have a dynamically sized type

Le mot clé contextuel ne fonctionne pas en dehors du contexte racine du module / caisse:

fn main() {
    // all work
    struct Peach {}
    enum Pineapple {}
    trait Mango {}
    impl Mango for () {}
    type Strawberry = ();
    fn woah() {}
    mod even_modules {
        union WithUnions {}
    }
    use std;

    // does not work
    union Banana {}
}

On dirait une verrue de consistance assez méchante.

@nagisa
Utilisez-vous une ancienne version de rustc par accident?
Je viens de vérifier votre exemple sur le parc et cela fonctionne (erreurs modulo "union vide").
Il existe également une vérification de test d'exécution pour cette situation spécifique - https://github.com/rust-lang/rust/blob/master/src/test/run-pass/union/union-backcomp.rs.

@petrochenkov ah, j'ai utilisé play.rlo, mais il semble que cela soit revenu à stable ou quelque chose comme ça. Ne faites pas attention à moi, alors.

Je pense que les syndicats devront éventuellement soutenir les _safe fields_, les jumeaux diaboliques des champs dangereux de cette proposition .
cc https://github.com/rust-lang/rfcs/issues/381#issuecomment -246703410

Je pense qu'il serait logique d'avoir une façon de déclarer des «syndicats sûrs», en fonction de divers critères.

Par exemple, une union contenant exclusivement des champs Copy non-Drop, tous de même taille, semble sûre; peu importe la façon dont vous accédez aux champs, vous pouvez obtenir des données inattendues, mais vous ne pouvez pas rencontrer de problème de sécurité de la mémoire ou de comportement indéfini.

@joshtriplett Vous devez également vous assurer qu'il n'y a pas de "trous" dans les types à partir desquels vous pourriez lire des données non initialisées. Comme j'aime le dire: «Les données non initialisées sont soit des données aléatoires imprévisibles, soit votre clé privée SSH, selon ce qui est le plus mauvais.

N'est-ce pas &T Copy and non-Drop? Mettez cela dans une union "sûre" avec usize , et vous avez un générateur de référence non autorisé. Les règles devront donc être un peu plus strictes que cela.

Par exemple, une union contenant exclusivement des champs Copy non-Drop, tous de même taille, semble sûre; peu importe la façon dont vous accédez aux champs, vous pouvez obtenir des données inattendues, mais vous ne pouvez pas rencontrer de problème de sécurité de la mémoire ou de comportement indéfini.

Je me rends compte qu'il ne s'agit que d'un exemple improvisé, pas d'une proposition sérieuse, mais voici quelques exemples pour illustrer à quel point c'est délicat:

  • u8 et bool ont la même taille, mais la plupart des valeurs u8 ne sont pas valides pour bool et ignorer cela déclenche UB
  • &T et &U ont la même taille et sont Copy + !Drop pour tout T et U (tant que les deux ou aucun ne sont Sized )
  • La transmutation entre uN / iN et fN n'est actuellement possible qu'en code non sécurisé. Je crois que de telles transmutes sont toujours sûres, mais cela élargit le langage sûr et peut donc être controversé.
  • Violer la vie privée (par exemple, punir entre struct Foo(Bar); et Bar ) est un grand non aussi bien, car la confidentialité peut être utilisée pour maintenir des invariants pertinents pour la sécurité.

@Amanieu Quand j'écrivais cela, j'avais l'intention d'inclure une note sur le fait de ne pas avoir de remplissage interne, et j'avais oublié de le faire. Merci d'avoir attrapé ça.

@cuviper J'essayais de définir des "vieilles données simples", comme dans quelque chose qui contient zéro pointeur. Vous avez raison, la définition devrait exclure les références. Probablement plus facile de mettre sur liste blanche un ensemble de types autorisés et de combinaisons de ces types.

@rkruppe

u8 et bool ont la même taille, mais la plupart des valeurs u8 ne sont pas valides pour bool et ignorer cela déclenche UB

Bon point; le même problème s'applique aux énumérations.

& T et & U ont la même taille et sont Copy +! Drop pour tous les T et U (tant que les deux ou aucun ne sont dimensionnés)

J'avais oublié ça.

La transmutation entre uN / iN et fN n'est actuellement possible qu'en code non sécurisé. Je crois que de telles transmutes sont toujours sûres, mais cela élargit le langage sûr et peut donc être controversé.

D'accord sur les deux points; cela semble acceptable de permettre.

La violation de la vie privée (par exemple, le jeu de mots entre la structure Foo (Bar) et Bar) est un grand non aussi bien, puisque la confidentialité peut être utilisée pour maintenir des invariants pertinents pour la sécurité.

Si vous ne connaissez pas les composants internes du type, vous ne pouvez pas savoir si les composants internes répondent aux exigences (telles que l'absence de remplissage interne). Vous pouvez donc exclure cela en exigeant que tous les composants soient des données anciennes, récursives, et que vous ayez une visibilité suffisante pour le vérifier.

La transmutation entre uN / iN et fN n'est actuellement possible qu'en code non sécurisé. Je crois que de telles transmutes sont toujours sûres, mais cela élargit le langage sûr et peut donc être controversé.

Les nombres à virgule flottante ont la signalisation NaN qui est une représentation de trappe qui se traduit par UB.

@ retep998 Est-

@petrochenkov

L'interprétation est que l'union est une énumération dont nous ne connaissons pas le discriminant, c'est-à-dire que nous pouvons garantir qu'à tout moment au moins une des variantes d'union a une valeur valide

Je pense que je suis arrivé à cette interprétation - enfin, pas exactement cela. J'y pense toujours car il existe un ensemble de variantes juridiques qui sont déterminées au moment où vous stockez, comme je l'ai toujours fait. Je pense que stocker une valeur dans une union est un peu comme la mettre dans un «état quantique» - elle pourrait maintenant potentiellement être transmutée en l'une des nombreuses interprétations juridiques. Mais je suis d'accord que lorsque vous quittez l'une de ces variantes, vous l'avez "forcée" à une seule de celles-ci, et vous avez consommé la valeur. Par conséquent, vous ne devriez pas pouvoir réutiliser l'énumération (si ce type n'est pas Copy ). Donc 👍, en gros.

Question sur #[repr(C)] : comme @pnkfelix me l'a récemment fait remarquer, la spécification actuelle indique que si une union n'est pas #[repr(C)] , il est illégal de stocker avec le champ x et de lire avec le champ y . C'est probablement parce que nous ne sommes pas obligés de commencer tous les champs avec le même décalage.

Je peux voir une utilité dans ceci: par exemple, un désinfectant pourrait implémenter des unions en les stockant comme une énumération normale (ou même une structure ...?) Et en vérifiant que vous utilisez la même variante que vous avez mise.

_Mais_ cela ressemble à une sorte de fusil à pied, et aussi une de ces garanties de représailles que nous ne pourrions jamais changer _actuellement_ dans la pratique, parce que trop de gens compteront dessus dans la nature.

Pensées?

@nikomatsakis

L'interprétation est que l'union est une énumération dont nous ne connaissons pas le discriminant, c'est-à-dire que nous pouvons garantir qu'à tout moment au moins une des variantes d'union a une valeur valide

Le pire, ce sont les fragments de variantes / champs, qui sont directement accessibles pour les unions.
Considérez ce code:

union U {
    a: (u8, bool),
    b: (bool, u8),
}
fn main() {
    unsafe {
        let mut u = U { a: (2, false) };
        u.b.1 = 2; // turns union's memory into (2, 2)
    }
}

Tous les champs sont Copy , aucune propriété n'est impliquée et le vérificateur de déplacement est heureux, mais l'affectation partielle au champ inactif b transforme l'union en état avec 0 variantes valides. Je n'ai pas encore pensé comment y faire face. Faire de telles affectations UB? Changer l'interprétation? Autre chose?

@petrochenkov

Faire de telles affectations UB?

Ce serait mon hypothèse, oui. Lorsque vous avez attribué avec a , la variante b n'était pas dans l'ensemble des variantes valides, et par conséquent, l'utilisation ultérieure de u.b.1 (qu'il faut lire ou affecter) est invalide.

Question sur # [repr (C)]: comme @pnkfelix me l'a récemment fait remarquer, la spécification actuelle indique que si une union n'est pas # [repr (C)], il est illégal de stocker avec le champ x et de lire avec le champ y . C'est probablement parce que nous ne sommes pas obligés de commencer tous les champs avec le même décalage.

Je pense que le libellé approprié ici est que 1) La lecture de champs qui ne sont pas "compatibles avec la mise en page" (c'est vague) avec des champs / fragments de champ précédemment écrits est UB 2) Pour #[repr(C)] unions, les utilisateurs savent ce que sont les mises en page (à partir de la documentation ABI) afin qu'ils puissent discerner entre UB et non-UB 3) Pour #[repr(Rust)] les mises en page d'union ne sont pas spécifiées afin que les utilisateurs ne puissent pas dire ce qu'est UB et ce qui ne l'est pas, mais tests) ont cette connaissance sacrée, nous pouvons donc séparer le bon grain de l'ivraie et utiliser #[repr(Rust)] de manière non-UB.

4) Une fois les questions de taille / foulée et de réorganisation des champs décidées, je m'attendrais à ce que les mises en page struct et union soient gravées dans le marbre et spécifiées, afin que les utilisateurs connaissent également les mises en page et pourront utiliser #[repr(Rust)] unions aussi librement que #[repr(C)] et le problème disparaîtra.

@nikomatsakis Dans la discussion sur le RFC union, les gens ont mentionné vouloir avoir un code Rust natif qui utilise des unions pour construire des structures de données compactes.

Y a-t-il quelque chose qui empêche les personnes utilisant #[repr(C)] ? Sinon, alors je ne vois pas la nécessité de fournir des garanties pour #[repr(Rust)] , laissez-le simplement comme "ici, des dragons". Il serait probablement préférable d'avoir une charpie qui est avertie par défaut pour les syndicats qui ne sont pas #[repr(C)] .

@ retep998 Il me semble raisonnable que repr(Rust) ne garantit aucune disposition ou chevauchement particulier. Je suggérerais simplement que repr(Rust) ne devrait pas en pratique casser les hypothèses des gens sur l'utilisation de la mémoire d'un syndicat ("pas plus grand que le plus grand membre").

Rust fonctionne-t-il sur des plates-formes qui ne prennent pas en charge la désactivation des interruptions en virgule flottante?

Ce n'est pas vraiment une question valable à poser. Tout d'abord, l'optimiseur lui-même peut s'appuyer sur l'UB-ness des représentations de trap et réécrire le programme de manière inattendue. De plus, Rust ne prend pas non plus en charge la modification de l'environnement FP.

Mais cela semble être une sorte de pistolet à pied, et aussi l'une de ces garanties que nous ne pourrons jamais changer dans la pratique, parce que trop de gens compteront dessus dans la nature.

Pensées?

Ajouter une charpie ou quelque chose qui inspecte le déroulement du programme et jette une plainte à l'utilisateur si la lecture est effectuée à partir d'un champ lorsque l'énumération a été écrite de manière prouvée dans un autre champ aiderait à cela. La charpie à base de MIR ferait un court travail de cela. Si un CFG ne permet pas de tirer des conclusions sur la légalité de la charge du champ union et que l'utilisateur fait une erreur, le comportement indéfini est le meilleur que l'on puisse spécifier sans avoir spécifié le Rust repr lui-même IMO.

¹: Particulièrement efficace si les gens commencent à utiliser l'union comme transmutation d'un pauvre pour une raison quelconque.

ne devrait pas en pratique rompre les hypothèses des gens sur l'utilisation de la mémoire d'une union ("pas plus grand que le membre le plus grand").

Je ne suis pas d'accord. Il peut être très judicieux d'étendre repr(Rust) à la taille d'un mot machine sur certaines architectures, par exemple.

Un problème à prendre en compte avant la stabilisation est https://github.com/rust-lang/rust/issues/37479. Il semble que la version la plus récente des unions de débogage LLDB ne fonctionne pas :(

@alexcrichton Fonctionne- t-il avec GDB?

D'après ce que je peux dire, oui. Les bots Linux semblent exécuter le test très bien.

Cela signifie que Rust fournit toutes les bonnes informations de débogage, et LLDB a juste un bogue ici. Je ne pense pas qu'un bogue dans l'un des multiples débogueurs, non présent dans un autre, devrait bloquer la stabilisation de cela. LLDB a juste besoin d'être réparé.

Ce serait cool de voir si nous pourrions intégrer cette fonctionnalité dans FCP pour le cycle 1.17 (c'est la version bêta du 16 mars). Quelqu'un peut-il résumer les questions en suspens et la situation actuelle de la fonctionnalité afin que nous puissions voir si nous pouvons parvenir à un consensus et tout résoudre?

@sans bateaux
Mes plans sont

  • Attendez la sortie à venir (3 février).
  • Proposer une stabilisation des unions avec des champs Copy . Cela couvrira tous les besoins FFI - les bibliothèques FFI pourront utiliser des unions sur stable. Les unions "POD" sont utilisées depuis des décennies en C / C ++ et bien comprises (aliasing basé sur le type modulo, mais Rust ne l'a pas), il n'y a pas non plus de bloqueurs connus.
  • Rédigez la RFC «Unions 1.2» jusqu'au 3 février. Elle décrira la mise en œuvre actuelle des syndicats et exposera les orientations futures. L'avenir des syndicats avec des champs non- Copy sera décidé dans le processus de discussion de cette RFC.

Notez que l'exposition de quelque chose comme ManuallyDrop ou NoDrop partir de la bibliothèque standard ne nécessite pas d'unions stabilisatrices.

MISE À JOUR DU STATUT (4 février): J'écris la RFC, mais j'ai un bloc d'écrivain après chaque phrase, comme d'habitude, donc il y a une chance que je le termine le week-end prochain (11-12 février) et pas ce week-end (4-5 février).
MISE À JOUR DU STATUT (11 février): Le texte est prêt à 95%, je le soumettrai demain.

@petrochenkov qui semble être une ligne de conduite très raisonnable.

@petrochenkov Cela me semble raisonnable. J'ai également examiné votre proposition 1.2 des syndicats et fourni quelques commentaires; dans l'ensemble, cela me semble bon.

@joshtriplett Je pensais que, alors que lors de la réunion @ rust-lang / lang nous parlions de garder les check-lists à jour, j'aimerais en fait que - pour chacun de ces points - nous prenions une décision affirmative (c'est-à-dire, idéalement avec @rfcbot). Cela suggérerait probablement un problème distinct (ou même un amendement RFC). Nous pourrions le faire avec le temps, mais jusque-là je n'ai pas l'impression que nous avons «réglé» définitivement les réponses aux questions ouvertes. Dans ce sens, extraire et résumer la conversation pertinente dans un RFC d'amendement ou même simplement dans un problème auquel nous pouvons faire un lien à partir d'ici semble être une excellente étape pour s'assurer que tout le monde est sur la même longueur d'onde - et quelque chose que toute personne intéressée peut faire , bien sûr, pas seulement les membres @ rust-lang / lang ou les bergers.

J'ai donc soumis le RFC "Unions 1.2" - https://github.com/rust-lang/rfcs/pull/1897.

Maintenant, j'aimerais proposer la stabilisation d'un sous-ensemble conservateur d'union - tous les champs de l'union devraient être Copy , le nombre de champs devrait être différent de zéro et l'union ne devrait pas implémenter Drop .
(Je ne suis pas sûr que la dernière exigence soit viable, car elle peut être facilement contournée en enveloppant l'union dans une structure et en implémentant Drop pour cette structure.)
Ces unions couvrent tous les besoins des bibliothèques FFI, qui sont censées être le principal consommateur de cette fonctionnalité de langage.

Le texte de la RFC "Unions 1.2" ne dit rien de nouveau sur les unions de style FFI, sauf qu'il confirme explicitement que le poinçonnage de type est autorisé.
EDIT : "Unions 1.2" RFC va également rendre les affectations aux champs Copy trivialement destructibles (voir https://github.com/rust-lang/rust/issues/32836#issuecomment-281296416, https : //github.com/rust-lang/rust/issues/32836#issuecomment-281748451), cela affecte également les unions de type FFI.

Ce texte fournit également la documentation nécessaire à la stabilisation.
La section "Vue d'ensemble" peut être copiée dans le livre et "Conception détaillée" dans la référence.

ping @nikomatsakis

Est-ce que quelque chose comme ça doit vraiment être ajouté dans le langage? Il m'a fallu environ 20 minutes pour créer une implémentation d'un syndicat en utilisant un peu de unsafe et de ptr::write() .

use std::mem;
use std::ptr;


/// A union of `f64`, `bool`, and `i32`.
#[derive(Default, Clone, PartialEq, Debug)]
struct Union {
    data: [u8; 8],
}

impl Union {
    pub unsafe fn get<T>(&self) -> &T {
        &*(&self.data as *const _ as *const T)
    }

    pub unsafe fn set<T>(&mut self, value: T) {
        // "transmute" our pointer to self.data into a &mut T so we can 
        // use ptr::write()
        let data_ptr: &mut T = &mut *(&mut self.data as *mut _ as *mut T);
        ptr::write(data_ptr, value);
    }
}


fn main() {
    let mut u = Union::default();
    println!("data: {0:?} ({0:#p})", &u.data);
    {
        let as_i32: &i32 = unsafe { u.get() };
        println!("as i32: {0:?} ({0:#p})", as_i32);
    }

    unsafe {
        u.set::<f64>(3.14);
    }

    println!("As an f64: {:?}", unsafe { u.get::<f64>() });
}

J'ai l'impression qu'il ne serait pas difficile pour quelqu'un d'écrire une macro qui peut générer quelque chose comme ça, sauf en s'assurant que le tableau interne est de la taille du plus grand type. Ensuite, au lieu de mon get::<T>() complètement générique (et horriblement dangereux), ils pourraient ajouter un trait lié pour limiter les types que vous pouvez obtenir et définir. Vous pouvez même ajouter des méthodes getter et setter spécifiques si vous voulez des champs nommés.

Je pense qu'ils pourraient écrire quelque chose comme ça:

union! { Foo(u64, Vec<u8>, String) };

Mon point est que c'est quelque chose que vous pourriez tout à fait faire dans le cadre d'une bibliothèque au lieu d'ajouter une syntaxe et une complexité supplémentaires à un langage déjà assez complexe. De plus, avec les macros proc, c'est déjà tout à fait possible, même si cela n'est pas encore totalement stable.

@ Michael-F-Bryan Cependant, nous n'avons pas encore de constante size_of .

@ Michael-F-Bryan Il ne suffit pas d'avoir un tableau [u8] , vous devez également obtenir l' alignement correct. En fait, j'utilise déjà des macros pour gérer les unions, mais en raison du manque de constantes size_of et align_of je dois allouer manuellement l'espace correct, plus parce qu'il n'y a pas de concaténation d'identifiant utilisable dans les macros déclaratives I doivent spécifier manuellement les noms des getters et des setters. Même l'initialisation d'une union est difficile pour le moment car je dois d'abord l'initialiser avec une valeur par défaut, puis définir la valeur sur la variante que je veux (ou ajouter un autre ensemble de méthodes pour construire l'union qui est encore plus verbeuse dans la définition du syndicat). Dans l'ensemble, c'est beaucoup plus de travail et plus sujet à l'erreur et plus laid que le soutien natif des syndicats. Vous devriez peut-être lire la RFC et la discussion qui l'accompagne pour comprendre pourquoi cette fonctionnalité est si importante.

Et la même chose pour l'alignement.

J'imagine que la concaténation d'identifiants ne devrait pas être trop difficile maintenant que syn existe. Il vous permet d'effectuer des opérations sur l'AST passé, vous pouvez donc prendre deux Idents , extraire leur représentation sous forme de chaîne ( Ident implements AsRef<str> ), puis créer un nouveau Ident qui est la concaténation des deux en utilisant Ident::From<String>() .

La RFC mentionne beaucoup de choses sur la façon dont les implémentations de macros existantes sont lourdes à utiliser, mais avec la création récente de caisses comme syn et quote , il est maintenant beaucoup plus facile de faire des macros proc. Je pense que cela contribuerait grandement à améliorer l'ergonomie et à rendre les choses moins sujettes aux erreurs.

Par exemple, vous pourriez avoir un MyUnion::default() qui est juste zéro le tampon interne de l'union, puis un fn MyUnion::new<T>(value:T) -> MyUnion , où T a un trait lié garantissant que vous ne pouvez initialiser qu'avec les types corrects .

En termes d'alignement et de taille, pouvez-vous utiliser le module mem de la bibliothèque standard (ie std :: mem :: align_of () et friends)? Je suppose que tout ce que je propose dépendra de la capacité à les utiliser au moment de l'expansion macro pour déterminer la taille et l'alignement requis. 99,9% des fois que les unions sont utilisées, c'est fait avec des types primitifs de toute façon, donc j'ai l'impression que vous pourriez écrire une fonction d'assistance qui prend le nom d'un type et renvoie son alignement ou sa taille (peut-être en demandant au compilateur, bien que ce soit plus un détail de mise en œuvre).

J'admets que la correspondance de motifs intégrée serait très agréable, mais la plupart du temps, les unions que vous utilisez dans FFI seraient de toute façon enveloppées dans une fine couche d'abstraction. Ainsi, vous pourrez peut-être vous en tirer avec quelques instructions if / else ou en utilisant une fonction d'assistance.

En termes d'alignement et de taille, pouvez-vous utiliser le module mem de la bibliothèque standard (ie std :: mem :: align_of () et friends)?

Cela ne fonctionnera dans aucun contexte de compilation croisée.

@ Michael-F-Bryan Toutes ces discussions et bien d'autres ont eu lieu dans l'histoire de https://github.com/rust-lang/rfcs/pull/1444 . Pour résumer les réponses à vos préoccupations spécifiques, en plus de celles déjà mentionnées: vous devrez réimplémenter les règles de remplissage et d'alignement de chaque plate-forme / compilateur cible, et utiliser une syntaxe maladroite dans tout votre code FFI (ce que @ retep998 a en fait fait largement pour les liaisons Windows et peut se porter garant de la maladresse de). De plus, les macros proc ne fonctionnent actuellement que pour la dérivation; vous ne pouvez pas étendre la syntaxe ailleurs.

Également:

99,9% des fois que les unions sont utilisées, c'est fait avec les types primitifs de toute façon

Pas vrai du tout. Le code C utilise largement un modèle «struct of unions of structs», où la plupart des champs d'union sont constitués de différents types de struct.

@rfcbot fusion fcp par @petrochenkov « s commentaire https://github.com/rust-lang/rust/issues/32836#issuecomment -279256434

Je n'ai rien à ajouter, je déclenche juste le bot

Le membre de l'équipe @withoutboats a proposé de fusionner cela. L'étape suivante est l'examen par le reste des équipes marquées:

  • [x] @aturon
  • [x] @eddyb
  • [x] @nikomatsakis
  • [x] @nrc
  • [x] @pnkfelix
  • [x] @sans bateaux

Aucun problème actuellement répertorié.

Une fois que ces examinateurs parviendront à un consensus, cela entrera dans sa période de commentaires finale. Si vous repérez un problème majeur qui n'a été soulevé à aucun moment de ce processus, veuillez en parler!

Consultez ce document pour plus d'informations sur les commandes que les membres de l'équipe marqués peuvent me donner.

PSA: Je vais mettre à jour le RFC "Unions 1.2" avec un autre changement affectant les unions de style FFI - je déplacerai les affectations sûres vers les champs d'unions banalement destructibles des "Directions futures" au RFC proprement dit.

union.trivially_destructible_field = 10; // safe

Pourquoi:

  • Les affectations à des champs d'union trivialement destructibles sont inconditionnellement sûres, quelle que soit l'interprétation des unions.
  • Cela supprimera environ la moitié des blocs unsafe liés
  • Ce sera plus difficile à faire plus tard en raison du grand nombre potentiel d'avertissements / d'erreurs unused_unsafe dans le code stable.

@petrochenkov Voulez-vous dire "champs d'union trivialement destructibles" ou "unions avec des champs totalement trivialement destructibles"?

Proposez-vous que tous les comportements dangereux se produisent lors de la lecture, lorsque vous choisissez une interprétation? Par exemple, avoir une union contenant une énumération et d'autres champs, où la valeur d'union contient un discriminant invalide?

À un niveau élevé qui semble plausible. Cela permet certaines choses que je considère comme dangereuses, mais Rust en général ne le permet pas, comme le contournement des destructeurs ou la fuite de mémoire. À un niveau bas, j'hésiterais à considérer ce son.

Je suis d'accord pour stabiliser ce sous-ensemble. Je ne connais pas encore mon opinion sur cette RFC Unions 1.2 car je n'ai pas eu le temps de la lire! Je ne suis pas sûr de ce que je pense de permettre un accès sécurisé aux champs dans certains cas. J'ai le sentiment que nos efforts pour faire une notion "minimale" de ce qui est dangereux (juste déréférencer les pointeurs) était une erreur, rétrospectivement, et nous aurions dû déclarer un plus large éventail de choses dangereuses (par exemple, beaucoup de casts), car ils interagir de manière complexe avec LLVM. Je pense que c'est peut-être le cas ici aussi. En d'autres termes, je pourrais plutôt retirer les règles sur unsafe de concert avec plus de progrès sur les directives de code non sécurisé.

@joshtriplett
«champs trivialement destructibles», j'ai modifié le libellé.

Proposez-vous que tous les comportements dangereux se produisent lors de la lecture, lorsque vous choisissez une interprétation?

Oui. L'écriture seule ne peut rien causer de dangereux sans une lecture ultérieure.

ÉDITER:

Rétrospectivement, je pense que nos efforts pour faire une notion "minimale" de ce qui n'est pas sûr (juste déréférencer les pointeurs) était une erreur.

Oh.
Les écritures sécurisées sont tout à fait conformes à l'approche actuelle de l'insécurité, mais si vous voulez la changer, je devrais probablement attendre.

Je ne me sens pas bien de stabiliser ce sous-ensemble. Habituellement, lorsque nous stabilisons un sous-ensemble, il s'agit d'un sous-ensemble syntaxique ou du moins d'un sous-ensemble assez évident. Ce sous-ensemble me semble un peu complexe. S'il y a tellement d'indécis sur la fonctionnalité que nous ne sommes pas prêts à stabiliser l'implémentation actuelle, alors je préfère laisser le tout instable pendant un moment plus longtemps.

@nrc
Eh bien, le sous-ensemble est plutôt évident - «unions FFI», ou «unions C», ou «unions pré-C ++ 11» - même s'il n'est pas syntaxique. Mon objectif initial était de stabiliser ce sous-ensemble dès que possible (ce cycle, idéalement) afin qu'il puisse être utilisé dans des bibliothèques comme winapi .
Il n'y a rien de particulièrement douteux à propos du sous-ensemble restant et de son implémentation, ce n'est tout simplement pas urgent et il faut attendre un laps de temps incertain jusqu'à ce que le processus de RFC "Unions 1.2" se termine. Mes attentes seraient de stabiliser les pièces restantes en 1, 2 ou 3 cycles après la stabilisation du sous-ensemble initial.

Je pense avoir un argument ultime pour des affectations sur le terrain sûres.
Affectation de champ non sécurisée

unsafe {
    u.trivially_destructible_field = value;
}

équivaut à une affectation syndicale complète sûre

u = U { trivially_destructible_field: value };

sauf que la version sûre est paradoxalement moins sûre car elle écrasera u de l » extérieur de la trivially_destructible_field avec undefs, en mission sur le terrain ont la garantie de les laisser intactes.

@petrochenkov L'extrême de cela est size_of_val(&value) == 0 , non?

L'équivalence entre les deux extraits n'est vraie que si le champ en question est
"trivialement destructible", non?

En ce sens, rendre les affectations comme celles-ci sûres, mais seulement dans certains cas
me semble extrêmement incohérent.

Le 22 février 2017 à 14h50, "Vadim Petrochenkov" [email protected]
a écrit:

Je pense avoir un argument ultime pour des affectations sur le terrain sûres.
Affectation de champ non sécurisée

peu sûr {
u.trivially_destructible_field = valeur;
}

équivaut à une affectation syndicale complète sûre

u = U {champ_destructible_ trivial: valeur};

sauf que la version sûre est paradoxalement moins sûre car elle
écraser les octets de u en dehors de trivially_destructible_field avec undefs,
tandis que les affectations sur le terrain ont la garantie de les laisser intacts.

-
Vous recevez cela parce que vous avez été mentionné.
Répondez directement à cet e-mail, affichez-le sur GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment-281660298 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AApc0lUOXLU5xNTfM5PEfEz9nutMZhXUks5rfC8UgaJpZM4IDXsj
.

@eddyb

L'extrême de cela est size_of_val (& value) == 0, non?

Oui.

@nagisa
Je ne comprends pas pourquoi une règle simple supplémentaire éliminant une grande partie des faux positifs est extrêmement incohérente. "seuls quelques cas" couvrent tous les syndicats FFI en particulier.
Je pense que «extrêmement incohérent» est une grande surestimation. Pas aussi grand que "seules des variables mut peuvent être assignées? Horrible incohérence!", Mais toujours dans cette direction.

@petrochenkov veuillez considérer un tel cas:

// Somebody Somewhere in some crate (v 1.0.0)
struct Peach; // trivially destructible
union Banana { pub actually: Peach }

// Somebody Else in their dependent crate
extern some crate;
fn somefn(banana: &mut Banana) {
    banana.actually = Peach;
}

Maintenant que l'ajout d'implémentations de traits n'est généralement pas un changement radical, M. Somebody Somewhere comprend que cela peut être une bonne idée d'ajouter l'implémentation suivante

impl Drop for Peach { fn drop(&mut self) { println!("Moi Peach!") }

et publiez une version 1.1.0 (semver compatible avec 1.0.0 AFAIK) de la caisse.

Soudain, la caisse de M. Somebody Else ne se compile plus:

fn somefn(banana: &mut Banana) {
    banana.actually = Peach; // ERROR: Something something… unsafe assingment… somewhat somewhat trivially indestructible… 
}

Et donc parfois, autoriser des affectations sûres aux champs d'union n'est pas aussi simple que seulement mut locaux pouvant être mutés.


En écrivant cet exemple, je ne suis plus sûr de la position que je devrais adopter, honnêtement. D'une part, j'aimerais préserver la propriété selon laquelle l'ajout d'implémentations n'est généralement pas un changement radical (en ignorant les cas potentiels de XID). D'un autre côté, changer n'importe quel champ d'union de trivialement destructible à non trivialement destructible est évidemment un changement semi-incompatible qui est extrêmement facile à ignorer et la règle proposée rendrait ces incompatibilités plus visibles (tant que l'affectation n'est pas dans un bloc dangereux déjà).

@nagisa
C'est un bon argument, je n'ai pas pensé à la compatibilité.

Le problème semble cependant résoluble. Pour éviter les problèmes de compatibilité, faites la même chose que la cohérence - évitez les raisonnements négatifs. Ie remplace "trivialement destructible" == "aucun composant n'implémente Drop " par l'approximation positive la plus proche - "implémente Copy ".
Copy ne peut pas désimplémenter Copy rétrocompatible, et les types Copy représentent toujours la majorité des types "trivialement destructibles", en particulier dans le contexte des unions FFI.

L'implémentation de Drop n'est déjà pas rétrocompatible et cela n'a rien à voir avec la fonction union:

// Somebody Somewhere in some crate (v 1.0.0)
struct Apple; // trivially destructible
struct Pineapple { pub actually: Apple }

// Somebody Else in their dependent crate
extern some crate;
fn pineapple_to_apple(pineapple: Pineapple) -> Apple {
    pineapple.actually
}
// some crate v 1.1.0
impl Drop for Pineapple { fn drop(&mut self) { println!("Moi Pineapple!") }
fn pineapple_to_apple(pineapple: Pineapple) -> Apple {
    pineapple.actually // ERROR: can't move out of Pineapple
}

Ce qui à son tour ressemble à l'implémentation de Drop drops implicit Copy. Et copie
on peut compter sur.

Le mercredi 22 février 2017 à 10 h 11, jethrogb [email protected] a écrit:

La mise en œuvre de Drop n'est déjà pas rétrocompatible et cela a
rien à voir avec la fonction union:

// Quelqu'un quelque part dans une caisse (v 1.0.0)
struct Apple; // trivialement destructible
struct Pineapple {pub en fait: Apple}

// Quelqu'un d'autre dans sa caisse dépendante
extern une caisse;
fn pineapple_to_apple (ananas: ananas) -> Apple {
ananas.
}

// une caisse v 1.1.0
impl Drop for Pineapple {fn drop (& mut self) {println! ("Moi Pineapple!")}

fn pineapple_to_apple (ananas: ananas) -> Apple {
banana.actually // ERREUR: impossible de sortir de Pineapple
}

-
Vous recevez ceci parce que vous êtes abonné à ce fil.
Répondez directement à cet e-mail, affichez-le sur GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment-281752949 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/ABxXhgbFgRNzYOsU4c6Gu1KFfwdjDHn3ks5rfHpYgaJpZM4IDXsj
.

@jethrogb
Je voulais mentionner ce problème, mais je ne l'ai pas fait car il a peu de conditions préalables assez spéciales - la structure implémentant Drop devrait avoir un champ public et ce champ ne devrait pas être Copy . Le cas d'union affecte toutes les structures de manière inconditionnelle.

@petrochenkov Je considère que peut-être un argument selon lequel la création d'un syndicat ne devrait pas être sûre :)

@petrochenkov quel est le avant qu'il ne se stabilise?

@pnkfelix
Ce que j'ai supposé, c'est juste de cesser d'exiger #[feature(untagged_unions)] pour ce sous-ensemble, pas de nouvelles fonctionnalités ou autre bureaucratie.
Les syndicats de type FFI sont censés être le type de syndicats le plus souvent utilisé, donc une nouvelle fonctionnalité signifierait une rupture garantie juste avant la stabilisation, ce qui, je suppose, serait ennuyeux.

Je voudrais juste noter que parce que les attributs d'alignement et d'empaquetage n'ont pas encore été implémentés (peu importe stabilisés), je n'ai pas vraiment besoin de stabiliser cela encore.

@ retep998
Emballage? Si vous voulez dire #[repr(packed)] il est actuellement pris en charge sur les unions (contrairement aux attributs align(>1) ).

@petrochenkov #[repr(packed(N))] . Un emballage autre que 1 est un peu nécessaire dans winapi. Ce n'est pas que j'ai besoin que ces choses soient prises en charge spécifiquement par les syndicats, je ne veux tout simplement pas passer à une nouvelle version majeure pour augmenter mon minimum requis de Rust à moins que je ne puisse obtenir toutes ces choses en même temps.

Pour clarifier un peu l'état des lieux ici:

La proposition actuelle du FCP concerne uniquement les unions pures - Copy . C'est le cas, pour autant que je sache, essentiellement sans questions en suspens autre que "Devrions-nous nous stabiliser?" La discussion depuis la motion de FCP a porté sur le nouveau RFC de @petrochenkov .

@nrc et @nikomatsakis , de la discussion sur IRC, je soupçonne que vous êtes tous les deux prêts à cocher vos cases, mais je vous laisse ça ;-)

: bell: Ceci entre maintenant dans sa période de commentaires finale , selon l' examen ci-dessus . :cloche:

@petrochenkov
Il me semble que cela bénéficierait d'une idée que j'ai lancée récemment. (https://internals.rust-lang.org/t/automatic-marker-trait-for-unconditionally-valid-repr-c-types/5054)

Bien qu'il ne soit pas proposé avec les syndicats à l'esprit, le trait Plain comme expliqué (sous réserve de bikeshedding) permettrait à toute union composée uniquement Plain types

Dans le contexte de FFI, être Plain tel que défini est également une exigence de facto pour qu'un tel code soit sain dans de nombreux cas, alors que les cas où les types non- Plain sont utiles sont rares et difficiles à configurer sans encombre.

Existence factuelle d'un trait nommé mis à part, il peut être prudent de diviser cette caractéristique en deux. Avec union non qualifié, appliquant les exigences et permettant une utilisation sans danger, et unsafe union avec des exigences détendues pour le contenu et plus de maux de tête pour les utilisateurs. Cela permettrait de stabiliser l'un ou l'autre sans empêcher l'ajout de l'autre à l'avenir.

@ le-jzr
Cela semble suffisamment orthogonal aux unions.
J'estimerais que l'acceptation de Plain dans Rust dans un proche avenir n'est pas très probable + rendre plus d'accès aux champs syndicaux sûrs est plus ou moins rétrocompatible (pas entièrement à cause des lints), donc je ne retarderais pas les syndicats en raison à lui.

@petrochenkov Je ne suggère pas de retarder les syndicats dans l'attente d'une fermeture sur ma proposition, mais plutôt de considérer l'existence de telles restrictions possibles de son propre chef. Rendre plus d'accès aux champs syndicaux sûrs à l'avenir peut se heurter à des obstacles, car cela crée plus d'incohérence dans la langue. En particulier, faire en sorte que la sécurité d'un accès aux champs varie d'un champ à l'autre semble moche.

D'où ma suggestion de faire la déclaration unsafe union , afin qu'une version sans conditions d'utilisation sûre puisse être introduite plus tard sans ajouter de nouveaux mots clés. Il est à noter qu'une version totalement sûre à utiliser est suffisante pour la plupart des cas d'utilisation.

Edit: J'ai essayé de clarifier ce que je voulais dire.

L'initialisation est un autre endroit où cette possibilité peut informer la conception actuelle et future. Pour une union sans conditions, il est nécessaire que toute la plage de mémoire qui lui est réservée soit remise à zéro. Même avec la version actuelle, garantir que cela réduirait les scénarios potentiels d'UB et faciliterait l'utilisation des syndicats.

La période de commentaires finale est maintenant terminée.

Maintenant que le FCP à fusionner est terminé, quelle est la prochaine étape ici? Ce serait bien de stabiliser cela pour 1.19.

Ce serait génial si quelqu'un pouvait ajouter plus de détails à ce message @rfcbot ( code source ici ). Cela pourrait permettre à une personne non familiarisée avec le processus d'intervenir et de faire avancer les choses.

Le chemin est clair pour obtenir cela en 1.19. Quelqu'un sur le crochet pour ça? cc @joshtriplett

L'optimisation de la mise en page NonZero enum est-elle garantie de s'appliquer via un union ? Par exemple, Option<ManuallyDrop<&u32>> ne doit None comme un pointeur nul. Some(ManuallyDrop::new(uninitialized::<[Vec<Foo>; 10]>())).is_some() ne doit pas lire la mémoire non initialisée.

https://crates.io/crates/nodrop (utilisé dans https://crates.io/crates/arrayvec) a des hacks pour gérer cela.

@SimonSapin
Ceci est actuellement marqué comme une question non résolue dans la RFC .
Dans la mise en œuvre actuelle, ce programme

#![feature(untagged_unions)]

struct S {
    _a: &'static u8
}
union U {
    _a: &'static u8
}

fn main() {
    use std::mem::size_of;
    println!("struct {}", size_of::<S>());
    println!("optional struct {}", size_of::<Option<S>>());
    println!("union {}", size_of::<U>());
    println!("optional union {}", size_of::<Option<U>>());
}

impressions

struct 8
optional struct 8
union 8
optional union 16

, c'est-à-dire que l'optimisation n'est pas effectuée.
cc https://github.com/rust-lang/rust/issues/36394

Il est peu probable que cela fasse 1.19.

@brson

Il est peu probable que cela fasse 1.19.

Le PR de stabilisation est fusionné.

Les syndicats non étiquetés étant désormais livrés en 1.19 (en partie à partir de https://github.com/rust-lang/rust/pull/42068) - y a-t-il quelque chose qui reste sur ce problème ou devrions-nous fermer?

@jonathandturner
Il existe encore tout un monde de syndicats avec des champs non- Copy !
La progression est en grande partie bloquée sur la clarification / documentation RFC (https://github.com/rust-lang/rfcs/pull/1897).

Y a-t-il eu des progrès sur les syndicats avec des champs autres que Copy depuis août? La RFC Unions 1.2 semble bloquée (je suppose à cause de la période impl?)

Autoriser les types ?Sized dans les unions - ne serait-ce que pour les unions d'un seul type - faciliterait la mise en œuvre de https://github.com/rust-lang/rust/issues/47034 :

`` rouille
union ManuallyDrop{
valeur: T
}

@mikeyhew Vous avez seulement vraiment besoin d'exiger qu'au plus un type puisse être non dimensionné.

Je regarde du code Rust en utilisant union s, et je n'ai aucune idée s'il invoque un comportement non défini ou non.

La référence [items :: unions] mentionne uniquement:

Les champs inactifs sont également accessibles (en utilisant la même syntaxe) s'ils sont suffisamment compatibles avec la mise en page de la valeur actuelle conservée par l'union. La lecture de champs incompatibles entraîne un comportement non défini.

Mais je ne trouve pas de définition de «mise en page compatible» ni dans [items :: unions] ni dans [type_system :: type_layout] .

En parcourant les RFC, je n'ai pas été en mesure de trouver une définition de "mise en page compatible" non plus, seulement des exemples à la main de ce qui devrait et ne devrait pas fonctionner dans le RFC 1897: Unions 1.2 (non fusionné).

La RFC1444: unions ne semble permettre de transmuter une union en ses variantes que tant qu'elle n'invoque pas un comportement indéfini, mais je ne trouve nulle part dans la RFC lorsque ce n'est / n'est pas le cas.

Les règles _précises_ qui me disent si un morceau de code utilisant des unions a défini un comportement sont écrites quelque part (et quel est le comportement défini)?

@gnzlbg Pour une première approximation: vous ne pouvez pas accéder au remplissage, ne pouvez pas accéder à une énumération qui contient un discriminant non valide, ne pouvez pas accéder à un bool qui contient une valeur autre que true ou false, ne peut pas accéder à une valeur à virgule flottante invalide ou signalant , et quelques autres choses comme celles-là.

Si vous pointez vers un code spécifique impliquant des syndicats, nous pourrions l'examiner et vous dire s'il fait quelque chose d'indéfini.

Pour une première approximation: vous ne pouvez pas accéder au remplissage, ne pouvez pas accéder à une énumération qui contient un discriminant non valide, ne pouvez pas accéder à un booléen qui contient une valeur autre que true ou false, ne pouvez pas accéder à une valeur à virgule flottante invalide ou de signalisation, et quelques autres choses comme celles-là.

En fait, le dernier consensus est que la lecture de flottants arbitraires est très bien (# 46012).

J'ajouterais une autre exigence: les variantes de l'union source et cible sont #[repr(C)] , de même que tous leurs champs (et récursivement) s'ils sont des structs.

@Amanieu je me tiens corrigé, merci.

Alors je suppose que les règles ne sont écrites nulle part?

Je cherche comment utiliser stdsimd avec sa nouvelle interface. À moins que nous ne stabilisions certains extras avec lui, il faudra utiliser des unions pour faire du jeu de mots avec certains des types simd, comme ceci:

https://github.com/rust-lang-nursery/stdsimd/blob/03cb92ddce074a5170ed5e5c5c20e5fa4e4846c3/coresimd/src/x86/test.rs#L17

AFAIK écrire un champ union et en lire un autre est très similaire à l'utilisation de transmute_copy , avec les mêmes restrictions. Le fait que ces restrictions soient encore un peu nébuleuses n'est pas propre au syndicat.

D'ailleurs, la fonction que vous avez liée pourrait simplement utiliser transmute::<__m128d, [f64; 2]> . Bien que la version de l'union soit plus agréable, au moins une fois la transmutation qui s'y trouve actuellement est supprimée: cela pourrait être juste A { a }.b[idx] .

@rkruppe J'ai rempli un problème clippy pour ajouter cette charpie: https://github.com/rust-lang-nursery/rust-clippy/issues/2361

la fonction que vous avez liée pourrait simplement utiliser transmute :: <__ m128d i = "8">

Je suppose que les règles que je recherche sont quand la transmutation appelle-t-elle alors un comportement indéfini (alors je vais les chercher).

Je pense que cela m'aurait aidé si la référence linguistique sur les syndicats avait spécifié les règles pour les syndicats en termes de transmutation (même si les règles de transmutation ne sont pas encore claires à 100%) au lieu de simplement mentionner «compatibilité de mise en page» et de la laisser à ce. Le saut de la "compatibilité de mise en page" à "si la transmutation n'invoque pas un comportement indéfini, alors les types sont compatibles avec la mise en page et peuvent être accédés via la punition de type" n'était pas évident pour moi.

Pour être clair, la transmutation [_copy] n'est pas "plus primitive" que les unions. En fait, transmute_copy est littéralement juste un pointeur as casts plus ptr::read . transmute également besoin de mem::uninitialized (obsolète) ou MaybeUninitialized (une union) ou quelque chose comme ça, et est implémenté comme intrinsèque pour l'efficacité, mais il se résume aussi à un type- punir memcpy. La principale raison pour laquelle j'ai établi le lien avec la transmutation est qu'elle est plus ancienne et historiquement surestimée.Par conséquent, nous avons actuellement plus d'écrits et de connaissances folkloriques qui se concentrent spécifiquement sur la transmutation. Le vrai concept sous-jacent, qui dicte ce qui est valide et ce qui ne l'est pas (et ce qu'une spécification décrirait), est de savoir comment les valeurs sont stockées en mémoire sous forme d'octets et quelles séquences d'octets doivent être lues sous forme de types.

Correction: transmute n'a pas réellement besoin de stockage non initialisé (via des intrinsèques, ou des unions, ou autre). Mis à part l'efficacité, vous pouvez faire quelque chose comme ceci (non testé, peut contenir des fautes de frappe embarrassantes):

fn transmute<T, U>(x: T) -> U {
    assert!(size_of::<T>() == size_of::<U>());
    let mut bytes = [0u8; size_of::<U>()];
    ptr::write(bytes.as_mut_ptr() as *mut T, x);
    mem::forget(x);
    ptr::read(bytes.as_ptr() as *const U)
}

La seule partie "magique" de la transmutation est qu'elle peut contraindre les paramètres de type à être de taille égale au moment de la compilation .

La référence et et Unions 1.2 RFC sont volontairement vagues à ce sujet car les règles de transmutation en général ne sont pas établies.
L'intention était "pour repr(C) unions voir les spécifications ABI tierces, pour repr(Rust) compatibilité de mise en page des unions n'est généralement pas spécifiée (sauf si elle l'est)".

Est-il trop tard pour revoir la sémantique du contrôle de chute des unions avec des champs de dépôt?

Le problème original est que l'ajout de ManuallyDrop rendu Josephine défectueuse, car elle reposait (plutôt mal) sur des valeurs empruntées en permanence sans que leur magasin de sauvegarde ne soit récupéré sans avoir d'abord exécuté leur destructeur.

Un exemple simplifié se trouve sur https://play.rust-lang.org/?gist=607e2dfbd51f4062b9dc93d149815695&version=nightly. L'idée est qu'il existe un type Pin<'a, T> , avec une méthode pin(&'a self) -> &'a T dont la sécurité repose sur l'invariant "après avoir appelé pin.pin() , si la mémoire supportant la broche est un jour récupérée, alors le destructeur de la broche doit avoir été exécuté ".

Cet invariant a été maintenu par Rust jusqu'à l'ajout de #[allow(unions_with_drop_fields)] , et utilisé par ManuallyDrop https://doc.rust-lang.org/src/core/mem.rs.html#949.

L'invariant serait restauré si le vérificateur de suppression considérait les unions avec des champs de suppression pour avoir une impl. Drop. Il s'agit d'un changement radical, mais je doute que tout code à l'état sauvage repose sur la sémantique actuelle.

Conversation IRC: https://botbot.me/mozilla/rust-lang/2018-02-01/?msg=96386869&page=3

Numéro de Joséphine: https://github.com/asajeffrey/josephine/issues/52

cc: @nox @eddyb @pnkfelix

Le problème initial est que l'ajout de ManuallyDrop a rendu Josephine malsaine, car elle reposait (plutôt mal) sur des valeurs empruntées en permanence sans que leur magasin de sauvegarde ne soit récupéré sans exécuter au préalable leur destructeur.

Les destructeurs ne sont pas garantis de fonctionner. Rust ne le garantit pas. Il essaie, mais, par exemple, std::mem::forget été transformé en une fonction sûre.

L'invariant serait restauré si le vérificateur de suppression considérait les unions avec des champs de suppression pour avoir une impl. Drop. Il s'agit d'un changement radical, mais je doute que tout code à l'état sauvage repose sur la sémantique actuelle.

Les syndicats sont dangereux en grande partie parce que vous ne pouvez pas savoir quel domaine du syndicat est valide. L'union ne peut pas avoir une implémentation automatique Drop ; si vous vouliez un tel impl, vous auriez besoin de l'écrire manuellement, en tenant compte de tous les moyens que vous devez savoir si le champ union avec un Drop impl est valide.

Une clarification ici: je ne crois pas que nous devrions jamais autoriser les unions avec des champs Drop par défaut sans au moins une charpie d'avertissement par défaut, si ce n'est une lint d'erreur par défaut. unions_with_drop_fields ne devrait pas disparaître dans le cadre du processus de stabilisation.

EDIT: oups, ne voulait pas dire "fermer et commenter".

@joshtriplett oui, Rust ne garantit pas que les destructeurs s'exécuteront, mais il est arrivé (avant la version 1.19) de maintenir l'invariant selon lequel les valeurs empruntées en permanence n'auraient que leur mémoire récupérée si le destructeur s'exécutait. Cela est même vrai en présence de mem::forget , car vous ne pouvez pas l'appeler sur une valeur empruntée en permanence.

C'est ce sur quoi Joephine s'appuyait plutôt mal, mais ce n'est plus vrai à cause de la façon dont le vérificateur de baisse traite unions_with_drop_fields .

Ce serait bien si allow(unions_with_drop_fields) était considéré comme une annotation non sécurisée, ce ne serait pas un changement radical, AFAICT, il faudrait juste deny(unsafe_code) pour vérifier allow(unions_with_drop_fields) .

@asajeffrey J'essaie toujours de comprendre la chose Pin ... donc, si je suis correctement l'exemple, la raison pour laquelle cela "fonctionne" est que fn pin(&'a Pin<'a, T>) -> &'a T force l'emprunt à durer comme tant que la durée 'a vie

C'est une observation intéressante! Je n'étais pas au courant de cette astuce. Mon instinct est que cela fonctionne "par accident", c'est-à-dire que Rust sécuritaire ne fournit pas un moyen d'empêcher le destructeur de fonctionner, mais cela ne fait pas partie du "contrat". Notamment, https://doc.rust-lang.org/nightly/reference/behavior-consemed-undefined.html ne répertorie pas les fuites.

OMI, peu importe si cela fonctionne par accident ou exprès. Il n'y avait aucun moyen d'éviter que Drop s'exécute avec cette astuce avant ManuallyDrop existant (qui nécessite l'implémentation d'un code non sécurisé), et maintenant nous ne pouvons plus compter sur cela.

L'ajout de ManuallyDrop fondamentalement tué ce comportement soigné de Rust et dire qu'il n'aurait pas dû être invoqué en premier lieu me semble être un raisonnement circulaire. Si ManuallyDrop ne permettait pas d'appeler Pin::pin , y aurait-il un autre moyen de rendre l'appel Pin::pin défectueux? Je ne pense pas.

Je ne pense pas que nous puissions nous engager à préserver toutes les garanties que rustc fournit accidentellement en ce moment. Nous n'avons aucune idée de ce que peuvent être ces garanties, donc nous stabiliserions un porc en un clin d'œil (d'accord, j'espère que cet idiome a du sens ... c'est ce que le dictionnaire me dit correspond à mon idiome de langue maternelle, qui se traduirait littéralement par " le chat dans le sac ";) - ce que je veux dire, c'est que nous n'aurions aucune idée de ce que nous stabiliserions).

En outre, il s'agit d'une arme à deux tranchants - chaque garantie supplémentaire que nous décidons de fournir est quelque chose dont le code non sécurisé doit prendre en charge. Ainsi, la découverte d'une nouvelle garantie peut tout aussi bien casser le code unsafe existant (assis silencieusement quelque part sur crates.io sans en être conscient) que permettre un nouveau code unsafe (ce dernier était le cas ici).

Par exemple, il est tout à fait concevable que les durées de vie lexicales permettent un code non sûr qui est brisé par des durées de vie non lexicales. Actuellement, toutes les durées de vie sont bien imbriquées, peut-être existe-t-il un moyen pour le code non sécurisé d'exploiter cela? Ce n'est qu'avec des durées de vie non lexicales qu'il peut y avoir des durées de vie qui se chevauchent, mais aucune n'est incluse dans l'autre. Cela fait-il de NLL un changement radical? J'espère que non!

Si ManuallyDrop ne permettait pas d'appeler Pin :: pin, y aurait-il un autre moyen de rendre l'appel Pin :: pin défectueux? Je ne pense pas.

Avec unsafe code, il y en aurait. Donc, en déclarant ce son astuce Pin , vous déclarez un code non sûr unsound qui serait valable si nous décidons que ManuallyDrop est correct.

Ce que nous avons décrit est une manière très ergonomique d'intégrer Rust avec les GC. Ce que j'essaie de dire, c'est que cela me semble mal de nous dire que c'était juste un accident qui a fonctionné et que nous devrions l'oublier, alors que je ne trouve aucun cas d'utilisation pour ne pas contraindre les syndicats avec Drop comme décrit par @asajeffrey ici, et quand c'est vraiment la seule verrue qui brise Joséphine.

Je serai heureux de l'oublier si quelqu'un peut montrer qu'il n'est pas sain même sans ManuallyDrop .

Ce que j'essaie de dire, c'est que ça me semble mal de nous dire que c'était juste un accident que ça a marché

Je ne vois aucune indication que cette astuce ait jamais été "conçue", donc je pense qu'il est tout à fait juste de l'appeler un accident.

et que nous devrions l'oublier

J'aurais dû préciser que cette partie n'est que mon instinct personnel. Je pense que cela pourrait aussi être un point d'action raisonnable pour déclarer ceci un "accident heureux" et en faire une garantie - si nous sommes raisonnablement sûrs que tous les autres codes unsafe respectent cette garantie, et que fournir cette garantie est plus important que le cas d'utilisation ManuallyDrop . C'est un compromis, similaire à leakpocalypse, où nous ne pouvons pas manger notre gâteau et l'avoir aussi (nous ne pouvons pas avoir à la fois Rc avec son API actuelle et le drop - basé sur des threads étendus; nous ne pouvons pas avoir à la fois ManuallyDrop et Pin ), nous devons donc prendre une décision de toute façon.

Cela dit, j'ai du mal à exprimer la garantie réelle fournie ici d'une manière précise, ce qui me fait personnellement pencher davantage vers le côté " ManuallyDrop c'est bien".

si nous sommes raisonnablement sûrs que tous les autres codes unsafe respectent cette garantie, et que fournir cette garantie est plus important que le cas d'utilisation ManuallyDrop . C'est un compromis, similaire à leakpocalypse, où nous ne pouvons pas manger notre gâteau et l'avoir aussi (nous ne pouvons pas avoir à la fois Rc avec son API actuelle et le drop - basé sur des threads étendus; nous ne pouvons pas avoir à la fois ManuallyDrop et Pin ), nous devons donc prendre une décision de toute façon.

Très bien, je suis tout à fait d'accord avec cela. Notez que si nous considérons ce que @asajeffrey a décrit comme un comportement indéfini à la fin, cela peut ramener une API de thread à portée basée sur drop .

Autant que je sache, la proposition d'Alan n'est pas de supprimer ManuallyDrop , seulement de faire supposer à dropck qu'il (et d'autres unions avec des champs Drop ) a un destructeur. (Que les destructeurs ne font rien, mais leur simple existence affecte les programmes que dropck accepte ou rejette.)

Je serai heureux de l'oublier si quelqu'un peut montrer qu'il n'est pas sain même sans ManuallyDrop.

Je ne sais pas si cela se qualifie, mais voici ma première tentative: Une implémentation idiote de quelque chose comme ManuallyDrop qui fonctionne dans pre- union Rust.

pub mod manually_drop {
    use std::mem;
    use std::ptr;
    use std::marker::PhantomData;

    pub struct ManuallyDrop<T> {
        data: [u8; 32],
        phantom: PhantomData<T>,
    }

    impl<T> ManuallyDrop<T> {
        pub fn new(x: T) -> ManuallyDrop<T> {
            assert!(mem::size_of::<T>() <= 32);
            let mut data = [0u8; 32];
            unsafe {
                ptr::copy(&x as *const _ as *const u8, &mut data[0] as *mut _, mem::size_of::<T>());
            }
            mem::forget(x);
            ManuallyDrop { data, phantom: PhantomData }
        }

        pub fn deref(&self) -> &T {
            unsafe {
                &*(&self.data as *const _ as *const T)
            }
        }
    }
}

(Ouais, je dois probablement faire plus de travail pour obtenir le bon alignement, mais cela pourrait également être fait en sacrifiant quelques octets.)
Aire de jeu montrant ces pauses Pin : https://play.rust-lang.org/?gist=fe1d841cedb13d45add032b4aae6321e&version=nightly

C'est ce que je voulais dire par épée à deux tranchants ci-dessus - pour autant que je sache, mon ManuallyDrop respecte toutes les règles que nous avons édictées. Donc, nous avons deux morceaux de code unsafe incompatibles - ManuallyDrop et Pin . Qui a «raison»? Je dirais que Pin repose sur des garanties que nous n'avons jamais faites et donc c'est «faux» ici, mais c'est un jugement, pas une preuve.

Maintenant c'est intéressant. Dans certaines versions de nos trucs d'épinglage, Pin::pin prend un &'this mut Pin<'this, T> , mais il ne serait pas déraisonnable pour votre ManuallyDrop d'avoir un DerefMut impl, à droite ?

Voici un terrain de jeu qui montre que @RalfJung (sans surprise) casse toujours Pin avec une méthode &mut -taking pin .

https://play.rust-lang.org/?gist=5057570b54952e245fa463f8d7719663&version=nightly

il ne serait pas déraisonnable pour votre ManuallyDrop d'avoir un impl DerefMut, non?

Ouais, je viens d'ajouter l'API dont j'avais besoin pour cet exemple. L'évident deref_mut devrait fonctionner correctement.

Pour autant que je sache, la proposition d'Alan n'est pas de supprimer ManuallyDrop, mais seulement de faire supposer à dropck qu'il (et d'autres unions avec des champs Drop) a un destructeur. (Que les destructeurs ne font rien, mais leur simple existence affecte les programmes que dropck accepte ou rejette.)

Ah, j'avais raté ça; Désolé pour ça. L'ajout de ce qui suit à mon exemple le maintient cependant:

    unsafe impl<#[may_dangle] T> Drop for ManuallyDrop<T> {
        fn drop(&mut self) {}
    }

Seulement si je supprime le #[may_dangle] Rust le rejette. Donc, à tout le moins, nous devrions trouver une règle que le code ci-dessus enfreint - simplement dire "il existe un code avec lequel nous voulons être sain et avec lequel il est incompatible" est un mauvais appel parce que cela le rend quasiment impossible de regarder du code et de vérifier s'il est sain.


Je pense que ce qui m'inquiète le plus à propos de cette "garantie accidentelle", c'est que je ne vois pas une seule bonne raison pour laquelle cela fonctionne. La façon dont les choses sont câblées dans Rust rend cela cohérent, mais dropck a été ajouté non pas pour empêcher les fuites, mais pour éviter les références erronées aux données mortes (un problème courant dans les destructeurs). Le raisonnement pour que Pin fonctionne n'est pas basé sur "voici un mécanisme dans le compilateur Rust, ou une garantie de système de type, qui dit assez clairement que les données empruntées en perma ne peuvent pas être divulguées" - il est plutôt basé sur "Nous avons fait de gros efforts et nous n'avons pas été en mesure de divulguer des données empruntées en permanence, donc nous pensons que ce n'est pas grave". Compter sur cela pour la solidité me rend assez nerveux. EDIT: Le fait que dropck soit impliqué me rend encore plus nerveux car cette partie du compilateur a une histoire de bugs de solidité désagréables. La raison pour laquelle cela fonctionne semble être que les emprunts permanents sont en contradiction avec les drop sûrs. Cela semble vraiment être "un raisonnement basé sur une analyse de cas exhaustive de ce que l'on peut faire avec des données perma-empruntées".

Maintenant, pour être honnête, on pourrait dire des choses similaires à propos de la mutabilité intérieure - il se trouve que permettre des modifications via des références partagées fonctionne en toute sécurité dans certains cas, si nous choisissons la bonne API. Cependant, faire ce travail nécessitait en fait un support explicite dans le compilateur ( UnsafeCell ) car il entre en conflit avec les optimisations, et il y a du code dangereux qui serait sain sans mutabilité intérieure mais qui ne serait pas sonore avec la mutabilité intérieure. Une autre différence est que la mutabilité intérieure était un objectif de conception depuis le début (ou depuis très tôt - c'est bien avant mon passage dans la communauté Rust), ce qui n'est pas le cas pour "perma-borrowed don't get leaked". Et enfin, pour la mutabilité intérieure, je pense qu'il y a une assez bonne histoire à propos du "partage rend la mutation dangereuse , mais pas impossible , et l'API des références partagées dit simplement que vous n'obtenez pas la mutabilité en général, mais n'exclut pas d'autoriser plus d'opérations pour des types », ce qui donne une image globale cohérente. Bien sûr, j'ai passé beaucoup de temps à réfléchir à des références partagées, alors il y a peut-être une image tout aussi cohérente pour le problème en question dont je ne suis tout simplement pas au courant.

Les fuseaux horaires sont amusants, je viens de me lever! Il semble y avoir deux problèmes ici (les invariants en général, et dropck en particulier), je vais donc les mettre dans des commentaires séparés ...

@RalfJung : oui, c'est un problème concernant les invariants maintenus par Rust unsafe. Pour toute version de Rust + std, il existe plus d'un choix d'invariant I qui est maintenu en utilisant le raisonnement de la garantie de confiance. Et en effet, il peut y avoir deux bibliothèques L1 et L2 , qui ont choisi incompatible I1 et I2 , de sorte que Rust + L1 est sûr et Rust + L2 est sûr, mais Rust + L1 + L2 n'est pas sûr.

Dans ce cas, L1 est ManuallyDrop et L2 est Josephine , et il est assez clair que ManuallyDrop va gagner puisque c'est maintenant dans std , qui a des contraintes de compatibilité ascendante beaucoup plus fortes que Josephine.

Fait intéressant, les directives sur https://doc.rust-lang.org/nightly/reference/behavior-consemed-undefined.html sont écrites comme suit: "Il est de la responsabilité du programmeur lors de l'écriture de code non sécurisé qu'il n'est pas possible de laisser du code sécurisé présentent ces comportements: ... "c'est-à-dire que c'est une propriété contextuelle (pour tous les contextes sûrs, C, C [P] ne peut pas se tromper) et dépend donc de la version (puisque la v1.20 de Rust + std est plus sûre contextes que v1.18). En particulier, je prétendrais que l'épinglage satisfaisait en fait cette contrainte pour Rust avant la 1.20, car il n'y avait pas de contexte sûr C st C [Pinning] va mal.

Cependant, il ne s'agit que de juristes dans les casernes, je pense que tout le monde convient qu'il y a un problème avec cette définition contextuelle, d'où toutes les discussions sur les directives de code non sécurisées.

Si rien d'autre, je pense que l'épinglage a montré un exemple intéressant d'invariants accidentels qui tournent mal.

La chose particulière que les unions non étiquetées (et donc ManuallyDrop ) ont fait était dans l'interaction avec le vérificateur de baisse, en particulier ManualDrop agit comme sa defn est:

unsafe impl<#[may_dangle] T> Drop for ManuallyDrop<T> { ... }

et ensuite vous pouvez avoir une conversation pour savoir si cela est autorisé ou non :) En effet, cette conversation se déroule dans le fil may_dangle à partir de https://github.com/rust-lang/rust/issues/ 34761 # issuecomment -362375924

@RalfJung votre code montre un cas d'angle intéressant, où le type d'exécution pour data est T , mais son type à la compilation est [u8; N] . Quel type compte en ce qui concerne may_dangle ?

Fait intéressant, les directives sur https://doc.rust-lang.org/nightly/reference/behavior-consemed-undefined.html sont écrites comme suit: "Il est de la responsabilité du programmeur lors de l'écriture de code non sécurisé qu'il n'est pas possible de laisser du code sécurisé présentent ces comportements: ... "c'est-à-dire que c'est une propriété contextuelle

Ah, intéressant. Je suis d'accord que ce n'est clairement pas suffisant - cela ferait sonner les threads d'origine. Pour être significatif, cela doit (au moins) spécifier l'ensemble de code non sécurisé que le code sécurisé est autorisé à appeler.

Personnellement, je pense qu'une meilleure façon de spécifier cela est de donner les invariants à maintenir. Mais je suis clairement biaisé ici, car la méthodologie que j'utilise pour prouver des choses sur Rust nécessite un tel invariant. ;)

Je suis un peu surpris que la page ne contienne pas une sorte d'avertissement d'être préliminaire; nous ne savons pas encore vraiment quelle sera exactement la limite - comme le montre cette discussion. Nous avons besoin d'un code non sécurisé pour au moins faire ce que dit ce document, mais nous devons probablement en exiger davantage.

Par exemple, les limites d'un comportement indéfini et ce que le code non sécurisé peut faire ne sont pas les mêmes. Voir https://github.com/nikomatsakis/rust-memory-model/issues/44 pour une discussion récente sur ce sujet: la duplication d'un &mut T pour mem::size_of::<T>() == 0 ne conduit à aucun comportement indéfini directement, et pourtant il est clairement considéré comme illégal pour le code dangereux à faire. La raison en est qu'un autre code dangereux peut s'appuyer sur le respect de sa discipline de propriété, et la duplication de choses enfreint cette discipline.

Si rien d'autre, je pense que l'épinglage a montré un exemple intéressant d'invariants accidentels qui tournent mal.

Oh, certainement. Et je me demande ce que nous pouvons faire pour éviter cela à l'avenir? Peut-être mettre un gros avertissement sur https://doc.rust-lang.org/nightly/reference/behavior-consemed-undefined.html en disant "simplement parce qu'un invariant se trouve dans rustc + libstd, ne signifie pas que du code non sécurisé peut comptez-y, voici quelques invariants sur lesquels vous pouvez compter "?

@RalfJung oui, je ne pense pas que quiconque soit amoureux de la définition contextuelle de «l'exactitude», principalement parce qu'elle est fragile par rapport à la puissance d'observation des contextes. Je serais beaucoup plus heureux avec une définition sémantique en termes d'invariants.

La seule chose que je demanderais, c'est de nous donner une marge de manœuvre et de définir deux invariants pour le raisonnement de la garantie de confiance (le code peut s'appuyer sur R et devrait garantir G, où G implique R). De cette façon, il y a de la place pour renforcer R et affaiblir G. Si nous n'avons qu'un seul invariant (c'est-à-dire R = G), nous sommes bloqués de ne jamais pouvoir les changer!

La vérification de constante ne contient actuellement pas de champs d'union de cas spéciaux: (cc @solson @ oli-obk)

union Transmute<T, U> { from: T, to: U }

const SILLY: () = unsafe {
    (Transmute::<usize, Box<String>> { from: 1 }.to, ()).1
};

fn main() {
    SILLY
}

Le code ci-dessus produit l'erreur d'évaluation miri "appelant non-const fn std::ptr::drop_in_place::<(std::boxed::Box<std::string::String>, ())> - shim(Some((std::boxed::Box<std::string::String>, ()))) ".

Le changer pour forcer le type de .to à être observé par le vérificateur de const:

const fn id<T>(x: T) -> T { x }

const SILLY: () = unsafe {
    (id(Transmute::<usize, Box<String>> { from: 1 }.to), ()).1
};

résultats dans "les destructeurs ne peuvent pas être évalués à la compilation".

Le code d'implémentation pertinent est ici (en particulier l'appel restrict ):
https://github.com/rust-lang/rust/blob/5e4603f99066eaf2c1cf19ac3afbac9057b1e177/src/librustc_mir/transform/qualify_consts.rs#L557

Une meilleure analyse de # 41073 a révélé que la sémantique du moment où les destructeurs s'exécutent lors de l'assignation à des sous-champs d'unions n'est pas suffisamment prête pour la stabilisation. Consultez ce numéro pour plus de détails.

Est-il réaliste d'exclure entièrement les types Drop dans les unions, et d'implémenter ManuallyDrop séparément (en tant qu'élément lang)? D'après ce que je peux dire, ManuallyDrop semble être la plus grande motivation pour Drop dans les syndicats, mais c'est un cas très particulier.

En l'absence d'un trait positif "no drop", nous pourrions alors dire qu'une union est bien formée si chaque champ est soit Copy soit de la forme ManuallyDrop<T> . Cela contournerait entièrement toutes les complications liées à la suppression lors de l'attribution de champs d'union (où il semble que chaque solution possible sera pleine de footguns surprenants), et le ManuallyDrop est un marqueur clair pour les programmeurs qu'ils doivent gérer Drop eux-mêmes ici. (La vérification pourrait être plus intelligente, par exemple, elle pourrait traverser les types de produits et les types nominaux qui sont déclarés dans la même caisse. Bien sûr, avoir une manière positive de dire "ce type n'implémentera jamais Drop " serait plus gentil.)


La liste de contrôle dans le premier article ne mentionne pas les unions non dimensionnées, ni la RFC --- mais nous avons toujours

Ceci entre en conflit avec la façon dont les unions sont parfois utilisées en C, qui est un "point d'extension" (IIRC @joshtriplett était celui qui le mentionnait à toutes les mains): Un fichier d'en-tête peut déclarer 3 variantes pour une union, mais ceci est considéré comme compatible avec l'ajout de variantes plus tard (à condition que cela n'augmente pas la taille de l'union). L'utilisateur de la bibliothèque promet de ne pas toucher aux données de l'union si la balise (assise ailleurs) indique qu'il ne connaît pas la variante courante. Surtout, si vous ne connaissez une seule variante, cela ne signifie pas qu'il n'y a qu'une seule variante!

Le contrôle pourrait être plus intelligent, par exemple, il pourrait traverser les types de produits et les types nominaux qui sont déclarés dans la même caisse.

Ce prédicat existe déjà, mais est conservateur dans les génériques car il n'y a pas de trait à lier.
Vous pouvez y accéder via std::mem::needs_drop (qui utilise un intrinsèque que rustc implémente).

@eddyb prendra needs_drop en tenant compte de la compatibilité ascendante, ou examinera-t-il volontiers d'autres créations pour déterminer si leurs types implémentent Drop ? Le but ici est d'avoir une vérification qui ne rompra jamais avec les changements compatibles semver, où par exemple l'ajout d'un impl Drop à une structure sans paramètres de type ou de durée de vie et seuls les champs privés est compatible semver.

@RalfJung

Ceci entre en conflit avec la façon dont les unions sont parfois utilisées en C, qui est un "point d'extension" (IIRC @joshtriplett était celui qui le mentionnait à toutes les mains): Un fichier d'en-tête peut déclarer 3 variantes pour une union, mais ceci est considéré comme compatible avec l'ajout de variantes plus tard (à condition que cela n'augmente pas la taille de l'union). L'utilisateur de la bibliothèque promet de ne pas toucher aux données de l'union si la balise (assise ailleurs) indique qu'il ne connaît pas la variante courante. Surtout, si vous ne connaissez qu'une seule variante, cela ne signifie pas qu'il n'y a qu'une seule variante!

C'est un cas très particulier.
Cela n'affecte que les unions de style C (donc il n'y a pas de destructeurs et tout est Copy , exactement le sous-ensemble disponible sur stable) généré à partir des en-têtes C.
Nous pouvons facilement ajouter un champ _dummy: () ou _future: () à de telles unions et continuer à bénéficier d'un modèle "enum" plus sûr par défaut. Un syndicat FFI étant un «point d'extension» est quelque chose qui doit de toute façon être bien documenté.

Le 17 avril 2018 10:08:54 PDT, Vadim Petrochenkov [email protected] a écrit:

Nous pouvons facilement ajouter un champ _dummy: () ou _future: () à de telles unions
et continuez à bénéficier de notre modèle "enum" plus sûr par défaut.

J'ai vu des gens parler de traiter les syndicats comme des énumérations dont nous ne connaissons tout simplement pas les discriminants, mais à ma connaissance, je ne connais aucun modèle ou traitement réel à leur égard. Dans la discussion initiale, même les syndicats non FFI voulaient le modèle des «variantes multiples valides à la fois», y compris les cas d'utilisation motivants pour vouloir des syndicats non FFI.

Ajouter une variante () à une union ne devrait rien changer, et les unions ne devraient

Syndicat FFI
être un "point d'extension" est quelque chose qui doit être bien documenté
en tous cas.

Nous devrions certainement documenter la sémantique aussi précisément que possible.

@RalfJung Non, il se comporte comme auto trait s, exposant tous les détails internes.

Il y a actuellement des discussions sur les "champs actifs" et la baisse des syndicats sur https://github.com/rust-lang/rust/issues/41073#issuecomment -380291471

Les syndicats devraient continuer à être un sac de bits, Rust n'ayant aucune idée de ce qu'ils pourraient contenir à un moment donné jusqu'à ce qu'un code dangereux y accède.

C'est exactement ainsi que je m'attendrais à ce que les syndicats fonctionnent. Ils sont une fonctionnalité avancée pour obtenir des performances supplémentaires et interagir avec le code C, où il n'y a pas de destructeurs.

Pour moi, si vous voulez supprimer le contenu d'une union, vous devriez ~ avoir à lancer / transmuter (peut-être qu'il ne peut pas être transmuté car il pourrait être plus gros avec des bits inutilisés à la fin pour une autre variante) au type vous voulez déposer ~ prenez des pointeurs vers les champs à supprimer et utilisez std::ptr::drop_in_place , ou utilisez la syntaxe de champ pour extraire la valeur.

Si je ne savais rien des syndicats, voici comment je m'attendrais à ce qu'ils fonctionnent:

Exemple - Représenter mem::uninitialized comme une union

pub union MaybeValid<T> {
    valid: T,
    invalid: ()
}

impl<T> MaybeValid<T> {
    #[inline] // this should optimize to a no-op
    pub fn from_valid(valid: T) -> MaybeValid<T> {
        MaybeValid { valid }
    }

    pub fn invalid() -> MaybeValid<T> {
        MaybeValid { invalid: () }
    }

   pub fn zeroed() -> MaybeValid<T> {
        // do whatever is necessary here...
        unimplemented!()
    }
}

fn example() {
    let valid_data = MaybeValid::from_valid(1_u8);
    // Destructor of a union always does nothing, but that's OK since our 
    // data type owns nothing.
    drop(valid_data);
    let invalid_data = MaybeValid::invalid();
    // Destructor of a union again does nothing, which means it needs to know 
    // nothing about its surroundings, and can't accidentally try to free unused memory.
    drop(invalid_data);
    let valid_data = MaybeValid::from_valid(String::from("test string"));
    // Now if we dropped `valid_data` we would leak memory, since the string 
    // would never get freed. This is already possible in safe rust using e.g. `Rc`. 
    // `union` is a similarly advanced feature to `Rc` and so new users are 
    // protected by the order in which concepts are introduced to them. This is 
    // still "safe" even though it leaks because it cannot trigger UB.
    //drop(valid_data)
    // Since we know that our union is of a particular form, we can safely 
    // move the value out, in order to run the destructor. I would expect this 
    // to fail if the drop method had run, even though the drop method does 
    // nothing, because that's the way stuff works in rust - once it's dropped
    // you can't use it.
    let _string_to_drop = unsafe { valid_data.valid };
    // No memory leak and all unsafety is encapsulated.
}

Je vais poster ceci puis le modifier pour ne pas perdre mon travail.
EDIT @SimonSapin façon de supprimer des champs.

si vous voulez supprimer le contenu d'une union, vous devriez avoir à le caster / transmuter (peut-être qu'il ne peut pas être transmuté car il pourrait être plus gros avec des bits inutilisés à la fin pour une autre variante) vers le type que vous voulez supprimer , ou utilisez la syntaxe du champ pour extraire la valeur

(Si ce n'est que pour le laisser tomber, il n'est pas nécessaire d'extraire la valeur dans le sens de le déplacer, vous pouvez prendre un pointeur vers l'un des champs et utiliser std::ptr::drop_in_place .)

En relation: Pour les constantes, je soutiens actuellement qu'au moins un champ d'une union à l'intérieur d'une constante doit être correct: https://github.com/rust-lang/rust/pull/51361 (si vous avez un champ ZST qui est toujours vrai)

Je vais poster ceci puis le modifier pour ne pas perdre mon travail.

Veuillez noter que les modifications ne sont pas reflétées dans les notifications par e-mail. Si vous souhaitez apporter des modifications importantes à votre commentaire, envisagez de faire un nouveau commentaire à la place ou en complément.

@derekdreery (et tout le monde) Je serais intéressé par vos commentaires pour https://internals.rust-lang.org/t/pre-rfc-unions-drop-types-and-manuallydrop/8025

En relation: Pour les constantes, je soutiens actuellement qu'au moins un champ d'une union dans une constante doit être correct: # 51361

J'ai vu l'implémentation mais pas vu l'argument. ;)

Eh bien ... l'argumentation en "ne vérifiant pas du tout semblait bizarre".

Je serai heureux de mettre en œuvre tout schéma que nous proposons dans le vérificateur de const, mais mon intuition a toujours été qu'une variante d'un syndicat doit être entièrement correcte.

Sinon, les unions ne sont qu'un joli moyen de spécifier un type avec une taille et un alignement spécifiques et une certaine commodité générée par le compilateur pour la transmutation entre un ensemble fixe de types.

Je pense que les syndicats sont des «sacs de morceaux non interprétés» avec un moyen pratique d'y accéder. Je ne vois rien de bizarre à ne pas les vérifier.

AFAIK il y a en fait quelques cas d'utilisation @joshtriplett mentionnés au all-hands de Berlin où la première moitié de l'union correspond à un champ et la seconde moitié correspond à un autre champ.

Je pense que les syndicats sont des «sacs de morceaux non interprétés» avec un moyen pratique d'y accéder. Je ne vois rien de bizarre à ne pas les vérifier.

J'ai toujours pensé que cette interprétation va quelque peu à l'encontre de l'esprit de la langue.
Dans d'autres endroits, nous utilisons l'analyse statique pour empêcher les pistolets de pied, vérifiez que les valeurs non initialisées ou empruntées ne sont pas accessibles, mais pour les unions, l'analyse est soudainement désactivée, veuillez tirer.

Je vois cela exactement comme le but de union . Je veux dire que nous avons également des pointeurs bruts où toutes les analyses sont désactivées. Les syndicats offrent un contrôle total sur la disposition des données, tout comme les pointeurs bruts fournissent un contrôle total sur l'accès à la mémoire. Les deux vont au détriment de la sécurité.

De plus, cela rend union simple . Je pense qu'être simple est important, et encore plus important lorsque du code dangereux est impliqué (ce qui sera toujours le cas avec les syndicats). Nous ne devrions accepter ici une complexité supplémentaire que si elle offre des avantages tangibles.

Nous ne pensons pas devoir payer ce coût pour les syndicats, car le modèle du sac de bits ne donne pas de nouvelles opportunités par rapport au modèle enum-with-unknown-variant.

La propriété en question ici est au moins autant un fardeau pour un code dangereux à respecter que c'est une sauvegarde. Il n'y a pas d'analyse statique qui puisse empêcher toutes les erreurs qui pourraient briser cette propriété puisque nous voulons utiliser des unions pour le type unsafe punning 1 , donc "enum with unknown variant" signifie vraiment que les unions de gestion de code doivent faire très attention à la façon dont elles écrivent dans le union ou risque UB instantané, sans vraiment réduire l'insécurité impliquée dans la lecture de l'union, car la lecture nécessite déjà de savoir (à travers des canaux que le compilateur ne comprend pas) que les bits sont valides pour la variante que vous lisez. Nous ne pouvons en fait avertir les utilisateurs qu'une union qui n'est valide pour aucune de ses variantes est lorsqu'elle s'exécute sous miri, pas dans la grande majorité des cas où cela se produit au moment de l'exécution.

1 Par exemple, en supposant que les tuples sont repr (C) pour plus de simplicité, union Foo { a: (bool, u8), b: (u8, bool) } vous permet de construire quelque chose qui n'est pas valide simplement par des affectations de champ.

@rkruppe

union Foo {a: (bool, u8), b: (u8, bool)}

Hé, c'est mon exemple :)
Et c'est valide sous le modèle de la RFC 1897 (au moins un des fragments "feuille" bool -1, u8 -1, u8 -2, bool -2 est valable après toute attribution partielle).

les syndicats de gestion de code doivent être très prudents avec la façon dont ils écrivent au syndicat ou risquent de prendre instantanément UB

C'est le but du modèle de la RFC 1897, la vérification statique garantit qu'aucune opération sûre (comme une affectation ou une affectation partielle) ne peut transformer l'union en état invalide, vous n'avez donc pas besoin d'être très prudent tout le temps et de ne pas obtenir instantanément UB .
Seules les opérations non sécurisées non liées à l'union, telles que les écritures via des pointeurs génériques, peuvent rendre une union invalide.

D'un autre côté, sans vérification de mouvement, l'union peut être mise en état invalide très facilement.

let u: Union;
let x = u.field; // UB

C'est le but du modèle de la RFC 1897, la vérification statique garantit qu'aucune opération sûre (comme une affectation ou une affectation partielle) ne peut transformer l'union en état invalide, vous n'avez donc pas besoin d'être très prudent tout le temps et de ne pas obtenir instantanément UB .
Seules les opérations non sécurisées non liées à l'union, telles que les écritures via des pointeurs génériques, peuvent rendre une union invalide.

Vous pouvez automatiquement reconnaître certains types d'écritures comme ne violant pas les invariants supplémentaires imposés aux unions, mais ce sont toujours des invariants supplémentaires qui doivent être respectés par les auteurs. Étant donné que la lecture est toujours dangereuse et nécessite de s'assurer manuellement que les bits seront valides pour la variante lue, cela n'aide pas réellement les lecteurs, cela rend simplement la vie des écrivains plus difficile. Ni "sac de bits" ni "enum avec variante inconnue" ne permettent de résoudre le problème difficile des unions: comment s'assurer qu'il stocke réellement le type de données que vous voulez lire.

Comment la vérification de type plus sophistiquée affecterait-elle Dropping? Si vous créez un syndicat puis le transmettez à C, qui en prend possession, la rouille tentera-t-elle de libérer les données, provoquant peut-être un double-libre? Ou implémenteriez-vous toujours Drop vous-même?

modifier ce serait bien si les unions ressemblaient à des "énumérations où la variante est vérifiée statiquement au moment de la compilation", si j'ai compris la suggestion

edit 2 les unions pourraient-elles commencer comme un sac de bits et ensuite permettre un accès sécurisé tout en étant rétrocompatibles?

Et il est valide selon le modèle de la RFC 1897 (au moins un des fragments "feuille" bool-1, u8-1, u8-2, bool-2 est valide après toute affectation partielle).

Si nous décidons que nous voulons que cela soit valide, je pense que @ oli-obk devrait mettre à jour les vérifications de miri pour refléter cela - avec https://github.com/rust-lang/rust/pull/51361 fusionné, il serait rejeté par miri.

@petrochenkov La partie que je ne comprends pas, c'est ce que cela nous achète. Nous obtenons une complexité supplémentaire, en termes de mise en œuvre (analyse statique) et d'utilisation (l'utilisateur doit toujours connaître les règles exactes). Cette complexité supplémentaire s'ajoute au fait que lorsque les syndicats sont utilisés, nous sommes déjà dans un contexte dangereux, donc les choses sont naturellement plus complexes. Je pense que nous devrions avoir une motivation claire pour expliquer pourquoi cette complexité supplémentaire en vaut la peine. Je ne considère pas «cela viole quelque peu l'esprit du langage» comme une motivation claire.

La seule chose à laquelle je peux penser est l'optimisation de la mise en page. Dans un modèle «sac de bits», un syndicat n'a jamais de niche. Cependant, je pense que ce sont de meilleures adresses en donnant au programmeur plus de contrôle manuel sur la niche, ce qui serait également utile dans d'autres cas .

Je pense qu'il me manque quelque chose de fondamental ici. Je suis d'accord avec @rkruppe que
le problème difficile avec les syndicats est de s'assurer que le syndicat stocke actuellement
les données que le programme souhaite lire.

Mais AFAIK ce problème ne peut pas être résolu «localement» par une analyse statique. nous
serait au moins une analyse complète du programme, et même dans ce cas, il serait toujours
un problème difficile à résoudre.

Alors ... y a-t-il une solution à ce problème sur la table? Ou, qu'est-ce que le
les solutions exactes proposées nous achètent-elles réellement? Dis que je reçois une union de C,
sans analyser l'ensemble du programme Rust et C, que peut
analyses statiques réellement garanties pour les lecteurs?

@gnzlbg Je pense que la seule garantie que nous aurions est ce que @petrochenkov a écrit ci-dessus

la vérification statique garantit qu'aucune opération sûre (comme l'affectation ou l'affectation partielle) ne peut transformer l'union en état invalide

D'un autre côté, sans vérification de mouvement, l'union peut être mise en état invalide très facilement.

Votre proposition ne protège pas non plus contre les mauvaises lectures, je ne pense pas que ce soit possible.

Aussi, j'ai imaginé un suivi "initialisé" très basique le long des lignes de "l'écriture dans n'importe quel champ initialise l'union". Nous aurions besoin de quelque chose de toute façon quand impl Drop for MyUnion est autorisé. Pour le meilleur ou pour le pire, nous devons décider quand et où insérer des appels de retrait automatiques pour un syndicat. Ces règles doivent être aussi simples que possible car il s'agit d'un code supplémentaire que nous insérons dans un code subtil et dangereux existant. Pour les syndicats qui implémentent Drop , j'ai également imaginé une restriction similaire à struct qui ne permet pas d'écrire dans un champ à moins que la structure de données ne soit déjà initialisée.

@derekchiang

les syndicats pourraient-ils commencer comme un sac de bits et ensuite permettre un accès sûr tout en étant rétrocompatibles?
Non. Une fois que nous disons que c'est un sac de bits, il peut y avoir du code dangereux en supposant que cela soit autorisé.

Je pense qu'il y a de la valeur dans la vérification des mouvements au strict minimum pour voir si une union est initialisée. La RFC d'origine spécifiait explicitement que l'initialisation ou l'affectation à n'importe quel champ d'union rend l'ensemble de l'union initialisé. Au-delà de cela, cependant, rustc ne devrait pas essayer de déduire quoi que ce soit sur la valeur d'une union que l'utilisateur ne spécifie pas explicitement; une union peut contenir n'importe quelle valeur, y compris une valeur qui n'est valide pour aucun de ses champs.

Un cas d'utilisation pour cela, par exemple: considérez une union balisée de style C qui est explicitement extensible avec plus de balises à l'avenir. Le code C et Rust lisant cette union ne doit pas supposer qu'il connaît tous les types de champs possibles.

@RalfJung

Je devrais peut-être partir de l'autre sens.

Ce code devrait-il fonctionner 1) pour les syndicats 2) pour les non-syndicats?

let x: T;
let y = x.field;

Pour moi, la réponse est évidente "non" dans les deux cas, car c'est toute une classe d'erreurs que Rust peut et veut éviter, quelle que soit la "union" -ness de T .

Cela signifie que le vérificateur de mouvement doit avoir une sorte de schéma selon lequel il implémente ce support. Étant donné que le vérificateur de mouvement (et le vérificateur d'emprunt) fonctionnent généralement en mode par champ, le schéma le plus simple pour les unions serait «les mêmes règles que pour les structs + (dé) initialisation / emprunt d'un champ également (dé) initialise / emprunte ses champs frères ".
Cette règle simple couvre toutes les vérifications statiques.

Ensuite, le modèle enum est simplement une conséquence de la vérification statique décrite ci-dessus + une condition supplémentaire.
Si 1) la vérification d'initialisation est activée et 2) le code non sécurisé n'écrit pas d'octets non valides arbitraires dans la zone appartenant à l'union, alors l'un des champs «feuille» des unions est automatiquement valide. Il s'agit d'une garantie dynamique non vérifiable (au moins pour les unions avec> 1 champs et en dehors de const-evaluator), mais elle cible d'abord les personnes lisant le code.

Ce cas de @joshtriplett , par exemple

Un cas d'utilisation pour cela, par exemple: considérez une union balisée de style C qui est explicitement extensible avec plus de balises à l'avenir. Le code C et Rust lisant cette union ne doit pas supposer qu'il connaît tous les types de champs possibles.

serait beaucoup plus clair pour les personnes lisant du code si le syndicat avait explicitement un champ supplémentaire pour les "extensions futures possibles".

Bien sûr, nous pouvons conserver la vérification d'initialisation statique de base, mais rejeter la deuxième condition et autoriser l'écriture de données arbitraires éventuellement invalides dans l'union par le biais de moyens "tiers" non sécurisés sans qu'il s'agisse d'un UB instantané. Ensuite, nous n'aurions plus cette garantie dynamique ciblée sur les personnes, je pense simplement que ce serait une perte nette.

@petrochenkov

Ce code devrait-il fonctionner 1) pour les syndicats 2) pour les non-syndicats?

let x: T;
let y = x.field;

Pour moi, la réponse est évidente "non" dans les deux cas, car c'est toute une classe d'erreurs que Rust peut et veut éviter, quelle que soit la "union" -ness de T .

D'accord, ce niveau de vérification des valeurs non initialisées semble raisonnable, et tout à fait faisable.

Cela signifie que le vérificateur de mouvement doit avoir une sorte de schéma selon lequel il implémente ce support. Étant donné que le vérificateur de mouvement (et le vérificateur d'emprunt) fonctionnent généralement en mode par champ, le schéma le plus simple pour les unions serait «les mêmes règles que pour les structs + (dé) initialisation / emprunt d'un champ également (dé) initialise / emprunte ses champs frères ".
Cette règle simple couvre toutes les vérifications statiques.

D'accord jusqu'à présent, en supposant que je comprends les règles des structs.

Ensuite, le modèle enum est simplement une conséquence de la vérification statique décrite ci-dessus + une condition supplémentaire.
Si 1) la vérification d'initialisation est activée et 2) le code non sécurisé n'écrit pas d'octets non valides arbitraires dans la zone appartenant à l'union, alors l'un des champs «feuille» des unions est automatiquement valide. Il s'agit d'une garantie dynamique non vérifiable (au moins pour les unions avec> 1 champs et en dehors de const-evaluator), mais elle cible d'abord les personnes lisant le code.

Cette condition supplémentaire n'est pas valable pour les syndicats.

Ce cas de @joshtriplett , par exemple

Un cas d'utilisation pour cela, par exemple: considérez une union balisée de style C qui est explicitement extensible avec plus de balises à l'avenir. Le code C et Rust lisant cette union ne doit pas supposer qu'il connaît tous les types de champs possibles.

serait beaucoup plus clair pour les personnes lisant du code si le syndicat avait explicitement un champ supplémentaire pour les "extensions futures possibles".

Ce n'est pas ainsi que les syndicats C fonctionnent, ni comment les syndicats Rust ont été désignés pour fonctionner. (Et je me demande si ce serait plus clair, ou simplement si cela correspond à un ensemble différent d'attentes.) Changer cela rendrait les syndicats Rust ne plus adaptés à certains des objectifs pour lesquels ils ont été conçus et proposés.

Bien sûr, nous pouvons conserver la vérification d'initialisation statique de base, mais rejeter la deuxième condition et autoriser l'écriture de données arbitraires éventuellement invalides dans l'union par le biais de moyens "tiers" non sécurisés sans qu'il s'agisse d'un UB instantané. Ensuite, nous n'aurions plus cette garantie dynamique ciblée sur les personnes, je pense simplement que ce serait une perte nette.

Ces «moyens tiers non sûrs» incluent «obtenir un syndicat de FFI», qui est un cas d'utilisation tout à fait valable.

Voici un exemple concret:

union Event {
    event_id: u32,
    event1: Event1,
    event2: Event2,
    event3: Event3,
}

struct Event1 {
    event_id: u32, // always EVENT1
    // ... more fields ...
}
// ... more event structs ...

match u.event_id {
    EVENT1 => { /* ... */ }
    EVENT2 => { /* ... */ }
    EVENT3 => { /* ... */ }
    _ => { /* unknown event */ }
}

C'est un code tout à fait valide que les gens peuvent et vont écrire en utilisant des syndicats.

@petrochenkov

Ce code devrait-il fonctionner 1) pour les syndicats 2) pour les non-syndicats?
Pour moi, la réponse est évidente "non" dans les deux cas, car il s'agit de toute une classe d'erreurs que Rust peut et veut éviter, indépendamment de la "union" -ness de T.

Très bien pour moi.

le schéma le plus simple pour les unions serait "les mêmes règles que pour les structs + (dé) initialisation / emprunt d'un champ également (dé) initialise / emprunte ses champs frères".

Woah. Les règles de structure ont du sens car elles sont toutes basées sur le fait que différents champs sont disjoints . Vous ne pouvez pas simplement invalider cette hypothèse de base et continuer à utiliser les mêmes règles. Le fait que vous ayez besoin d'un addendum aux règles le montre. Je ne m'attendrais jamais à ce que les syndicats soient vérifiés de la même manière que les structures. Si quoi que ce soit, on pourrait s'attendre à ce qu'ils soient vérifiés de la même manière que les enums - mais bien sûr cela ne peut pas fonctionner, car les enums ne sont accessibles que via match.

Si 1) la vérification d'initialisation est activée et 2) le code non sécurisé n'écrit pas d'octets non valides arbitraires dans la zone appartenant à l'union, alors l'un des champs «feuille» des unions est automatiquement valide. Il s'agit d'une garantie dynamique non vérifiable (au moins pour les unions avec> 1 champs et en dehors de const-evaluator), mais elle cible d'abord les personnes lisant le code.

Je pense qu'il est extrêmement souhaitable que les hypothèses de validité de base soient vérifiables dynamiquement (informations de type données). Ensuite, nous pouvons les vérifier pendant CTFE dans miri, nous pouvons même les vérifier pendant les exécutions "complètes" de miri (par exemple d'une suite de tests), nous pouvons éventuellement avoir une sorte de désinfectant ou peut-être un mode où Rust émet debug_assert! aux endroits critiques pour vérifier les invariants de validité.
Je pense que l'expérience des règles incontrôlables de C montre amplement que celles-ci sont problématiques. Habituellement, la première étape pour comprendre et clarifier les règles est de trouver un moyen dynamiquement vérifiable de les exprimer. Même pour les modèles de mémoire de concurrence, des variantes «vérifiables dynamiquement» (sémantique opérationnelle expliquant tout en termes d'exécution étape par étape d'une machine virtuelle) apparaissent et semblent être le seul moyen de résoudre les problèmes ouverts de longue date de l'axiomatique modèles qui étaient précédemment utilisés ("ouf de problème d'air mince" est ici un mot-clé).

Je ne peux guère exagérer à quel point je pense qu’il est important d’avoir des règles vérifiables dynamiquement. Je pense que nous devrions viser à avoir 0 cas non contrôlables d'UB. (Nous n'en sommes pas encore là, mais c'est l'objectif que nous devrions avoir.) C'est la seule façon responsable d'avoir UB dans votre langue, tout le reste est un cas d'auteurs de compilateurs / de langage qui facilitent leur vie aux dépens de tous doit vivre avec les conséquences. (Je travaille actuellement sur des règles vérifiables dynamiquement pour l'aliasing et les accès de pointeurs bruts.)
Même si ce serait le seul problème, en ce qui me concerne, "non vérifiable dynamiquement" est une raison suffisante pour ne pas utiliser cette approche.

Cela dit, je ne vois aucune raison fondamentale pour laquelle cela ne devrait pas être vérifiable: pour chaque octet de l'union, examinez toutes les variantes pour voir quelles valeurs sont autorisées pour cet octet dans cette variante, et prenez l'union (heh;)) de tous de ces ensembles. Une séquence d'octets est valide pour une union si chaque octet est valide selon cette définition.
Il est cependant assez difficile d'implémenter une vérification pour - de loin l'invariant de validité de type de base le plus complexe que nous aurions dans Rust. C'est une conséquence directe du fait que cette règle de validité est quelque peu délicate à décrire, c'est pourquoi je ne l'aime pas.

Bien sûr, nous pouvons conserver la vérification d'initialisation statique de base, mais rejeter la deuxième condition et autoriser l'écriture de données arbitraires éventuellement invalides dans l'union par le biais de moyens "tiers" non sécurisés sans qu'il s'agisse d'un UB instantané. Ensuite, nous n'aurions plus cette garantie dynamique ciblée sur les personnes, je pense simplement que ce serait une perte nette.

Qu'est-ce que cette garantie nous achète ? Où cela aide-t-il réellement? À l'heure actuelle, tout ce que je vois, c'est que tout le monde doit travailler dur et veiller à le respecter. Je ne vois pas l'avantage que nous, les gens, retirons de cela.

@joshtriplett

considérez une union balisée de style C qui est explicitement extensible avec plus de balises à l'avenir. Le code C et Rust lisant cette union ne doit pas supposer qu'il connaît tous les types de champs possibles.

Le modèle proposé par @petrochenkov permet ces cas d'utilisation, en ajoutant un champ __non_exhaustive: () à l'union. Cependant, je ne pense pas que cela devrait être nécessaire. En théorie, les générateurs de liaison pourraient ajouter un tel champ.

@RalfJung

Ceci est dynamique non vérifiable (au moins pour les unions avec> 1 champs et en dehors de const-évaluator)

Je pense qu'il est extrêmement souhaitable que les hypothèses de base de validité soient vérifiables dynamiquement

Une clarification: je voulais dire non vérifiable en "par défaut" / "en mode de libération", bien sûr, il peut être vérifiée en "mode lent" avec une instrumentation supplémentaire, mais vous avez déjà écrit à ce sujet mieux que moi.

@RalfJung

Le modèle proposé par @petrochenkov permet ces cas d'utilisation, en ajoutant un champ __non_exhaustive: () à l'union.

Oui, j'ai compris que c'était la proposition.

Cependant, je ne pense pas que cela devrait être nécessaire. En théorie, les générateurs de liaison pourraient ajouter un tel champ.

Ils pourraient, mais ils devraient systématiquement l'ajouter à chaque syndicat.

Je n'ai pas encore vu d'argument pour expliquer pourquoi il est logique de briser les cas d'utilisation primaires des unions en faveur de certains cas d'utilisation non spécifiés qui dépendent de la limitation des modèles de bits qu'ils peuvent contenir.

@joshtriplett

principaux cas d'utilisation des syndicats

Je ne vois pas du tout pourquoi c'est le cas d'utilisation principal.
Cela peut être vrai pour les unions repr(C) si vous supposez que toutes les utilisations d'unions pour les unions marquées / "émulation Rust enum" dans FFI supposent une extensibilité (ce qui n'est pas vrai), mais d'après ce que j'ai vu, les utilisations de repr(Rust) unions

@petrochenkov Je n'ai pas dit "briser le cas d'utilisation principal", j'ai dit "briser les cas d'utilisation primaires". FFI est l' un des principaux cas d'utilisation des syndicats.

et prenez l'union (heh;)) de tous ces ensembles

Il y a certainement une évidence attrayante à une déclaration que "les valeurs possibles d'une union sont l'union des valeurs possibles de toutes ses variantes possibles" ...

Vrai. Cependant, ce n'est pas la proposition - nous convenons tous que ce qui suit devrait être légal:

union F {
  x: (u8, bool),
  y: (bool, u8),
}
fn foo() -> F {
  let mut f = F { x: (5, false) };
  unsafe { f.y.1 = 17; }
  f
}

En fait, je pense que c'est un bogue que cela nécessite même unsafe .

Donc, l'union doit être prise par octets, au moins.
De plus, je ne pense pas que «l'évidence attrayante» soit en soi une raison suffisante. Tout invariant que nous décidons est un fardeau important pour les auteurs de code non sûr, nous devrions avoir des avantages concrets que nous obtenons à notre tour.

@RalfJung

En fait, je pense que c'est un bogue que cela nécessite même dangereux.

Je ne connais pas la nouvelle implémentation du vérificateur d'insécurité basé sur MIR, mais dans l'ancienne version basée sur HIR, il s'agissait certainement d'une limitation / simplification du vérificateur - seules les expressions de la forme expr1.field = expr2 ont été analysées pour un champ "possible" cession «désengagement non sécuritaire», tout le reste a été traité de façon conservatrice comme un «accès sur le terrain» générique qui n'est pas sûr pour les syndicats.

Répondre au commentaire dans https://github.com/rust-lang/rust/issues/52786#issuecomment -408645420:

Donc, l'idée est que le compilateur ne sait toujours rien du Wrap<T> et ne peut pas par exemple faire des optimisations de mise en page. Ok, cette position est comprise.
Cela signifie qu'en interne, à l'intérieur du module de Wrap , l'implémentation du module Wrap<T> peut, par exemple, y écrire temporairement des "valeurs inattendues", si elles ne les divulguent pas aux utilisateurs, et le compilateur sera d'accord avec eux.

Je ne suis pas sûr de savoir comment exactement la partie du Wrap de

Tout d'abord, que les champs soient privés ou publics, les valeurs inattendues ne peuvent pas être écrites directement dans ces champs. Vous avez besoin de quelque chose comme un pointeur brut, ou du code de l'autre côté de FFI pour le faire, et cela peut être fait sans aucun accès au champ, simplement en ayant un pointeur vers l'union entière. Nous devons donc aborder cela dans une autre direction que l'accès à un champ restreint.

Comme j'interprète votre commentaire, l'approche consiste à dire qu'un champ privé (en union ou en structure, peu importe) implique un invariant arbitraire inconnu de l'utilisateur, donc toute opération modifiant ce champ (directement ou via des pointeurs sauvages, ne le fait pas) t importe) résultent en UB car ils peuvent potentiellement casser cet invariant non spécifié.

Cela signifie que si une union a un seul champ privé, son implémenteur (mais pas le compilateur) peut supposer qu'aucun tiers n'écrira une valeur inattendue dans cette union.
C'est une "clause de documentation d'union par défaut" pour l'utilisateur dans un certain sens:
- (Par défaut) Si une union a un champ privé, vous ne pouvez pas y écrire de déchets.
- Sinon, vous pouvez écrire des déchets dans une union à moins que ses documents ne l'interdisent explicitement.

Si un syndicat veut interdire les valeurs inattendues tout en fournissant pub accès

@RalfJung
Est-ce que cela décrit votre position avec précision?

Comment des scénarios comme celui-ci sont-ils traités?

mod m {
    union MyPrivateUnion { /* private fields */ }
    extern {
        fn my_private_ffi_function() -> MyPrivateUnion; // Can return garbage (?)
    }
}

Comme j'interprète votre commentaire, l'approche consiste à dire qu'un champ privé (en union ou en structure, peu importe) implique un invariant arbitraire inconnu de l'utilisateur, donc toute opération modifiant ce champ (directement ou via des pointeurs sauvages, ne le fait pas) t importe) résultent en UB car ils peuvent potentiellement casser cet invariant non spécifié.

Non, ce n'est pas ce que je voulais dire.

Il existe plusieurs invariants. Je ne sais pas combien nous en aurons besoin, mais il y en aura au moins deux (et je n'ai pas de grands noms pour eux):

  • L '«invariant au niveau de la disposition» (ou «invariant syntaxique») d'un type est complètement défini par la forme syntaxique du type. Ce sont des choses comme " &mut T est non NULL et aligné", " bool est 0 ou 1 ", " ! impossible exister". À ce niveau, *mut T est identique à usize - les deux permettent n'importe quelle valeur (ou peut-être n'importe quelle valeur initialisée , mais cette distinction est pour une autre discussion). Nous allons, à terme, avoir un document expliquant ces invariants pour tous les types, par récursivité structurelle: L'invariant au niveau de la disposition d'une structure est que tous ses champs ont leur invariant maintenu, etc. La visibilité ne joue pas ici un rôle.
Violating the layout-level invariant is instantaneous UB. This is a statement we can make because we have defined this invariant in very simple terms, and we make it part of the definition of the language itself. We can then exploit this UB (and we already do), e.g. to perform enum layout optimizations.
  • L '«invariant au niveau du type personnalisé» (ou «l'invariant sémantique») d'un type est choisi par celui qui implémente le type. Le compilateur ne peut pas connaître cet invariant car nous n'avons pas de langage pour l'exprimer, et il en va de même pour la définition du langage. Nous ne pouvons pas faire violer cet invariant UB, car nous ne pouvons même pas dire ce qu'est cet invariant! Le fait qu'il soit même possible d'avoir des invariants personnalisés est une caractéristique de tout système de type utile: l'abstraction. J'ai écrit plus à ce sujet dans un précédent article de blog .

    La connexion entre l'invariant sémantique personnalisé et UB est que nous déclarons que le code unsafe peut compter sur la préservation de ses invariants sémantiques par un code étranger . Cela rend incorrect de simplement mettre des éléments aléatoires dans un champ de taille Vec . Notez que j'ai dit incorrect (j'utilise parfois le terme «non valable» ) - mais pas un comportement indéfini! Un autre exemple pour illustrer cette différence (vraiment, le même exemple) est la discussion sur les règles d'alias pour &mut ZST . La création d'une non-null &mut ZST pendante et bien alignée n'est jamais une UB immédiate, mais elle est toujours incorrecte / non valable car on peut écrire du code dangereux qui repose sur cela pour ne pas se produire.

Ce serait bien d'aligner ces deux concepts, mais je ne pense pas que ce soit pratique. Tout d'abord, pour certains types (pointeurs de fonction, traits dyn), la définition de l'invariant sémantique personnalisé utilise en fait la définition de UB dans le langage. Cette définition serait circulaire si nous voulions dire que c'est UB de jamais violer l'invariant sémantique coutumier. Deuxièmement, je préférerais si la définition de notre langage, et si une certaine trace d'exécution présente UB, était une propriété décidable. Les invariants sémantiques et personnalisés ne sont souvent pas décidables.


Je ne suis pas sûr de savoir comment exactement la partie du contrat Wraps concernant l'absence de valeurs inattendues est liée à la confidentialité des champs.

Essentiellement, lorsqu'un type choisit son invariant personnalisé, il doit s'assurer que tout ce que le code sécurisé peut faire préserve l'invariant . Après tout, la promesse est que la simple utilisation de l'API sûre de ce type ne peut jamais conduire à UB. Cela s'applique à la fois aux structures et aux unions. L'une des choses que le code sécurisé peut faire est d'accéder aux champs publics, d'où provient cette connexion.

Par exemple, un champ public d'une structure ne peut pas avoir un invariant personnalisé différent de l' invariant personnalisé du type de champ : après tout, tout utilisateur sûr peut écrire des données arbitraires dans ce champ, ou lire le champ et s'attendre à «bien» Les données. Une structure où tous les champs sont publics peut être construite en toute sécurité, imposant des restrictions supplémentaires sur le champ.

Un syndicat avec un champ public ... enfin c'est assez intéressant. La lecture des champs d'union est de toute façon dangereuse, donc rien n'y change. L'écriture de champs d'union est sûre, donc une union avec un champ public doit être capable de gérer des données arbitraires qui satisfont l'invariant personnalisé de ce type de champ placé dans le champ. Je doute que cela soit très utile ...

Donc, pour récapituler, lorsque vous choisissez un invariant personnalisé, il est de votre responsabilité de vous assurer que le code de sécurité étranger ne peut pas casser cet invariant (et vous disposez d'outils comme les champs privés pour vous aider à atteindre cet objectif). Il est de la responsabilité du code étranger non sécurisé de ne pas violer votre invariant lorsque ce code fait quelque chose que le code sécurisé ne peut pas faire.


Cela signifie qu'en interne, à l'intérieur du module de Wrap, l'implémentation de Wrapmodule peut, par exemple, y écrire temporairement des "valeurs inattendues", s'il ne les divulgue pas aux utilisateurs, et le compilateur les acceptera.

Correct. (la panique-sécurité est un problème ici mais vous en êtes probablement conscient). C'est juste comme, dans Vec , je peux faire en toute sécurité

let sz = self.size;
self.size = 1337;
self.size = sz;

et il n'y a pas d'UB.


mod m {
    union MyPrivateUnion { /* private fields */ }
    extern {
        fn my_private_ffi_function() -> MyPrivateUnion; // Can return garbage (?)
    }
}

En termes d'invariant de mise en page syntaxique, my_private_ffi_function peut tout faire (en supposant que l'appel de fonction ABI et la signature correspondent). En termes d'invariant sémantique personnalisé, ce n'est pas visible dans le code - celui qui a écrit ce module avait un invariant en tête, il devrait le documenter à côté de sa définition d'union, puis s'assurer que la fonction FFI renvoie une valeur qui satisfait l'invariant .

J'ai finalement écrit ce billet de blog pour savoir si et quand &mut T doit être initialisé, et les deux types d'invariants que j'ai mentionnés ci-dessus.

Reste-t-il quelque chose à suivre ici qui n'est pas déjà couvert par https://github.com/rust-lang/rust/issues/55149 , ou devrions-nous fermer?

E0658 pointe toujours ici:

erreur [E0658]: les unions avec des champs non- Copy sont instables (voir le problème # 32836)

Cela joue actuellement terriblement avec les atomiques, car ils n'implémentent pas Copy . Quelqu'un connaît-il une solution de contournement?

Lorsque https://github.com/rust-lang/rust/issues/55149 est implémenté, vous pourrez utiliser ManuallyDrop<AtomicFoo> dans une union. Jusque-là, la seule solution est d'utiliser Nightly (ou de ne pas utiliser union et de trouver une alternative).

Avec cela implémenté, vous ne devriez même pas avoir besoin de ManuallyDrop ; après tout rustc sait que Atomic* n'implémente pas Drop .

M'affecter à faire passer le problème de suivi au nouveau.

Cette page vous a été utile?
0 / 5 - 0 notes