Rust: Problème de suivi pour la RFC 1892, "Déprécier non initialisé en faveur d'un nouveau type MaybeUninit"

Créé le 19 août 2018  ·  382Commentaires  ·  Source: rust-lang/rust

NOUVEAU PROBLÈME DE SUIVI = https://github.com/rust-lang/rust/issues/63566

Il s'agit d'un problème de suivi pour la RFC "Deprecate uninitialized en faveur d'un nouveau type MaybeUninit " (rust-lang / rfcs # 1892).

Pas:

  • [x] Implémenter la RFC (cc @ rust-lang / libs)
  • [x] Ajuster la documentation (sur https://github.com/rust-lang/rust/pull/60445)
  • [x] Stabilisation PR (sur https://github.com/rust-lang/rust/pull/60445)

Questions non résolues:

  • Devrions-nous avoir un setter sûr qui renvoie un &mut T ?
  • Doit-on renommer MaybeUninit ?
  • Doit-on renommer into_inner ?
  • Est-ce que MaybeUninit<T> devrait être Copy pour T: Copy ?
  • Devrions-nous autoriser l'appel de get_ref et get_mut (mais pas de lecture à partir des références renvoyées) avant que les données ne soient initialisées? (AKA: "Les références à des données non initialisées sont insta-UB, ou seulement UB lors de la lecture?") Devrions-nous le renommer de la même manière que into_inner ?
  • Pouvons-nous faire paniquer into_inner (ou ce qu'il finit par être appelé) quand T est inhabité, comme le fait mem::uninitialized actuellement? (terminé)
  • On dirait que nous ne voulons pas déprécier mem::zeroed .
B-RFC-approved C-tracking-issue E-mentor T-lang T-libs

Commentaire le plus utile

mem::zeroed() est utile pour certains cas FFI où l'on s'attend à ce que vous mettiez à zéro une valeur avec memset(&x, 0, sizeof(x)) avant d'appeler une fonction C. Je pense que c'est une raison suffisante pour que cela ne soit pas déconseillé.

Tous les 382 commentaires

cc @RalfJung

[] Mettre en œuvre la RFC

Je peux aider à mettre en œuvre la RFC.

Génial, je peux aider à revoir :)

J'aimerais quelques éclaircissements sur cette partie de la RFC:

Rendre l'appel non initialisé sur un type vide déclenche une panique d'exécution qui imprime également le message d'obsolescence.

Est-ce que seulement mem::uninitialized::<!>() devrait paniquer? Ou est-ce que cela devrait également couvrir les structures (et peut-être les énumérations?) Qui contiennent le type vide (par exemple (!, u8) )?

AFAIK nous ne faisons que la génération de code vraiment dangereux pour ! . La plupart des autres utilisations de mem::uninitialized sont tout aussi incorrectes, mais le compilateur ne les exploite pas.

Donc je le ferais pour ! seulement, mais aussi pour mem::zeroed . (J'ai oublié de modifier cette partie quand j'ai ajouté zeroed à la RFC, semble-t-il.)

Nous pourrions commencer par faire ceci:
https://github.com/rust-lang/rust/blob/8928de74394f320d1109da6731b12638a2167945/src/librustc_codegen_llvm/intrinsic.rs#L184 -L198

vérifier si fn_ty.ret.layout.abi est Abi::Uninhabited et émettre au moins un piège, par exemple: https://github.com/rust-lang/rust/blob/8928de74394f320d1109da6731b12638a2167945/src/librustc_codegen_llvm/mir/ opérande.rs # L400 -L403

Une fois que vous avez vu le piège (c'est- intrinsics::abort dire https://github.com/rust-lang/rust/blob/8928de74394f320d1109da6731b12638a2167945/src/librustc_codegen_llvm/mir/block.rs#L445 - L447

Pour paniquer réellement, vous auriez besoin de quelque chose comme ceci: https://github.com/rust-lang/rust/blob/8928de74394f320d1109da6731b12638a2167945/src/librustc_codegen_llvm/mir/block.rs#L360 -L407
(vous pouvez ignorer le bras EvalErrorKind::BoundsCheck )

@eddyb Merci pour les pointeurs.


Je corrige maintenant (plusieurs) avertissements de dépréciation et je me sens (très) tenté de simplement lancer sed -i s/mem::uninitialized()/mem::MaybeUninit::uninitialized().into_inner()/g mais je suppose que cela manquerait le point ... Ou est-ce OK si je sais que la valeur est une valeur concrète (Copier) type? par exemple let x: [u8; 1024] = mem::uninitialized(); .

Cela manquerait exactement le but, ouais. ^^

Au moins pour l'instant, j'aimerais considérer mem::MaybeUninit::uninitialized().into_inner() UB pour tous les types non syndiqués. Notez que Copy n'est certainement pas suffisant; les deux bool et &'static i32 sont Copy et votre extrait est destiné à être insta-UB pour eux. Nous pouvons vouloir une exception pour les «types où tous les modèles de bits sont corrects» (types entiers, essentiellement), mais je serais opposé à faire une telle exception car undef n'est pas un modèle de bits normal. C'est pourquoi le RFC dit que vous devez complètement initialiser avant d'appeler into_inner .

Il dit également que pour get_mut , mais la discussion RFC a soulevé le désir de certaines personnes d'assouplir la restriction ici. C'est une option avec laquelle je pourrais vivre. Mais pas pour into_inner .

J'ai peur que toutes ces utilisations de uninitialized devront être examinées plus attentivement, et en fait c'était l'une des intentions de la RFC. Nous aimerions que l'écosystème plus large soit plus prudent ici, si tout le monde utilise juste into_inner immédiatement, alors le RFC ne vaut rien.

Nous aimerions que l'écosystème plus large soit plus prudent ici, si tout le monde utilise juste into_inner immédiatement, alors la RFC ne vaut rien.

Cela me donne une idée ... peut-être faudrait-il lint (groupe: "correctness") pour ce genre de code? cc @ oli-obk

Je corrige maintenant (plusieurs) avertissements d'obsolescence

Nous ne devrions expédier Nightly avec ces avertissements qu'une fois que le remplacement recommandé est disponible au moins sur Stable. Voir une discussion similaire sur https://github.com/rust-lang/rust/pull/52994#issuecomment -411413493

@RalfJung

Nous pouvons vouloir une exception pour les "types où tous les modèles de bits sont corrects" (types entiers, essentiellement)

Vous avez déjà participé à une discussion à ce sujet, mais je posterai ici pour diffuser plus largement: c'est déjà quelque chose pour lequel nous avons de nombreux cas d'utilisation existants dans Fuchsia, et nous avons un trait pour cela ( FromBytes ) et une macro de dérivation pour ces types. Il y avait aussi un pré-RFC interne pour les ajouter à la bibliothèque standard (cc @gnzlbg @joshlf).

Je serais contre l'idée de faire une telle exception car undef n'est pas un motif de bits normal.

Oui, c'est un aspect dans lequel mem::zeroed() est significativement différent de mem::uninitialized() .

@cramertj

Vous avez déjà participé à une discussion à ce sujet, mais je posterai ici pour circuler plus largement: c'est déjà quelque chose pour lequel nous avons de nombreux cas d'utilisation existants dans Fuchsia, et nous avons un trait pour cela (FromBytes) et une macro de dérivation pour ces types. Il y avait aussi un pré-RFC interne pour les ajouter à la bibliothèque standard (cc @gnzlbg @joshlf).

Ces discussions portaient sur les moyens d'autoriser des memcpy s sûrs

Le consensus était également qu'il ne serait pas judicieux pour toute approche discutée de permettre la lecture des octets de remplissage, qui sont une forme de mémoire non initialisée, dans Rust sécurisé. Autrement dit, si vous mettez de la mémoire initialisée, vous ne pouvez pas sortir de la mémoire non initialisée.

IIRC, personne là-bas n'a suggéré ou discuté d'une approche dans laquelle vous pourriez mettre de la mémoire non initialisée et obtenir la mémoire initialisée, donc je ne suis pas ce que ces discussions ont à voir avec celle-ci. Pour moi, ils sont complètement orthogonaux.

Pour conduire un peu plus le point à la maison, LLVM définit les données non initialisées comme Poison, qui est distincte de «un certain modèle de bits arbitraire mais valide». Le branchement basé sur une valeur de poison ou son utilisation pour calculer une adresse qui est ensuite déréférencée est UB. Donc, malheureusement, les "types où tous les modèles de bits sont corrects" ne sont toujours pas sûrs à construire car les utiliser sans les initialiser séparément sera UB.

Bon, désolé, j'aurais dû clarifier ce que je voulais dire. J'essayais de dire que "les types où tous les modèles de bits sont corrects" est déjà quelque chose que nous voulons définir pour d'autres raisons. Comme @RalfJung l'a dit ci-dessus,

Je serais contre l'idée de faire une telle exception car undef n'est pas un motif de bits normal.

Dieu merci, il y a des gens qui savent lire, car apparemment je ne peux pas ...

Bon, donc ce que je voulais dire, c'est: nous avons définitivement des types où tous les modèles de bits initialisés sont corrects - tous les types i* et u* , pointeurs bruts, je pense que f* ainsi, puis les tuples / structures constitués uniquement de ces types.

Ce qui est une question ouverte, c'est dans quelles circonstances quels types de ces types peuvent être non initialisés , c'est-à-dire poison. Ma propre réponse préférée est «jamais».

Le consensus était également qu'il ne serait pas judicieux pour toute approche discutée de permettre la lecture des octets de remplissage, qui sont une forme de mémoire non initialisée, dans Rust sécurisé. Autrement dit, si vous mettez de la mémoire initialisée, vous ne pouvez pas sortir de la mémoire non initialisée.

La lecture des octets de remplissage comme MaybeUninit<u8> devrait convenir.

Le consensus était également qu'il ne serait pas judicieux pour toute approche discutée de permettre la lecture des octets de remplissage, qui sont une forme de mémoire non initialisée, dans Rust sécurisé. Autrement dit, si vous mettez de la mémoire initialisée, vous ne pouvez pas sortir de la mémoire non initialisée.

Lecture des octets de remplissage comme MaybeUninitdevrait être bien.

La discussion en bref portait sur la fourniture d'un trait, Compatible<T> , avec une méthode sûre fn safe_transmute(self) -> T qui "réinterprète" / "memcpys" les bits de self en T . La garantie de cette méthode est que si self est correctement initialisé, le résultat T . Il a été proposé au compilateur de remplir automatiquement les implémentations transitives, par exemple, s'il y a un impl Compatible<V> for U , et un impl Compatible<W> for V alors il y a un impl Compatible<W> for U (soit parce qu'il a été fourni manuellement, ou le compilateur le génère automatiquement - la façon dont cela pourrait être implémenté a été complètement expliquée).

Il a été proposé que ce soit unsafe pour implémenter le trait: si vous l'implémentez pour un T qui a des octets de remplissage où Self a des champs, alors tout va bien au moins jusqu'à ce que vous essayiez d'utiliser le T et que le comportement de votre programme dépende du contenu de la mémoire non initialisée.

Je n'ai aucune idée de ce que tout cela a à voir avec MaybeUninit<u8> , peut-être pourriez-vous élaborer là-dessus?

La seule chose que je peux imaginer est que nous pourrions ajouter une couverture impl: unsafe impl<T> Compatible<[MaybeUninit<u8>; size_of::<T>()]> for T { ... } puisque la transmutation de n'importe quel type en un [MaybeUninit<u8>; N] de sa taille est sans danger pour tous les types. Je ne sais pas à quel point un tel impl serait utile, étant donné que MaybeUninit est une union, et quiconque utilise le [MaybeUninit<u8>; N] n'a aucune idée de si un élément particulier du tableau est initialisé ou non .

@gnzlbg à l'époque, vous parliez de FromBits<T> for [u8] . C'est là que je dis que nous devons utiliser [MaybeUninit<u8>] place.

J'ai discuté de cette proposition avec @nikomatsakis à RustConf, et il m'a encouragé à aller de l'avant avec une RFC. J'allais le faire dans quelques semaines, mais s'il y a de l'intérêt, je peux essayer d'en faire un ce week-end. Cela serait-il utile pour cette discussion?

@joshlf de quelle proposition parlez-vous?

@RalfJung

@gnzlbg à l'époque, vous parliez de FromBitspour [u8]. C'est là que je dis que nous devons utiliser [MaybeUninit] au lieu.

Gotcha, entièrement d'accord ici. Avait complètement oublié que nous voulions aussi faire ça 😆

@joshlf de quelle proposition parlez-vous?

Une proposition FromBits / IntoBits . TLDR: T: FromBits<U> signifie que tout motif de bits qui est un U valide correspond à un T valide. U: IntoBits<T> signifie la même chose. Le compilateur déduit automatiquement les deux pour toutes les paires de types selon certaines règles, ce qui déverrouille beaucoup de bonté amusante qui nécessite actuellement unsafe . Il y a un brouillon de cette RFC ici que j'ai écrit il y a quelque temps, mais j'ai l'intention d'en changer de grandes parties, alors ne prenez pas ce texte comme autre chose qu'un guide approximatif.

@joshlf Je pense qu'une telle paire de traits s'appuierait davantage sur cette discussion que d'en faire partie. AFAIK nous avons deux questions ouvertes en termes de validité:

  • Est-il récurrent ci-dessous les références? Je pense de plus en plus fermement que ce ne devrait pas être le cas, car nous voyons plus d'exemples. Il est donc probable que nous devrions adapter les documents MaybeUninit::get_mut conséquence (ce n'est pas réellement UB de l'utiliser avant de terminer l'initialisation, mais c'est UB de le déréférencer avant de terminer l'initialisation). Cependant, nous devons d'abord prendre cette décision pour la validité, et je ne sais pas quel est le bon lieu pour cela. Probablement un RFC dédié?
  • Est-ce qu'un u8 (et d'autres types d'entiers, virgule flottante, pointeur brut) doit être initialisé, c'est-à-dire est MaybeUinit<u8>::uninitialized().into_inner() insta-UB? Je pense que oui, mais principalement basé sur un sentiment instinctif que nous voulons garder les endroits où nous autorisons poison / undef au minimum. Cependant, je pourrais être persuadé du contraire s'il y a beaucoup d'utilisations de ce modèle (et j'espère utiliser miri pour aider à déterminer cela).

Est-il récurrent ci-dessous les références?

@RalfJung pouvez-vous montrer un exemple de ce que vous entendez par «récurer sous les références»?

Un u8 (et d'autres types entiers, virgule flottante, pointeur brut) doit-il être initialisé, c'est-à-dire est MaybeUinit:: uninitialized (). into_inner () insta-UB?

Que se passe-t-il s'il ne s'agit pas d'un UB instantané? Que puis-je faire avec cette valeur? Puis-je correspondre dessus? Si oui, le comportement du programme est-il déterministe?

J'ai l'impression que si je ne peux pas faire correspondre la valeur sans introduire UB, alors nous avons réinventé mem::uninitialized . Si je peux correspondre sur la valeur et que la même branche est toujours prise dans toutes les architectures, niveaux opt, etc., nous avons réinventé mem::zeroed (et utilisons en quelque sorte le MaybeUninit type un peu discutable). Si le comportement du programme n'est pas déterministe et change avec les niveaux d'optimisation, à travers les architectures, en fonction de facteurs externes (comme si le système d'exploitation a donné le processus à zéro pages), etc., alors j'ai l'impression que nous introduirions un énorme footgun dans le Langue.

Est-ce qu'un u8 (et d'autres types entiers, virgule flottante, pointeur brut) doit être initialisé, c'est-à-dire est MaybeUinit<u8>::uninitialized().into_inner() insta-UB? Je pense que oui, mais principalement basé sur un sentiment instinctif que nous voulons garder les endroits où nous autorisons poison / undef au minimum. Cependant, je pourrais être persuadé du contraire s'il y a beaucoup d'utilisations de ce modèle (et j'espère utiliser miri pour aider à déterminer cela).

FWIW, deux des avantages de ne pas être UB sont que a) il s'aligne avec ce que fait LLVM et, b) il permet plus de flexibilité dans les optimisations. Cela semble également plus cohérent avec votre proposition récente de définir la sécurité au moment de l'utilisation, et non au moment de la construction.

Que se passe-t-il s'il ne s'agit pas d'un UB instantané? Que puis-je faire avec cette valeur? Puis-je correspondre dessus? Si oui, le comportement du programme est-il déterministe?

J'ai l'impression que si je ne peux pas égaler la valeur sans introduire UB, alors nous avons réinventé mem::uninitialized . Si je peux correspondre sur la valeur et que la même branche est toujours prise dans toutes les architectures, niveaux opt, etc., nous avons réinventé mem::zeroed (et utilisons en quelque sorte le MaybeUninit type un peu discutable). Si le comportement du programme n'est pas déterministe et change avec les niveaux d'optimisation, à travers les architectures, en fonction de facteurs externes (comme si le système d'exploitation a donné le processus à zéro pages), etc., alors j'ai l'impression que nous introduirions un énorme footgun dans le Langue.

Pourquoi voudriez-vous pouvoir faire correspondre quelque chose qui n'est pas initialisé? Le définir comme UB pour créer une branche ou un index basé sur des valeurs non initialisées offre à LLVM plus de marge d'optimisation, donc je ne pense pas que se lier davantage les mains soit une bonne idée, surtout s'il n'y a pas de cas d'utilisation convaincant.

Pourquoi voudriez-vous pouvoir faire correspondre quelque chose qui n'est pas initialisé?

Je n'ai pas dit que je voulais, j'ai déclaré que si cela ne peut pas être fait, je ne comprends pas la différence entre MaybeUinit<u8>::uninitialized().into_inner() et juste mem::uninitialized() .

@RalfJung pouvez-vous montrer un exemple de ce que vous entendez par «récurer sous les références»?

Essentiellement, la question est de savoir si nous autorisons ce qui suit:

let mut b = MaybeUninit::<bool>::uninitialized();
let bref = b.get_mut(); // insta-UB?

Si nous décidons qu'une référence n'est valide que si elle pointe vers quelque chose de valide (c'est ce que j'entends par «récurer sous les références»), ce code est UB.

Que se passe-t-il s'il ne s'agit pas d'un UB instantané? Que puis-je faire avec cette valeur? Puis-je correspondre dessus? Si oui, le comportement du programme est-il déterministe?

Vous ne pouvez en aucun cas inspecter un u8 non initialisé. match peut faire beaucoup de choses, à la fois des noms de liaison et en fait des tests d'égalité; le premier est correct mais le second non. Mais vous pouvez l' écrire en mémoire.

C'est essentiellement ce que miri met en œuvre actuellement.

J'ai l'impression que si je ne peux pas faire correspondre la valeur sans introduire UB, alors nous avons réinventé mem :: uninitialized.

Pourquoi ça? Le plus gros problème avec mem::uninitialized concernait les types qui ont des restrictions quant à leurs valeurs valides. Nous pourrions décider que u8 n'a pas de telles restrictions, donc mem::uninitialized() convenait pour u8 . Il était presque impossible de l'utiliser correctement dans le code générique, il est donc préférable de s'en débarrasser complètement.
Quoi qu'il en soit, il n'est toujours pas acceptable de passer un u8 non initialisé à un code sécurisé, mais il peut être correct de l'utiliser avec précaution dans un code non sécurisé.

Vous ne pouvez pas non plus "faire correspondre" un &mut pointant vers des données invalides. IOW, je pense que l'exemple bool j'ai donné ci-dessus est bien, mais ce qui suit ne l'est certainement pas:

let mut b = MaybeUninit::<bool>::uninitialized();
let bref = b.get_mut();
match bref {
  &b => // insta-UB! We have a bad bool in scope.
}

Ceci utilise match pour faire une déréférence normale de pointeur.

FWIW, deux des avantages de ne pas être UB sont que a) il s'aligne avec ce que fait LLVM et, b) il permet plus de flexibilité dans les optimisations. Cela semble également plus cohérent avec votre proposition récente de définir la sécurité au moment de l'utilisation, et non au moment de la construction.

Quelles optimisations cela permettrait-il?
Notez que LLVM effectue des optimisations sur du code essentiellement non typé, donc rien de tout cela n'est un problème. Nous ne parlons ici que des optimisations MIR.

Je viens essentiellement de la perspective que nous devrions autoriser le moins possible jusqu'à ce que nous ayons une utilisation claire. Nous pouvons toujours autoriser plus de choses plus tard, mais pas l'inverse. Cela dit, de bonnes utilisations de tranches d'octets capables d'ancrer toutes les données ont récemment été utilisées, ce qui pourrait être un argument suffisant pour le faire au moins pour u* et i* .

Si nous décidons qu'une référence n'est valide que si elle pointe vers quelque chose de valide (c'est ce que j'entends par «récurer sous les références»), ce code est UB.

Je t'ai eu.

Le plus gros problème avec mem :: uninitialized concernait les types qui ont des restrictions quant à leurs valeurs valides.

mem::uninitialized également le problème que vous avez signalé ci-dessus: que la création d'une référence à une valeur non initialisée peut être un comportement non défini (ou non). Alors est-ce que l'UB suivant?

let mut b = MaybeUninit::<u8>::uninitialized().into_inner();
let bref = &mut b; // Insta UB ?

Je pensais que l'une des raisons pour introduire MaybeUninit était d'éviter ce problème en ayant toujours le syndicat initialisé (par exemple à l'unité), ce qui vous permet de prendre une référence et de muter son contenu, en définissant par exemple le champ actif au u8 et lui donner une valeur via ptr::write sans introduire UB.

C'est pourquoi je suis un peu confus. Je ne vois pas comment into_inner est meilleur que:

let mut b: u8 = uninitialized();
let bref = &mut b; // Insta UB ? 

Les deux ressemblent à des bombes à retardement à comportement indéfini pour moi.

Quelles optimisations cela permettrait-il?
Notez que LLVM effectue des optimisations sur du code essentiellement non typé, donc rien de tout cela n'est un problème. Nous ne parlons ici que des optimisations MIR.

Si nous disons que la mémoire non définie a une certaine valeur, et que vous êtes donc autorisé à y brancher selon la sémantique Rust, nous ne pouvons pas la réduire à la version LLVM de undefined, car elle ne serait pas saine.

Je viens essentiellement de la perspective que nous devrions autoriser le moins possible jusqu'à ce que nous ayons une utilisation claire. Nous pouvons toujours autoriser plus de choses plus tard, mais pas l'inverse.

C'est juste.

Cela dit, de bonnes utilisations de tranches d'octets capables d'ancrer toutes les données sont apparues récemment, ce qui pourrait être un argument suffisant pour le faire au moins pour u* et i* .

L'un de ces cas d'utilisation inclut-il des tranches d'octets contenant des valeurs non initialisées?

Un endroit où un &mut [u8] non initialisé mais pas un poison pourrait être utile est pour Read::read - nous aimerions pouvoir éviter d'avoir à remettre à zéro le tampon simplement parce que certains Read bizarres

Un endroit où un &mut [u8] non initialisé mais pas un poison pourrait être utile est pour Read::read - nous aimerions pouvoir éviter d'avoir à remettre à zéro le tampon simplement parce que certains Read bizarres

Je vois, donc l'idée est que MaybeUninit représenterait un type qui est initialisé, mais avec un contenu non défini, tandis que d'autres types de données non initialisées (par exemple, les champs de remplissage) seraient toujours entièrement non initialisés au sens du poison LLVM?

Je ne pense pas que cela devrait s'appliquer à MaybeUninit en général. Il pourrait en théorie y avoir une API pour "geler" le contenu de non défini à défini-mais-arbitraire.

Si nous disons que la mémoire non définie a une certaine valeur, et que vous êtes donc autorisé à y brancher selon la sémantique Rust, nous ne pouvons pas la réduire à la version LLVM de undefined, car elle ne serait pas saine.

Ce n'était jamais la proposition. Il est et restera UB de branchement sur poison .

La question est de savoir si UB doit simplement "avoir" un poison dans un u8 local.

L'un de ces cas d'utilisation inclut-il des tranches d'octets contenant des valeurs non initialisées?

Les tranches sont comme des références, donc &mut [u8] de données non initialisées convient tant qu'elles ne sont écrites que dans (en supposant que c'est la solution que nous prenons pour la validité de référence).

@sfackler

Un endroit où un non initialisé mais pas un poison & mut [u8] pourrait être utile pour Read :: read - nous aimerions pouvoir éviter d'avoir à remettre à zéro le tampon juste parce qu'un implément de lecture étrange pourrait en lire plutôt que de simplement y écrire.

Eh bien, sans &out vous ne pourrez le faire que si vous connaissez l'impl. La question n'est pas de savoir si le code sécurisé doit gérer poison en u8 (ce n'est pas le cas, ce n'est pas une utilisation correcte du code sécurisé!), La question est de savoir si un code non sécurisé peut le gérer avec soin par ici. (Voir ce billet de blog que je voulais écrire aujourd'hui sur la distinction entre les invariants de sécurité et les invariants de validité ...)

Peut-être que je suis en retard, mais je suggérerais de changer la signature de la méthode set() pour renvoyer &mut T . De cette façon, il serait sûr d'écrire du code complètement sûr fonctionnant avec MaybeUninit (au moins dans certaines situations).

fn init(dest: &mut MaybeUninit<u8>) -> &mut u8 {
    dest.set(produce_value())
}

C'est pratiquement une garantie statique que init() initialisera la valeur ou divergera. (S'il essayait de renvoyer quelque chose d'autre, la durée de vie serait erronée et &'static mut u8 est impossible en code sécurisé.) Peut-être qu'il pourrait être utilisé dans le cadre de l'API placer à l'avenir.

@Kixunil Il en a été ainsi avant, et je suis d'accord que c'est bien. Je trouve juste le même set déroutant pour une fonction qui renvoie quelque chose.

@Kixunil

C'est pratiquement une garantie statique que init() initialisera la valeur ou divergera. (S'il essayait de renvoyer quelque chose d'autre, la durée de vie serait fausse et &'static mut u8 est impossible en code sécurisé.)

Pas assez; vous pouvez en obtenir un avec Box::leak .

Dans une base de code que j'ai écrite récemment, j'ai proposé un schéma similaire; c'est un peu plus compliqué, mais fournit une véritable garantie statique que la référence fournie a été initialisée. Au lieu de

fn init(dest: &mut MaybeUninit<u8>) -> &mut u8

j'ai

fn init<'a>(dest: Uninitialized<'a, u8>) -> DidInit<'a, u8>

L'astuce est que Uninitialized et DidInit sont tous deux invariants sur leurs paramètres de durée de vie, il n'y a donc aucun moyen de réutiliser un DidInit avec un paramètre de durée de vie différent, même par exemple 'static .

DidInit impls Deref et DerefMut , donc le code sécurisé peut l'utiliser comme référence, comme dans votre exemple. Mais la garantie qu'il s'agissait en fait de la référence d'origine transmise qui a été initialisée, et non d'une autre référence aléatoire, est utile pour le code non sécurisé . Cela signifie que vous pouvez définir des initialiseurs structurellement:

struct Foo {
    a: i32,
    b: u8,
}

fn init_foo<'a>(dest: Uninitialized<'a, Foo>,
                init_a: impl for<'x> FnOnce(Uninitialized<'x, i32>) -> DidInit<'x, i32>,
                init_b: impl for<'x> FnOnce(Uninitialized<'x, u8>) -> DidInit<'x, u8>)
                -> &'a mut DidInit<'a, Foo> {
    let ptr: *mut Foo = dest.ptr;
    unsafe {
        init_a(Uninitialized::new(&mut (*ptr).a));
        init_b(Uninitialized::new(&mut (*ptr).b));
        dest.did_init()
    }
}

Cette fonction initialise un pointeur vers struct Foo en initialisant chacun de ses champs tour à tour, à l'aide des rappels d'initialisation fournis par l'utilisateur. Cela nécessite que les rappels retournent DidInit s, mais ne se soucie pas de leurs valeurs; le fait qu'ils existent suffit. Une fois que tous les champs ont été initialisés, il sait que l'intégralité de Foo est valide - il appelle donc did_init() sur le Uninitialized<'a, Foo> , qui est une méthode non sûre qui le convertit simplement en correspondant DidInit type, que init_foo retourne ensuite.

J'ai également une macro qui automatise le processus d'écriture de telles fonctions, et la version réelle est un peu plus prudente avec les destructeurs et les paniques (bien qu'elle ait besoin d'être améliorée).

Quoi qu'il en soit, je me demande si quelque chose comme ça pourrait être implémenté dans la bibliothèque standard.

Lien Playground

(Remarque: DidInit<'a, T> est en fait un alias de type pour &'a mut _DidInitMarker<'a, T> , pour éviter les problèmes à vie avec DerefMut .)

À propos, alors que l'approche liée ci-dessus ignore les destructeurs, une approche légèrement différente consisterait à rendre DidInit<‘a, T> responsable de l'exécution du destructeur de T . Dans ce cas, il faudrait que ce soit une structure, pas un alias; et il ne pourrait donner que des références à T qui vivent aussi longtemps que le DidInit lui-même, pas pour tout ’a (sinon vous pourriez continuer à y accéder après la destruction).

+1 pour avoir inclus une méthode pour donner le comportement que j'avais précédemment demandé dans set , mais je suis d'

Avez-vous de bonnes idées pour ce que pourrait être ce nom? set_and_as_mut ? ^^

set_and_borrow_mut ?

insert / insert_mut ? Le type Entry a une méthode or_insert quelque peu similaire (mais OccupiedEntry également insert qui renvoie l'ancienne valeur, donc ce n'est pas du tout similaire).

Y a-t-il une raison vraiment convaincante d'avoir deux méthodes distinctes? Cela semble assez simple pour ignorer la valeur de retour, et j'imagine que la fonction serait marquée comme #[inline] donc je ne m'attendrais pas à un coût d'exécution réel.

Y a-t-il une raison vraiment convaincante d'avoir deux méthodes distinctes? Il semble assez simple d'ignorer la valeur de retour

Je suppose que la seule raison est que voir set retourner quelque chose est assez surprenant.

Peut-être que je manque quelque chose, mais qu'est-ce qui pourrait nous éviter d'avoir une valeur invalide? Je veux dire si nous

let mut foo: MaybeUninit<T> = MaybeUninit {
    uninit: (),
};
let mut foo_ref = &mut foo as *mut MaybeUninit<T>;

unsafe {
    some_native_function(&mut (*foo_ref).value, val);
}

et si some_native_function est no-op et n'initialise pas réellement la valeur? Est-ce toujours UB? Comment cela pourrait-il être géré?

@Pzixel, tout cela est couvert par la documentation de l'API pour MaybeUninit .

Si some_native_function est un NOP, rien ne se passe; si vous utilisez ensuite foo_ref.value (ou plutôt faites foo_ref.as_mut() car vous ne pouvez utiliser que l'API publique), c'est UB car la fonction ne peut être appelée qu'une fois que tout est initialisé.

MaybeUninit n'empêche pas d'avoir des valeurs invalides - si c'était possible, ce serait sûr, mais ce n'est pas possible. Cependant, cela rend le travail avec des valeurs invalides moins un footgun parce que maintenant les informations que la valeur pourrait être invalide sont codées dans le type, pour que le compilateur et le programmeur les voient.

Je voulais documenter une conversation IRC que j'ai eue avec @sfackler concernant un problème hypothétique qui pourrait survenir à l'avenir.

La question principale est de savoir si mem::zeroed est une représentation en mémoire valide pour la proposition d'implémentation actuelle pour MaybeUninit<NonZeroU8> . À mon avis, dans l'état «uninit», la valeur est uniquement un remplissage, que le compilateur peut utiliser dans n'importe quel but, et dans l'état «valeur», toutes les valeurs possibles sauf mem::zeroed sont valides (à cause de NonZero ).

Un futur système de disposition de type avec un empaquetage discriminant d'énumération plus avancé (que celui que nous avons maintenant) pourrait alors stocker un discriminant dans le remplissage de l'état "uninit" / mémoire remise à zéro dans l'état "valeur". Dans ce système hypothétique, la taille de Option<MaybeUninit<NonZeroU8>> est de 1, alors qu'elle est actuellement de 2. De plus, dans ce système hypothétique, Some(MaybeUninit::uninitialized()) serait indiscernable de None . Je pense que nous pouvons probablement résoudre ce problème en modifiant l'implémentation de MaybeUninit (mais pas son API publique) une fois que nous passerons à un tel système.

Je ne vois aucune différence entre NonZeroU8 et &'static i32 à cet égard. Ces deux types sont des types où "0" n'est pas valide . Donc, pour les deux, MaybeUninit<T>::zeroed().into_inner() est insta-UB.

Que Option<Union> puisse faire des optimisations de mise en page dépend de la validité d'une union. Ce n'est pas encore décidé pour tous les cas, mais il y a un accord général sur le fait que pour les unions qui ont une variante de type () , tout motif de bits est valide et donc aucune optimisation de mise en page n'est possible. Cela couvre MaybeUninit . Donc Option<MaybeUninit<NonZeroU8>> n'aura jamais la taille 1.

il y a un accord général sur le fait que pour les unions qui ont une variante de type (), tout motif de bits est valide et par conséquent aucune optimisation de mise en page n'est possible.

Est-ce un cas particulier pour les «unions qui ont une variante de type ()»? La stabilisation de cette fonction stabilise-t-elle implicitement cette partie de l'ABI Rust? Qu'en est-il d'un union contenant struct UnitType; ou struct NewType(()); ? Qu'en est-il de struct Padded (ci-dessous)? Et un union contenant struct Padded ?

#[repr(C, align(4))]
struct Padded {
    a: NonZeroU8,
    b: (),
    c: NonZeroU16
}

Ma formulation était terriblement précise, car c'est littéralement la seule chose sur laquelle je suis à peu près sûr que nous avons un accord général. :) Je pense que nous voudrions que cela dépende uniquement de la taille (c'est-à-dire que tous les ZST l'obtiendraient), mais en fait, je pense que cette variante ne devrait même pas être nécessaire et que les syndicats n'obtiendront jamais d'optimisation de la mise en page par défaut (mais finalement les utilisateurs peuvent être en mesure de s'inscrire à l'aide d'attributs). Mais ce n'est que mon opinion.

Nous aurons une discussion appropriée pour évaluer le consensus actuel et peut-être obtenir un accord sur plus de choses dans l' une des prochaines discussions sur le repo UCG , et vous êtes invités à vous joindre là-bas lorsque cela se produira.

La stabilisation de cette fonction stabilise-t-elle implicitement cette partie de l'ABI Rust?

Nous parlons ici des invariants de validité, pas de la mise en page des données (à laquelle je suppose que vous faites référence lorsque vous affichez l'ABI). Donc rien de tout cela ne stabiliserait un ABI. Celles-ci sont liées mais distinctes, et en fait, il y a actuellement une discussion en cours sur l'ABI des syndicats .

Celles-ci sont liées mais distinctes, et en fait, une discussion est en cours sur l'ABI des syndicats.

AFAICT que la discussion porte uniquement sur la représentation mémorielle des syndicats et n'inclut pas la manière dont les syndicats sont passés à travers les limites des fonctions et d'autres choses qui pourraient être pertinentes pour une ABI. Je ne pense pas que l'objectif du repo UCG soit de créer un ABI pour Rust.

Eh bien, l'objectif est de définir suffisamment de choses pour l'interopérabilité avec C. Des choses comme "Rust bool et C bool sont compatibles ABI".

Mais en effet, pour repr(Rust) , je pense qu'il n'est pas prévu de définir un appel de fonction ABI - mais cela devrait idéalement être une déclaration explicite quelle que soit la forme que prend le document résultant, pas simplement une omission.

Je suis curieux de savoir s'il existe un argument contre l'optimisation Option<Foo> mise en page Foo est défini comme ceci:

union Foo {
   bar: NonZeroUsize,
   baz: &'static str,
}

@Kixunil pourriez-vous soulever cela à https://github.com/rust-rfcs/unsafe-code-guidelines/issues/13? Votre question n'est vraiment pas liée à MaybeUninit .

Je veux savoir quelle section contient des variables
Dans « C » Je peux écrire uint8_t a[100]; à haut niveau de fichier, et je sais que le symbole sera mis à l' article .bss. Si je vous écris uint8_t a[100] = {}; le symbole sera mis à l' article .data (qui sera copié de la mémoire flash dans la RAM avant principale).

C'est un petit exemple dans Rust qui a utilisé MaybeUninit:

struct A {
    data: MaybeUninit<[u8; 100]>,
    len: usize,
}

impl A {
    pub const fn new() -> Self {
        Self {
            data: MaybeUninit::uninitialized(),
            len: 0,
        }
    }
}

static mut a: MaybeUninit<[u8; 100]> = MaybeUninit::uninitialized();
static mut b: A = A::new();

Quelle section sera contient a et b symboles?

PS Je connais la mutilation des symboles mais ce n'est pas grave pour cette question.

@ qwerty19106 Dans votre exemple, a et b seront placés dans .bss . LLVM traite les valeurs undef , comme MaybeUninit::uninitialized() , comme des zéros lors de la sélection de la section dans laquelle une variable va entrer.

Si A::new() initialisé len à 1 alors b serait terminé par .data . Si un static contient une valeur non nulle alors la variable ira dans .data . Le remplissage est traité comme des valeurs nulles.

C'est ce que fait LLVM. Rust ne fait aucune ~ garantie ~ promesse (*) sur la section de l'éditeur de liens dans laquelle une variable static entrera. Elle hérite simplement du comportement de LLVM.

(*) Sauf si vous utilisez #[link_section]

Fait amusant: À un moment donné, LLVM a considéré undef comme une valeur non nulle, donc la variable a dans votre exemple s'est terminée par .data . Voir # 41315.

Merci @japaric pour votre réponse. Cela a été très utile pour moi.

J'ai la nouvelle idée.
On peut utiliser la section .init_array pour initialiser des variables main .

Ceci est une preuve de concept:

#[macro_export]
macro_rules! static_singleton {
    ($name_var: ident, $ty:ty, $name_init_fn: ident, $name_init_var: ident, $init_block: block) => {
        static mut $name_var: MaybeUninit<$ty> = unsafe {MaybeUninit::uninitialized()};

        extern "C" fn $name_init_fn() {
            unsafe {
                $init_block
            }
        }

        #[link_section = ".init_array"]
        #[used]
        static $name_init_var: [extern "C" fn(); 1] = [$name_init_fn];
    };
}

Le code de test :

static_singleton!(A, u8, a_init_fn, A_INIT_VAR, {
    let ptr = A.get_mut();
    *ptr = 5;
});

fn main() {
    println!("A inited to {}", unsafe {&A.get_ref()});
}

Résultat : A inited à 5

Exemple complet : aire de jeux

Question non résolue :
Je ne pouvais pas utiliser concat_idents pour générer a_init_fn et A_INIT_VAR . Il semble que # 1628 ne soit pas encore prêt à être utilisé.

Ce test n'est pas très utile. Mais il peut être utile en embarqué pour initialiser des structures compliquées (il se placera en .bss , permet donc d' FLASH ).

Pourquoi rustc n'utilise pas la section .init_array ? Il s'agit d'une section standardisée au format ELF ( lien ).

@ qwerty19106 Parce que la vie avant main () est considérée comme une erreur et a été explicitement désinvitée de la sémantique de Rust.

Ok, c'est un bon design lang.

Mais dans # [no_std] nous n'avons pas de bonne alternative maintenant (peut-être que je cherchais mal).

Nous pouvons utiliser spin :: Once , mais c'est très cher ( Ordering :: SeqCst sur chaque référence get).

Je voudrais avoir une vérification à la compilation sur embarqué .

c'est très cher ( Ordering::SeqCst sur chaque référence obtenue).

Cela ne me semble pas juste. Toutes les abstractions "une fois" ne sont-elles pas censées être assouplies lors de l'accès et se synchroniser lors de l'initialisation? Ou est-ce que je pense à autre chose?
cc @Amanieu @alexcrichton

@ qwerty19106 :

Quand vous dites «embarqué», parlez-vous du bare-metal? Il convient de noter que .init_array ne fait pas, en fait, partie du format ELF lui-même ¹ - Il ne fait même pas partie de l' ABI System V² qui l'étend; seulement .init est. Vous ne trouvez pas .init_array tant que vous n'avez pas projet de mise à jour de l'ABI System V , dont l'ABI Linux hérite.

En conséquence, si vous utilisez du bare metal, .init_array peut même ne pas fonctionner de manière fiable pour votre cas d'utilisation - après tout, il est implémenté sur un système non bare metal par code dans le chargeur dynamique et / ou la libc. À moins que votre chargeur de démarrage ne prenne la responsabilité d'exécuter le code référencé dans .init_array , il ne ferait rien du tout.

1: Voir page 28, figure 1-13 "Sections spéciales"
2: Voir page 63, figure 4-13 "Sections spéciales (suite)"

@eddyb Vous avez au moins besoin d'une charge Acquire lors de la lecture du Once . Il s'agit d'une charge normale sur x86 et d'une charge + clôture sur ARM.

L'implémentation actuelle utilise load(SeqCst) , mais en pratique cela génère le même asm que load(Acquire) sur toutes les architectures.

(Pourriez-vous déplacer ces discussions ailleurs? Elles n'ont plus rien à voir avec MaybeUninit vs mem :: uninitialized. Les deux se comportent de la même manière que LLVM - génère undef. Ce qui se passe avec undef plus tard n'est pas sur le sujet ici. )

Am 13. septembre 2018 00:59:20 MESZ schrieb Amanieu [email protected] :

@eddyb Vous avez au moins besoin d'une charge Acquire lors de la lecture du
Once . Il s'agit d'une charge normale sur x86 et d'une charge + clôture sur ARM.

L'implémentation actuelle utilise load(SeqCst) , mais en pratique ceci
génère le même asm que load(Acquire) sur toutes les architectures.

-
Vous recevez cela parce que vous avez été mentionné.
Répondez directement à cet e-mail ou affichez-le sur GitHub:
https://github.com/rust-lang/rust/issues/53491#issuecomment -420825802

MaybeUninit a atterri en master et sera dans la prochaine soirée. :)

https://github.com/rust-lang/rust/issues/54470 propose d'utiliser Box<[MaybeUninit<T>]> en RawVec<T> . Pour activer cela et éventuellement d'autres combinaisons intéressantes avec des boîtes et des tranches avec moins de transmutes, nous pourrions peut-être ajouter plus d'API à la bibliothèque standard?

En particulier pour allouer sans initialiser (je pense que Box::new(MaybeUninit::uninitialized()) copierait toujours size_of::<T>() padding bytes?):

impl<T> Box<MaybeUninit<T>> {
    pub fn new_uninit() -> Self {…}
    pub unsafe fn assert_init(s: Self) -> Box<T> { transmute(s) }
}

impl<T> Box<[MaybeUninit<T>]> {
    pub fn new_uninit_slice(len: usize) -> Self {…}
    pub unsafe fn assert_init(s: Self) -> Box<[T]> { transmute(s) }
}

Dans core::slice / std::slice , peut être utilisé après avoir pris une sous-tranche:

pub unsafe fn assert_init<T>(s: &[MaybeUninit<T>]) -> &[T] { transmute(s) }
pub unsafe fn assert_init_mut<T>(s: &mut [MaybeUninit<T>]) -> &mut [T] { transmute(s) }

Je pense que Box :: new (MaybeUninit :: uninitialized ()) copierait toujours size_of ::() octets de remplissage

Cela ne devrait pas, et il existe un test de codegen destiné à le tester.

Les octets de remplissage n'ont pas besoin d'être copiés car leur représentation binaire n'a pas d'importance (tout ce qui observerait la représentation binaire est de toute façon UB).

Ok, alors peut-être que Box::new_uninit n'est pas nécessaire? La version slice est cependant différente, puisque Box::new nécessite T: Sized .

Je voudrais plaider pour que MaybeUninit::zeroed soit un const fn . Il y a quelques utilisations liées à FFI que j'aurais pour cela (par exemple, un statique qui doit être initialisé à zéro) et je pense que d'autres pourraient le trouver utile. Je serais heureux de donner mon temps pour la const-ify la fonction zeroed .

@mjbshaw vous devrez utiliser #[rustc_const_unstable(feature = "const_maybe_uninit_zeroed")] pour cela puisque zeroed fait des choses qui ne passent pas le min_const_fn chèque (https://github.com/rust-lang/ rust / issues / 53555) ce qui signifie que la constness de MaybeUninit::zeroed sera instable même si la fonction est stable.

La mise en œuvre / la stabilisation de cela pourrait-elle être divisée en deux étapes, afin de rendre le type MaybeUninit disponible plus tôt pour l'écosystème plus large? Les étapes pourraient être:

1) ajouter MaybeUninit
2) Convertissez toutes les utilisations de mem :: uninitialized / zeroed et obsolète

@scottjmaddox

ajouter MaybeUninit

https://doc.rust-lang.org/nightly/core/mem/union.MaybeUninit.html :)

Agréable! Alors, est-ce que le plan pour stabiliser MaybeUninit dès que possible?

La prochaine étape consiste à comprendre pourquoi https://github.com/rust-lang/rust/pull/54668 régresse si mal les performances (dans quelques benchmarks). Je n'aurai pas beaucoup de temps pour regarder cela cette semaine, je serais heureux si quelqu'un d'autre pouvait y jeter un coup d'œil. :RÉ

Je ne pense pas non plus que nous devrions précipiter cela. Nous avons la dernière API pour gérer les données non initialisées de manière erronée, ne nous précipitons pas et ne foutons pas à nouveau. ;)

Cela dit, je préférerais également ne pas ajouter de retards inutiles, afin que nous puissions enfin rendre obsolète l'ancienne arme à pied. :)

Oh, et quelque chose d'autre vient de me venir à l'esprit ... avec https://github.com/rust-lang/rust/pull/54667 débarqué, les anciennes API protègent en fait contre certaines des pires armes à feu. Je me demande si nous pourrions également en obtenir pour MaybeUninit ? Cela ne bloque pas la stabilisation, mais nous pourrions essayer de trouver un moyen de faire paniquer MaybeUninit::into_inner lorsqu'il est appelé sur un type inhabité. Dans les versions de débogage, je pourrais aussi imaginer *x paniquer quand x: &[mut] T avec T inhabitée.

Mise à jour du statut: pour progresser avec https://github.com/rust-lang/rust/pull/54668, nous avons probablement besoin de quelqu'un pour peaufiner le calcul de la mise en page pour les unions. @eddyb est prêt à encadrer, mais nous avons besoin de quelqu'un pour faire la mise en œuvre. :)

Je pense qu'une méthode qui sort du wrapper, en la remplaçant par une valeur non initialisée, serait utile:

pub unsafe fn take(&mut self) -> T

Dois-je soumettre ceci?

@shepmaster Cela ressemble beaucoup à la méthode into_inner existante. Peut-être pouvons-nous essayer d'éviter la duplication ici?

Aussi "remplacer par" est probablement la mauvaise image ici, cela ne devrait pas changer du tout le contenu de self . Seule la propriété est transférée, elle est donc désormais dans le même état que lorsqu'elle est construite non initialisée.

changer le contenu de self du tout

Bien sûr, l' implémentation serait essentiellement ptr::read , mais d'un point de vue d'utilisation, j'encouragerais l'encadrement en remplacement d'une valeur valide par une valeur non initialisée.

éviter la duplication

Je n'ai pas d'objection forte, car je m'attends à ce que la mise en œuvre de l'un appelle l'autre. Je ne sais tout simplement pas quel serait l'état final.

Je pense que into_inner est un nom de fonction bien trop innocent. Les gens, probablement sans lire les documents trop attentivement, continuent de faire MaybeUninit::uninitialized().into_inner() . Pouvons-nous changer le nom en quelque chose comme was_initialized_unchecked ou pour indiquer que vous ne devez l'appeler qu'après l'initialisation des données?

Je pense que la même chose s'applique probablement à take .

Bien qu'un peu gênant, quelque chose comme unchecked_into_initialized pourrait fonctionner?

Ou ces méthodes devraient-elles être entièrement supprimées et la documentation en donne des exemples avec x.as_ptr().read() ?

@SimonSapin into_inner consomme self ce qui est bien.

Mais pour take de @shepmaster , faire as_mut_ptr().read() ferait la même chose ... bien sûr, alors pourquoi vous embêteriez-vous même avec un pointeur mutable?

Que diriez-vous de take_unchecked et into_inner_unchecked ?

Ce serait un plan de sauvegarde, je suppose, mais je préférerais qu'il puisse indiquer que vous devez avoir initialisé.

Mettre à la fois l'accent sur le fait qu'il doit être initialisé et une description de ce qu'il fait (dérouler / dans_intérieur / etc.) Dans un seul nom devient assez difficile à manier, alors que diriez-vous de simplement faire le premier avec assert_initialized et laisser le second impliqué par la signature? Possible unchecked_assert_initialized pour éviter d'impliquer une vérification d'exécution comme assert!() .

Possible unchecked_assert_initialized pour éviter d'impliquer une vérification à l'exécution comme assert! ().

Nous faisons déjà la différence entre les hypothèses et les assertions via intrinsics::assume(foo) vs assert!(foo) , alors peut-être assume_initialized ?

assume est une API instable, un exemple stable de supposer vs assert est unreachable_unchecked vs unreachable et get_unchecked vs get . Je pense donc que unchecked est le bon terme.

Je dirais que foo_unchecked n'a de sens que lorsqu'il y a un foo , sinon la nature pure de la fonction étant unsafe m'indique que quelque chose de "différent" se passe sur.

Ce vélo est clairement de la mauvaise couleur

Avec cette API spécifique, nous avons déjà vu et continuerons de voir les programmeurs supposer que la non-sécurité est due au fait que "les données non initialisées sont des ordures, donc vous pouvez causer UB si vous les manipulez négligemment", plutôt que l'intention "c'est UB d'appeler cela sur les données non initialisées, période ". Je ne sais pas avec certitude si un ⚠️ sans doute redondant comme unchecked aidera avec cela, mais je préfère errer du côté d'être plus perplexe (= plus susceptible de pousser les gens à demander ou à lire la documentation très soigneusement).

@RalfJung

Je pense que into_inner est un nom de fonction bien trop innocent. Les gens, probablement sans lire les documents trop attentivement, continuent de faire MaybeUninit::uninitialized().into_inner() . Pouvons-nous changer le nom en quelque chose comme was_initialized_unchecked ou pour indiquer que vous ne devez l'appeler qu'après l'initialisation des données?

J'aime _ vraiment _ cette idée; Je suis convaincu que cela dit la bonne chose à la fois sur la sémantique et que c'est potentiellement dangereux.

@rkruppe

Mettre à la fois l'accent sur le fait qu'il doit être initialisé et une description de ce qu'il fait (dérouler / into_inner / etc.) Dans un seul nom devient plutôt difficile à manier, alors que diriez-vous de simplement faire le premier avec assert_initialized et laisser le second impliqué par la signature? Possible unchecked_assert_initialized pour éviter d'impliquer une vérification à l'exécution comme assert!() .

Je n'ai aucun scrupule à trouver des noms longs et lourds pour des choses dangereuses. Si cela fait que plus de gens réfléchissent à deux fois, même was_initialized_into_inner_unchecked me convient parfaitement. Rendre non ergonomique (dans des limites raisonnables) l'écriture de code non sécurisé est une fonctionnalité, pas un bogue;)

N'oubliez pas qu'une grande majorité de personnes utiliseront probablement un IDE avec une forme de saisie semi-automatique, donc un nom long est un problème mineur.

Je ne me soucie pas particulièrement de l'ergonomie d'utilisation de cette fonction, mais je pense qu'au-delà d'un certain point, les noms ont tendance à être écrémés plutôt que lus, et ce nom devrait vraiment être lu pour comprendre ce qui se passe. De plus, je m'attends à ce que cette fonction soit discutée / expliquée presque aussi souvent qu'elle est réellement utilisée (car elle est relativement niche et très subtile), et bien que la saisie de longs identifiants dans le code source puisse être bien (par exemple grâce aux IDE), les taper à partir de la mémoire dans un système de chat est ... moins agréable (je plaisante à moitié sur ce point, mais seulement à moitié).

@shepmaster Sure; J'utilise également un IDE avec complétion automatique; mais je pense qu'un nom plus long avec unchecked intérieur, y compris à l'intérieur d'un bloc unsafe , me donnerait au moins une pause supplémentaire.

@rkruppe

les taper de mémoire dans un système de chat est ... moins agréable (je plaisante à moitié sur ce point, mais seulement à moitié).

Je ferais ce compromis. Si un nom est un peu spécial, cela peut même le rendre plus mémorable. ;)

N'importe lequel de (ou noms similaires qui incluent les mêmes connotations sémantiques):

  • was_initialized_unchecked
  • was_initialized_into_inner_unchecked
  • is_initialized_unchecked
  • is_initialized_into_inner_unchecked
  • was_init_unchecked
  • was_init_into_inner_unchecked
  • is_init_unchecked
  • is_init_into_inner_unchecked
  • assume_initialized_unchecked
  • assume_init_unchecked

sont bien pour moi.

Et pour initialized_into_inner ? Ou initialized_into_inner_unchecked , si vous pensez que unchecked est vraiment nécessaire, même si j'ai tendance à être d'accord avec @shepmaster que unchecked n'est nécessaire que pour distinguer une autre variante _checked_ du même fonctionnalité, où les vérifications d'exécution ne se produisent pas.

Lors de l' implémentation manuelle d'un générateur auto-emprunteur, j'ai fini par utiliser ptr::drop_in_place(maybe_uninit.as_mut_ptr()) plusieurs fois, il semble que cela fonctionnerait bien comme une méthode inhérente unsafe fn drop_in_place(&mut self) sur MaybeUninit .

Il existe un précédent avec ManuallyDrop::drop .

Je dirais que foo_unchecked n'a de sens que lorsqu'il y a un toto correspondant, sinon la nature pure de la fonction étant non sûre m'indique que quelque chose de "différent" se passe.

Je ne pense pas que le fait de ne pas avoir de version sûre soit une bonne raison pour supprimer le panneau d'avertissement de la version non sûre.

supprimer le panneau d'avertissement de la version non sécurisée

Étant un peu hyperbolique, quand une fonction unsafe ne devrait-elle _unchecked coincé à la fin alors? Quel est l'intérêt d'avoir deux avertissements qui disent la même chose?

C'est une bonne question. :) Mais je pense que la réponse est "presque jamais", et je regrette en fait que nous ayons offset comme fonction non sécurisée sur les pointeurs qui n'exprime en aucun cas qu'elle est dangereuse. Il n'est pas nécessaire que ce soit littéralement unchecked , mais OMI il devrait y avoir quelque chose . Quand je suis de toute façon dans un bloc non sûr et que j'écris accidentellement .offset au lieu de .wrapping_offset , j'ai fait une promesse au compilateur que je n'avais pas l'intention de faire.

comme fonction non sécurisée sur des pointeurs qui n'exprime en aucun cas qu'il est dangereux

Cela résume mon étonnement à ce stade.

@shepmaster donc vous ne pensez pas qu'il est réaliste que quelqu'un édite du code à l'intérieur d'un bloc unsafe existant (peut-être un gros bloc, peut-être à l'intérieur d'un grand unsafe fn qui a implicitement un unsafe block), et ne soyez pas conscient que l'appel qu'ils ajoutent est unsafe ?

quelqu'un modifiera le code dans un bloc unsafe existant [...] et ne se rendra pas compte que l'appel qu'il ajoute est unsafe

Désolé, je n'avais pas l'intention d'écarter cette possibilité et cela semble réel. Mon opinion est que le fait d'ajouter des qualificatifs au nom d'une fonction pour indiquer qu'une fonction n'est pas sûre parce que le qualificatif unsafe existant ne nous aide pas semble indiquer un échec plus profond.

C'est peut-être un échec que nous ne pouvons pas corriger d'une manière rétrocompatible et l'ajout de mots aux noms est la seule solution possible, mais j'espère que ce n'est pas le cas.

peut-être un gros, peut-être dans un gros unsafe fn qui a implicitement un bloc unsafe

On m'a demandé pourquoi Rust autorise l'observation des variables parce que l'observation est clairement une mauvaise idée lorsque vous avez une fonction de plusieurs centaines de lignes. Je suis personnellement très dédaigneux de tels cas parce que je crois qu'un tel code est généralement accepté comme une mauvaise forme pour commencer.

Maintenant, si un aspect de Rust oblige les blocs non sécurisés à être plus gros que ce dont ils "ont besoin", peut-être que cela indique également un problème plus fondamental.


En passant, je me demande si les IDE + RLS peuvent identifier toute fonction marquée comme non sûre et les mettre en évidence spécialement. Mon éditeur met déjà en évidence le mot-clé unsafe , par exemple.

Maintenant, si un aspect de Rust oblige les blocs non sécurisés à être plus gros que ce dont ils "ont besoin", cela indique peut-être aussi un problème plus fondamental.

Eh bien, il y a https://github.com/rust-lang/rfcs/pull/2585;)

En passant, je me demande si les IDE + RLS peuvent identifier toute fonction marquée comme non sûre et les mettre en évidence spécialement.

Ce serait génial! Cependant, tout le monde ne lit pas uniquement le code dans les IDE, c'est-à-dire que les révisions ne sont généralement pas effectuées dans les IDE.

Maintenant, si un aspect de Rust oblige les blocs non sécurisés à être plus gros que ce dont ils "ont besoin", cela indique peut-être aussi un problème plus fondamental.

Je pense que les méthodes dangereuses dans les chaînes sont l'un des plus grands exemples - diviser une méthode au milieu en une liaison let peut être assez peu ergonomique, mais à moins que vous ne le fassiez, la chaîne entière est couverte.

Pas tout à fait «force», mais «motive» définitivement.

l il y a rust-lang / rfcs # 2585 ;)

Oui, mais je ne l'ai pas mentionné car cela n'aiderait pas non plus votre cas. Les gens peuvent toujours simplement ajouter un bloc unsafe autour de tout le corps (comme cela est mentionné dans les commentaires) et vous revenez au même problème: une fonction non sécurisée appelle "se faufiler".

Cependant, tout le monde ne lit pas uniquement le code dans les IDE

Oui, c'est pourquoi je mets cela de côté. Je suppose que j'aurais dû le dire plus clairement.


Je suppose que mon problème est que, effectivement , vous préconisez ceci:

unsafe fn unsafe_real_name_of_function() { ... }
          ^~~~~~ for humans
^~~~~~           for the compiler

Cela vous permet de voir clairement chaque fonction non sécurisée lors de la lecture du code. La répétition m'ennuie énormément et indique que quelque chose n'est pas optimal.

Cela vous permet de voir clairement chaque fonction non sécurisée lors de la lecture du code. La répétition m'ennuie énormément et indique que quelque chose n'est pas optimal.

Je comprends. Vous pouvez également voir cette répétition comme implémentant le principe des 4 yeux, où le compilateur fournit deux yeux. ;)

@shepmaster Je pense que cela invariants de cette méthode - c'est-à-dire quand le code unsafe n'est en fait pas UB , avec le nom plus simple.

Je suis d'accord que "décoché" n'est pas la meilleure option, mais il a un précédent en tant que "violant facilement les invariants".

Cela me fait souhaiter que nous ayons une convention de dénomination du type initialized_or_ub .

Je pense que ça dérape un peu

J'allais le dire moi-même. J'ai dit mon article (et apparemment personne n'est d'accord avec moi), alors je vais le laisser mentir; vous choisissez ce que vous voulez.

nous avions une convention de dénomination du type initialized_or_ub

Vous voulez dire comme maybe_uninit(ialized) ? Quelque chose qui pourrait être largement appliqué à un ensemble de méthodes connexes? 😇

Non, je veux dire comme unwrap_or_else - mettre ce qui se passe dans le "cas malheureux" dans le nom de la méthode.

@eddyb Hé ce n'est pas si mal ... .initialized_or_unsound peut-être?

En général, l'ajout d'informations de type aux noms d'identifiant est considéré comme un anti-pattern (par exemple foo_i32 , bar_mutex , baz_iterator ) car c'est pour cela que les types sont là.

Pourtant, en ce qui concerne les fonctions, même si unsafe fait partie du type fn , l'ajout de _unchecked , _unsafe , _you_better_know_what_you_are_doing semble être assez commun.

Je me demande, pourquoi est-ce le cas?

Aussi, pour info, il y a un problème (https://github.com/rust-analyzer/rust-analyzer/issues/190) dans rust-analyzer pour indiquer si les fonctions sont unsafe ou non. Les éditeurs et les IDE devraient être en mesure de mettre l'accent sur les opérations qui nécessitent unsafe dans des blocs unsafe , qui incluent non seulement l'appel des fonctions unsafe (indépendamment du fait qu'elles soient suffixées avec un identifiant comme, par exemple, _unchecked ou non), mais aussi déréférencer des pointeurs bruts, etc.

On peut soutenir que rust-analyzer ne peut pas encore le faire (EDIT: intellij-Rust peut en quelque sorte: https://github.com/intellij-rust/intellij-rust/issues/3013#issuecomment-440442306), mais si le l'intention est de préciser que l'appeler à l'intérieur d'un bloc unsafe nécessite unsafe , la coloration syntaxique est une alternative possible au suffixe avec n'importe quoi. Je veux dire, si vous voulez vraiment cela maintenant, vous pouvez probablement ajouter le nom de cette fonction en tant que "mot clé" à votre surligneur de syntaxe en quelques minutes et l'appeler un jour.

@gnzlbg

En général, l'ajout d'informations de type aux noms d'identifiant est considéré comme un anti-pattern (par exemple foo_i32 , bar_mutex , baz_iterator ) car c'est pour cela que les types sont là.

Bien sûr, la notation hongroise est généralement considérée comme un anti-modèle. Je serais d'accord. Cependant, en général, la sécurité n'est pas prise en compte dans ces discussions et je pense qu'étant donné le danger que présente UB, il y a de bonnes raisons de faire une exception ici.

Pourtant, en ce qui concerne les fonctions, même si unsafe fait partie du type fn , l'ajout de _unchecked , _unsafe , _you_better_know_what_you_are_doing semble être assez commun.

Je me demande, pourquoi est-ce le cas?

En termes simples: insécurité. En cas d'insécurité, la redondance est votre ami. Cela s'applique à la fois au code et au matériel critique pour la sécurité et autres.

De plus, pour info, il y a un problème ( rust-analyzer / rust-analyzer # 190 ) dans rust-analyzer pour indiquer si les fonctions sont unsafe ou non. Les éditeurs et les IDE devraient être en mesure de mettre l'accent sur les opérations qui nécessitent unsafe dans des blocs unsafe , qui incluent non seulement l'appel des fonctions unsafe (indépendamment du fait qu'elles soient suffixées avec un identifiant comme, par exemple, _unchecked ou non), mais aussi déréférencer des pointeurs bruts, etc.

On peut soutenir que rust-analyzer ne peut pas encore le faire, mais si l'intention est de préciser que l'appel à l'intérieur d'un bloc unsafe nécessite unsafe , la coloration syntaxique est une alternative possible au suffixe ceci avec n'importe quoi.

Tout cela est vraiment génial. Cependant, comme @RalfJung l'a noté: "Cependant, tout le monde ne lit pas seulement le code dans les IDE - c'est-à-dire que les révisions ne sont généralement pas effectuées dans les IDE." Il me semble peu probable que GitHub intègre un analyseur de rouille dans son interface utilisateur afin de montrer si une fonction / opération appelée est dangereuse ou non.

Si le compromis est entre être laid et être sujet à une utilisation incorrecte (et donc malsaine) de unsafe , je pense que nous devrions toujours préférer le premier. On peut dire beaucoup de choses pour faire en sorte qu'un programmeur _ ait_ de faire une pause et de penser à lui-même, "attendez, est-ce que je fais ça correctement?"

Par exemple, si vous souhaitez utiliser une opération cryptographique non sécurisée dans Mundane, vous devez:

  • Importez-le depuis le module insecure
  • Écrivez allow(deprecated) ou simplement en direct avec les avertissements du compilateur émis chaque fois que vous utilisez cette opération
  • Écrivez un code qui ressemble à let mut hash = InsecureSha1::default(); hash.insecure_write(bytes); ...

Tout est documenté ici plus en détail. Je ne pense pas que nous devions être _que_ agressifs en toutes circonstances, mais je pense que la bonne philosophie est que, si l'opération est suffisamment dangereuse pour être inquiète, alors il ne devrait y avoir aucun moyen pour un programmeur de manquer la gravité de ce qu'ils font.

Suggestion complètement sérieuse

Puisque nous sommes préoccupés à 95% par les gens qui utilisent mal ce type, et seulement 5% par les noms longs, commençons par renommer le type en MaybeUninitialized . Les 7 caractères supplémentaires en valent la peine.

Des suggestions surtout sérieuses

  1. Renommez-le en MaybeUninitializedOrUndefinedBehavior pour vraiment le marteler chez les utilisateurs finaux.

  2. Optez pour ce type pour ne pas avoir de méthode et tout peut être une fonction associée, renforçant le point à chaque appel de fonction, comme vous le souhaitez:

    MaybeUninitializedOrUndefinedBehavior::into_inner(value)
    

Suggestion idiote

MaybeUninitializedOrUndefinedBehaviorReadTheDocsAllOfThemYesThisMeansYou

Eh bien ... honnêtement, avoir un nom long tel que MaybeUninitializedOrUndefinedBehavior dans le type me semble déplacé. C'est l' opération .into_inner() qui a besoin du bon nom car c'est le bit potentiellement problématique qui nécessite une attention particulière. N'avoir aucune méthode pourrait être une bonne idée. MaybeUninit::initialized_or_undefined(foo) semble assez clair.

OMI, nous ne devrions pas faire tout notre possible pour rendre les opérations dangereuses non ergonomiques comme celle-ci. Nous avons besoin de noms ergonomiques et de moyens d'écrire un code non sécurisé correct. Si nous l'encombrons avec un tas de noms excessivement longs et d'utilitaires et de conversions peu clairs, cela découragera les utilisateurs d'écrire un code dangereux correct et rendra le code dangereux plus difficile à lire et à valider.

N'oubliez pas qu'une grande majorité de personnes utiliseront probablement un IDE avec une forme de saisie semi-automatique, donc un nom long est un problème mineur.

Tant que RLS n'est pas plus fonctionnel, ce n'est pas le cas pour moi du moins.

Je pense que la plupart d'entre nous conviennent que

  • Des noms plus descriptifs sont bons

  • Les noms moins ergonomiques sont mauvais

et la question est de savoir comment résoudre les choses lorsque celles-ci sont en tension.

Même si je pense que into_inner en particulier est un mauvais nom pour cette méthode (pour utiliser un terme sophistiqué, ce n'est pas à la frontière de Pareto). La convention générale est que nous avons un into_inner quand un Foo<T> contient exactement un T , et que vous voulez le sortir. Mais ce n'est pas le cas pour MaybeUninit<T> . Il contient zéro ou un T s.

Donc, une option moins mauvaise, au moins, serait de l'appeler unwrap , ou peut-être unwrap_unchecked .

Je pense aussi que from_initialized ou from_initialized_unchecked sonne bien, bien que "from" apparaisse généralement dans les noms des méthodes statiques.

Peut-être que unwrap_initialized_unchecked conviendrait?

Appelez-le take_initialized et faites-le &mut self au lieu de self . Le nom indique clairement qu'il s'attend à ce que la valeur interne soit initialisée. Dans le contexte de MaybeUninit , unsafe et le fait qu'il ne renvoie pas de Option / Result indique également clairement que cette opération n'est pas cochée.

Faire prendre un &mut self semble être une arme à pied qui permet de perdre plus facilement la trace de savoir si vous avez sémantiquement sorti du MaybeUninit .

Nom alternatif: puisqu'il s'agit vraiment de déplacer la propriété, tout comme les méthodes nommées into impliquent, peut-être into_initialized_unchecked ?

Le fait de prendre un & mut self semble être une arme à pied qui permet de perdre plus facilement la trace de votre sortie sémantique de MaybeUninit.

C'est une méthode qui a été demandée cependant, et tant que vous suivez autrement que cela ne se produit pas deux fois, ça va.

Et il ne semble pas utile d'avoir à la fois la variante empruntée et la variante consommatrice.

J'aime take_initialized , ou la variante plus explicite take_initialized_unchecked .

commençons par renommer le type en MaybeUninitialized

Quelqu'un est prêt à préparer un PR?

pour préparer un PR?

Je peux utiliser mes super compétences sed depuis que je l'ai suggéré ;-)

Je pense que ce serait une amélioration d'appeler la méthode into_inner quelque chose qui souligne qu'elle suppose qu'elle est initialisée, mais je pense que l'ajout de unchecked est superflu et inutile. Nous avons un moyen d'informer les utilisateurs que les fonctions non sécurisées ne sont pas sécurisées: nous générons une erreur de compilation si elles ne les encapsulent pas dans un bloc non sécurisé.

EDIT: take_initialized semble bon

Et pour assume_initialized ? Ce:

  • Se connecte au modèle d'`` obligation de preuve ''
  • Se connecte visuellement à `` les hypothèses sont risquées ''
  • Il suffit de deux mots
  • Décrit la signification sémantique de l'opération
  • Lit assez naturellement
  • Tout comme la LLVM assume intrinsèque, est UB si elle est mal supposée

Quelqu'un est prêt à préparer un PR?

Ça ne fait rien. L'équipe libs a décidé que cela n'en valait pas la peine.

Y a-t-il une raison pour laquelle MaybeUninit<T> n'est pas Copy quand T: Copy ?

@tommit parce que MaybeUninit<T> repose sur ManuallyDrop<T> , nous les programmeurs devrions garantir que la valeur interne est supprimée lorsque nos structures sont hors de portée. S'il implémente Copy , je pense qu'il sera peut-être plus difficile pour les nouveaux venus de Rust de se souvenir de supprimer la valeur interne T chaque fois, de la structure elle-même ou de ses copies. De cette façon, il peut produire des fuites de mémoire plus discrètes que nous ne nous attendons pas à.

@ luojia65 Pas sûr que cette ligne de raisonnement s'applique quand T lui-même est Copy , indépendamment de ce que font ManuallyDrop et MaybeUninit .

Je ne pense pas qu'il y ait de raison. Personne n'a pensé à ajouter #[derive(Copy)] ;)

Une observation sur peut-être un aspect quelque peu subtil de ceci:
Je crois que même si MaybeUninit<T> devrait être Copy quand T: Copy , MaybeUninit<T> ne devrait Clone quand T: Clone et T n'est pas Copy .

Oh oui, nous ne pouvons certainement pas simplement appeler clone .

J'oublie toujours que Copy: Clone ...

C'est bien, nous pouvons implémenter Clone for MaybeUninit<T> where T: Copy en retournant *self .

J'ai fait de mon mieux pour mettre à jour la description du problème avec toutes les questions soulevées ici. Faites-moi savoir si j'ai raté quelque chose!

La documentation pour ManuallyDrop::drop dit

Cette fonction exécute le destructeur de la valeur contenue et donc la valeur encapsulée représente désormais des données non initialisées. Il appartient à l'utilisateur de cette méthode de s'assurer que les données non initialisées ne sont pas réellement utilisées.

Avez-vous des suggestions pour améliorer ce libellé afin qu'il ne puisse pas être confondu avec le type de "non-initialisation" que MaybeUninit gère?

Dans mes conditions, a chuté ManuallyDrop<T> n'est plus un coffre - T , mais il est valide T ... au moins autant que les soins optimisations de mise en page.

"périmé" / "invalide", peut-être? Il est initialisé .

FWIW Je pense que le libellé est clair (du moins pour moi) en s'assurant qu'un
l'objet n'est pas lâché deux fois est un problème de «sécurité». Si nous avions un petit document
dans l'UCG définissant la «sécurité», il faudrait probablement créer un hyperlien. Vous pourriez
ajouter que T doit être une définition «valide» et un lien hypertexte «valide», mais puisque nous
je n'ai pas encore écrit ces définitions nulle part ... Je ne sais pas. je
ne pensez pas que nous devrions les paraphraser partout dans la documentation.

Pouvons-nous stabiliser MaybeUninit avant de déprécier non initialisé?

@RalfJung Je dirais que l'endroit est "déplacé de". FWIW, nous devrions utiliser le même type de terminologie dans std::ptr::read , mais ce n'est pas très clair non plus.

@bluss, nous ne devrions jamais abandonner tout ce qui est largement utilisé sans un "mieux
solution »/« chemin de migration »pour les utilisateurs actuels.

Les avertissements de dépréciation doivent être: «X est obsolète, utilisez Y à la place». Si nous
n'ont pas de Y et X est largement utilisé ... alors nous devrions envisager de tenir
l'avertissement d'obsolescence jusqu'à ce que nous ayons un Y.

Sinon, nous enverrions un message vraiment étrange.

@cramertj "invalide" n'est pas un bon choix, car il doit toujours (doit!) satisfaire l'invariant de validité.

Si nous avions un petit document dans l'UCG définissant la «sécurité», il faudrait probablement le mettre en hyperlien. Vous pourriez ajouter que T doit être une définition «valide» et un lien hypertexte «valide», mais comme nous n'avons encore écrit ces définitions nulle part ...

Nous devrions absolument le faire une fois que nous avons quelque chose: D

@RalfJung Je ne pense pas que "invariant de validité" soit dans le lexique de la plupart des utilisateurs de Rust (presque tous?) - Je pense que faire référence à des "données invalides" est familièrement acceptable (le ManuallyDrop<T> n'est plus utilisable comme a T ). Dire qu'il doit respecter certains invariants de représentation que le compilateur utilise pour les optimisations ne rend pas les données moins invalides.

Je ne pense pas que "l'invariant de validité" soit dans le lexique de la plupart (presque tous?) Des utilisateurs de Rust

Assez juste, le terme n'est pas (encore) officiel. Mais nous devrions finalement choisir un terme officiel pour cela, puis nous devrions éviter de tels affrontements. Nous pouvons dire que les données «valides» sont ce que j'appelle «sûres» dans mon message, mais nous avons besoin d'un autre mot pour ce que j'appelle «valide».

@shepmaster a écrit il y a quelque temps

Je pense qu'une méthode qui sort du wrapper, en la remplaçant par une valeur non initialisée, serait utile:

pub unsafe fn take(&mut self) -> T

Je pense que ma plus grande préoccupation à ce sujet est qu'avec une telle fonction, il est terriblement facile de copier accidentellement des données non copiées. Au cas où vous en auriez besoin, est-ce vraiment si mal de faire maybe_uninit.as_ptr().read() ?

Je pense que j'ai peut-être suggéré quelque part là-haut pour avoir quelque chose comme take remplacer quelque chose comme into_inner . Je ne pense plus que ce soit une bonne idée: la plupart du temps, la restriction supplémentaire que into_inner consomme self est en fait utile.

@RalfJung En fin de compte, toutes les méthodes de MaybeUninit sont pas sûres et ne sont que des enveloppes pratiques autour de as_ptr . Cependant, je m'attends take ce que MaybeUninit n'est en fait qu'un Option où le tag est géré en externe. Ceci est utile dans de nombreux cas, par exemple un tableau où tous les éléments ne sont pas initialisés (par exemple une table de hachage).

Dans https://github.com/rust-lang/rust/pull/57045 , je propose d'ajouter deux nouvelles opérations à MaybeUninit :

    /// Get a pointer to the first contained values.
    pub fn first_ptr(this: &[MaybeUninit<T>]) -> *const T {
        this as *const [MaybeUninit<T>] as *const T
    }

    /// Get a mutable pointer to the first contained values.
    pub fn first_mut_ptr(this: &mut [MaybeUninit<T>]) -> *mut T {
        this as *mut [MaybeUninit<T>] as *mut T
    }

Voir ce PR pour la motivation et la discussion.

Lors de la suppression de zeroed il semble qu'il ne soit remplacé que par MaybeUninit::zeroed().into_inner() ce qui devient une manière équivalente d'écrire la même chose. Il n'y a pas de changement pratique. Avec les valeurs uninit, nous avons à la place le changement pratique de toutes les données non initialisées conservées stockées dans des valeurs de type MaybeUninit ou union équivalente.

Pour cette raison, j'envisagerais de garder std::mem::zeroed tel quel, car il s'agit d'une fonction largement utilisée dans FFI. La dépréciation le ferait émettre des avertissements bruyants, ce qui équivaut presque à sa suppression, et au moins très ennuyeux - cela peut également conduire à un nombre croissant de #[allow(deprecated)] qui peuvent cacher d'autres problèmes plus importants.

Cet exercice de clarification du modèle et des directives de Rust pour le code marqué unsafe est super utile, mais évitons les changements comme pour zeroed où il ne fait que refondre le même effet pratique en utilisant une nouvelle façon de le dire .

@bluss Ma compréhension (ce qui peut être faux) est que std::mem:zeroed est tout aussi dangereux que std::mem::uninitialized , et est tout aussi susceptible de provoquer UB. Peut-être est-il utilisé pour initialiser des tableaux d'octets, qui seraient mieux initialisés avec vec![0; N] ou [0; N] , auquel cas peut-être une règle rustfix pourrait être ajoutée pour automatiser le changement? En dehors de l'initialisation des tableaux d'octets ou d'entiers, cependant, je crois comprendre qu'il y a de bonnes chances que l'utilisation de std::mem::zeroed puisse conduire à UB.

@scottjmaddox Il est très facile d'invoquer UB avec std::mem:zeroed , mais contrairement à std::mem::uninitialized , il existe certains types pour lesquels std::mem:zeroed est parfaitement valide (par exemple, les types natifs, de nombreux FFI struct s, etc.). Comme beaucoup de fonctions unsafe , zeroed() ne doit pas être utilisé à la légère, mais ce n'est pas aussi problématique que uninitialized() . Pour ma part, je serais triste de devoir utiliser MaybeUninit::zeroed().into_inner() au lieu de std::mem:zeroed() car il n'y a pas de différence entre les deux en termes de sécurité et la version MaybeUninit est plus lourde et un peu moins lisible (et quand je dois utiliser du code non sécurisé, j'apprécie beaucoup la lisibilité).

@mjbshaw

contrairement à std :: mem :: uninitialized, il existe certains types pour lesquels std :: mem: zeroed est parfaitement valide (par exemple, les types natifs,

Il existe certains types pour lesquels mem::uninitialized est parfaitement sûr ( par exemple unit ), tandis qu'il existe des types "natifs" (par exemple bool , &T , etc. .) pour laquelle mem::zeroed invoque un comportement indéfini.


Il semble y avoir une idée fausse ici que MaybeUninit est en quelque sorte une mémoire non initialisée (et je peux voir pourquoi: "Non initialisé" est dans son nom).

Le danger que nous essayons d'éviter est celui causé par la création d'une valeur _invalid_, que la valeur _invalid_ contienne tous les zéros, ou des bits non initialisés, ou autre chose (par exemple un bool partir d'un motif de bits qui n'est pas true ou false ), n'a pas vraiment d'importance - mem::zeroed et mem::uninitialized deux peuvent être utilisés pour créer une valeur _invalid_, et sont donc à peu près tout aussi dangereux de mon point de vue.

OTOH MaybeUninit::zeroed() et MaybeUninit::uninitialized() sont des méthodes _safe_ car elles renvoient un union . MaybeUninit::into_inner est unsafe , et l'appeler n'est _safe_ que si la condition préalable que les bits actuels dans MaybeUninit<T> représentent une valeur _valide_ de T est satisfaite. Si le modèle de bits est _invalid_, le comportement n'est pas défini. Que le motif binaire soit invalide car il contient tous les zéros, les bits non initialisés ou autre chose n'a pas vraiment d'importance.

@RalfJung Je commence à avoir le sentiment que le nom MaybeUninit peut être un peu trompeur. Peut-être devrions-nous le renommer en MaybeInvalid ou quelque chose comme ça pour mieux exprimer le problème qu'il résout et les dangers qu'il évite. EDIT: suite aux suggestions de @Centril , j'ai posté sur le problème du vélo.


EDIT: FWIW, je pense qu'avoir un moyen ergonomique (par exemple sans utiliser directement MaybeUninit ) pour créer de la mémoire à zéro en toute sécurité serait utile, mais mem::zeroed n'est-ce pas. Nous pourrions ajouter un trait Zeroed similaire à Default qui n'est implémenté que pour les types pour lesquels le motif binaire de tous les zéros est valide ou quelque chose comme ça, comme moyen d'obtenir un effet similaire à ce que mem::zeroed fait maintenant, mais sans ses pièges.

En général, je pense que nous ne devrions pas abandonner les fonctionnalités tant qu'un chemin de migration pour les utilisateurs actuels vers une meilleure solution n'est pas en place. MaybeUninit est une meilleure solution que mem::zeroed à mes yeux, même si ce n'est peut-être pas parfait (c'est plus sûr, mais ce n'est pas aussi ergonomique), donc je serais d' mem::zeroed avec la dépréciation de MaybeUninit atterrit, même si au moment où cela se produit, nous n'avons pas de remplacement plus ergonomique en place.

Peut-être devrions-nous le renommer en MaybeInvalid ou quelque chose comme ça pour mieux exprimer le problème qu'il résout et les dangers qu'il évite.

Bikeshed dans https://github.com/rust-lang/rust/pull/56138.

@gnzlbg

il existe des types "natifs" (par exemple bool

Tant que bool est FFI-safe (ce qui est généralement considéré comme étant, malgré le rejet de la RFC 954 puis l'acceptation officieusement officielle), il devrait être sûr d'utiliser mem::zeroed pour cela.

, &T , etc.) pour lesquels mem::zeroed invoque un comportement indéfini.

Oui, mais ces types qui ont UB pour mem::zeroed ont également UB pour MaybeUninit::zeroed().into_inner() (j'ai pris soin d'inclure intentionnellement .into_inner() dans mon commentaire d'origine). MaybeUninit n'ajoute rien si l'utilisateur appelle immédiatement .into_inner() (ce qui est précisément ce que moi et beaucoup d'autres ferions si mem::zeroed était obsolète, car je n'utilise que mem::zeroed pour les types qui sont sans sécurité).

Tant que bool est FFI-safe (ce qu'il est généralement considéré comme étant, malgré le rejet de la RFC 954 puis l'acceptation officieusement officielle), il devrait être sûr d'utiliser mem :: zeroed pour cela.

Je ne voulais pas entrer dans les détails de cela, mais bool est FFI-safe dans le sens où il est défini comme égal à C _Bool . Cependant, les valeurs true et false des _Bool C ne sont pas définies dans le standard C (bien qu'elles puissent être un jour, peut-être en C20), donc si mem::zeroed crée un bool ou non est techniquement défini par l'implémentation.

Oui, mais ces types qui ont UB pour mem :: zeroed ont aussi UB pour MaybeUninit :: zeroed (). Into_inner () (j'ai pris soin d'inclure intentionnellement .into_inner () dans mon commentaire d'origine). Peut-êtreUninit n'ajoute rien si l'utilisateur appelle immédiatement .into_inner () (ce qui est précisément ce que moi et beaucoup d'autres ferions si mem :: zeroed était obsolète, car j'utilise uniquement mem :: zeroed pour les types qui sont à sécurité zéro) .

Je ne comprends pas vraiment quel point vous essayez de faire valoir ici. MaybeUninit ajoute l'option d'appeler ou de ne pas appeler into_inner , ce que mem::zeroed n'a pas, et il y a de la valeur à cela puisque c'est l'opération qui peut introduire un comportement indéfini ( la construction de l'union comme non initialisée ou remise à zéro est sûre).

Pourquoi quelqu'un convertirait aveuglément mem::zeroed en MayeUninit + into_inner ? Ce n'est pas la manière appropriée de "corriger" l'avertissement de dépréciation de mem::zeroed , et le désactiver a le même effet et un coût bien inférieur.

La façon appropriée de passer de mem::zeroed à MaybeUninit est d'évaluer s'il est sûr d'appeler into_inner , auquel cas on peut simplement le faire et écrire un commentaire expliquant pourquoi cela est sûr, ou continuez simplement à travailler avec MaybeUninit tant que union jusqu'à ce que l'appel à into_inner devienne sûr (il peut être nécessaire de changer beaucoup de code jusqu'à ce que ce soit le cas, faites une rupture d'API changements pour retourner MaybeUninit au lieu de T s, etc.).

Je ne voulais pas entrer dans les détails de cela, mais bool est FFI-safe dans le sens où il est défini comme égal à C _Bool . Cependant, les valeurs true et false de C's _Bool are not defined in the C standard (although they might be some day, maybe in C20), so whether mem :: zeroed creates a valid bool` ou non sont techniquement définies par l'implémentation .

Toutes mes excuses pour continuer la tangente, mais C11 exige que tous les bits mis à zéro représente la valeur 0 pour les types entiers (voir section 6.2.6.2 "Types entiers", paragraphe 5) (qui inclut _Bool ) . De plus, les valeurs de true et false sont explicitement définies (voir la section 7.18 "Type booléen et valeurs <stdbool.h> ").

Je ne comprends pas vraiment quel point vous essayez de faire valoir ici. MaybeUninit ajoute l'option d'appeler ou de ne pas appeler into_inner , ce que mem::zeroed n'a pas, et il y a de la valeur à cela puisque c'est l'opération qui peut introduire un comportement indéfini ( la construction de l'union comme non initialisée ou remise à zéro est sûre).

Il y a une valeur en MaybeUninit et MaybeUninit::zeroed . Nous sommes tous les deux d'accord là-dessus. Je ne veux pas que MaybeUninit::zeroed soit supprimé. Mon point est qu'il y a aussi une valeur en std::mem::zeroed .

Il existe certains types pour lesquels mem :: uninitialized est parfaitement sûr (par exemple unit), alors qu'il existe des types "natifs" (par exemple bool, & T, etc.) pour lesquels mem :: zeroed invoque un comportement non défini.

C'est un hareng rouge. Le simple fait que zeroed et uninitialized soient valides pour certains sous-ensembles de types ne les rend pas comparables en utilisation réelle. Vous devez examiner la taille de ces sous-ensembles. Le nombre de types pour lesquels mem::uninitialized est valide est très très petit (en fait, s'agit-il uniquement de types de taille nulle?), Et personne n'écrirait de code qui fait cela (par exemple, pour les ZST, vous utiliseriez simplement le constructeur de type). D'autre part, il existe de nombreux types pour lesquels mem::zeroed est valide. mem::zeroed est valide pour au moins les types suivants (j'espère avoir bien compris):

  • tous les types d'entiers (y compris bool , comme mentionné ci-dessus)
  • tous les types de pointeurs bruts
  • Option<T> où T déclenche l'optimisation de la mise en page enum. T comprend:

    • NonZeroXXX (tous les types entiers)

    • NonNull<U>

    • &U

    • &mut U

    • fn -pointeurs

    • tout tableau de tout type dans cette liste

    • any struct où n'importe quel champ est un type dans cette liste.

  • Tout tableau, struct , ou union constitué uniquement des types de cette liste.

Oui, uninitialized et zeroed traitent tous deux des valeurs potentiellement invalides. Cependant, les programmeurs utilisent ces primitives de manières très différentes.

Le modèle courant pour mem::uninitialized est:

let val = MaybeUninit::uninitialized();
initialize_value(val.as_mut_ptr()); // or val.set
val.into_inner()

Si vous n'écrivez pas votre utilisation de valeurs non initialisées de cette façon, vous faites probablement une grosse erreur.

L' utilisation la plus courante de mem::zeroed aujourd'hui est pour les types décrits ci-dessus, et c'est parfaitement valable. Je suis tout à fait d'accord avec @bluss pour mem::zeroed() partout par MaybeUninit::zeroed().into_inner() .

Pour résumer, l'utilisation courante de uninitialized est pour les types pour lesquels peuvent avoir des valeurs non valides. L'utilisation courante de zeroed est pour les types qui sont valides s'ils sont mis à zéro.

Un trait Zeroed ou similaire (par exemple Pod , mais notez que T: Zeroed n'implique pas T: Pod ) comme cela a été suggéré semble être une bonne chose à ajouter le futur, mais n'abandonnons pas fn zeroed<T>() -> T jusqu'à ce que nous ayons réellement un fn zeroed2<T: Zeroed>() -> T stable.

@mjbshaw

Toutes mes excuses pour continuer la tangente, mais C11 exige que

En effet! C'est seulement le bool C ++ qui laisse les valeurs valides non spécifiées! Merci de m'avoir corrigé, je vais envoyer un PR à l'UCG avec cette garantie.

@jethrogb

Vous devez examiner la taille de ces sous-ensembles. Le nombre de types pour lesquels mem::uninitialized est valide est très très petit (en fait, s'agit-il uniquement de types de taille nulle?), Et personne n'écrirait de code qui le fasse (par exemple, pour les ZST, vous utiliseriez simplement le constructeur de type).

Ce n'est même pas correct pour tous les ZST si vous prenez en compte la confidentialité avec laquelle il est possible d'avoir des ZST comme une sorte de «preuve de travail» ou de «jeton de ressource» ou simplement de «témoin de preuve» en général. Un exemple trivial :

mod refl {
    use core::marker::PhantomData;
    use core::mem;

    /// Having an object of type `Id<A, B>` is a proof witness that `A` and `B`
    /// are nominally equal type according to Rust's type system.
    pub struct Id<A, B> {
        witness: PhantomData<(
            // Make sure `A` is Id is invariant wrt. `A`.
            fn(A) -> A,
            // Make sure `B` is Id is invariant wrt. `B`.
            fn(B) -> B,
        )>
    }

    impl<A> Id<A, A> {
        /// The type `A` is always equal to itself.
        /// `REFL` provides a proof of this trivial fact.
        pub const REFL: Self = Id { witness: PhantomData };
    }

    impl<A, B> Id<A, B> {
        /// Casts a value of type `A` to `B`.
        ///
        /// This is safe because the `Id` type is always guaranteed to
        /// only be inhabited by `Id<A, B>` types by construction.
        pub fn cast(self, value: A) -> B {
            unsafe {
                // Transmute the value;
                // This is safe since we know by construction that
                // A == B (including lifetime invariance) always holds.
                let cast_value = mem::transmute_copy(&value);

                // Forget the value;
                // otherwise the destructor of A would be run.
                mem::forget(value);

                cast_value
            }
        }
    }
}

fn main() {
    use core::mem::uninitialized;

    // `Id<?A, ?B>` is a ZST; let's make one out of thin air:
    let prf: refl::Id<u8, String> = unsafe { uninitialized() };

    // Segfault:
    let _ = prf.cast(42u8);
}

@Centril c'est une sorte de tangente, mais je ne suis pas sûr que votre code soit en fait un exemple d'un type pour lequel l'appel de uninitialized crée une valeur invalide. Vous utilisez un code non sécurisé pour violer les invariants internes que Id est censé respecter. Il existe de nombreuses façons de procéder, par exemple transmute(()) , ou des pointeurs bruts de conversion de type.

@jethrogb Mes seuls points sont que a) s'il vous plaît soyez plus prudent avec le libellé, b) la confidentialité ne semble pas suffisamment raisonnée dans les discussions sur les valeurs valides. Il me semble que «violer les invariants internes» et «valeur invalide» sont la même chose; il y a une condition secondaire ici "si A != B alors Id<A, B> est inhabité.".

Il me semble que «violer les invariants internes» et «valeur invalide» sont la même chose; il y a une condition secondaire ici "si A != B alors Id<A, B> est inhabité.".

Les invariants «imposés par le code de la bibliothèque» sont différents des invariants «imposés par le compilateur» de plusieurs manières, voir le billet de blog de . Dans cette terminologie, votre exemple Id a un invariant de sécurité et mem::zeroed ou d'autres moyens de synthétiser de manière générique un Id<A, B> ne peuvent pas être sûrs , mais ce n'est pas UB immédiat à juste construire une Id erronée avec mem::zeroed ou mem::uninitialized car Id n'a pas d'invariant de validité . Alors que les auteurs de code non sûr doivent certainement garder à l'esprit les deux types d'invariants, il y a certaines raisons pour lesquelles ces discussions se concentrent principalement sur la validité:

  • Les invariants de sécurité sont définis par l'utilisateur, rarement formalisés et peuvent être arbitraires compliqués, il y a donc peu d'espoir de raisonner de manière générique à leur sujet ou du compilateur / langage aidant à maintenir un invariant de sécurité particulier.
  • Briser l'invariant de sécurité peut parfois être nécessaire (en interne dans une bibliothèque de sons), donc même si nous pouvions exclure mécaniquement mem::zeroed::<T>() basé sur l'invariant de sécurité de T , nous ne le voudrions peut-être pas.
  • De même, les conséquences des invariants de validité cassés sont à certains égards pires qu'un invariant de sécurité cassé (moins de chance de le déboguer car tout l'enfer se déchaîne immédiatement, et souvent le comportement réel résultant de l'UB est moins compréhensible car tout le compilateur et l'optimiseur facteurs en elle, tandis que l'invariant de sécurité n'est directement exploité que par le code dans le même module / crate).

Après avoir lu le commentaire de @jethrogb , je suis d'accord que mem::zeroed ne devrait MaybeUninit .

@jethrogb Petite nit:

tout tableau de tout type dans cette liste
toute structure où n'importe quel champ est un type dans cette liste.

Je ne sais pas s'il s'agit d'une simple faute de frappe ou d'une différence sémantique, mais je pense que vous devez surpasser ces deux points - je ne pense pas que ce soit nécessairement le cas que None par exemple Option<[&u8; 2]> a des zéros au niveau du bit comme représentation valide (il pourrait par exemple utiliser [0, 24601] comme représentation du cas None - une seule des valeurs internes doit prendre une représentation de niche - cc @ eddyb pour me vérifier). Je doute que nous le fassions aujourd'hui, mais il ne semble pas complètement impossible qu'une telle chose puisse apparaître à l'avenir.

@jethrogb

L'utilisation la plus courante de mem :: zeroed aujourd'hui est pour les types décrits ci-dessus, et c'est parfaitement valable.

Y a-t-il une source pour cela?

D'un autre côté, il existe de nombreux types pour lesquels mem :: zeroed est valide.

Il existe également une infinité de cas pour lesquels il peut être utilisé de manière incorrecte.

Je comprends que pour ceux qui utilisent mem::zeroed lourdement et correctement, retarder la dépréciation jusqu'à ce qu'une solution plus ergonomique soit disponible est une alternative très attrayante.

Je préfère le compromis de réduire ou d'éliminer le nombre d'utilisations incorrectes de mem::zeroed même si cela entraîne un coût ergonomique temporaire. Une dépréciation avertit les utilisateurs que ce qu'ils font appelle potentiellement un comportement indéfini (en particulier les nouveaux utilisateurs qui l'utilisent pour la première fois), et nous avons une solution solide pour ce qu'il faut faire à la place, ce qui rend l'avertissement exploitable.

J'utilise souvent MaybeUninit et il est moins ergonomique à utiliser que mem::zeroed et mem::uninitialized , mais cela n'a pas été douloureusement anti-ergonomique pour moi. Si MaybeUninit est aussi douloureux que le prétendent certains commentaires de cette discussion, alors une bibliothèque et / ou RFC pour une alternative sûre mem::zeroed apparaîtra en un rien de temps (rien ne bloque quiconque ici AFAICT).

Alternativement, les utilisateurs peuvent ignorer l'avertissement et continuer à utiliser mem::zeroed , c'est à eux de décider, nous ne pouvons jamais supprimer mem::zeroed de libcore toute façon.

Mais les gens qui utilisent beaucoup mem::zeroed devraient vérifier activement si toutes leurs utilisations sont correctes de toute façon. En particulier ceux qui utilisent beaucoup mem::zeroed , ceux qui l'utilisent dans du code générique, ceux qui l'utilisent comme une alternative "moins effrayante" à mem::uninitialized , etc. Retarder la dépréciation retarde juste l'avertissement des utilisateurs de ce qu'ils font peut être un comportement indéfini.

@bluss

Lors de la suppression de zeroed, il semble qu'il n'est remplacé que par MaybeUninit :: zeroed (). Into_inner () qui devient une manière équivalente d'écrire la même chose. Il n'y a pas de changement pratique. Avec les valeurs uninit, nous avons à la place le changement pratique de toutes les données non initialisées conservées stockées dans des valeurs de type MaybeUninit ou union équivalente.

Ceci est vrai quand nous parlons d'entiers, mais une fois que nous regardons par exemple les types de référence, mem::zeroed() devient également un problème.

Cependant, je conviens qu'il est beaucoup plus probable que les gens se rendent compte que mem::zeroed::<&T>() est un problème, que les gens se rendent compte que mem::uninitialized::<bool>() est un problème. Alors peut-être qu'il est logique de garder mem::zeroed() .

Notez, cependant, que nous pourrions toujours décider que mem::uninitialized::<u32>() convient - si nous autorisons les bits non initialisés dans les types entiers, mem::uninitialized() devient valide pour presque tous les "types POD". Je ne pense pas que nous devrions permettre cela, mais nous devons encore avoir cette discussion.

Le nombre de types pour lesquels mem :: uninitialized est valide est très très petit (en fait, s'agit-il uniquement de types de taille nulle?), Et personne n'écrirait réellement de code qui fait cela (par exemple, pour les ZST, vous utiliseriez simplement le type constructeur).

FWIW, un certain code d'itérateur de tranche doit en fait créer un ZST en code générique sans pouvoir écrire un constructeur de type. Il utilise mem::zeroed() / MaybeUninit::zeroed().into_inner() pour cela.

mem::zeroed() est utile pour certains cas FFI où l'on s'attend à ce que vous mettiez à zéro une valeur avec memset(&x, 0, sizeof(x)) avant d'appeler une fonction C. Je pense que c'est une raison suffisante pour que cela ne soit pas déconseillé.

@Amanieu Cela semble inutile. La construction Rust correspondant à memset est write_bytes .

mem :: zeroed () est utile pour certains cas FFI

De plus, la dernière fois que j'ai vérifié, mem::zeroed était la manière idiomatique d'initialiser les structures libc avec des champs privés ou dépendants de la plate-forme.

@RalfJung Le code complet en question est généralement Type x; memset(&x, 0, sizeof(x)); et la première partie n'a pas d'équivalent Rust. Utiliser MaybeUninit pour ce modèle est beaucoup de bruit de ligne (et bien pire codegen sans optimisations) lorsque la mémoire n'est jamais réellement invalide après le memset .

J'ai une question sur la conception de MaybeUninit : Y a-t-il un moyen d'écrire dans un seul champ du T contenu dans un MaybeUninit<T> sorte que vous puissiez au fil du temps écrire dans tous les champs et se retrouvent avec un type valide / initialisé?

Supposons que nous ayons une structure comme celle-ci:

// Let us suppose that Foo can in principle be any struct containing arbitrary types
struct Foo {bar: bool, baz: String}

Générer une référence & mut Foo, puis y écrire, déclenche-t-il UB?

main () {
    let uninit_foo = MaybeUninitilized::<Foo>::uninitialized();
    unsafe { *uninit_foo.get_mut().bar = true; }
    unsafe { *uninit_foo.get_mut().baz = "hello world".to_owned(); }
}

L'utilisation d'un pointeur brut au lieu d'une référence évite-t-elle ce problème?

main () {
    let uninit_foo = MaybeUninitilized::<Foo>::uninitialized();
    unsafe { *uninit_foo.as_mut_pointer().bar = true; }
    unsafe { *uninit_foo.as_mut_pointer().baz = "hello world".to_owned(); }
}

Ou y a-t-il une autre manière dont ce modèle peut être implémenté sans déclencher UB? Intuitivement, il me semble que tant que je ne lis pas de mémoire non initialisée / invalide, tout devrait bien se passer, mais plusieurs des commentaires de ce fil m'amènent à en douter.

Mon cas d'utilisation pour cette fonctionnalité serait pour un modèle de générateur en place pour les types où certains des champs doivent être spécifiés par l'utilisateur (et n'ont pas de valeur par défaut raisonnable), mais certains des champs ont une valeur par défaut valeur.

Existe-t-il un moyen d'écrire dans un seul champ du T contenu dans un MaybeUninittel que vous pourriez au fil du temps écrire dans tous les champs et vous retrouver avec un type valide / initialisé?

Oui. Utilisation

ptr::write(&mut *(uninit.as_mut_ptr()).bar, val1);
ptr::write(&mut *(uninit.as_mut_ptr()).baz, val2);
...

Vous ne devez pas utiliser get_mut() pour cela, c'est pourquoi la documentation pour get_mut indique que la valeur doit être initialisée avant d'appeler cette méthode. Nous pourrions assouplir cette règle à l'avenir, qui est en cours de discussion sur https://github.com/rust-rfcs/unsafe-code-guidelines/.

@RalfJung *(uninit.as_mut_ptr()).bar = val1; ne risquerait -il pas *(uninit.as_mut_ptr()).bar = val1; perdre la valeur précédemment dans bar , qui pourrait ne pas être initialisée? Je pense qu'il faut faire

ptr::write(&mut (*uninit.as_mut_ptr()).bar, val1);

@scottjmaddox ah, à droite. J'ai oublié Drop . Je mettrai à jour le message.

En quoi cette variante d'écriture dans des champs non initialisés présente-t-elle moins de comportement indéfini que get_mut() ? Au point de code où le premier argument de ptr::write est évalué, le code a créé un &mut _ dans le champ interne qui devrait être aussi indéfini que la référence à la structure entière qui serait autrement établi. Le compilateur ne devrait-il pas être autorisé à supposer que cela est déjà dans l'état initialisé?

Cela ne nécessiterait-il pas une nouvelle méthode de projection de pointeur qui ne nécessite pas d'intermédiaires exposés &mut _ ?


Exemple légèrement intéressant:

pub struct A { inner: bool }

pub fn init(mut uninit: MaybeUninit<A>) -> A {
    unsafe {
        let mut previous: [u8; std::mem::size_of::<bool>()] = [0];

        {
            // Doesn't the temorary reference assert inner was in valid state before?
            let inner_ptr: *mut _ = &mut (*uninit.as_mut_ptr()).inner;
            ptr::copy(inner_ptr as *const [u8; 1], (&mut previous) as *mut _, 1);

            // With the assert below, couldn't the compiler drop this?
            std::ptr::write(inner_ptr, true);
        }

        // Assert Inner wasn't false before, so it must have been true already!
        assert!(previous[0] != 0);

        // initialized all fields, good to proceed.
        uninit.into_inner()
    }
}

Mais si le compilateur peut supposer que &mut _ est une représentation valide, il peut simplement le jeter à ptr::write ? Si nous dépassons l'assertion, le contenu n'était pas 0 mais le seul autre booléen valide est true/1 . Il pourrait donc supposer que ce n'est pas une opération si nous dépassons l'affirmation. Puisque la valeur n'est pas accédée avant, après la réorganisation, nous pourrions nous retrouver avec cela? Il ne semble pas que llvm l'exploite pour le moment, mais je ne suis pas sûr que cela soit garanti.


Si nous créons plutôt notre propre MaybeUninit dans la fonction, nous obtenons une réalité légèrement différente. Sur le terrain de jeu, nous découvrons plutôt qu'il suppose que l'assertion ne peut jamais se déclencher, vraisemblablement car elle suppose que str::ptr::write est la seule écriture dans inner donc cela doit déjà avoir eu lieu avant de lire à partir de previous ? Cela semble un peu louche de toute façon. Pour soutenir cette théorie, regardez ce qui se passe lorsque vous changez l'écriture du pointeur en false place.


Je sais que ce problème de suivi n'est peut-être pas le meilleur endroit pour cette question.

@RalfJung @scottjmaddox Merci pour vos réponses. Ces nuances sont exactement pourquoi j'ai demandé.
@HeroicKatora Oui, je me

C'est peut-être la bonne incantation?

struct Foo {bar: bool, baz: String}

fn main () {
    let mut uninit_foo = MaybeUninit::<Foo>::uninitialized();
    unsafe { ptr::write_unaligned(&mut ((*uninit_foo.as_mut_ptr()).bar) as *mut bool, true); }
    unsafe { ptr::write_unaligned(&mut ((*uninit_foo.as_mut_ptr()).baz) as *mut String, "".to_string()); }
}

( aire de jeux )

J'ai lu un commentaire sur Reddit (que je ne trouve malheureusement plus) qui suggérait que le fait de lancer immédiatement une référence à un pointeur ( &mut foo as *mut T ) se compile en fait pour créer un pointeur. Cependant, le bit *uninit_foo.as_mut_ptr() m'inquiète. Est-il correct de déréférencer le pointeur vers la mémoire unifiée de cette manière? Nous ne lisons rien en fait, mais je ne sais pas si le compilateur le sait.

J'ai pensé que la variante unaligned de ptr::write pourrait être requise pour le code générique supérieur à MaybeUninit<T> car tous les types n'auront pas de champs alignés?

Pas besoin de write_unaligned . Le compilateur gère l'alignement des champs pour vous. Et le as *mut bool ne devrait pas non plus être nécessaire, puisque le compilateur peut en déduire qu'il a besoin de forcer le &mut en un *mut . Je pense que cette coercition inférée est la raison pour laquelle elle est sûre / valide. Si vous voulez être explicite et faire as *mut _ , cela devrait également convenir. Si vous souhaitez enregistrer le pointeur dans une variable, il est nécessaire de le contraindre à devenir un pointeur.

@scottjmaddox Est-ce que ptr::write toujours sûr même si la structure est #[repr(packed)] ? ptr::write indique que le pointeur doit être correctement aligné, donc je suppose que ptr::write_unaligned est nécessaire dans les cas où vous écrivez un code générique qui doit gérer des représentations compactées (bien que pour être honnête, je ne suis pas sûr Je peux penser à un exemple de "code générique sur MaybeUninit<T> " qui ne saurait pas si le champ était correctement aligné ou non).

@nicoburns

qui suggérait que la conversion immédiate d'une référence à un pointeur (& mut foo comme * mut T) se compile en fait pour créer simplement un pointeur.

Ce sur quoi il compile est distinct de la sémantique que le compilateur est autorisé à utiliser pour effectuer cette compilation. Même s'il s'agit d'un no-op dans IR, il peut toujours avoir un effet sémantique tel que l'affirmation d'hypothèses supplémentaires au compilateur. @scottjmaddox est correct dans lequel les opérations sont en jeu ici, mais la partie critique de la question est la création de la référence mutable qui se produit avant et indépendamment de la coercition ref-to-ptr. Alors @mjbshaw est techniquement correct sur la sécurité générale exigeant ptr::write_unaligned lorsque l'argument est un argument générique inconnu.

Je ne me souviens pas où j'ai lu ceci (nomicon? Un des articles de blog de

De quelle manière cette variante d'écriture dans des champs non initialisés présente-t-elle moins de comportement indéfini que get_mut ()? Au point de code où le premier argument de ptr :: write est évalué, le code a créé un & mut _ dans le champ interne qui devrait être aussi indéfini que la référence à la structure entière qui serait autrement créée. Le compilateur ne devrait-il pas être autorisé à supposer que cela est déjà dans l'état initialisé?

Très bonne question! Ces préoccupations sont l'une des raisons pour lesquelles j'ai ouvert https://github.com/rust-lang/rfcs/pull/2582. Avec cette RFC acceptée, le code que j'ai montré ne crée pas un &mut , il crée un *mut .

@mjbshaw Touché. Oui, je suppose que vous avez raison sur la possibilité que la structure soit compressée et que vous ayez donc besoin de ptr::write_unaligned . Je n'avais pas envisagé cela auparavant, principalement parce que je n'ai pas encore utilisé de structures compactées dans la rouille. Cela devrait probablement être une charpie coupante, si ce n'est déjà fait.

Edit: Je n'ai pas vu de charpie clippy pertinente, j'ai donc soumis un problème: https://github.com/rust-lang/rust-clippy/issues/3659

J'ai ouvert un PR pour déprécier mem::zeroed : https://github.com/rust-lang/rust/pull/57825

J'ai ouvert un problème dans le référentiel RFC pour fourcher la discussion sur la remise à zéro de la mémoire sûre, afin que nous puissions abandonner mem::zeroed à un moment donné une fois que nous aurons une meilleure solution à ce problème: https://github.com / rust-lang / rfcs / issues / 2626

Serait-il possible de stabiliser const uninitialized , as_ptr et
as_mut_ptr avant le reste de l'API? Il me semble très probable que ces
seront stabilisés comme ils le sont actuellement. De plus, le reste de l'API peut être construit sur
haut de as_ptr et as_mut_ptr , donc une fois stabilisé, il serait possible de
avoir un trait MaybeUninitExt sur crates.io qui fournit, sur stable, l'API
il est actuellement discuté de laisser plus de personnes (par exemple, les utilisateurs stables uniquement)
donner des commentaires à ce sujet.

En embarqué, au lieu d'un allocateur global (instable), on utilise des variables statiques,
beaucoup . Sans MaybeUninit il n'y a aucun moyen d'avoir de la mémoire non initialisée
variables statiques sur stable. Cela nous empêche de placer une capacité fixe
collections dans des variables statiques et initialisation des variables statiques à l'exécution, à
coût nul. La stabilisation de ce sous-ensemble de l'API débloquera ces cas d'utilisation.

Pour vous donner une idée de l'importance de cela pour la communauté intégrée, nous avons
[une enquête] demandant à la communauté ses points faibles et ses besoins. Stabilisation
MaybeUninit est apparu comme la deuxième chose la plus demandée pour stabiliser (derrière
const fn avec des limites de trait) et, dans l'ensemble, a terminé à la 7e place sur des dizaines de
requêtes liées à rust-lang / *. Après de nouvelles délibérations au sein du groupe de travail, nous avons
sa priorité, globalement, à la troisième place en raison de son impact attendu sur l'écosystème.

(Sur une note plus personnelle, je suis l'auteur d'un framework de concurrence intégré
qui gagnerait à utiliser en interne MaybeUninit (utilisation de la mémoire dans
les applications pourraient être réduites de 10 à 50% sans aucune modification du code utilisateur). je
pourrait fournir une fonction Cargo pour la nuit uniquement, mais après des années
intégré de nuit seulement et seulement récemment rendu stable, je pense que
fournir une fonctionnalité uniquement nocturne serait le mauvais message à envoyer à mes utilisateurs
j'attends donc avec impatience que cette API soit stabilisée.)

@japaric Cela éviterait certainement les discussions sur les noms autour de into_inner et des amis. Cependant, je suis toujours préoccupé par la discussion sémantique, par exemple à propos des personnes faisant let r = &mut *foo.as_mut_ptr(); et affirmant par conséquent qu'elles ont une référence valide, alors que nous ne sommes pas encore sûrs des conditions de validité des références Je ne sais pas encore si avoir une référence à des données invalides est insta-UB. Pour un exemple concret:

let x: MaybeUninit<!> = MaybeUninit::uninitialized();
let r: &! = &*x.as_ptr() // is this UB?

Cette discussion a commencé récemment au sein du groupe de travail UCG.

J'espérais que cela permettrait de stabiliser MaybeUninit dans un seul "package" cohérent avec une histoire appropriée pour les données non initialisées, de sorte que les gens n'aient à réapprendre ces choses qu'une seule fois, au lieu de les publier petit à petit. pièce et peut-être devoir changer certaines règles en cours de route. Mais ce n'est peut-être pas une bonne idée, et il est plus important de sortir quelque chose pour améliorer le statu quo?

Mais de toute façon, je pense que nous ne devrions rien stabiliser avant d'accepter https://github.com/rust-lang/rfcs/pull/2582 , afin que nous puissions au moins dire aux gens avec certitude que ce qui suit n'est pas UB:

let x: MaybeUninit<(!, u32)> = MaybeUninit::uninitialized();
let r1: *const ! = &(*x.as_ptr()).1; // immediately coerced to raw ptr, no UB
let r2 = &(*x.as_ptr()).1 as *const !; // immediately cast to raw ptr, no UB

(Notez que comme d'habitude ! est un hareng rouge ici, et tous les exemples de cet article sont les mêmes, du point de vue UB, si nous avons utilisé bool place.)

J'espérais que cela permettrait de stabiliser MaybeUninit dans un «package» unique et cohérent avec une histoire appropriée pour les données non initialisées, de sorte que les gens n'aient à réapprendre ces choses qu'une seule fois, au lieu de les publier pièce par pièce et peut-être d'avoir à changer certaines règles en cours de route.

Je trouve cet argument très convaincant.

Je pense que le besoin le plus immédiat est d'avoir un message clair sur la façon de gérer la mémoire non initialisée sans UB. Si c'est actuellement simplement "utiliser des pointeurs bruts et ptr::read_unaligned et ptr::write_unaligned ", alors c'est bien, mais nous avons certainement besoin d'un moyen bien défini pour obtenir des pointeurs bruts vers des valeurs de pile non initialisées et vers des champs struct / tuple . rust-lang / rfcs # 2582 (plus une documentation) semble répondre aux besoins immédiats, contrairement à MaybeUninit .

@scottjmaddox comment est cette RFC mais sans MaybeUninit tout bon pour la mémoire (pile) non initialisée?

@RalfJung Je suppose que cela dépend de si oui ou non ce qui suit est UB:

let x: bool = mem::uninitialized();
ptr::write(&x as *mut bool, false);
assert_eq!(x, false);

Mon hypothèse implicite était que rust-lang / rfcs # 2582 rendrait l'exemple ci-dessus valide et bien défini. N'est-ce pas le cas?

@scottjmaddox

let x: bool = mem::uninitialized();

C'est UB. Cela n'a rien à voir avec les références.

Mon hypothèse implicite était que rust-lang / rfcs # 2582 rendrait l'exemple ci-dessus valide et bien défini.

Je suis très surpris par cela. Cette RFC concerne uniquement les références. Pourquoi pensez-vous que cela change quelque chose sur les booléens?

@RalfJung

C'est UB. Cela n'a rien à voir avec les références.

La documentation de mem :: uninitialized () dit:

Contourne les vérifications normales d'initialisation de la mémoire de Rust en prétendant produire une valeur de type T , sans rien faire du tout.

La documentation ne dit rien sur T* .

@kpp Qu'essayez -vous de dire? Il n'y a pas de * ni de & dans cette ligne de code:

let x: bool = mem::uninitialized();

Pourquoi prétendez-vous que cette ligne est UB?

Parce qu'un bool doit toujours être true ou false , et celui-ci ne l'est pas. Voir également https://github.com/rust-rfcs/unsafe-code-guidelines/blob/master/reference/src/glossary.md#validity -and-safety-invariant.

@kpp pour que cette instruction ait un comportement défini mem::uninitialized aurait besoin de matérialiser un _valid_ bool .

Sur toutes les plates-formes actuellement prises en charge, bool n'a que deux valeurs _valid_, true (modèle de bits: 0x1 ) et false (modèle de bits: 0x0 ).

mem::uninitialized , cependant, produit un modèle de bits où tous les bits ont la valeur uninitialized . Ce motif de bits n'est ni 0x0 ni 0x1 , par conséquent, le résultat bool est _invalid_, et le comportement n'est pas défini.

Pour définir le comportement, nous aurions besoin de changer la définition de bool pour prendre en charge trois valeurs valides: true , false ou uninitialized . Nous ne pouvons pas, cependant, faire cela, parce que T-lang et T-compilateur ont déjà RFC'ed que bool est identique à C _Bool et nous ne pouvons pas briser cette garantie (cela permet bool à utiliser de manière portative dans C FFI).

On peut soutenir que C n'a pas exactement la même définition de validité que Rust, mais les «représentations de piège» en C sont très proches. En un mot, il n'y a pas grand-chose à faire en C avec un _Bool dont la valeur ne représente pas true ou false sans invoquer un comportement indéfini.

Si vous avez raison, le code de sécurité suivant doit également être UB:

let x: bool;
x = true;

Ce qui n'est évidemment pas.

Si vous avez raison, le code de sécurité suivant doit également être UB:

let x: bool; n'initialise pas x à un uninitialized bit-pattern, il n'initialise pas du tout x . Le x = true; initialise x (note: si vous n'initialisez pas x avant de l'utiliser, vous obtenez une erreur de compilation).

Ceci est différent du comportement de C, où, selon le contexte, _Bool x; initialise x à une valeur _indeterminate_.

Non, là, le compilateur sait que x n'est pas initialisé.

Le problème avec mem::uninitialized est qu'il est initialise une variable, pour autant que le suivi de l' initialisation du compilateur est concerné.

let x: bool; ne réserve même pas d'espace pour x pour y être stocké, il réserve juste un nom. let x = foo; réserve de l'espace et l'initialise avec foo . let x: bool = mem::uninitialized(); réserve 1 octet d'espace pour x mais le laisse non initialisé, et c'est un problème.

C'est un moyen si simple de tirer sur votre API conçue pour la jambe qu'elle doit être documentée à la fois dans mem :: uninitialized et intrinsics :: uninit avec une spécialisation pour mem :: uninitializedpaniquer lors de la compilation.

Cela signifie-t-il également que l'initialisation d'une structure avec un booléen avec mem :: uninitialized est également UB?

@kpp

Cela signifie-t-il également que l'initialisation d'une structure avec un booléen avec mem :: uninitialized est également UB?

Oui - comme vous le découvrez probablement, mem::uninitialized rend trivial de se tirer une balle dans le pied, j'irais jusqu'à dire qu'il est presque impossible de l'utiliser correctement. C'est pourquoi nous essayons de le déprécier au profit de MaybeUninit , qui est un peu plus verbeux à utiliser, mais présente l'avantage que, comme il s'agit d'une union, vous pouvez initialiser des valeurs "par parties" sans réellement se matérialiser la valeur elle-même dans un état _invalid_. La valeur doit seulement être entièrement _valide_ au moment où l'on appelle into_inner() .

Vous pourriez être intéressé par la lecture des sections du nomicon sur l'initialisation (non) cochée et non cochée: https://doc.rust-lang.org/nomicon/checked-uninit.html Elles expliquent comment l'initialisation let x: bool; fonctionne en toute sécurité Rust. Veuillez remplir les questions si l'explication n'est pas claire ou s'il y a quelque chose que vous ne comprenez pas. Gardez également à l'esprit que la plupart des explications sont "non normatives" puisqu'elles n'ont pas encore suivi le processus RFC. Le groupe de travail Lignes directrices sur les codes non sécurisés tentera de soumettre une RFC documentant et garantissant le comportement actuel dans le courant de l'année.

C'est un moyen si simple de tirer sur votre API conçue pour les jambes qu'elle doit être documentée à la fois dans mem :: uninitialized et intrinsics :: uninit

Le problème est qu'il n'y a actuellement aucune façon correcte de faire cela - c'est pourquoi nous travaillons dur pour stabiliser MaybeUninit afin que ces fonctions puissent voir leur documentation remplacée par un gros "NE PAS UTILISER".


Des discussions comme celle-ci et des problèmes comme celui-ci me font de plus en plus d' accord avec quelque chose dès que possible. Fondamentalement, nous avons besoin de ceci et de cette liste de cases à cocher, je dirais. Ensuite, nous en avons assez pour fournir quelques modèles de base.

Serait-il possible de stabiliser const non initialisé, as_ptr et
as_mut_ptr avant le reste de l'API? Il me semble très probable que ces
seront stabilisés comme ils le sont actuellement.

+1 pour cela. Ce serait formidable d'avoir cette fonctionnalité disponible sur stable. Cela permettrait aux gens d'expérimenter une variété d'API de niveau supérieur (et potentiellement sûres) en plus de cette API basique de base. Et il semble que cet aspect de l'API ne soit pas controversé.

De plus, je voudrais suggérer que get_ref et get_mut ne sont jamais stabilisés et sont entièrement supprimés. Normalement, travailler avec des références est plus sûr que travailler avec des pointeurs bruts (et donc les gens pourraient être tentés d'utiliser ces méthodes sur as_ptr et as_mut_ptr même si elles sont marquées comme non sûres), mais dans ce cas, elles sont strictement plus dangereuses que les méthodes de pointeur brutes car elles peuvent provoquer UB alors que les méthodes de pointeur ne le peuvent pas.

Si la règle est "jamais créé une référence à la mémoire non initialisée" alors je pense que nous devrions aider les gens à se conformer à cette règle en permettant uniquement de créer une telle référence en le faisant explicitement, plutôt que d'avoir une méthode d'assistance qui le fait en interne .

En supposant que https://github.com/rust-lang/rfcs/pull/2582 , sommes-nous complètement sûrs que (1) n'est même pas UB même si (2) l'est, et (1) contient également un déréférencement d'un pointeur qui pointe vers la mémoire non initialisée?

(1) unsafe { ptr::write_unaligned(&mut ((*uninit_foo.as_mut_ptr()).bar) as *mut bool, true); }
(2) let x: bool = mem::uninitialized();

Et si oui, quelle est la logique derrière cela (j'espère que nous pourrons mettre une partie de la discussion sur cette question dans la documentation de MaybeUninit)? Je devine quelque chose comme parce que dans (1) la valeur déréférencée reste toujours une "rvalue" et ne devient jamais et "lvalue", alors que dans (2) le bool non valide devient une "lvalue" et doit donc être matérialisé en mémoire (Je ne sais pas trop quel est le terme correct pour cela dans Rust, mais j'ai vu ces termes utilisés pour C ++).

Et est-ce que d'autres pensent qu'il vaudrait la peine de créer une RFC pour la syntaxe d'accès au champ sur des pointeurs bruts qui évalue directement dans un pointeur brut vers le champ afin d'éviter cette confusion en premier lieu?

Si la règle est "jamais créé une référence à la mémoire non initialisée"

Je ne pense pas que cela devrait être la règle, mais c'est peut-être le cas. Cela fait actuellement l'objet de discussions à l'UCG.

Sommes-nous complètement sûrs que (1) n'est même pas UB même si (2) l'est, et (1) contient également un déréférencement d'un pointeur qui pointe vers la mémoire non initialisée?

Bonne question! Mais oui, nous sommes - par nécessité absolue, fondamentalement. Pensez à &mut foo as *mut bool comme &raw mut foo , une expression atomique de type *mut bool . Il n'y a pas de référence ici, juste un ptr brut à la mémoire non initialisée - et c'est vraiment bien.

let x: bool = mem::uninitialized();

C'est UB. Cela n'a rien à voir avec les références.

Mon hypothèse implicite était que rust-lang / rfcs # 2582 rendrait l'exemple ci-dessus valide et bien défini.

Je suis très surpris par cela. Cette RFC concerne uniquement les références. Pourquoi pensez-vous que cela change quelque chose sur les booléens?

@RalfJung Je suppose que je pensais que ce n'était pas UB parce que la valeur indéfinie n'était pas observable car elle était immédiatement écrasée par une valeur booléenne valide. Mais je suppose que ce n'est pas le cas?

Pour des exemples plus compliqués, dans lesquels la valeur dans x implémente Drop, un pointeur brut serait nécessaire pour écraser la valeur, et c'est pourquoi j'ai pensé que rfc 2582 était nécessaire pour éviter UB.

Je suppose que je pensais que ce n'était pas UB car la valeur indéfinie était inobservable car elle a été immédiatement écrasée par une valeur booléenne valide. Mais je suppose que ce n'est pas le cas?

La sémantique procède déclaration par déclaration (en regardant le MIR). Chaque déclaration doit avoir un sens. let x: bool = mem::uninitialized(); matérialise un mauvais booléen, et peu importe ce qui se passe plus tard - vous ne devez pas matérialiser un mauvais booléen.

Je comprends que la valeur de x n'est pas valide, mais cela nécessite-t-il un comportement indéfini? Je peux voir comment cela pourrait, en général, sortir de son contexte. Mais dans le contexte de cet exemple particulier, le comportement n'est-il pas bien défini? Je suppose que mon problème de base est que je ne comprends pas entièrement la signification de «comportement indéfini».

Nous voulons que le compilateur puisse s'appuyer sur certains invariants. Ce ne sont des invariants que s'ils sont toujours valables. Une fois que nous commençons à ajouter des exceptions, cela devient un gâchis.

Peut-être que vous attendez quelque chose de plus de la forme «l' inspection d' une valeur nécessite que l'invariant de validité soit maintenu». Ici, "inspecter" un bool serait l'utiliser dans un if . C'est une spécification raisonnable, mais moins utile: maintenant, le compilateur doit prouver que la valeur est réellement "inspectée" avant de pouvoir assumer l'invariant.

cela nécessite-t-il un comportement indéfini?

Nous choisissons ce qui est et n'est pas un comportement indéfini. Cela fait partie de la conception d'un langage. Un comportement non défini n'est pratiquement jamais «nécessaire» en soi - mais il est nécessaire de permettre davantage d'optimisations. L'art ici est donc de trouver une définition du comportement indéfini (aussi contradictoire que cela puisse paraître ^^) qui permet à la fois les optimisations souhaitées et se conforme aux attentes (peu sûres) des programmeurs.

Je ne comprends pas entièrement le sens de «comportement indéfini».

J'ai écrit un article de blog à ce sujet , mais la réponse courte est qu'un comportement indéfini est un contrat entre vous et le compilateur - et le contrat dit qu'il est de votre devoir de vous assurer qu'aucun comportement indéfini ne se produit. C'est une obligation de preuve. "déréférencer un pointeur NULL est UB" équivaut à dire "chaque fois qu'un pointeur est déréférencé, le programmeur doit prouver que ce pointeur ne peut pas être NULL". Cela aide le compilateur à comprendre le code, car chaque fois qu'un pointeur est déréférencé, le compilateur peut maintenant déduire "aha! Ici le programmeur a prouvé que le pointeur n'est pas NULL, et je peux donc utiliser ces informations pour les optimisations et la génération de code. Merci , programmeur! "

Ce que dit exactement le contrat dépend du langage de programmation. Il y a bien sûr des contraintes (par exemple, nous sommes contraints par LLVM). Dans notre cas, l'UCG estime (conformément à ce que nous avons entendu des équipes de langage et de compilation) que nous voulons que le contrat contienne la clause suivante: "Chaque fois qu'une rvalue est créée, le programmeur doit prouver que cette rvalue sera toujours satisfont l'invariant de validité. " Aucune loi physique ou informatique ne nous oblige à inclure cette clause dans le contrat, mais elle est considérée comme un compromis raisonnable entre de nombreux choix différents.

En particulier, nous émettons déjà des informations pour LLVM que nous ne pourrions légitimement émettre avec un contrat plus faible. Nous pourrions décider de changer ce que nous disons à LLVM, bien sûr - mais si le choix est entre "le code non sécurisé doit utiliser MaybeUninit chaque fois qu'il s'agit de mémoire non initialisée" et " tout le code peut être moins optimisé", le premier semble comme le meilleur choix.

Prenons votre exemple:

let x: bool = mem::uninitialized();

Ce code est UB dans rustc aujourd'hui. Si vous regardez le LLVM IR (non optimisé) pour mem::uninitialized::<bool>() , voici ce que vous obtenez:

; core::mem::uninitialized
; Function Attrs: inlinehint nonlazybind uwtable
define zeroext i1 @_ZN4core3mem13uninitialized17h6c99c480737239c2E() unnamed_addr #0 !dbg !5 {
start:
  %tmp_ret = alloca i8, align 1
  %0 = load i8, i8* %tmp_ret, align 1, !dbg !14, !range !15
  %1 = trunc i8 %0 to i1, !dbg !14
  br label %bb1, !dbg !14

bb1:                                              ; preds = %start
  ret i1 %1, !dbg !16
}
; snip
!15 = !{i8 0, i8 2}

Essentiellement, cette fonction alloue 1 octet sur la pile, puis charge cet octet. Cependant, la charge est marquée par !range , ce qui indique à LLVM que l'octet doit être compris entre 0 <= x <2, c'est-à-dire qu'il ne peut être que 0 ou 1. LLVM supposera que c'est vrai, et le comportement n'est pas défini si cette contrainte est violée.

En résumé, le problème n'est pas tant les variables non initialisées elles-mêmes, c'est le fait que vous copiez et déplacez des valeurs qui violent leurs contraintes de type.

Merci à tous les deux pour l'exposition! C'est beaucoup plus clair maintenant!

Je suppose que mon problème de base est que je ne comprends pas entièrement la signification de «comportement indéfini».

Cette série d'articles de blog (qui contient des exemples plutôt intéressants / effrayants dans le deuxième article) est assez utile, je pense: http://blog.llvm.org/2011/05/what-every-c-programmer-should-know .html

Je pense que cela a vraiment besoin d'une bonne documentation. Le changement ici est probablement une bonne chose pour plusieurs raisons que je peux énumérer et probablement d'autres que je ne peux pas. Mais l'utilisation correcte de la mémoire non initialisée (et d'autres utilisations de unsafe) peut être remarquablement contre-intuitive. Le Nomicon a une section sur uninitialized (qui serait probablement mis à jour pour parler de ce type), mais il ne semble pas exprimer toute la complexité du problème.

(Non pas que je me porte volontaire pour écrire une telle documentation. Je nomme ... celui qui en sait plus que moi.)

Une idée intéressante de https://github.com/rust-lang/rust/issues/55422#issuecomment -433943803: Nous pourrions transformer des méthodes comme into_inner en fonctions, de sorte que vous deviez écrire MaybeUninit::into_inner(foo) au lieu de foo.into_inner() - qui documente beaucoup plus clairement ce qui se passe.

Dans https://github.com/rust-lang/rust/pull/58129 , j'ajoute des documents, renvoie un &mut T de set et renomme into_inner en into_initialized .

Je pense qu'après cela, et une fois que https://github.com/rust-lang/rust/pull/56138 est résolu, nous pourrions procéder à la stabilisation des parties de l'API (les constructeurs, as_ptr , as_mut_ptr , set , into_initialized ).

Pourquoi MaybeUninit::zeroed() n'est-il pas const fn ? ( MaybeUninit::uninitialized() est un const fn )

EDIT: peut-il réellement être fait un const fn utilisant la nuit Rust?

Pourquoi MaybeUninit::zeroed() n'est-il pas const fn ? ( MaybeUninit::uninitialized() est un const fn )

@gnzlbg J'ai essayé , mais cela nécessite l'un des éléments suivants:

  • Rendre le init intrinsèque un const fn . C'est faisable mais Ralf tentera de le faire ; ou
  • Faites std::ptr::write_bytes a const fn . Cela nécessite de prendre un &mut dans un const , ce qui n'est actuellement

La seule chose qui m'inquiète le plus à propos du passage prochain à la stabilisation est le manque total de commentaires des personnes qui utilisent réellement ce type. Il semble que tout le monde attend que cela se stabilise avant de commencer à l'utiliser. C'est un problème, car cela signifie que nous remarquerons les problèmes d'API trop tard.

@ rust-lang / libs quelles sont les conditions habituelles dans lesquelles vous utiliserez une fonction au lieu d'une méthode? Je me demande si certaines des opérations ici devraient être des fonctions pour que les gens doivent écrire, par exemple MaybeUninit::as_ptr(...) . Je crains que cela ne fasse exploser le code pour le rendre illisible - mais OTOH, certaines fonctions sur ManuallyDrop fait exactement cela.

@RalfJung Je crois comprendre que les méthodes sont évitées sur les choses qui se réfèrent à des paramètres génériques, pour éviter de masquer les méthodes du type de l'utilisateur - d'où ManuallyDrop::take .

Puisque MaybeUninit<T> ne sera jamais Deref<Target = T> , je pense que les méthodes sont appropriées ici.

Demandez des commentaires et vous recevrez. J'ai récemment utilisé MaybeUninit pour implémenter de nouvelles fonctionnalités dans std .

  1. Dans sys / sgx / ext / arch.rs, je l'utilise en combinaison avec l'assemblage en ligne. En fait, j'ai mal utilisé get_mut , pensant que les références et les pointeurs bruts seraient équivalents (corrigés dans 928efca1). J'étais déjà dans un bloc dangereux, donc je n'ai pas vraiment remarqué la différence au début.
  2. Dans sys / sgx / rwlock.rs , je l'utilise pour m'assurer que le modèle de bits d'un const fn new() est le même qu'un initialiseur de tableau dans un fichier d'en-tête C. J'utilise zeroed suivi de set pour essayer de m'assurer que les bits "indifférents" sont à 0. Je ne sais pas si cette utilisation est correcte, mais cela semble fonctionner correctement .
  1. Je serais très confus si out.get_mut() as *mut _ ! = out.as_mut_ptr() . Ressemble vraiment à C ++. J'espère que ce serait réglé d'une manière ou d'une autre.

Quel est l'intérêt de get_mut() ?

Une chose que je me demandais récemment était de savoir si MaybeUninit<T> était garanti d'avoir la même disposition que T , et si quelque chose comme ça pouvait être utilisé pour initialiser partiellement les valeurs sur le tas puis le transformer en un valeur initialisée, par exemple quelque chose comme ( terrain de jeu complet )

struct Foo {
    x: i32,
}

let mut partial: Box<MaybeUninit<Foo>> = Box::new(MaybeUninit::uninitialized());
let complete: Box<Foo> = unsafe {
    ptr::write(&mut (*partial.as_mut_ptr()).x, 5);
    mem::transmute(partial)
};

selon Miri, cet exemple fonctionne (bien que je réalise maintenant que je ne sais pas si la transmutation de boîtes de types avec une disposition identique est elle-même saine).

@ Nemo157 pourquoi avez-vous besoin de la même disposition de mémoire lorsque vous avez into_inner ?

@Pzixel pour éviter de copier la valeur après l'initialisation, imaginez qu'il contient un tampon de 100 Mo qui provoquera un débordement de pile s'il est alloué sur la pile. Bien que l' écriture d'un testcase, il semble que cela nécessite une API supplémentaire fn uninit_boxed<T>() -> Box<MaybeUninit<T>> pour permettre d'allouer une boîte non initialisée sans toucher la pile.

En utilisant la syntaxe box pour autoriser l'allocation de l'espace de tas non initialisé, vous pouvez voir que la transmutation comme celle-ci fonctionne, tout en essayant d'utiliser into_initialized provoque un débordement de pile: terrain de jeu

@ Nemo157 Peut-être

@ Nemo157

Une chose que je me demandais récemment était de savoir si MaybeUninit<T> était garanti d'avoir la même disposition que T , et si quelque chose comme ça pouvait être utilisé pour initialiser partiellement les valeurs sur le tas puis le transformer en un valeur initialisée,

Je pense que cela est garanti et que votre code est valide, avec quelques mises en garde:

  • Selon le type que vous utilisez (et notamment dans le code générique), vous aurez peut-être besoin de ptr::write_unaligned .
  • S'il y a plus de champs et que seuls certains d'entre eux sont initialisés, vous ne devez pas transmuter en T tant que tous les champs ne sont pas complètement initialisés .

C'est également un cas d'utilisation qui m'intéresse, car je pense qu'il pourrait être combiné avec une proc-macro pour fournir une abstraction de constructeur sur place sûre.

@Pzixel S'il a la même disposition de mémoire, vous pouvez éviter de copier la structure de données entière une fois que vous l'avez construite. Bien sûr, le compilateur peut éliminer la copie, et cela n'a pas d'importance pour les petites structures. Mais c'est définitivement un bon à avoir.

@nicoburns oui, je le vois maintenant. Je parle simplement qu'il peut y avoir un attribut, par exemple #[same_layout] ou #[elide_copying] , ou les deux, ou autre chose, pour s'assurer que cela fonctionne de la même manière que transmute . Ou peut-être changer l'implémentation de into_constructed pour éviter des copies supplémentaires. Je m'attendrais à ce que ce soit un comportement par défaut, pas seulement pour les gars intelligents qui lisent les documents sur la mise en page. Je veux dire que j'ai mon code qui appelle into_constructed et j'en reçois une copie supplémentaire, mais @ Nemo157 appelle juste transmute et il va bien. Il n'y a aucune raison pour que into_constructed ne puisse pas faire la même chose.

Je serais très confus si out.get_mut() as *mut _ ! = out.as_mut_ptr() . Ressemble vraiment à C ++. J'espère que ce serait réglé d'une manière ou d'une autre.

Quel est l'intérêt de get_mut() ?

J'ai fait un point similaire plus haut que get_mut() et get_ref() sont potentiellement source de confusion / le rendre facile à accidentellement invoquer un comportement non défini (parce qu'ils donnent l'illusion d'être des alternatives plus sûres à as_ptr() et as_mut_ptr() , mais sont en fait moins sûrs que ces méthodes).

Je pense qu'ils ne font @RalfJung a proposé de stabiliser (voir: https://www.ralfj.de/blog/2019/02/12/all-hands-recap.html)

@RalfJung Concernant votre proposition ptr::freeze() méthode

Serait-il judicieux d'avoir une méthode similaire pour construire MaybeUninit ? ( MaybeUninit::frozen() , MaybeUninit::abitrary() ou similaire). Intuitivement, il semble qu'une telle mémoire serait aussi performante qu'une mémoire véritablement non initialisée pour de nombreux cas d'utilisation, sans avoir le coût d'écriture dans la mémoire comme zeroed . Peut-être pourrait-il même être recommandé par rapport au constructeur uninitialized moins que les gens ne soient vraiment sûrs d'avoir besoin de mémoire non initialisée?

Sur cette note, quels sont les cas d'utilisation où vous avez vraiment besoin de mémoire "non initialisée" plutôt que de mémoire "gelée"?

@Pzixel

1. I'd be very confused if `out.get_mut() as *mut _` != `out.as_mut_ptr()`. Looks really C++ish. I hope it would be fixed somehow.

C'est noté. La raison pour laquelle certaines personnes proposent cela est qu'il peut être utile de déclarer &mut ! inhabité (comme dans, avoir une telle valeur est UB). Cependant, avec MaybeUninit::<!>::uninitiailized().get_mut() , nous avons créé une telle valeur. C'est pourquoi as_mut_ptr est moins dangereux - cela évite de créer une référence.

@nicoburns (Notez que freeze n'est pas mon idée, j'ai juste fait partie de la discussion et j'aime beaucoup la proposition.)

Je pense qu'ils ne font pas partie du sous-ensemble de l'API que @RalfJung a proposé de stabiliser

Correct. Et peut-être que nous ne devrions tout simplement pas les avoir du tout.

Serait-il judicieux d'avoir une méthode similaire pour construire MaybeUninit ? ( MaybeUninit::frozen() , MaybeUninit::abitrary() ou similaire).

Oui! J'allais proposer d'ajouter ceci une fois qu'un MaybeUninit est stable et que ptr::freeze a atterri.

Sur cette note, quels sont les cas d'utilisation où vous avez vraiment besoin de mémoire "non initialisée" plutôt que de mémoire "gelée"?

Cela nécessite plus d'étude et d'analyse comparative, on s'attend à ce que cela coûte des performances car LLVM ne fera pas les optimisations qu'il pourrait faire autrement.

(Je reviendrai également sur les autres commentaires, une fois que j'aurai le temps.)

@Pzixel être capable de construire des objets directement dans la mémoire pré-allouée n'est pas trivial, Rust avait deux RFC acceptés pour implémenter une telle chose (il y a plus de 4 ans!), Mais ils ont depuis été rejetés et la plupart de l'implémentation supprimée (sauf la syntaxe box j'ai utilisée ci-dessus). Si vous voulez plus de détails, le fil i.rl.o sur la suppression serait le meilleur endroit pour commencer.

Comme @nicoburns le mentionne, MaybeUninit pourrait potentiellement être utilisé comme élément constitutif d'une solution basée sur une bibliothèque moins ergonomique au même problème, très utile pour commencer à expérimenter le concept et voir quel type d'API il permet la construction. Cela dépend simplement du fait que MaybeUninit peut fournir les garanties nécessaires à la construction d'une telle solution.

@ Nemo157 Je suggère de ne l'utiliser qu'à un seul endroit, rien pour traiter des cas génériques non triviaux.

@jethrogb Merci beaucoup! Il semble donc que l'API fonctionne correctement pour vous en ce moment?

2. Dans sys / sgx / rwlock.rs , je l'utilise pour m'assurer que le motif de bits d'un const fn new() est le même que celui d'un initialiseur de tableau dans un fichier d'en-tête C.

Woah, c'est fou. ^^ Mais je suppose que ça devrait marcher, c'est un const fn sans argument après tout donc ça devrait toujours retourner la même chose ...

Une chose que je me demandais récemment était de savoir si MaybeUninit<T> était garanti d'avoir la même mise en page que T , et si quelque chose comme ça pouvait être utilisé pour initialiser partiellement les valeurs sur le tas puis le transformer en un complètement valeur initialisée

Sur la liste des choses que nous devrions éventuellement ajouter, il y a quelque chose comme

fn into_initialized_box(Box<MaybeUninit<T>>) -> Box<T>

qui transmute le Box .

Mais oui, je pense que nous devrions permettre de telles transmutes. Y a-t-il un précédent pour dire dans la documentation "vous pouvez transmuter ceci de la manière suivante"? Je pense généralement que nous préférons ajouter des méthodes d'aide au lieu de faire leurs propres transmutes.

  • Selon le type que vous utilisez (et notamment dans le code générique), vous aurez peut-être besoin de ptr::write_unaligned .

Dans le code générique, vous ne pouvez pas accéder aux champs. Je pense que si vous pouvez accéder aux champs, vous savez généralement si la structure est compressée, et si ce n'est pas le cas, ptr::write est assez bon. (N'utilisez pas d'affectation car cela pourrait chuter! J'oublie toujours ça ...)

Bien que l' écriture d'un testcase, il semble que cela nécessite une API supplémentaire fn uninit_boxed<T>() -> Box<MaybeUninit<T>> pour permettre d'allouer une boîte non initialisée sans toucher la pile.

C'est un bogue , mais comme ce bogue peut être difficile à corriger, il peut être judicieux de proposer un constructeur séparé pour cela. Je ne sais pas comment le mettre en œuvre, cependant. Et puis nous voulons probablement aussi quelque chose comme zeroed_box qui évite de remettre à zéro un emplacement de pile, puis de mémoriser, et ainsi de suite ... Je n'aime pas toute cette duplication. : /

Je propose donc qu'après / parallèlement à la stabilisation initiale, certaines personnes qui utilisent des cas de mémoire non initialisée sur le tas (en gros, en mélangeant Box et MaybeUninit ) se réunissent et conçoivent le minimum extension API possible pour cela. @eddyb a également exprimé son intérêt pour cela. Ce n'est pas vraiment lié à la dépréciation de mem::uninitialized , donc je pense que cela devrait avoir sa propre place de discussion, en dehors de ces problèmes de suivi (déjà bien trop gros).

Mes propres commentaires: je suis généralement satisfait de MaybeUninit<T> . Je n'ai pas de gros reproches. C'est moins un footgun que mem::uninitialized , ce qui est bien. Les méthodes const new et uninitialized sont bien. J'aurais aimé que plus de méthodes soient const, mais si je comprends bien, beaucoup d'entre elles nécessitent plus de progrès sur const fn en général avant de pouvoir être faites const .

Je voudrais une garantie plus forte que "même mise en page" pour T et MaybeUninit<T> . Je voudrais qu'ils soient compatibles ABI (effectivement, #[repr(transparent)] , même si je sais que cet attribut ne peut pas être appliqué aux syndicats) et FFI-safe (c'est-à-dire si T est FFI-safe , alors MaybeUninit<T> devrait également être sûr FFI). (Tangentiellement, j'aimerais pouvoir utiliser #[repr(transparent)] sur les unions qui n'ont qu'un seul champ de taille positive (comme nous pouvons le faire pour les structs))

Je compte en fait sur l'ABI de MaybeUninit<T> dans mon projet pour aider à une optimisation (mais pas de manière dangereuse, alors ne paniquez pas). Je suis heureux d'entrer dans les détails si quelqu'un est intéressé, mais je vais garder ce commentaire bref et omettre les détails pour le moment.

@mjbshaw Merci!

Je souhaite que nous puissions utiliser #[repr(transparent)] sur les unions qui n'ont qu'un seul champ de taille positive (comme nous pouvons le faire pour les structs).

Une fois que cet attribut existe, l'ajouter à MaybeUninit serait une évidence. Et en fait , la logique de ce qui a déjà été mis en œuvre dans rustc ( MaybeUninit<T> de facto est ABI compatible avec T , mais nous ne garantissons pas que.)

Tout ce qu'il faut, c'est que quelqu'un écrive une RFC et la voit à travers, et ajoute des vérifications qui s'assurent que repr(transparent) unions n'ont qu'un seul champ non-ZST. Souhaitez-vous essayer? :RÉ

Tout ce qu'il faut, c'est que quelqu'un écrive une RFC et la voit à travers, et ajoute des vérifications qui s'assurent que les unions repr(transparent) n'ont qu'un seul champ non-ZST. Souhaitez-vous essayer? :RÉ

@RalfJung Demandez et vous recevrez!

Cc https://github.com/rust-lang/rust/pull/58468

Cela ne laisse que l'API, je pense que nous pouvons raisonnablement stabiliser dans maybe_uninit , et déplace le reste dans des portes de fonctionnalités distinctes.

D'accord, les PR préparatoires ont tous atterri, et into_inner disparu.

Cependant, j'aimerais vraiment que https://github.com/rust-lang/rfcs/pull/2582 soit accepté avant la stabilisation, sinon nous n'avons même pas de moyen d'initialiser une structure champ par champ - et cela semble être un cas d'utilisation majeur pour MaybeUninit . Nous sommes très près d'avoir toutes les cases nécessaires pour que FCP démarre.

Je viens de convertir mon code pour utiliser MaybeUninit . Il y a pas mal d'endroits où j'aurais pu utiliser une méthode take qui fonctionne sur &mut self plutôt que self . J'utilise actuellement x.as_ptr().read() mais je pense que x.take() ou x.take_initialized() serait beaucoup plus clair.

@Amanieu Cela ressemble beaucoup à la méthode into_inner existante. Peut-être pouvons-nous essayer d'éviter la duplication ici?

😉

La méthode take de Option a une autre sémantique. x.as_ptr().read() ne change pas la valeur interne de x, mais Option::take essayez de remplacer la valeur. Cela peut être trompeur pour moi.

@ qwerty19106 x.as_ptr().read() sur un MaybeUninit _semantically_ prend la valeur et laisse le wrapper non initialisé à nouveau, il arrive juste que la valeur non initialisée laissée derrière ait le même modèle de bit que la valeur qui a été retirée .

J'utilise actuellement x.as_ptr().read() mais je pense que x.take() ou x.take_initialized() serait beaucoup plus clair.

Je trouve ça curieux, pouvez-vous expliquer pourquoi?

À mon avis, une méthode semblable à take est quelque peu trompeuse car contrairement à la fois à take et à into_initialized , elle ne protège pas contre la prise deux fois. En fait, pour les types Copy (et en fait pour les valeurs Copy telles que None as Option<Box<T>> ), prendre deux fois est tout à fait correct! Donc, l'analogie avec take ne tient pas vraiment, de mon point de vue.

Nous pourrions l'appeler read_initialized() , mais à ce stade, je me demande sérieusement si cela est effectivement plus clair que as_ptr().read() .

x.as_ptr().read() sur un MaybeUninit _sémantiquement_ prend la vallée et laisse le wrapper non initialisé à nouveau, il arrive juste que la valeur non initialisée laissée derrière ait le même modèle de bit que la valeur qui a été retirée.

MaybeUninit n'a pas vraiment d'invariant sémantique utile, donc je ne suis pas sûr d'être entièrement d'accord avec cette affirmation. TBH Je ne suis pas convaincu qu'il soit utile de considérer les opérations sur MaybeUninit d'une autre manière que simplement leur effet opérationnel brut.

@RalfJung hmm, peut-être que "sémantiquement" est le mauvais mot ici. En ce qui concerne la façon dont un utilisateur doit utiliser le type, vous devez supposer que la valeur est à nouveau non initialisée après l'avoir lue (à moins que vous ne sachiez concrètement que le type est Copy ).

Si vous ne regardez que l'effet opérationnel brut, vous obtenez des interactions étranges comme celle-ci où vous pouvez violer les invariants de sécurité d'autres API non sécurisées sans lire techniquement la mémoire non initialisée. (J'espérais en quelque sorte que Miri suivrait toujours 0 lectures de longueur de mémoire non initialisée, mais cela ne semble pas le cas).

@RalfJung Dans tous mes cas, cela implique un static mut dans lequel une valeur est placée, puis retirée. Comme je ne peux pas consommer de statique, je ne peux pas utiliser into_uninitialized .

@Amanieu ce que je demandais, c'est pourquoi pensez-vous que x.take_initialized() est plus clair que x.as_ptr().read() ?

@ Nemo157

J'espérais en quelque sorte que Miri suivrait toujours 0 lectures de longueur de mémoire non initialisée, mais cela ne semble pas

Une lecture de longueur 0 de la mémoire non initialisée n'est jamais UB, alors pourquoi Miri s'en soucierait-elle?

Si vous ne regardez que l'effet opérationnel brut, vous obtenez des interactions étranges comme celle-ci où vous pouvez violer les invariants de sécurité d'autres API non sécurisées sans lire techniquement la mémoire non initialisée.

Bien sûr, vous pouvez violer les invariants de sécurité sans jamais lire la mémoire non initialisée. Vous pouvez également simplement utiliser MaybeUninit::zeroed().into_initialized() pour cela. Je ne vois pas le problème.
L '"interaction étrange" ici est que vous avez créé deux valeurs d'un type que vous n'aviez pas le droit de créer. Il s'agit de l'invariant de sécurité de Spartacus , et n'a rien à voir avec les invariants de validité.

C'est pourquoi je pense que read_initialized() transmet mieux ce qui se passe: nous lisons les données et nous affirmons qu'elles sont correctement initialisées (ce qui implique de nous assurer que nous sommes réellement autorisés à créer cette valeur à ce type). Cela n'a aucun effet sur le motif de bits encore stocké dans MaybeUninit .

@RalfJung Je traite essentiellement MaybeUninit comme un Option , mais sans la balise. En fait, j'utilisais auparavant le crate d'option non étiquetée exactement dans ce but, et il a une méthode take pour extraire la valeur de l'union.

@Amanieu @shepmaster J'ai ajouté un read_initialized dans https://github.com/rust-lang/rust/pull/58660. Je pense toujours que c'est un meilleur nom que take_initialized . Cela répond-il à vos besoins?

Ce PR ajoute également des exemples à certaines des autres méthodes, commentaires bienvenus!

Je suis content de read_initialized .

Pendant que j'y étais, j'ai aussi fait MaybeUninit<T>: Copy if T: Copy . Cela ne semble pas une bonne raison de ne pas le faire.

Hm, peut-être que get_initialized serait un meilleur nom? Il s'agit en quelque sorte de compléments set , après tout.

Ou peut-être que set devrait être renommé en write ? Cela permettrait également d'assurer la cohérence.

J'ai converti mon code pour utiliser MaybeUninit et j'ai trouvé que travailler avec des tranches non initialisées est très peu ergonomique. Je pense que cela pourrait être amélioré si nous avions des fonctions pour ce qui suit:

  • Conversion sécurisée de &mut [T] en &mut [MaybeUninit<T>] . Cela permet effectivement d'émuler les paramètres &out en utilisant &mut [MaybeUninit<T>] , ce qui est utile par exemple pour read .
  • Conversion non sécurisée de &mut [MaybeUninit<T>] en &mut [T] (et de même pour &[T] ), à utiliser une fois que nous avons appelé .set sur chaque élément de la tranche.

Les API que j'ai ressemblent à ceci:

// The returned slice is truncated to the number of elements actually read.
fn read<T>(out: &mut [MaybeUninit<T>]) -> Result<&mut [T]>;

Je suis d'accord que travailler avec des tranches n'est actuellement pas ergonomique, et c'est pourquoi j'ai ajouté first_ptr et first_ptr_mut . Mais c'est probablement loin d'être la meilleure API.

Cependant, je préférerais que nous puissions nous concentrer sur la livraison de "l'API principale" en premier, puis examiner l'interaction avec les tranches (et avec Box ).

J'aime l'idée de renommer set en write , ce qui donne une cohérence avec ptr::write .

Dans le même ordre d'idées, read_initialized vraiment meilleur que juste read ? Si le problème concerne l'utilisation accidentelle qui devient cachée, peut-être en faire une fonction au lieu d'une méthode, c'est- MaybeUninit::read(&mut v) dire write , c'est- MaybeUninit::write(&mut v) dire

Quoi qu'il en soit, jusqu'à ce que ces API soient élaborées, je soutiens fermement la stabilisation avec une API minimale, c'est- new dire uninitialized , zeroed , as_ptr , as_mut_ptr , et peut-être get_ref et get_mut .

et peut-être get_ref et get_mut .

Ceux-ci ne devraient être stabilisés qu'une fois que nous avons résolu https://github.com/rust-rfcs/unsafe-code-guidelines/issues/77 , et cela semble que cela pourrait prendre un certain temps ...

stabilisation avec une API minimum, c'est- new dire uninitialized , zeroed , as_ptr , as_mut_ptr

Mon plan était de into_initialized , set / write et read_initialized pour faire partie de cet ensemble minimal. Mais peut-être que ça ne devrait pas l'être? set / write et read_initialized peuvent facilement être implémentés avec le reste, donc je penche maintenant pour ne pas les stabiliser dans le premier lot. Mais avoir quelque chose comme into_initialized dès le départ est souhaitable, IMO.

peut-être en faire une fonction au lieu d'une méthode, ie MaybeUninit::read(&mut v) ? La même chose pourrait être faite pour write , c'est- MaybeUninit::write(&mut v) dire

D'après ce qui a été discuté ici auparavant, nous n'utilisons que l'approche de la fonction explicite pour éviter les problèmes avec les instances Deref . Je ne pense pas que nous devrions introduire la priorité pour une autre raison d'utiliser une fonction au lieu d'une méthode.

est-ce que read_initialized vraiment meilleur que juste read ?

Bonne question! Je ne sais pas. C'était pour la symétrie avec into_initialized . Mais into_inner est une méthode courante où l'on peut perdre la vue d'ensemble du type sur lequel il est appelé, read est beaucoup moins courant. Et peut-être que cela devrait être juste initialized au lieu de into_initialized ? Tant d'options ...

D'après ce qui a été discuté ici auparavant, nous n'utilisons que l'approche de la fonction explicite pour éviter les problèmes avec les instances Deref . Je ne pense pas que nous devrions introduire la priorité pour une autre raison d'utiliser une fonction au lieu d'une méthode.

Sauf que ptr::read et ptr::write sont des fonctions, pas des méthodes. Ainsi, la priorité est déjà établie en faveur de MaybeUninit::read et MaybeUninit::write .

Edit : OK, apparemment, il y a des méthodes read et write sur les pointeurs, aussi ... Jamais remarqué celles-ci auparavant ... Mais elles consomment le pointeur, ce qui n'a pas vraiment de sens pour MaybeUninit .

Tant d'options ...

D'accord. Jusqu'à ce qu'il y ait beaucoup plus d'abandon de vélo sur les autres méthodes, je pense que seulement new , uninitialized , zeroed , as_ptr , as_mut_ptr sont vraiment prêts pour la stabilisation.

Sauf que ptr::read et ptr::write sont des fonctions, pas des méthodes. Donc la priorité est déjà établie

Ils ne font pas partie d'une structure de données, bien sûr, ce sont des fonctions autonomes. Et comme vous le remarquez, elles existent aussi de nos jours en tant que méthodes.

Mais ils consomment le pointeur

Les pointeurs bruts sont Copy , donc rien n'est vraiment consommé.

Les pointeurs bruts sont Copy , donc rien n'est vraiment consommé.

Bon point...

Eh bien, v.as_ptr().read() est déjà assez concis et clair. Le as_ptr suivi de read devrait le faire ressortir comme quelque chose à réfléchir attentivement, bien plus que ne le fait into_initialized . Personnellement, je suis en faveur d'exposer uniquement as_ptr et as_mut_ptr , du moins pour le moment. Et, new , uninitialized et zeroed , bien sûr.

@Amanieu Qu'en est-il de quelque chose de plus comme ce que Cell a, où il y a des conversions sûres pour &mut MaybeUninit<[T]> vers et depuis &mut [MaybeUninit<T>] ?

Cela permettrait ce qui suit, ce qui me semble assez naturel:

fn read<T>(out: &mut MaybeUninit<[T]>) -> Result<&mut [T]> {
    let split = out.as_mut_slice_of_uninit();
    // ... operate on split ...
    return Some(unsafe { split[0..n].as_uninit_mut_slice().get_mut() })
}

On a également l'impression qu'il représente plus précisément la sémantique pour l'appelant. La fonction prenant un &mut [MaybeUninit<T>] me donnerait l'impression qu'elle pourrait avoir une logique de distinction pour laquelle celles qui conviennent et celles qui ne le sont pas. Prendre &mut MaybeUninit<[T]> , d'autre part, exprime qu'il ne fera pas de distinction entre les cellules en ce qui concerne les données qui y sont déjà.

(Les noms des méthodes sont, bien sûr, soumis au bikeshedding - j'ai juste imité ce que fait Cell .)

@eternaleye MaybeUninit<[T]> n'est pas un type valide car les unions ne peuvent pas être des DST.

Mm, à droite

Jusqu'à ce qu'il y ait beaucoup plus d'abandon de vélos sur les autres méthodes, je pense que seulement new , uninitialized , zeroed , as_ptr , as_mut_ptr sont vraiment prêts pour la stabilisation.

Eh bien, je pense que nous devrions accepter cette RFC avant de stabiliser quoi que ce soit - sinon nous n'avons même pas de moyen sanctionné d'initialiser une structure champ par champ, ce qui semble être le strict minimum.

Donc, pendant que nous attendons les expériences , nous pouvons faire un peu de vélo sur les noms de ce qui est actuellement appelé set , read_initialized et into_initialized . Les changements de noms suivants ont été suggérés:

  1. set -> write . La meilleure métaphore pour .as_ptr().read() semble être "read", pas "get", mais alors le complément ( .as_ptr_mut().write() ) devrait être "write", pas "set".
  2. read_initialized -> read . Correspond bien à write , mais n'est pas sûr. Est-ce (plus la documentation) un avertissement suffisant pour que vous deviez vous assurer manuellement que les données sont déjà initialisées? Il y avait beaucoup d'accord sur le fait qu'un into_inner dangereux ne suffisait into_initialized .
  3. into_initialized -> initialized . Si nous avons à la fois read_initialized et into_initialized , cela a une belle cohérence IMO - mais si c'est read , alors into_initialized dépasse un peu. Le nom de la méthode est assez long. Pourtant, la plupart des opérations consommatrices sont appelées into_* , d'après ce que je sais.

Des objections pour (1)? Et je suis surtout appuyé contre (3). Pour (2) je suis indécis: read est plus facile à taper, mais read_initialized IMO fonctionne mieux lors de la lecture d'un tel code - et le code est lu et révisé plus souvent qu'écrit. Cela semble bien d'appeler l'endroit où nous supposons que les choses doivent être initialisées.

Pensées, opinions?

Eh bien, je pense que nous devrions accepter cette RFC avant de stabiliser quoi que ce soit - sinon nous n'avons même pas de moyen sanctionné d'initialiser une structure champ par champ, ce qui semble être le strict minimum.

Est-ce là que je mets un plug-in pour offset_of! ? :)

Notez que read_initialized est un sur-ensemble strict de into_initialized (prend &self au lieu de self ). Cela a-t-il beaucoup de sens de soutenir les deux?

Est-ce là que je mets un plug-in pour offset_of! ? :)

Si vous pouvez stabiliser cela avant que ma RFC soit acceptée, bien sûr. ;)

Cela a-t-il beaucoup de sens de soutenir les deux?

OMI oui. into_initialized est plus sûr car il empêche d'utiliser la même valeur deux fois, et par conséquent, il devrait être préféré à read_initialized chaque fois que possible.

Donc @nikomatsakis a en quelque sorte fait valoir ce point avant, mais n'en a pas fait un bloqueur dur.

Je viens de porter beaucoup de code pour utiliser MaybeUninit<T> et into_initialized et je le trouve inutilement verbeux. Le code est déjà beaucoup plus détaillé qu'avant où il était "incorrectement" en utilisant mem::uninitialized .

Je pense que MaybeUninit<T> devrait être simplement appelé Uninit<T> , car à toutes fins pratiques, si vous obtenez un MaybeUninit<T> inconnu, vous devez supposer qu'il n'est pas initialisé, donc Uninit<T> résumerait cela correctement. De plus, into_uninitialized ne devrait être que into_init() ou similaire pour des raisons de cohérence.

Nous pourrions également appeler le type Uninitialized<T> et la méthode into_initialized , mais utiliser une abréviation pour le type et la forme longue pour la méthode ou vice-versa est une incohérence douloureuse. Idéalement, je devrais juste avoir besoin de me rappeler que "les API Rust utilisent des abréviations / des formes longues" et c'est tout.

Parce que les abréviations peuvent être ambiguës pour différentes personnes, je préfère simplement utiliser des formes longues partout et l'appeler un jour. Mais l'utilisation d'un mélange est à l'OMI le pire des deux mondes. Rust a tendance à utiliser des abréviations plus souvent que des formes plus longues, donc je n'aurais rien contre Uninit<T> comme abréviation et .into_init() comme une autre abréviation pour la méthode.

Je n'aime pas into_initialized() , car il semble qu'une transformation soit en cours pour initialiser la valeur. Je préfère take_initialized() loin take , mais je pense que c'est beaucoup plus clair, sémantiquement, et je pense que la clarté sémantique devrait remplacer la cohérence emprunt / déplacement. D'autres alternatives qui n'ont pas déjà la priorité d'être des emprunts mutables pourraient être move_initialized ou consume_initialized .

Quant à set() vs write() , je préfère fortement write() afin d'invoquer la similitude avec as_ptr().write() , pour laquelle ce serait un alias.

Et enfin, s'il doit y avoir un take_initialized() ou similaire, alors je préfère read_initialized() à read() raison de l'explication du premier.

Edit : mais pour clarifier, je pense que s'en tenir à as_ptr().write() et as_ptr().read() est encore plus clair et plus susceptible de déclencher les circuits mentaux DANGER DANGER .

@gnzlbg nous avions un FCP pour le nom du type, je ne sais pas si nous devrions rouvrir cette discussion.

Cependant, j'aime la proposition d'utiliser "init" de manière cohérente, comme dans MaybeUninit::uninit() et x.into_init() .

Je n'aime pas into_initialized() , car il semble qu'une transformation soit en cours pour initialiser la valeur.

into méthodes into_vec .

Je suis d' take_initialized(&mut self) avec un undef .

rétablir l'état interne

https://github.com/rust-lang/rust/issues/53491#issuecomment -437811282

cela ne devrait pas changer du tout le contenu de self . Seule la propriété est transférée, elle est donc désormais dans le même état que lorsqu'elle est construite non initialisée.

Beaucoup de ces choses ont déjà été discutées dans les plus de 200 commentaires cachés.

Beaucoup de ces choses ont déjà été discutées dans les plus de 200 commentaires cachés.

Je suis la discussion depuis un certain temps et je me trompe peut-être, mais je ne pense pas que ce point ait déjà été soulevé. En particulier, le commentaire que vous citez ne suggère undef ", mais le rend équivalent à ptr::read (ce qui revient à laisser l'état interne inchangé). Ce que je suggère, c'est l'équivalent conceptuel de mem::replace(self, MaybeUninit::uninitialized()) .

l'équivalent conceptuel de mem::replace(self, MaybeUninit::uninitialized()) .

En raison de la signification de undef , cela équivaut à read : https://rust.godbolt.org/z/e0-Gyu

@scottmcm non, ce n'est pas le cas. Avec read , ce qui suit est légal:

let mut x = MaybeUninit::<u32>::uninitialized();
x.set(13);
let x1 = unsafe { x.read_initialized() };
// `u32` is `Copy`, so we may read multiple times.
let x2 = unsafe { x.read_initialized() };
assert_eq!(x1, x2);

Avec le take proposé, ce serait illégal puisque x2 serait undef .

Ce n'est pas parce que deux fonctions génèrent le même assemblage qu'elles sont équivalentes.

Cependant, je ne vois aucun avantage à remplacer le contenu par undef . Cela introduit simplement plus de façons pour les gens de se tirer une balle dans le pied. @jethrogb vous n'avez donné aucune motivation, pouvez-vous expliquer pourquoi vous pensez que c'est une bonne idée?

Je suis d' take_initialized(&mut self) avec un undef .

Je proposais take_initialized(self) au lieu de into_initialized(self) , car je pense que l'ancien nom décrit plus précisément l'opération. Encore une fois, je comprends que take prend généralement un &mut self et into prend généralement un self , mais je crois qu'une dénomination sémantiquement précise est plus importante qu'une saisie cohérente appellation. Cependant, il faudrait peut-être utiliser un nom différent, tel que move_initialized ou transmute_initialized .

Et, encore une fois, comme pour v.write() et v.read_initialized() , je ne vois aucune valeur positive supérieure à v.as_ptr().write() et v.as_ptr().read() . Les deux derniers semblent moins susceptibles d'être mal utilisés.

Et, encore une fois, comme pour v.write() et v.read_initialized() , je ne vois aucune valeur positive supérieure à v.as_ptr().write() et v.as_ptr().read() . Les deux derniers semblent moins susceptibles d'être mal utilisés.

v.write() (ou v.set() ou peu importe ce que nous l'appelons ces jours-ci) est sûr. v.as_ptr().write() nécessite un bloc unsafe , ce qui est assez ennuyeux. Bien que je sois d'accord sur v.read_init() vs v.as_ptr().read() . v.read_init() semble superflu.

Je proposais take_initialized (self) au lieu de into_initialized (self), car je crois que l'ancien nom décrit plus précisément l'opération. Encore une fois, je comprends que la prise prend généralement un self & mut et prend généralement un self, mais je crois qu'une dénomination sémantiquement précise est plus importante qu'une dénomination typée de manière cohérente.

Je pense fortement que into_init(ialized) aussi sémantiquement plus précis ici - il consomme le MaybeUninit , après tout.

@mjbshaw Ah, oui, c'est ainsi. Je n'ai pas remarqué que ... Bon, eh bien, dans ce cas, je révoque tous mes commentaires précédents sur set / write . Peut-être que set plus de sens; Cell et Pin définissent déjà les méthodes set . La principale différence serait que MaybeUninit::set ne supprimerait aucune valeur précédemment stockée; c'est peut-être encore plus proche de write ... Je ne sais pas. Quoi qu'il en soit, la documentation est assez claire.

@RalfJung D'accord, oubliez take... alors. Qu'en est-il d'un nouveau nom, tel que move... , consume... , ou transmute... ou quelque chose? Je pense que into_init(ialized) est trop déroutant; moi aussi, cela implique que la valeur est en cours d'initialisation, alors qu'en réalité, nous affirmons implicitement qu'elle a déjà été initialisée.

alors qu'en réalité, nous affirmons implicitement qu'il a déjà été initialisé.

Je pense que cela vaut la peine de rappeler que la seule chose que into_init affirme est que la valeur satisfait l'invariant de validité de T , qui ne doit pas être confondu avec T étant "initialisé" dans n'importe quel sens général du mot.

Par exemple:

pub mod foo {
    pub struct AlwaysTrue(bool);
    impl AlwaysTrue { 
        pub fn new() -> Self { Self(true) }
        /// It is impossible to initialize `AlwaysTrue` to false
        /// and unsafe code can rely on `is_true` working properly:
        pub fn is_true(x: bool) -> bool { x == self.0 }
    }
}

pub unsafe fn improperly_initialized() -> foo::AlwaysTrue {
    let mut v: MaybeUninit<foo::AlwaysTrue> = MaybeUninit::uninitialized();
    // let v = v.into_init(); // UB: v is invalid
    *(v.as_mut_ptr() as *mut u8) = 3; // OK
    // let v = v.inti_init(); // UB v is invalid
    *(v.as_mut_ptr() as *mut bool) = false; // OK
    let v = v.into_init(); // OK: v is valid, even though AlwaysTrue is false
    v
}

Ici, la valeur de retour de improperly_initialized est "initialisée" dans le sens où elle satisfait l'invariant de validité de T , mais pas dans le sens où elle satisfait l'invariant de sécurité de T , et la distinction est subtile mais importante, car dans ce cas, cette distinction est ce qui nécessite que improperly_initialized soit déclaré comme un unsafe fn .

Lorsque la plupart des utilisateurs parlent de quelque chose qui est "initialisé", ils n'ont généralement pas la sémantique "valide mais MaybeUnsafe" de MaybeUninit::into_init .

Si nous voulions être extrêmement verbeux à ce sujet, nous pourrions avoir Invalid<T> et Unsafe<T> , avoir Invalid<T>::into_valid() -> Unsafe<T> et demander aux utilisateurs d'écrire uninit.into_valid().into_safe() . Ensuite, au-dessus de improperly_initialized renverrait Unsafe<T> , et ce n'est qu'après que l'utilisateur a correctement défini la valeur de AlwaysTrue à true peut réellement obtenir le T sûr:

// note: this is now a safe fn
fn improperly_uninitialized() -> Unsafe<foo::AlwaysTrue>;
fn initialized() -> foo::AlwaysTrue {
    let mut v: Unsafe<foo::AlwaysTrue> = improperly_uninitialized();
    unsafe { v.as_mut_ptr() as *mut bool } = true;
    unsafe { v.into_safe() }
}

Notez que cela permet à improperly_uninitialized de devenir un coffre-fort fn , car maintenant l'invariant selon lequel le AlwaysTrue n'est pas sûr n'est pas encodé dans des "commentaires" autour de la fonction, mais dans le les types.

Je ne sais pas si l'approche douloureusement atroce vaut la peine d'être poursuivie. MaybeUninit objectif de MaybeUninit . Sinon, les gens pourraient écrire fn improperly_uninitialized() -> AlwaysTrue comme un coffre-fort fn , et simplement renvoyer un AlwaysTrue non sûr parce que bien, ils l'ont "initialisé".

Une chose que l'on pourrait aussi faire avec Invalid<T> et Unsafe<T> est d'avoir deux traits, ValidityCheckeable et UnsafeCheckeable , avec deux méthodes, ValidityCheckeable::is_valid(Invalid<Self>) UnsafeCheckeable::is_safe(Unsafe<Self>) , et utilisez les méthodes Invalid::into_valid et Unsafe::into_safe assert_validity! et assert_safety! .

Au lieu d'écrire l'invariant de sécurité dans un commentaire, vous pouvez simplement écrire le code de la vérification.

Je pense qu'il vaut la peine de rappeler que la seule chose qu'affirme into_init est que la valeur satisfait l'invariant de validité de T, ce qui ne doit pas être confondu avec T étant «initialisé» dans un sens général du mot.

C'est correct. OTOH, je pense que "initialisé" est un proxy raisonnable pour cela dans une première explication.

Sinon, les gens pourraient écrire fn incorrectly_uninitialized () -> AlwaysTrue comme une fn sûre, et simplement retourner un AlwaysTrue non sûr parce que, bien, ils l'ont "initialisé".

Je pense que nous pouvons raisonnablement faire valoir que ce n'est pas correctement «initialisé». Je suis d'accord que nous avons besoin d'une documentation appropriée sur la manière dont ces deux invariants interagissent quelque part (et je ne suis pas sûr de savoir quel serait le meilleur endroit), mais je pense aussi que l'intuition de la plupart des gens dira que improperly_uninitialized n'est pas acceptable fonction à exporter. "Briser les invariants des autres" est un concept qui, je pense, surgit naturellement quand on pense à "toutes les fonctions sûres que j'exporte doivent être telles que le code sûr ne puisse pas les utiliser pour faire des ravages".

Une chose que l'on peut aussi faire avec Invalidet dangereuxa deux caractéristiques, ValidityCheckeable et UnsafeCheckeable, avec deux méthodes, ValidityCheckeable :: is_valid (Invalid) et UnsafeCheckeable :: is_safe (Unsafe), et ont les méthodes Invalid :: into_valid et Unsafe :: into_safe assert_validity! et assert_safety! sur eux.

Dans la grande majorité des cas, l'invariant de sécurité ne sera pas vérifiable. Même l'invariant de validité n'est probablement pas vérifiable pour les références. (Eh bien, cela dépend un peu de la façon dont nous factorisons les choses.)

@scottjmaddox

Qu'en est-il d'un nouveau nom, comme bouger ..., consommer ..., ou transmuter ... ou quelque chose? Je pense que into_init (ialized) est trop déroutant; moi aussi, cela implique que la valeur est en cours d'initialisation, alors qu'en réalité, nous affirmons implicitement qu'elle a déjà été initialisée.

Comment move_init transmet-il une "assertion" plus que into_init ?

assert_init(italized) a déjà été suggéré.

Cependant, notez que read ou read_initialized ou as_ptr().read ne disent pas vraiment quoi que ce soit sur l'affirmation de quoi que ce soit.

Si nous voulions être extrêmement verbeux à ce sujet, nous pourrions avoir Invalid<T> et Unsafe<T> , avoir Invalid<T>::into_valid() -> Unsafe<T> et demander aux utilisateurs d'écrire uninit.into_valid().into_safe() . Ensuite, au-dessus de improperly_initialized renverrait Unsafe<T> , et ce n'est qu'après que l'utilisateur a correctement défini la valeur de AlwaysTrue à true peut réellement obtenir le T sûr:

@gnzlbg Hé, c'est plutôt chouette. J'aime que cela jette la distinction dans les visages des utilisateurs d'une manière inévitable. C'est probablement un bon moment d'enseignement. «validité» et «sécurité» qui feront réfléchir les gens à deux fois? uninit.into_valid().into_safe() n'est pas si verbeux que uninit.assume_initialized() ou tout le reste. Bien entendu, pour faire cette distinction, nous devrons trouver un accord autour du modèle en premier lieu. 😅 Je pense que nous devrions étudier davantage ce modèle.

assert_init(italized) a déjà été suggéré.

@RalfJung Nous avons aussi assume_initialized grâce à @eternaleye (je pense). Voir https://github.com/rust-lang/rust/issues/53491#issuecomment -440730699 avec une liste de justifications assez convaincantes.

TBH J'ai l'impression qu'avoir deux types est beaucoup trop verbeux.

@RalfJung Pouvons-nous approfondir cela? éventuellement avec quelques comparaisons d'exemples qui, selon vous, montrent le haut degré de verbosité?

Hmm ... si nous envisageons des API plus verbeuses, alors

uninit.into_inner(uninit.assert_initialized());

pourrait très bien fonctionner sémantiquement. La première méthode retourne un jeton qui enregistre votre assertion. La deuxième méthode renvoie le type interne, mais vous oblige à affirmer qu'il est valide.

Je ne suis pas tout à fait convaincu que cela vaut l'effort supplémentaire, car l'abstraction pourrait simplement rendre les gens plus confus et donc susceptibles de faire des erreurs.

Nous avons également assume_initialized en raison de @eternaleye (je pense). Voir # 53491 (commentaire) avec une liste de justifications assez convaincantes.

Juste. assume_initialized me semble bien.

Ou peut-être que c'est assume_init ? Cela devrait probablement être cohérent avec le constructeur, MaybeUninit::uninit() vs MaybeUninit::uninitialized() - et celui- ci devrait être stabilisé avec le premier lot, nous devrions donc faire cet appel bientôt.

@nicoburns Je ne vois pas l'avantage que nous

Pouvons-nous approfondir cela? éventuellement avec quelques comparaisons d'exemples qui, selon vous, montrent le haut degré de verbosité?

Eh bien, il est clair que c'est plus verbeux que "juste" MaybeUninit , non? Il y a beaucoup de charge mentale supplémentaire (devoir comprendre deux types), il y a le double déballage, et cela signifie que je dois choisir le type à utiliser. Il y a donc ici un coût supplémentaire que je pense que vous devez justifier.

En fait, je doute généralement de l'utilité de Unsafe . Du point de vue du compilateur, ce serait entièrement un NOP; le compilateur ne suppose jamais que vos données satisfont l'invariant de sécurité. Du point de vue de l'implémentation de bibliothèque, je doute fortement que la lisibilité du code s'améliorera si, dans l'implémentation Vec , nous transmutons des choses en Unsafe<Vec<T>> chaque fois que nous violons temporairement l'invariant de sécurité. Et du point de vue de l'enseignement, je doute que quiconque soit surpris lorsqu'il crée un Vec<T> qui est valide mais non sûr, le donne à un code sûr, puis tout explose.
Comparez cela avec MaybeUninit qui est nécessaire du point de vue du compilateur, et où le fait que vous deviez même faire attention aux "mauvais" bool dans votre propre code privé pourrait surprendre certains .

Compte tenu de son coût important, je pense que Unsafe besoin d'une motivation beaucoup plus forte. Je ne vois pas comment cela aiderait réellement à éviter les bogues ou à améliorer la lisibilité du code.

Je peux voir les arguments pour renommer MaybeUninit en MaybeInvalid . Cependant, "invalide" est extrêmement vague (invalide pour quoi ?), J'ai vu des gens confus par ma distinction entre "valide" et "sûr" - on pourrait supposer qu'un "valide Vec " est valide pour tout type d'utilisation. "non initialisé" déclenche au moins les bonnes associations pour la plupart des gens. Peut-être devrions-nous renommer «invariant de validité» en «invariant d'initialisation» ou alors?

De plus, la simple présence de Unsafe<T> peut être trompeuse (en impliquant à tort que toutes les valeurs qui n'y sont @RalfJung a donné de bonnes raisons contre cela ci-dessus), et avec des arguments plus faibles de son côté que MaybeUninit car il n'y a pas d'UB impliqué - c'est essentiellement une question de style. En tant que tel, je suis sceptique quant à savoir si une telle convention sera un jour universelle dans la communauté Rust même si un RFC est accepté et que la bibliothèque standard et les documents sont mis à jour.

Donc, OMI, quiconque veut voir cette convention se concrétiser a plus de poisson à faire frire que de faire du vélo avec l'API MaybeUninit , et je suggérerais de ne pas retarder davantage sa stabilisation pour attendre la résolution de ce processus. Si nous stabilisons les conversions MaybeUninit<T> -> T , les futures générations de Rust pourraient toujours écrire MaybeUninit<Unsafe<T>> pour indiquer des données qui sont d'abord non initialisées, puis éventuellement toujours non sécurisées après avoir été initialisées.

@RalfJung

Ou peut-être que c'est assume_init ? Cela devrait probablement être cohérent avec le constructeur, MaybeUninit::uninit() vs MaybeUninit::uninitialized() - et _that_ devrait être stabilisé avec le premier lot, nous devrions donc faire cet appel bientôt.

Si nous pouvons avoir une cohérence à trois voies avec le type, le constructeur et la fonction -> T , ce serait encore mieux. Comme le type n'a pas le suffixe -ialized je pense que ::uninit() et .assume_init() est probablement la voie à suivre.

Eh bien, il est clair que c'est plus verbeux que "juste" MaybeUninit , non?

Ça dépend ... Je pense que foo.assume_init().assume_safe() (ou foo.init().safe() si l'on est enclin à être bref) n'est pas si long. Nous pouvons également proposer la combinaison en foo.assume_init_safe() si nécessaire. La combinaison a toujours l'avantage d'énoncer les deux hypothèses.

Il y a beaucoup de charge mentale supplémentaire (devoir comprendre deux types), il y a le double déballage, et cela signifie que je dois choisir le type à utiliser. Il y a donc ici un coût supplémentaire que je pense que vous devez justifier.

Espérons que la complexité vient du fait d'avoir à comprendre les concepts sous-jacents de validité et de sécurité. Une fois que cela est fait, je ne pense pas qu'il y ait beaucoup de complexité mentale supplémentaire. Je pense que les concepts sous-jacents sont importants à transmettre.

En fait, je doute généralement de l'utilité de Unsafe . Du point de vue du compilateur, ce serait entièrement un NOP; le compilateur ne suppose jamais que vos données satisfont l'invariant de sécurité.

Sûr; Je suis d'accord que d'un compilateur POV c'est inutile. Toute utilité de la distinction est comme une sorte d'interface de "types de session".

Compte tenu de son coût important, je pense que Unsafe besoin d'une motivation beaucoup plus forte. Je ne vois pas comment cela aiderait réellement à éviter les bogues ou à améliorer la lisibilité du code.

L'aspect qui a attiré mon attention était celui de l'apprentissage. Je pense que des erreurs sont inévitables lorsque les gens pensent que .assume_init() signifie que "OK; j'ai vérifié l'invariant de validité et maintenant j'ai un bon T ". Le schéma actuel de MaybeUninit<T> est en quelque sorte inutile de cette manière. Je ne suis cependant pas marié à Unsafe<T> et Invalid<T> comme noms. Je pense simplement que la séparation en deux types, quel que soit leur nom, peut être utile sur le plan éducatif. Peut-être y a-t-il d'autres moyens, comme le renforcement de la documentation, qui peuvent compenser cela dans le cadre actuel?

Je _can_ voir les arguments pour renommer MaybeUninit en MaybeInvalid . Cependant, "invalide" est extrêmement vague (invalide pour _quel_?), J'ai vu des gens confus par ma distinction entre "valide" et "sûr" - on pourrait supposer qu'un "valide Vec " est valide pour tout type d'utilisation. "non initialisé" déclenche au moins les bonnes associations pour la plupart des gens. Peut-être devrions-nous renommer «invariant de validité» en «invariant d'initialisation» ou alors?

Je suis tout à fait d'accord avec le fait que «validité» et «sécurité» prêtent à confusion en raison de la façon dont «valide» sonne. J'ai été partial pour "machine invariant" en remplacement de "validité" et "type invariant de système" pour "sécurité".

@rkruppe

Donc, OMI, quiconque veut voir cette convention se concrétiser a plus de poisson à faire frire que de faire du vélo avec l'API MaybeUninit , et je suggérerais de ne pas retarder davantage sa stabilisation pour attendre la résolution de ce processus. Si nous stabilisons les conversions MaybeUninit<T> -> T , les futures générations de Rust pourraient toujours écrire MaybeUninit<Unsafe<T>> pour indiquer les données qui sont d'abord non initialisées, puis éventuellement toujours non sécurisées après avoir été initialisées.

Bons points, en particulier re. MaybeUninit<Unsafe<T>> ; Vous pouvez probablement également ajouter un alias de type pour rendre le nom de type moins détaillé.

Si nous pouvons avoir une cohérence à trois niveaux avec le type, le constructeur et la fonction -> T, ce serait encore mieux. Comme le type n'a pas le suffixe -ialized, je pense que :: uninit () et .assume_init () est probablement la voie à suivre.

D'accord. Je suis un peu triste de perdre le préfixe into , mais je ne vois aucun bon moyen de le conserver.

Alors qu'en est-il de read / read_init alors? La similitude avec ptr::read suffisante pour déclencher "vous feriez mieux de vous assurer qu'il est réellement initialisé"? Est -ce que read_init ont question semblable à into_init , où il semble que cela rend initialisé au lieu d'avoir cela comme une hypothèse? Est-ce que assume_init devrait être comme read est maintenant?

Espérons que la complexité vient du fait d'avoir à comprendre les concepts sous-jacents de validité et de sécurité. Une fois que cela est fait, je ne pense pas qu'il y ait beaucoup de complexité mentale supplémentaire. Je pense que les concepts sous-jacents sont importants à transmettre.

Pourriez-vous donner un exemple de code si quelque chose dans Vec utilise correctement ceci pour refléter quand les invariants de Vec sont violés? Je pense que ce serait extrêmement verbeux et totalement obscur ce qui se passe réellement.

Je pense que l'ajout d'un type comme celui-ci est la mauvaise façon de transmettre le concept sous-jacent.

Je pense que des erreurs sont inévitables lorsque les gens pensent que .assume_init () signifie que "OK; j'ai vérifié l'invariant de validité et maintenant j'ai un bon T".

Je trouve très improbable que quelqu'un se dise "J'ai initialisé ce Vec<i32> en l'écrivant plein de 0xFF , maintenant il est initialisé, cela signifie que je peux le pousser". J'aimerais voir au moins une indication, de meilleures données solides, que c'est en fait une erreur que les gens font.
D'après mon expérience, les gens ont une intuition assez solide que lorsqu'ils transmettent des données à un code inconnu ou appellent des opérations de bibliothèque sur certaines données, les invariants de bibliothèque doivent être respectés.

Les choses se sont un peu calmées ici. Alors qu'en est-il du plan suivant:

  • Je prépare un PR pour déprécier MaybeUninit::uninitialized et le renommer en MaybeUninit::uninit .
  • Une fois que cela a atterri (nécessite la mise à jour de stdsimd, donc il reste du temps ici si les gens pensent que ce n'est pas la voie à suivre), je prépare un PR pour stabiliser MaybeUninit::{new, uninit, zeroed, as_ptr, as_mut_ptr} .

Cela laisse ouverte la question autour de set / write , into_init[ialized] / assume_init[ialized] et read[_init[italized]] . Actuellement, je penche vers assume_init , write et read , mais j'ai changé d'avis à ce sujet avant. Malheureusement, je ne sais pas trop comment prendre une décision ici.

  • Une fois que cela a atterri

Cela signifie-t-il qu'il y aura une période où il n'y aura aucun moyen de créer une valeur non initialisée sans (a) un avertissement d'obsolescence ou (b) en utilisant des fonctionnalités instables? Ce n'est pas une pratique durable.

Lors de la désapprobation de quelque chose que nous ne prévoyons pas de supprimer efficacement, un remplacement stable doit être disponible chaque fois que l'avertissement d'obsolescence est ajouté. Sinon, les gens ajouteront simplement une annotation pour ignorer l'avertissement et continuer leur vie.

Cela signifie-t-il qu'il y aura une période où il n'y aura aucun moyen de créer une valeur non initialisée sans (a) un avertissement d'obsolescence ou (b) en utilisant des fonctionnalités instables?

Je suis confus. Je propose de désapprouver une méthode instable et d'introduire une autre méthode instable à la place.

Remarquez que je parlais de MaybeUninit::uninitialized , pas de mem::uninitialized .

Malheureusement, je ne sais pas trop comment prendre une décision ici.

@RalfJung Faites -le (et r? Moi si vous le souhaitez) comme vous l'avez fait avant avec les autres PR renommés et si quelqu'un s'y oppose, nous pouvons gérer cela dans FCP. :)

Faites-le (et r? Moi si vous le souhaitez) comme vous l'avez fait avant avec les autres PR renommés et si quelqu'un s'y oppose, nous pouvons gérer cela dans FCP. :)

Eh bien, je vais attendre un peu parce que ceux-ci ne doivent pas faire partie de la stabilisation initiale.

déprécier une méthode instable et introduire une autre méthode instable à la place

Ah, gotcha. Continuez alors.

Très bien, faites les changements de noms dans https://github.com/rust-lang/rust/pull/59284 :

non initialisé -> uninit
into_initialized -> assume_init
read_initialized -> read
set -> écrire

J'aime les noms nouvellement proposés. Je suis un peu inquiet de l'utilisation abusive de read , mais cela semble beaucoup moins probable que into_initialized abusive de ptr::read . Dans l'ensemble, je pense que la nouvelle dénomination est tout à fait acceptable pour la stabilisation.

Je prépare un PR pour stabiliser MaybeUninit :: {new, uninit, zeroed, as_ptr, as_mut_ptr}.

Y a-t-il une chance que cela devienne la version 1.35-beta (prévue dans ~ 2 semaines)?

Je suis un peu en désaccord sur l'idée de pousser cela étant donné à quel point https://github.com/rust-lang/rfcs/pull/2582 est toujours en l'air. : / Sans ce RFC, l'initialisation progressive d'une structure n'est toujours pas possible, mais les gens le feront quand même.
OTOH, MaybeUninit a attendu assez longtemps. Et ce n'est pas comme si le code d'initialisation progressive que les gens écrivent actuellement est meilleur que ce qu'ils écriraient avec MaybeUninit .

Cela dit, https://github.com/rust-lang/rust/pull/59284 n'a même pas encore atterri, nous devrons donc nous précipiter pour obtenir cela en 1.35. TBH Je préfère attendre un cycle de plus pour que les gens aient au moins un peu de temps pour jouer avec les nouveaux noms de méthodes et voir ce qu'ils ressentent.

Y a-t-il une chance que les fonctions de construction sur MaybeInit soient const ?

init et new sont const . zeroed n'est pas, nous avons besoin d'extensions de ce que les fonctions const peuvent faire avant de pouvoir être const .

Je voulais fournir quelques commentaires sur MaybeUninit , les changements de code réels peuvent être vus ici https://github.com/Thomasdezeeuw/mio-st/pull/71. Dans l'ensemble, mon expérience (limitée) avec l'API a été positive.

Le seul petit problème que j'ai rencontré était que le retour de &mut T dans MaybeUninit::set conduit à devoir utiliser let _ = ... (https://github.com/Thomasdezeeuw/mio-st/pull/ 71 / files # diff-1b9651542d08c6eca04e6025b1c6fd53R116), ce qui est un peu gênant mais pas un gros problème.

Je dois également ajouter des API que j'aimerais lorsque je travaille avec des tableaux unitialisés, souvent en combinaison avec C.

  1. Une méthode pour passer de &mut [MaybeUninit<T>] à &mut [T] serait bien, l'utilisateur doit s'assurer que toutes les valeurs de la tranche sont correctement initialisées
  2. Une fonction ou une macro d'initialisation de tableau public, comme uninitialized_array , serait également un très bon ajout.

Je voulais faire part de vos commentaires sur MaybeUninit

Merci beaucoup!

renvoyer & mut T dans MaybeUninit :: set conduit à devoir utiliser let _ = ...

Pourquoi ça? Vous pouvez simplement "jeter" les valeurs de retour, en fait les exemples dans la documentation ne font pas let _ = ... . ( write / set n'a pas encore d'exemple ... mais en réalité, c'est à peu près la même chose que read , peut-être que cela devrait simplement être lié.)

foo.write(bar); fonctionne très bien sans let .

travailler avec des baies unitisées

Oui, c'est certainement un domaine d'intérêt futur.

@RalfJung

renvoyer & mut T dans MaybeUninit :: set conduit à devoir utiliser let _ = ...

Pourquoi ça? Vous pouvez simplement "jeter" les valeurs de retour, en fait les exemples dans la documentation ne font pas let _ = ... . ( write / set n'a pas encore d'exemple ... mais en réalité, c'est à peu près la même chose que read , peut-être que cela devrait simplement être lié.)

J'ai activé l'avertissement pour unused_results , donc sans let _ = ... cela produirait un avertissement. J'ai oublié que ce n'est pas la valeur par défaut.

Ah, je ne connaissais pas cet avertissement. Intéressant.

Cela pourrait être un argument pour que write ne renvoie pas de référence, et fournisse une méthode distincte pour cela s'il y a plus de demande.

Une fonction ou une macro d'initialisation de tableau public, comme uninitialized_array , serait également un très bon ajout.

Ce serait juste [MaybeUninit::uninit(); EVENTS_CAP] . Voir https://github.com/rust-lang/rust/issues/49147.

J'ai oublié que ce n'est pas la valeur par défaut.

Cela pourrait être un argument pour que write ne renvoie pas de référence, et fournisse une méthode distincte pour cela s'il y a plus de demande.

Semble niche? S'il y a plus de demande à l'avenir, nous pouvons ajouter une méthode qui ne renvoie

Semble niche?

Ouais, il y a des tonnes de méthodes qui définissent une valeur et retournent ensuite une référence mutable.

@Centril Heh, je ne pense pas avoir vu votre commentaire ici quand j'ai écrit ceci ailleurs: https://github.com/rust-lang/rust/issues/54542#issuecomment -478261027

Suppression des anciennes fonctions renommées obsolètes dans https://github.com/rust-lang/rust/pull/59912.

Après cela, je suppose que la prochaine chose à faire est de proposer une stabilisation ...: tada:

Je suis un peu en désaccord sur l'idée de pousser cela étant donné à quel point rust-lang / rfcs # 2582 est toujours en l'air. : / Sans ce RFC, l'initialisation progressive d'une structure n'est toujours pas possible, mais les gens le feront quand même.
OTOH, MaybeUninit a attendu assez longtemps. Et ce n'est pas comme si le code d'initialisation progressive que les gens écrivent actuellement est meilleur que ce qu'ils écriraient avec MaybeUninit .

Après cela, je suppose que la prochaine chose à faire est de proposer une stabilisation ... 🎉

@RalfJung Comment est l'état de la documentation ici? Si nous pouvons atténuer "les gens le feront de toute façon" avec des documents clairs qui m'aideraient à mieux dormir ... :)

En lisant la documentation de MaybeUninit , en particulier celle de assume_init , il n'est pas clair dans la section "Sécurité" que si vous appelez mu.assume_init() puis renvoyez ce résultat dans un coffre-fort fn , vous devez également respecter les invariants de sécurité. Avant de stabiliser, il serait bon d'améliorer ces documents et de donner des extraits avec des invariants de sécurité fournis par la bibliothèque qui doivent également être respectés lors de l'utilisation de MaybeUninit .

Comment est l'état de la documentation ici? Si nous pouvons atténuer "les gens le feront de toute façon" avec des documents clairs qui m'aideraient à mieux dormir ... :)

J'ajouterai probablement une section sur l'initialisation progressive des structures, en disant que ce n'est actuellement pas pris en charge. Les gens qui liront ceci seront comme "WTF, vraiment?".

TBH Je trouve cela plutôt frustrant. :( Je pense qu'il était très possible pour nous de trouver des conseils pour cela maintenant et je suis triste que nous n'ayons pas pu le faire.

il n'est pas clair dans la section "Sécurité" que si vous appelez mu.assume_init () et que vous retournez ensuite ce résultat dans une fn sûre, alors vous devez également respecter les invariants de sécurité. Avant de stabiliser, il serait bon d'améliorer ces documents et de donner des extraits avec des invariants de sécurité fournis par la bibliothèque qui doivent également être respectés lors de l'utilisation de MaybeUninit.

Vous suggérez essentiellement de transformer cela en documents expliquant toute l'idée des invariants de type de données et comment cela se déroule dans Rust. Je pense que MaybeUninit n'est pas le bon endroit pour cela; cela donnerait l'impression que cette préoccupation est spécifique à MaybeUninit alors qu'en réalité ce n'est pas le cas. Les choses que vous demandez devraient être expliquées dans un endroit plus élevé comme le Nomicon. Je prévois de concentrer la documentation de MaybeUninit sur le problème central de ce type. N'hésitez pas à les développer si vous pensez que cela est utile. :)

Vous suggérez essentiellement de transformer cela en documents expliquant toute l'idée des invariants de type de données et comment cela se déroule dans Rust.

C'est un peu fort ... Je suggère seulement un "Oh, __ au fait__, rappelez - MaybeUninit<T> . Je ne suggère pas d'ajouter un roman. ;) Ce roman peut résider dans le Nomicon mais il est probable que la plupart des gens utilisant MaybeUninit<T> s'interfaceront principalement avec la documentation standard de la bibliothèque.

D'accord, j'ai essayé d'incorporer tout cela dans le PR de stabilisation: https://github.com/rust-lang/rust/pull/60445

Je viens de tomber sur une utilisation de mem::uninitialized dans la documentation de la bibliothèque de normes, je ne savais pas vraiment où noter que le dernier exemple de core::ptr::drop_in_place doit être mis à jour (aussi un peu ironique qu'il présente l'autre forme d'UB qui ne serait sanctionnée que par https://github.com/rust-lang/rfcs/pull/2582, donc personnellement je le supprimerais).

@HeroicKatora merci! J'ai incorporé le correctif pour cela dans https://github.com/rust-lang/rust/pull/60445.

Nous ne pouvons pas vraiment faire quoi que ce soit à propos du champ ref-to-unaligned-field actuellement, pas sûr que la suppression du document soit une bonne idée.

Peut-être ajouter le trait PartialUninit (ou PartialInit ) qui initialiserait les données partiellement en fonction des métadonnées.

Exemple: MODULEENTRY32W .
Le premier champ ( dwSize ) doit être initialisé par la taille de la structure ( size_of::<MODULEENTRY32W>() ).

pub trait PartialUninit: Sized {
    fn uninit() -> MaybeUninit<Self>;
}

impl<T> PartialUninit for T {
    default fn uninit() -> MaybeUninit<Self> {
        MaybeUninit::uninit()
    }
}

impl PartialUninit for MODULEENTRY32W {
    unsafe fn uninit() -> MaybeUninit<MODULEENTRY32W> {
        let uninit = MaybeUninit { uninit: () };
        uninit.get_mut().dwSize = size_of::<MODULEENTRY32W>();
        uninit
    }
}

Comment penses-tu?

@kgv J'ai bien peur de ne pas comprendre votre suggestion. Peut-être qu'un contexte supplémentaire expliquant le problème que vous essayez de résoudre pourrait vous aider? Et peut-être un exemple plus complet de votre solution suggérée?

@scottjmaddox corrigé . Est-ce plus clair?

@kgv quel est le problème que cela résout (par opposition à quelqu'un qui écrit simplement une fonction d'aide pour cela)? Je ne vois pas pourquoi libstd doit faire quoi que ce soit ici.

Notez que l'initialisation partielle basée sur l'affectation des structures ne fonctionne que pour les types qui n'ont pas besoin d'être supprimés. uninit.get_mut().foo = bar perdra sinon foo , qui est UB.

@RalfJung Le problème que j'essaie de résoudre - travail unifié avec des structures FFI, dont certains champs ne dépendent pas de self (seulement Self ou ne dépendent de rien (constante)), par exemple - l'un des champs est la taille de Self .

@kgv Je suis d'accord avec @RalfJung ici pour dire qu'un tel cas d'utilisation est mieux géré par un module d'aide ou une caisse.

Le PR de stabilisation a atterri, juste à temps pour la version bêta. :) Cela fait environ 8 mois que j'ai commencé à étudier la situation autour des syndicats et de la mémoire non initialisée, et enfin nous avons quelque chose qui sera (très probablement) expédié dans 6 semaines. Quel voyage! Merci beaucoup à tous ceux qui ont contribué à cela. :RÉ

Bien sûr, nous sommes loin d'avoir terminé. Il y a https://github.com/rust-lang/rfcs/pull/2582 à résoudre. libstd a encore un certain nombre d'utilisations de mem::uninitialized (principalement dans le code spécifique à la plate-forme) qui nécessitent un portage. L'API stable que nous avons maintenant est très minime: nous devons déterminer quoi faire avec read et write , et nous devrions proposer des API qui aident à travailler avec des tableaux et des boîtes de MaybeUninit . Et nous avons beaucoup d'explications à faire pour déplacer lentement tout l'écosystème loin de mem::uninitialized .

Mais nous y arriverons, et cette première étape était probablement la plus importante. :)

et nous devrions proposer des API qui aident à travailler avec des tableaux et des boîtes de MaybeUninit .

@RalfJung À cette fin; peut-être est-ce le moment de commencer à travailler sur https://github.com/rust-lang/rust/issues/49147? = P

En outre, nous devrions probablement diviser et fermer ce problème de suivi en faveur de plus petits pour les bits restants.

À cette fin; le moment est peut-être venu de commencer à travailler sur # 49147? = P

Vous venez de faire du bénévolat? ;) (J'ai peur de ne pas avoir le temps pour ça.)

nous devrions probablement diviser et fermer ce problème de suivi en faveur de plus petits pour les bits restants.

Je laisse cela aux experts en processus. Mais j'ai tendance à être d'accord.

Vous venez de faire du bénévolat? ;) (J'ai peur de ne pas avoir le temps pour ça.)

Qu'est-ce que j'ai fait ... = D - J'ai déjà un projet sur lequel je travaille donc cela prendra probablement du temps. Peut-être que quelqu'un d'autre est intéressé? (si c'est le cas, entrez dans le problème de suivi)

Je laisse cela aux experts en processus. Mais j'ai tendance à être d'accord.

Ce serait moi ...;) J'essaierai de le diviser et de le fermer bientôt.

@RalfJung à propos de votre déclaration selon laquelle let x: bool = mem::uninitialized(); est UB, la question est de savoir pourquoi les primitives invalides sont considérées comme telles? Si je comprends bien, vous devez lire une valeur pour observer qu'elle n'est pas valide pour déclencher UB. Mais si vous ne le lisez pas, alors quoi?

Je pense que même créer une valeur est une mauvaise chose, mais j'aimerais savoir pourquoi la rouille l'interdit de toute façon? Il semble qu'il n'y ait aucun mal si vous n'observez pas un état invalide. Est-ce juste pour des erreurs précoces ou peut-être autre chose?

Existe-t-il des cas réels dans le compilateur lorsqu'il s'appuie sur ces hypothèses?

Par exemple, nous annotons des fonctions comme foo(x: bool) indiquant à LLVM que x est un booléen valide. Cela fait que UB passe un bool qui n'est pas true ou false même si la fonction à l'origine ne regardait pas x . Ceci est utile car parfois le compilateur veut introduire des utilisations de variables précédemment inutilisées (en particulier, cela se produit lors du déplacement d'instructions hors de boucles sans prouver que la boucle est prise au moins une fois).

AFAIK nous définissons également (ou souhaitons définir) certaines de ces annotations dans une fonction, pas seulement aux limites des fonctions. Et nous pourrions trouver d'autres endroits dans le futur où de telles informations peuvent être utiles. Nous pourrions peut-être couvrir cela avec une définition intelligente de «utiliser une variable» (un terme que vous avez utilisé sans le définir, et ce n'est en effet pas facile à définir), mais je pense que quand il s'agit d'UB dans un code non sécurisé, c'est important d'avoir des règles simples là où nous le pouvons.

Donc, nous voulons nous assurer que même dans un code non sécurisé, les types dans le code ont une signification. Ce n'est possible qu'en traitant correctement la mémoire non initialisée avec un type dédié, au lieu de l'approche ad hoc "yolo" consistant à mentir au compilateur sur le contenu d'une variable ("Je prétends que c'est un bool , mais vraiment je ne l'initialiserai pas ").

Par exemple, nous annotons des fonctions comme foo (x: bool) indiquant à LLVM que x est un booléen valide. Cela fait que UB passe un booléen qui n'est pas vrai ou faux même si la fonction à l'origine n'a pas regardé x. Ceci est utile car parfois le compilateur veut introduire des utilisations de variables précédemment inutilisées (en particulier, cela se produit lors du déplacement d'instructions hors de boucles sans prouver que la boucle est prise au moins une fois).

Cela peut être considéré comme un usage. Je demande d'initier une valeur et de ne jamais la lire / la transmettre nulle part avant qu'elle ne soit remplacée par une valeur valide.
Je ne vois pas de cas d'utilisation utiles pour initier la valeur de manière aussi nuancée, mais je me demande juste.

En un mot, ma question est de savoir si ce code est UB (selon la documentation - il le), et si oui, qu'est-ce qui peut casser si j'écris ainsi?

let _: bool = unsafe { mem::unitialized };

Autre question sur le sujet lui-même: nous savons que nous avons la syntaxe box qui permet d'allouer de la mémoire directement sur le tas, et cela fonctionne toujours contrairement à Box::new() qui parfois stackalloc de la mémoire. Donc, si je fais box MaybeUninit::new() et que je le remplis ensuite, comment pourrais-je convertir Box<MaybeUninit<T>> en Box<T> ? Dois-je écrire des transmutes ou quoi? Peut-être ai-je simplement manqué ce point dans la documentation.

@Pzixel, nous avons déjà discuté des interactions entre Box et MaybeUninit déjà sur ce fil : smile:

@Centril ayant un sous-problème à discuter qui pourrait être bon lorsque vous le séparerez.

Oui, je me souviens de cette discussion, mais je ne me souviens d'aucune API spécifique.

En un mot, je veux avoir quelque chose comme

fn into_inner<A,T>(value: A<MaybeUninit<T>>) -> A<T> { unsafe { std::mem::transmute() } }

Mais je ne pense pas qu'il existe une telle API, et il semble qu'elle ne pourrait pas être implémentée sans le support du compilateur à ce stade de l'évolution du langage.


J'y ai réfléchi un peu plus et il semble que cela devrait fonctionner à n'importe quel niveau de nidification. Donc Vec<Result<Option<MaybeUninit<u8>>>> devrait avoir la méthode into_inner qui renvoie Vec<Result<Option<u8>>>

Je supposais que get_ref et get_mut allaient être stabilisés en même temps (toutes les fonctionnalités pointent vers ce problème). Y a-t-il une raison de ne pas le faire? Ils sont gentils et sont la seule indication que l'exécution de l'action qu'ils effectuent est autorisée (ce qui devrait évidemment être vrai).

Cela peut être considéré comme un usage.

Donc let x: bool = mem::uninitialized() n'utilise pas le bool (même s'il est assigné à x !), Mais

fn id(x: bool) -> bool { x }
let x: bool = id(mem::uninitialized());

l'utilise? Qu'en est-il de

fn uninit() -> bool { mem::uninitialized() }
let x: bool = uninit();

Le retour est-il ici une utilité?

Cela devient très vite très subtil. Donc, la réponse que je pense que nous devrions donner est que chaque affectation (vraiment chaque copie, comme dans, chaque affectation après l'abaissement à MIR) est une utilisation, et cela inclut l'affectation dans let x: bool = mem::uninitialized() .


Je supposais que get_ref et get_mut allaient être stabilisés en même temps (toutes les fonctionnalités pointent vers ce problème). Y a-t-il une raison de ne pas le faire? Ils sont gentils et sont la seule indication que l'exécution de l'action qu'ils effectuent est autorisée (ce qui devrait évidemment être vrai).

Ceci est bloqué lors de la résolution de https://github.com/rust-lang/unsafe-code-guidelines/issues/77 : est-il sûr d'avoir un &mut bool qui pointe vers une mémoire non initialisée? Je pense que la réponse devrait être «oui», mais les gens ne sont pas d'accord.

Ceci est bloqué lors de la résolution de rust-lang / unsafe-code-guidelines # 77

Je ne pense pas que le blocage soit nécessaire. Vous pouvez le stabiliser et dire "c'est UB d'utiliser ceci si la mémoire n'est pas initialisée" et ensuite adoucir l'exigence si nous déterminons que tout va bien. C'est une méthode intéressante pour la post-initialisation.

puis adoucir plus tard l'exigence

Ce qui signifie que si je code par rapport à la documentation de la future version mais que quelqu'un compile mon code en utilisant l'ancienne version (compatible API!) Du compilateur, il y a maintenant UB?

@Gankro

Je ne pense pas que le blocage soit nécessaire. Vous pouvez le stabiliser et dire "c'est UB d'utiliser ceci si la mémoire n'est pas initialisée" et ensuite adoucir l'exigence si nous déterminons que tout va bien. C'est une méthode intéressante pour la post-initialisation.

Cela me semble très insensé. Pourquoi ne pas simplement écrire &mut *foo.as_mut_ptr() ? Une fois que tout est initialisé, pourquoi cela ne fonctionnerait-il pas? IOW, je me demande maintenant si vous dites

la seule indication que l'exécution de l'action qu'ils effectuent est autorisée

car pourquoi ne le serait-il pas ? Si nous listons de manière exhaustive tout ce que vous pouvez faire une fois que vous avez initialisé la valeur, ce sera une longue liste. ^^

@hepmaster

Ce qui signifie que si je code par rapport à la documentation de la future version mais que quelqu'un compile mon code en utilisant l'ancienne version (compatible API!) Du compilateur, il y a maintenant UB?

C'est vrai aujourd'hui si les gens font &mut *foo.as_mut_ptr() . Je ne vois aucun moyen de l'éviter.

De plus, il n'y a UB que si nous devons réellement changer quoi que ce soit en faisant cette documentation. Sinon, nous sommes dans une situation étrange où il y aurait eu UB si ce même code avait été exécuté avec le même compilateur avant de faire une garantie, mais maintenant que nous garantissons qu'il n'y a plus d'UB. UB est une propriété non seulement du compilateur mais aussi de la spécification, et la spécification peut changer rétroactivement. ;)

Bien, je supposais que le processus était

  • le stabiliser avec une exigence stricte mais sans signification pour la mise en œuvre maintenant
  • commencer à travailler sur le modèle de mémoire et qu'as-tu
  • une fois le modèle terminé

    • si ça doit être UB, cool, laissez les docs inchangés, ajoutez des optimisations si c'est utile

    • s'il n'a pas besoin d'être UB, cool, supprimez-le de la documentation et appelez-le un jour

@RalfJung

Le retour est-il ici une utilité?

Oui, renvoyer une valeur ou la transmettre n'importe où est une utilisation.

Cela devient très vite très subtil. Donc, la réponse que je pense que nous devrions donner est que chaque affectation (vraiment chaque copie, comme dans, chaque affectation après l'abaissement à MIR) est une utilisation, et cela inclut l'affectation dans let x: bool = mem :: uninitialized ().

Ça a l'air valide.

Quoi qu'il en soit, c'est à propos de l'imbrication peut-être arbitraire? Pourrait-il être transmuté en toute sécurité sans que l'utilisateur n'ait besoin d'écrire le transmut pour chaque type d'enveloppe?

@Pzixel Je ne sais pas si je comprends votre question, mais je pense qu'elle est en cours de discussion sur https://github.com/rust-lang/rust/issues/61011

J'ai vu que la méthode MaybeUninit::write() encore non stabilisée n'est pas unsafe bien qu'elle puisse ignorer l'appel de drop sur un T déjà présent, que j'aurais supposé être dangereux. Y a-t-il un précédent pour que cela soit considéré comme sûr?

https://doc.rust-lang.org/nomicon/leaking.html#leaking
https://doc.rust-lang.org/nightly/std/mem/fn.forget.html

forget n'est pas marqué comme unsafe , car les garanties de sécurité de Rust n'incluent pas une garantie que les destructeurs seront toujours exécutés.

Pouvons-nous ajouter une méthode MaybeUninit<T> -> NonNull<T> à MaybeUninit ? AFAICT le pointeur retourné par MaybeUninit::as_mut_ptr() -> *mut T n'est jamais nul. Cela réduirait le taux de désabonnement d'avoir à s'interfacer avec des API qui utilisent NonNull<T> , à partir de:

let mut x = MaybeUninit<T>::uninit();
foo(unsafe { NonNull::new_unchecked(x.as_mut_ptr() });

à:

let mut x = MaybeUninit<T>::uninit();
foo(x.ptr());

le pointeur renvoyé par MaybeUninit :: as_mut_ptr () -> * mut T n'est jamais nul.

C'est correct.

Généralement (et je pense avoir vu @Gankro dire ceci), NonNull fonctionne assez bien "au repos" mais quand on utilise réellement des pointeurs, on veut arriver à un pointeur brut dès que possible. C'est juste beaucoup plus lisible.

Cependant, ajouter une méthode qui retourne NonNull semble correct. Comment devrait-il être appelé? Y a-t-il une préséance?

Il y a un précédent avec https://github.com/rust-lang/rust/issues/47336 mais le nom n'est pas bon et je ne suis pas sûr que nous allons stabiliser cette méthode.

La course de cratère mentionnée dans https://github.com/rust-lang/rust/pull/60445#issuecomment -488818677 s'est-elle produite?

L'idée de 3 mois de temps disponible que @centril mentionne ne se concrétise pas pour les personnes qui veulent être sans avertissement sur l'ensemble de la version bêta, stable et nocturne. 1.36.0 est sorti il ​​y a moins d'une semaine et émet déjà des avertissements tous les soirs.

La dépréciation pourrait-elle être reportée à 1.40.0?

Les avertissements de dépréciation ne sont pas toujours isolés de la caisse qui en est responsable. Par exemple, lorsqu'une caisse expose une macro qui utilise std::mem::uninitialized interne, les utilisations par des caisses tierces invoquent toujours l'avertissement d'obsolescence. J'ai remarqué cela aujourd'hui lorsque j'ai compilé un de mes projets avec le compilateur nocturne. Même si le code ne contient pas une seule mention de uninitialized , j'ai reçu l'avertissement d'obsolescence car il a invoqué la macro implement_vertex glium.

L'exécution de cargo +nightly test sur glium master me donne plus de 1400 lignes de sortie, principalement composées d'avertissements d'obsolescence de la fonction uninitialized (je compte l'avertissement 200 fois, mais il est probablement plafonné au nombre que rg "uninitialized" | wc -l sorties est 561).

Quelles sont les dernières inquiétudes qui bloquent la stabilisation du reste des méthodes? Tout faire avec *foo.as_mut_ptr() devient très fastidieux, et parfois (pour write ) implique plus de unsafe blocs que nécessaire.

@SimonSapin Pour émuler write , vous pouvez remplacer le tout MaybeUninit par no unsafe en utilisant *val = MaybeUninit::new(new_val)val: &mut MaybeUninit<T> et new_val: T ou vous pouvez utiliser std::mem::replace si vous voulez l'ancienne valeur.

@ est31 ce sont de bons points. Je ferais bien de repousser la dépréciation par une version ou deux.

Des objections?

Nous l'avons déjà dit dans le billet de blog de la version 1.36.0:

Comme MaybeUninitest l'alternative la plus sûre, à partir de Rust 1.38, la fonction mem :: uninitialized sera obsolète.

En tant que tel, je pense que nous devrions éviter la flip-floppery sur celui-ci car cela n'envoie pas un bon message et c'est déroutant. De plus, la date de dépréciation doit également être largement connue étant donné qu'elle a été mentionnée dans le billet de blog.

Il sera peut-être tard pour revenir sur la dépréciation de uninitialized . Mais peut-être pourrions-nous décider d'une politique pour émettre uniquement des avertissements d'obsolescence dans Nightly après que le remplaçant soit sur le canal Stable depuis un certain temps?

Par exemple, Firefox a compromis la nécessité d'une nouvelle version de Rust deux semaines après sa sortie .

Nous l'avons déjà dit dans le billet de blog de la version 1.36.0:

Je ne suis pas d'accord pour dire que la mention d'une date dans un article de blog est un tel degré de fer. C'est dans un dépôt et nous pouvons soumettre une modification.

En tant que tel, je pense que nous devrions éviter la flip-floppery sur celui-ci car cela n'envoie pas un bon message et c'est déroutant.

"flip-floppery" est une mauvaise chose, mais changer d'avis en fonction des données et des retours n'est pas cela.

Je ne me soucie pas beaucoup d'une manière ou d'une autre de la décision réelle, mais je ne pense pas que les gens seront déroutés par la proposition. Ceux qui ont vu l'article de blog ou l'avertissement d'obsolescence peuvent passer à la nouvelle chose. Les gens qui ne se soucient pas simplement de quelques autres versions.

"flip-floppery" est une mauvaise chose, mais changer d'avis en fonction des données et des retours n'est pas cela.

Entièrement d'accord. Je ne vois pas un mauvais message envoyé en disant "hé, notre calendrier de désapprobation était un peu trop agressif, nous avons reculé les choses par une version". Bien au contraire, en fait.
En fait, l'IIRC, j'ai mentionné lors de l'atterrissage du PR de stabilisation que le précédent est de déprécier 3 versions à l'avenir et non 2, mais pour une raison quelconque, nous sommes allés avec 2. Trois versions signifie 1 version complète entre stable-gets-release-with-the -deprecation-annonce et obsolète-le-soir, cela semble être le bon moment pour les personnes qui effectuent un suivi nocturne. 6 semaines, c'est un eon, non? ;)

Je prévois donc de soumettre demain un PR qui change la version obsolète de la version 1.39.0. Je peux également soumettre un PR pour mettre à jour cet article de blog si les gens pensent que c'est important.

Je prévois donc de soumettre demain un PR qui change la version obsolète de la version 1.39.0. Je peux également soumettre un PR pour mettre à jour cet article de blog si les gens pensent que c'est important.

J'accepterai 1.39 mais au plus tard. Vous devrez également mettre à jour les notes de publication en plus du billet de blog.

PR soumis pour le calendrier de dépréciation modifié: https://github.com/rust-lang/rust/pull/62599.

@SimonSapin

Quelles sont les dernières inquiétudes qui bloquent la stabilisation du reste des méthodes? Tout faire via * foo.as_mut_ptr () devient très fastidieux, et parfois (pour l'écriture) implique plus de blocs dangereux que nécessaire.

Pour as_ref / as_mut , je voulais honnêtement attendre de savoir si les références doivent pointer vers des données initialisées. Sinon, la documentation de ces méthodes est tellement préliminaire.

Pour read / write , je les stabilise bien si tout le monde convient que les noms et les signatures ont un sens. Je pense que cela devrait être coordonné avec ManuallyDrop::take/read , et peut-être qu'il devrait également y avoir ManuallyDrop::write ?

Je voulais honnêtement attendre de savoir si les références doivent pointer vers des données initialisées.

Que faut-il pour que le groupe de travail sur les directives sur les codes non sécurisés et l'équipe linguistique prennent une décision à ce sujet? Pensez-vous que cela se produira plus probablement dans quelques semaines, quelques mois ou quelques années?

En attendant, as_mut étant instable n'empêche pas les utilisateurs d'écrire &mut *manually_drop.as_mut_ptr() ce qu'ils doivent faire pour faire quelque chose.

Que faut-il pour que le groupe de travail sur les directives sur les codes non sécurisés et l'équipe linguistique prennent une décision à ce sujet? Pensez-vous que cela se produira plus probablement dans quelques semaines, quelques mois ou quelques années?

Des mois, peut-être des années.

En attendant, as_mut étant instable n'empêche pas les utilisateurs d'écrire & mut * manuellement_drop.as_mut_ptr () ce qu'ils doivent faire.

Oui je sais. L'espoir est d'inciter les gens à retarder le plus possible la partie &mut et à travailler avec des pointeurs bruts. Bien sûr sans https://github.com/rust-lang/rfcs/pull/2582 qui est souvent difficile.

La documentation sur MaybeUninit semble être un endroit privilégié pour au moins discuter du fait qu'il s'agit d'une ambiguïté dans la sémantique du langage et que les utilisateurs devraient raisonnablement supposer que ce n'est pas OK.

Certes, ce serait l'autre option.

Même avec une hypothèse prudente, as_mut est valide une fois que la valeur est complètement initialisée.

Une façon d'être prudent avec les tableaux consiste à utiliser MaybeUninit<[MaybeUninit<Foo>; N]> . Les wrappers externes permettent de créer le tableau avec un seul appel uninit() . (Je pense que le littéral [expr; N] nécessite Copy ?) Les wrappers internes le rendent sûr même dans l'hypothèse conservatrice d'utiliser la commodité de slice::IterMut afin de traverser le tableau, et puis initialisez les valeurs Foo une par une.

@SimonSapin voit la uninitialized_array! dans libcore .

@RalfJung peut-être que uninit_array! serait un meilleur nom.

@Stargateur Absolument, cela ne va certainement pas être stabilisé avec son nom actuel. Espérons que cela ne se stabilisera jamais si https://github.com/rust-lang/rust/issues/49147 arrive bientôt (TM).

@RalfJung Ugh, c'est ma faute, je bloquais le PR sans raison: https://github.com/rust-lang/rust/pull/61749#issuecomment -512867703

@eddyb cela fonctionne pour libcore, yay! Mais d'une manière ou d'une autre, lorsque j'essaie d'utiliser la fonctionnalité dans liballoc, elle ne se compile pas même si j'ai défini l'indicateur. Voir https://github.com/rust-lang/rust/commit/4c2c7e0cc9b2b589fe2bab44173acc2170b20c09.

Building stage1 std artifacts (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu)
   Compiling alloc v0.0.0 (/home/r/src/rust/rustc.2/src/liballoc)
error[E0277]: the trait bound `core::mem::MaybeUninit<K>: core::marker::Copy` is not satisfied
   --> <::core::macros::uninit_array macros>:1:32
    |
1   |   ($ t : ty ; $ size : expr) => ([MaybeUninit :: < $ t > :: uninit () ; $ size])
    |   -                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `core::marker::Copy` is not implemented for `core::mem::MaybeUninit<K>`
    |  _|
    | |
2   | | ;
    | |_- in this expansion of `uninit_array!`
    | 
   ::: src/liballoc/collections/btree/node.rs:109:19
    |
109 |               keys: uninit_array![_; CAPACITY],
    |                     -------------------------- in this macro invocation
    |
    = help: consider adding a `where core::mem::MaybeUninit<K>: core::marker::Copy` bound
    = note: the `Copy` trait is required because the repeated element will be copied

error[E0277]: the trait bound `core::mem::MaybeUninit<V>: core::marker::Copy` is not satisfied
   --> <::core::macros::uninit_array macros>:1:32
    |
1   |   ($ t : ty ; $ size : expr) => ([MaybeUninit :: < $ t > :: uninit () ; $ size])
    |   -                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `core::marker::Copy` is not implemented for `core::mem::MaybeUninit<V>`
    |  _|
    | |
2   | | ;
    | |_- in this expansion of `uninit_array!`
    | 
   ::: src/liballoc/collections/btree/node.rs:110:19
    |
110 |               vals: uninit_array![_; CAPACITY],
    |                     -------------------------- in this macro invocation
    |
    = help: consider adding a `where core::mem::MaybeUninit<V>: core::marker::Copy` bound
    = note: the `Copy` trait is required because the repeated element will be copied

error[E0277]: the trait bound `core::mem::MaybeUninit<collections::btree::node::BoxedNode<K, V>>: core::marker::Copy` is not satisfied
   --> <::core::macros::uninit_array macros>:1:32
    |
1   |   ($ t : ty ; $ size : expr) => ([MaybeUninit :: < $ t > :: uninit () ; $ size])
    |   -                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `core::marker::Copy` is not implemented for `core::mem::MaybeUninit<collections::btree::node::BoxedNode<K, V>>`
    |  _|
    | |
2   | | ;
    | |_- in this expansion of `uninit_array!`
    | 
   ::: src/liballoc/collections/btree/node.rs:162:20
    |
162 |               edges: uninit_array![_; 2*B],
    |                      --------------------- in this macro invocation
    |
    = help: the following implementations were found:
              <core::mem::MaybeUninit<T> as core::marker::Copy>
    = note: the `Copy` trait is required because the repeated element will be copied

error: aborting due to 3 previous errors

Mystère résolu: les utilisations de l'expression de répétition dans libcore étaient en fait pour les types qui sont des copies.

Et la raison pour laquelle cela ne fonctionne pas dans liballoc est que MaybeUninit::uninit n'est pas promotable.

@RalfJung Peut-être ouvrir un PR supprimant les utilisations de la macro là où c'est complètement inutile?

@eddyb J'ai créé cette partie de https://github.com/rust-lang/rust/pull/62799.

Concernant maybe_uninit_ref

Pour as_ref / as_mut, je voulais honnêtement attendre de savoir si les références doivent pointer vers des données initialisées. Sinon, la documentation de ces méthodes est tellement préliminaire.

Unstable get_ref / get_mut sont certainement recommandés à cause de cela; cependant, il y a des cas où get_ref / get_mut peut être utilisé lorsque MaybeUninit a été initialisé: pour obtenir un handle sûr des données (maintenant connues initialisées) tout en évitant memcpy ( au lieu de assume_init , ce qui peut déclencher un memcpy ).

  • cela peut sembler une situation particulièrement spécifique, mais la principale raison pour laquelle les gens (veulent) utiliser des données non initialisées est précisément pour ce type d'épargne bon marché.

Pour cette raison, j'imagine que assume_init_by_ref / assume_init_by_mut pourrait être agréable à avoir (puisque into_inner a été appelé assume_init , il semble plausible que le ref getters ref mut obtiennent également un nom spécial pour refléter cela).

Il existe deux / trois options pour cela, liées à l'interaction Drop :

  1. Exactement la même API que get_ref et get_mut , ce qui peut entraîner des fuites de mémoire en cas de goutte de colle;

    • (Variante): même API que get_ref / get_mut , mais avec un Copy lié;
  2. API de style de fermeture, pour garantir la chute:

impl<T> MaybeUninit<T> {
    /// # Safety
    ///
    ///   - the contents must have been initialised
    unsafe
    fn assume_init_with_mut<R, F> (mut self: MaybeUninit<T>, f: F) -> R
    where
        F : FnOnce(&mut T) -> R,
    {
        if mem::needs_drop::<T>().not() {
            return f(unsafe { self.get_mut() });
        }
        let mut this = ::scopeguard::guard(self, |mut this| {
            ptr::drop_in_place(this.as_mut_ptr());
        });
        f(unsafe { MaybeUninit::<T>::get_mut(&mut *this) })
    }
}

(Où la logique de scopeguard peut facilement être réimplémentée, il n'est donc pas nécessaire d'en dépendre)


Celles-ci pourraient être stabilisées plus rapidement que get_ref / get_mut , étant donné l'exigence explicite de assume_init .

Désavantages

Si une variante de l'option .1 était choisie et que get_ref / get_mut devenait utilisable sans la situation assume_init , alors cette API deviendrait presque strictement inférieure (Je dis presque parce qu'avec l'API proposée, la lecture de la référence serait acceptable, ce qui ne peut jamais être dans le cas de get_ref et get_mut )

Semblable à ce que @danielhenrymantilla a écrit à propos de get_{ref,mut} , je commence à penser que read devrait probablement être renommé en read_init ou read_assume_init ou plus, quelque chose qui indique que cela ne peut être fait qu'une fois l'initialisation terminée.

@RalfJung J'ai une question à ce sujet:

fn foo<T>() -> T {
    let newt = unsafe { MaybeUninit::<T>::zeroed().assume_init() };
    newt
}

Par exemple, nous appelons foo<NonZeroU32> . Cela déclenche-t-il UB lorsque nous déclarons une fonction foo (car elle doit être valide pour tous les T s ou lorsque nous l'instancions avec un type qui déclenche UB? Désolé si c'est un mauvais endroit pour poser une question.

Le code

Donc, foo::<i32>() est très bien. Mais foo::<NonZeroU32>() est UB.

La propriété d'être valide pour toutes les manières possibles d'appeler est appelée «solidité», voir aussi la référence . Le contrat général de Rust est que la surface API sûre d'une bibliothèque doit être solide. Ceci afin que les utilisateurs d'une bibliothèque n'aient pas à se soucier de l'UB. Toute l'histoire de la sécurité de Rust repose sur des bibliothèques avec des API sonores.

@RalfJung merci.

Donc, si je vous comprends correctement, cette fonction est défectueuse (et donc invalide), mais si nous la marquons comme unsafe alors ce corps devient valide et son

@Pzixel si vous le marquez comme dangereux, la solidité n'est tout simplement pas un concept qui s'applique même plus. «Est-ce que ce son» n'a de sens que comme question pour un code sûr.

Oui, vous devez marquer la fonction unsafe car certaines entrées peuvent déclencher UB. Mais même si vous le faites, ces entrées déclenchent toujours UB, donc la fonction ne doit toujours pas être appelée de cette façon. Il n'est jamais acceptable de déclencher UB, même pas dans un code non sécurisé.

Oui, bien sûr, je comprends cela. Je voulais seulement conclure que le fonctuon partiel doit être marqué comme unsafe . Cela a du sens pour moi, mais je n'y ai pas pensé avant que vous ne répondiez.

Étant donné que la discussion sur ce problème de suivi est si longue maintenant, pouvons-nous la décomposer en quelques autres problèmes de suivi pour chaque fonctionnalité de MaybeUninit qui est encore instable?

  • maybe_uninit_extra
  • maybe_uninit_ref
  • maybe_uninit_slice

Cela semble raisonnable. Il existe également https://github.com/rust-lang/rust/issues/63291.

Clôture en faveur d'un méta-problème qui suit MaybeUninit<T> plus généralement: # 63566

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