1 points par GN⁺ 2024-07-22 | 1 commentaires | Partager sur WhatsApp
  • En Rust, Pin est un élément fondamental introduit pour manipuler en toute sécurité l’état auto-référentiel à l’intérieur des Future produites par async/await
  • Une Future async enregistre son état à chaque point d’await, si bien que des champs d’un même objet peuvent référencer d’autres champs du même objet, formant un type auto-référentiel
  • Les conceptions reposant sur un move constructor, des offset pointers ou ?Move n’ont pas été retenues en raison de problèmes de coût de suivi à l’exécution, de faisabilité à la compilation et de rétrocompatibilité avec les API existantes
  • La conception finale est Pin, qui encapsule un pointeur pour placer sa cible dans un pinned typestate ; grâce à l’auto trait Unpin, la plupart des types peuvent toujours être déplacés comme avant
  • La difficulté de Pin vient moins de la notion d’immuabilité elle-même que des limites d’un type de bibliothèque ; le reborrowing, Pin::set, la pinned projection et les interactions avec Drop nuisent fortement à son ergonomie

Le problème auquel Pin répond

  • Dans l’écosystème async de Rust, Pin et le pinning constituent une base essentielle, mais restent un sujet difficile et souvent mal compris pour celles et ceux qui apprennent l’async en Rust
  • L’objectif de Pin n’est pas de permettre aux utilisateurs de créer eux-mêmes des types auto-référentiels uniquement en Rust sûr
    • Il sert à manipuler en toute sécurité les Future auto-référentielles générées par le compilateur pour les fonctions async, ou les types auto-référentiels construits en code unsafe par des runtimes comme tokio
  • Dans l’exemple async fn bar, au point foo(&mut z).await, il faut stocker ensemble dans le même état de Future z et la Future Foo qui référence z
    • Un champ de l’objet Future référence alors un autre champ du même objet
    • Ce type de Future devient donc un type auto-référentiel
  • Si l’objet est déplacé après être entré dans cet état, ses références internes pointent toujours vers l’ancien emplacement mémoire, qui peut correspondre à de la mémoire morte ou avoir été réutilisé pour une autre valeur
  • Avant Pin, Rust permettait de déplacer un objet dès lors qu’on en avait la propriété ou une référence mutable ; il fallait donc un moyen d’exprimer qu’un objet ne devait plus être déplacé à partir d’un certain moment

Les approches qui n’ont pas abouti

  • move constructor

    • Un move constructor exécute du code lors du déplacement d’une valeur, à la manière d’un destructeur, pour corriger les pointeurs auto-référentiels vers le nouvel emplacement
    • En Rust, ces pointeurs ne se trouvent pas forcément seulement « dans » la valeur déplacée ; ils peuvent aussi être, par exemple, dans un vecteur de pointeurs visant son propre état
    • Pour tous les suivre, il faudrait au final une gestion mémoire à l’exécution proche d’un garbage collector
    • Rust a choisi très tôt de ne pas avoir de move constructor, et beaucoup de code unsafe repose sur l’hypothèse qu’un déplacement de valeur peut se faire par simple copie mémoire
    • Ajouter un move constructor plus tard constituerait un breaking change
  • offset pointer

    • L’offset pointer consiste à compiler une auto-référence non pas en référence ordinaire, mais en décalage relatif à l’adresse de l’objet auto-référentiel
    • Il n’est pas toujours possible de déterminer à la compilation quelle référence est auto-référentielle
    • Selon les branches, une même valeur peut pointer tantôt à l’intérieur de son propre objet, tantôt à l’extérieur
    • Pour gérer cela, il faudrait compiler les références sous une forme proche d’un enum offset/reference, ce qui a été jugé irréaliste au moment de la conception d’async/await

Les exigences du pinned typestate

  • Une Future auto-référentielle n’a pas besoin d’être immuable au déplacement dès sa création ; elle doit pouvoir être déplacée librement jusqu’à un certain point de son cycle de vie, puis ne plus bouger ensuite
    • Elle doit pouvoir être déplacée lorsqu’elle est combinée avec d’autres Future
    • Une fois placée à son emplacement de poll, elle ne doit plus être déplacée
  • Le modèle de Ralf Jung ajoute, aux typestates existants « owned » et « shared », un troisième état : le pinned typestate pour les Future auto-référentielles
  • Une fois qu’un objet entre dans le pinned typestate, il ne doit plus jamais être déplacé
    • Plus précisément, il ne faut pas invalider la mémoire de cet objet sans exécuter d’abord son destructeur
    • En pratique, cela revient à exiger que l’objet ne soit pas déplacé vers un nouvel emplacement
  • La plupart des types ne peuvent pas contenir d’auto-référence, si bien que le pinned typestate n’a pas de signification particulière pour eux
    • Il est alors souhaitable que ces types échappent aux contraintes du pinning et puissent à nouveau être déplacés
  • Le modèle formel détaillé du pinned typestate est présenté par Ralf Jung dans A Formal Look at Pinning

Pourquoi la conception ?Move a échoué

  • Avant Pin, une conception fondée sur un nouveau trait Move a été tentée
    • La plupart des types implémentent Move
    • Les types susceptibles de contenir des auto-références n’implémentent pas Move
    • Créer une référence vers une valeur d’un type qui n’implémente pas Move fait entrer cette valeur dans le pinned typestate, empêchant tout déplacement ultérieur
  • Cette approche paraissait intuitive, car elle liait la création de référence et la transition vers le pinning pour garantir la sûreté
    • Elle a d’ailleurs été implémentée dans une branche du compilateur
  • Sa limite fondamentale est qu’on peut vouloir référencer brièvement une valeur destinée à devenir auto-référentielle, sans encore vouloir la pinner
    • Par exemple, on peut vouloir la stocker temporairement dans Option, puis la récupérer avec Option::take
  • Le problème le plus important était toutefois la rétrocompatibilité
    • Move ne pouvait pas être un auto trait
    • Des API stables existent déjà, comme mem::swap, qui supposent qu’on peut toujours déplacer une valeur à partir d’une référence mutable
  • L’ajout sous la forme ?Move n’était pas non plus rétrocompatible à cause des associated type
    • L’endroit où l’on ajoute une borne ?Trait à un associated type est la définition du trait
    • Assouplir la borne d’un associated type existant peut casser du code qui dépendait de cette borne
    • Le type future associé de IntoFuture, Target de DerefMut, les types de retour de fonction, les éléments d’itérateur, les valeurs renvoyées par l’opérateur d’indexation ou les opérateurs arithmétiques, entre autres, sont fortement liés aux associated type
  • Une edition n’aurait pas non plus facilement résolu le problème
    • Pour que des crates de différentes editions puissent être combinées, les interfaces de trait doivent rester identiques

La conception de Pin

  • La conception finale représente le pinned typestate non comme une propriété du type de l’objet, mais comme un état créé par un pointeur spécial
  • Pin est un wrapper autour d’un pointeur
    • Il peut encapsuler les types de référence built-in
    • Il peut aussi encapsuler des smart pointers définis en bibliothèque, comme Box
  • Pin place dans le pinned typestate la cible pointée par ce pointeur, et cette cible ne doit plus être déplacée
  • Pour minimiser les changements, cette conception a été implémentée comme une API de bibliothèque plutôt que comme une fonctionnalité du compilateur
    • Le code qui doit réellement modifier un objet pinné passe par des API unsafe
    • Il doit alors garantir que l’objet ne sera pas déplacé via une référence mutable ordinaire
  • Comme, pour la plupart des types, l’état pinné et l’état ordinaire ne diffèrent pas de manière significative, l’auto trait Unpin a été ajouté
    • Si un type ne peut pas être auto-référentiel, on peut obtenir une référence mutable depuis un pointeur pinné sans passer par unsafe
    • Un objet qui implémente Unpin peut être déplacé hors de Pin en toute sécurité
  • Comme le pinning ne s’applique qu’aux pointeurs pinnés, les références ordinaires non pinnées continuent de fonctionner, y compris avec des types qui ne sont pas Unpin
  • Des explications supplémentaires figurent dans la documentation standard du type Pin et du module pin
  • Le principal avantage de cette conception est qu’elle a pu être ajoutée sans casser le code existant
    • Les API comme swap, qui peuvent déplacer des données référencées, exigent une référence mutable
    • Une fois qu’un objet est pinné via Pin, ces API ne peuvent plus lui être appliquées
    • Comme le pinned typestate ne s’applique qu’aux références pinnées spéciales, il ne remet pas en cause les garanties de rétrocompatibilité de l’ensemble du langage Rust

Les problèmes d’ergonomie de Pin

  • Pin satisfait les exigences de manière rétrocompatible, mais dès qu’un utilisateur doit le manipuler directement, on se heurte à un mur de complexité
  • Une première explication est que modifier un objet pinné nécessite du code unsafe
    • Il ne faut toutefois pas exagérer ce point
    • Pin::set permet d’affecter en toute sécurité un objet pinné
    • En pratique, le code qui doit réellement modifier un objet pinné est le plus souvent du code généré par le compilateur lorsqu’il abaisse une fonction async en Future ; les cas où l’utilisateur l’écrit à la main sont rares
  • Dire que Pin est difficile parce qu’il fonctionne de manière conditionnelle ne touche pas non plus la cause profonde
    • Rust possède d’autres fonctionnalités dont le comportement dépend du contexte tout en restant relativement compréhensibles
    • Les non-lexical lifetimes en sont un exemple : elles permettent à une lifetime de se terminer à des points différents selon les branches
  • Le vrai problème est que Pin est un pur type de bibliothèque, alors que les types de référence ordinaires sont intégrés au langage et bénéficient d’un grand nombre de syntaxes et de sugars
    • Des opérations naturelles avec une référence ordinaire disparaissent avec une référence pinnée
    • Le modèle mental que les utilisateurs construisent à partir du comportement des références acceptées par le compilateur s’effondre avec les références pinnées

Reborrowing et Pin::as_mut

  • Une référence mutable ordinaire &mut T n’implémente pas Copy, mais peut être passée plusieurs fois comme argument
    • Le compilateur effectue implicitement un reborrowing, comme s’il insérait &mut *x à la place de x
  • Pin<&mut T> est un type de bibliothèque ordinaire qui n’implémente pas Copy, et ne bénéficie donc pas de cette commodité
    • Si l’on utilise Pin<&mut T> plus d’une fois, on peut obtenir une erreur d’utilisation après move, ou une erreur de lifetime encore plus difficile à comprendre
    • Il faut reborrow explicitement en appelant Pin::as_mut
  • Avec une référence mutable ordinaire, on peut affecter directement via la déréférence et l’opérateur d’affectation ; avec Pin, il faut apprendre la méthode set
    • Cette multiplication d’API spéciales vient du fait que Pin est un type de bibliothèque sans support syntaxique du langage

Pinned projection et Drop

  • La pinned projection est le problème qui consiste à obtenir, à partir d’une référence pinnée sur un objet, une référence pinnée sur l’un de ses champs
    • Ici, projection désigne l’accès d’un objet vers l’un de ses champs
  • Comme c’est bien plus difficile qu’un simple accès de champ sur une référence ordinaire, on utilise des crates tierces comme pin-project-lite
    • Ces crates imposent d’apprendre une nouvelle API complexe, souvent avec des macros
  • L’interaction la plus problématique survient entre pinned projection et le trait Drop
    • Drop::drop reçoit une référence mutable ordinaire
    • Si un type possède un champ auto-référentiel, qu’on fait une pin projection vers ce champ pour le poll, puis qu’on déplace ce champ dans le destructeur, on peut violer les garanties du pinning
    • Par exemple, si l’on pinne ensuite cette future sur la stack dans le destructeur avant de la poll, on enfreint les garanties de pinning précédentes
  • Des crates comme pin-project-lite traitent ce problème en limitant la manière de définir un destructeur
    • En pratique, cela fonctionne, mais cela ajoute une couche de complexité documentaire lorsqu’il faut expliquer les garanties du pinning
    • Drop a été stabilisé avant Pin, d’où la nécessité de ce contournement

Évaluation actuelle et pistes d’amélioration

  • Pin a permis de compiler des fonctions async contenant des références arbitraires en objets auto-référentiels sûrs
    • Or les références sont un élément central de la façon dont les utilisateurs de Rust écrivent leur code ; sans cela, l’ergonomie d’async/await aurait été bien plus faible
  • Dans le même temps, Pin a été ajouté d’une manière totalement rétrocompatible avec le Rust existant
  • Pin est devenu un composant fondamental de l’écosystème qui soutient les services réseau haute performance et d’autres usages de la programmation asynchrone
  • Mais manipuler des références pinnées reste bien plus difficile que manipuler des références ordinaires, et Pin crée effectivement un mur de complexité
  • Le concept clé de la prochaine direction d’amélioration est celui des pinned places

1 commentaires

 
GN⁺ 2024-07-22
Avis sur Hacker News
  • J’ai toujours trouvé que Pin était difficile à comprendre, car la documentation officielle ne l’explique pas clairement
    On voit notamment beaucoup d’explications du type « Pin garantit qu’un objet ne sera jamais déplacé », mais ce n’est pas vrai
    Ce n’est exact que lorsque l’objet n’est pas Unpin, et comme la plupart des objets ordinaires sont Unpin, Pin ne fait généralement rien
    Il m’a fallu très longtemps pour comprendre cela, et je trouve que l’ensemble des types T pour lesquels Pin a réellement un sens est assez particulier et étrange, ce que la documentation ne souligne pas suffisamment

    • C’est un bon retour, et ce serait bien que la documentation clarifie davantage ce point
      Bien sûr, les types que l’on manipulera réellement comme épinglés — les futures et les streams — ont beaucoup plus de chances d’être ce genre d’objets particuliers
      Cela dit, je pense que la documentation s’est beaucoup améliorée au fil des années
      Quand je l’ai consultée en écrivant ce billet, j’ai été surpris de la voir se concentrer sur des points assez pertinents ; dans mon souvenir, vers 2019, elle penchait bien davantage vers une spécification de contrat qui aurait plutôt eu sa place dans la référence Rust que dans la documentation d’API de std
  • À mon avis, si les utilisateurs ont du mal avec Pin, c’est parce que Pin n’a pas de signification à lui seul
    C’est différent des autres wrappers du langage ; la seule exception serait peut-être AssertUnwindSafe, que quasiment personne n’utilise pour son usage d’origine
    Quand on a un Pin<&mut InnerType>, Pin, pris seul dans le langage ou la bibliothèque standard, ne dit pas ce que l’on peut ou ne peut pas faire
    En revanche, si InnerType se déclare Unpin, cela signifie que l’on peut faire tout ce qu’un pointeur ordinaire permet de faire
    À la place, Pin fonctionne sur le mode « apportez vous-même la sémantique » : le fournisseur de InnerType ajoute des méthodes et des API supplémentaires, unsafe en interne, pour manipuler en toute sûreté des objets épinglés
    Le but de Pin lui-même est de fournir un pointeur doté de moins de capacités intrinsèques, par exemple remplacer via &mut ou extraire et déplacer depuis une Box, afin que le type interne puisse autoriser en toute sûreté des capacités supplémentaires par-dessus
    Je pense que cette ambiguïté de sens est ce qui déroute le plus les gens, et il m’a aussi fallu pas mal de temps pour la comprendre
    Les notions de champs structurels et non structurels ne sont qu’un dispositif permettant des schémas d’accès courants, du type « ce champ est une donnée ordinaire, mais celui-là contient un objet qui veut lui-même être épinglé »

    • Pin a bien une signification. Tant que le type cible n’implémente pas Unpin, cela veut dire que la cible de ce pointeur ne pourra plus jamais être déplacée
      Plus précisément, cela signifie que l’on ne peut pas invalider la cible sans exécuter son destructeur, et c’est justement pour cela que le déplacement pose problème
      En renonçant à certains droits, on en obtient d’autres, comme le droit de stocker des valeurs auto-référentielles
      Les contrats entre composants fonctionnent généralement de cette manière
      De même, si l’on renonce au droit de modifier via une référence, on peut en même temps permettre à cette référence d’avoir des alias
      Chaque fois que j’y pense, même si c’est un sujet complètement différent et beaucoup plus lourd, cela me rappelle cette réplique du film Lincoln : « Si nous obéissons à la loi, Alex, si nous lui obéissons jusqu’à perdre des libertés — par exemple la liberté d’opprimer —, nous découvrirons peut-être d’autres libertés que nous ne connaissions pas auparavant »
      Cela dit, je suis d’accord pour dire que le fait de ne pas pouvoir utiliser directement ces droits dans du code sûr pose un problème pédagogique
      Car il est difficile de montrer simplement ce que l’on peut faire avec une référence épinglée, à part « appeler la méthode poll générée par le compilateur »
  • Je développe professionnellement en Rust depuis plusieurs années, mais honnêtement, je ne comprends pas si bien Pin que ça
    Je connais la théorie, mais je n’ai pas vraiment d’intuition sur le moment où il faut l’utiliser
    En pratique, l’usage de Pin ressemble surtout à « j’ai essayé quelque chose, le compilateur s’est plaint, j’ai épinglé ceci et cela, et ça compile »
    Dans le code que j’écris au quotidien, cela n’a encore jamais été un obstacle au point de devoir vraiment m’asseoir pour le comprendre en profondeur

    • Pareil pour moi. C’est l’un des cas les plus fréquents de « évitons simplement unsafe et soyons reconnaissants envers les gens très intelligents du compilateur qui ont déjà tout réglé »
      À l’inverse, en C++, je me retrouvais souvent à marcher au bord du marécage des « choses que je ne comprends pas mais que je dois absolument utiliser », avant de me faire dévorer par un crocodile
  • Quand on l’enseigne, pour bien faire comprendre que les éléments Unpin ne sont pas affectés par Pin, on pourrait utiliser une analogie du monde réel avec un outil conçu pour maintenir quelque chose en place mais qui n’a aucun effet dans certains cas
    Les crochets de velcro n’adhèrent pas aux surfaces lisses : Pin → velcro, Unpin → surface lisse
    Les aimants n’affectent pas les matériaux non magnétiques : Pin → aimant, Unpin → non magnétique/verre/laiton
    La colle n’adhère pas aux surfaces antiadhésives : Pin → colle, Unpin → antiadhésif
    Ainsi, on comprend clairement que le « velcro » fixe un objet en place, mais que si l’objet est « lisse », il n’est pas affecté par le mécanisme du velcro
    Vu l’ambiance des choix de noms dans l’écosystème Rust, cela aurait été élégant de renommer le trait dans une direction du type aimant et non magnétique

    • Mais on ne peut pas accrocher un objet lisse avec du velcro, et le bois ne peut pas retenir un aimant
      Il me semble que Unpin signifie qu’un objet est prêt à être épinglé à tout moment
      J’ai lu l’article hier soir, mais j’ai déjà oublié si l’épinglage nécessite une étape de correction
      Du coup, je vois plutôt T: Pin + !Unpin comme une feuille de papier que l’on ne peut fixer qu’avec des agrafes, et T: Pin + Unpin comme un tableau muni d’un crochet, que l’on peut suspendre à un clou puis décrocher sans abîmer le crochet
  • Le terme « identité de valeur » n’est défini nulle part dans cet article, et je ne l’ai pas trouvé non plus dans la documentation de Mojo ; la raison pour laquelle Modular affirme que Mojo résout le problème que Pin cherche à résoudre n’est donc pas claire.
    Je ne prétends pas non plus connaître la réponse, mais cela me fait penser à l’excellente présentation de Dave Abrahams, qui a travaillé avec Chris Lattner sur la sémantique de valeur de Swift.
    Le titre de la présentation est « Value Semantics: Safety, Independence, Projection, & Future of Programming ».
    [0] https://www.youtube.com/watch?v=QthAU-t3PQ4

    • Il est clair que Mojo a, en un certain sens, hérité du concept de sémantique de valeur de Swift, mais Rust possède aussi une sémantique de valeur au même sens.
      Rust dispose également de références comme types de première classe, tandis que Swift et, à mon avis, Mojo n’autorisent les références que comme mode de passage de paramètres.
      Mojo semble avoir étendu les paramètres inout de Swift pour inclure aussi un mode de passage par référence immuable.
      Empêcher de stocker des références dans un objet résout effectivement le problème des « structures auto-référentielles », puisqu’il devient impossible d’implémenter le genre de code que Rust compile.
      Mais le paragraphe cité ne dit absolument pas cela à propos de Mojo, donc je trouve assez confus de comprendre ce qu’il veut dire.
  • À mon avis, le problème tient au fait que, lorsqu’on a une référence &mut vers une valeur, on peut déplacer cette valeur avec quelque chose comme mem::swap/replace.
    Mais il est rare d’avoir réellement besoin de le faire.
    Si cela n’avait pas été autorisé, il me semble qu’avoir une référence &mut vers une valeur auto-référentielle aurait été parfaitement sûr.
    Il aurait peut-être pu exister un moyen d’opter explicitement pour le déplacement via une référence uniquement quand c’est nécessaire, et si swap et replace avaient été rendus unsafe, tout ce problème aurait peut-être pu être évité.
    J’aimerais que quelqu’un explore cet espace de conception.

    • Oui. À l’époque où il travaillait sur ce problème, Aaron Turon disait que &mut était trop puissant.
      Si &mut n’avait pas donné le droit de déplacer la valeur qu’il contient, toute la conception aurait été bien plus simple.
      J’aborderai ce point dans le prochain article.
      Rust doit préserver la compatibilité ascendante et a déjà décidé qu’il était possible de déplacer des valeurs depuis &mut, mais si l’on n’était pas lié par les décisions passées, une conception beaucoup plus propre serait clairement possible.
    • C’est vrai, mais ça ne passe pas à l’échelle. Cela aurait cassé trop de code existant pour être vraiment envisageable dès le départ.
      mem::swap n’est qu’une des manières de déplacer une valeur via une référence mutable, et il y en a beaucoup d’autres.
      Option::take est un exemple que j’utilise assez souvent, et ce serait vraiment étrange qu’il soit unsafe.
  • J’aime bien ce contexte historique. WithoutBoats a déjà suscité beaucoup de discussions très actuelles sur les itérateurs asynchrones, poll et pin.
    https://news.ycombinator.com/from?site=without.boats
    Je ne crois pas qu’il y ait beaucoup de communautés qui explorent publiquement avec autant de profondeur les détails internes d’un langage, et c’est très intéressant à suivre.

    • C’est chouette, mais cela signifie aussi que le développement du langage est très lent.
      L’asynchrone est encore à moitié inachevé et très complexe.
      Je dis cela en écrivant du code Rust 40 heures par semaine depuis trois ans.
  • On peut imaginer un langage proche de Rust qui aurait des constructeurs de déplacement, où tous les sous-types de Future créés seraient opaques et automatiquement alloués sur le tas
    Dans ce cas, l’utilisateur n’aurait aucun moyen de les détruire et, comme ils seraient opaques et situés ailleurs sur le tas, aucun moyen de les déplacer non plus, ce qui pourrait rendre Pin inutile
    Car le fait d’avoir un constructeur de déplacement signifie que, conceptuellement, un déplacement est une destruction suivie d’une recréation

    • Pin est plutôt un état qu’une propriété des données elles-mêmes
      Cela a l’effet bénéfique de permettre de fusionner et d’inliner les Future avant leur exécution
      C’est aussi similaire à l’immutabilité en Rust. Il n’y a pas de mémoire immuable, seulement des références immuables
    • Si toutes les futures étaient allouées sur le tas, les constructeurs de déplacement ne seraient pas nécessaires
      Mais cela entraînerait une allocation séparée à chaque appel de fonction asynchrone, ce qui est très mauvais pour la localité mémoire
      Une forme de pile virtuelle serait bien meilleure que cela, mais pour optimiser les piles afin qu’elles soient petites par défaut, il faudrait au final un garbage collector
    • J’imagine bien à quel point ce serait un changement destructeur, mais j’aimerais vraiment que Rust prenne le sujet de front et ajoute à std un trait Move intégré au langage, à un niveau comparable à Copy
      Move définirait une fonction qui déplace une valeur d’une adresse mémoire à une autre, et les structs sans impl Move ne pourraient pas être déplacées
      Presque tous les types recevraient #[derive(Move)], ce qui n’aurait qu’à implémenter une simple fonction de déplacement copiant les octets
      Mais cela ouvrirait la voie aux types auto-référentiels, aux futures, et à beaucoup d’autres choses nécessitant des comportements de déplacement plus complexes
      En pratique, il serait peut-être plus logique de le diviser en deux traits, reflétant la différence entre Copy et Clone
      L’un serait un trait marqueur indiquant au compilateur qu’il peut simplement déplacer les octets, et l’autre autoriserait l’implémentation d’un « constructeur de déplacement » défini par l’utilisateur
      Pin est tellement difficile à comprendre que j’aimerais qu’on ait Move
      Un concept complexe y est enveloppé dans des doubles négations, parfois des triples négations. Quand on voit quelque chose comme fn(...), on se dit « c’est quoi ça ? », et arrivé aux projections pin unsafe, on décroche
      Je ne sais pas quand c’est sûr ni quand ça ne l’est pas, et je finis simplement par laisser tomber
      Passer d’un Rust sans Move à un Rust avec Move serait pénible
      Il faudrait ajouter #[derive(Move)] à presque toutes les structs écrites jusqu’ici, et std aussi devrait le faire
      Pour tous les types non épinglés des éditions existantes, le compilateur devrait inférer une implémentation du trait Move
      Ce serait mécaniquement possible, mais ce serait juste beaucoup de travail
      Le Rust asynchrone est affreux. C’est particulièrement vrai quand on le compare aux futures/promises de presque tous les autres langages
      Un jour, quelqu’un améliorera le modèle de sûreté mémoire de Rust et créera un nouveau langage système proche de Rust, avec un trait Move et de meilleures futures
      Personnellement, j’aimerais aussi avoir de l’exécution à la compilation à la place du système de macros de Rust
      J’aime Rust, et j’aime aussi tout le travail que l’équipe a accompli pendant des années
      Mais le langage que j’attends vraiment, c’est celui qui viendra après Rust
      Un langage avec les mêmes idées, mais qui aura appris des erreurs de Rust, et l’on voit de plus en plus clairement à quoi pourrait ressembler un meilleur langage de cette famille
      J’ai vraiment hâte
  • Encore un excellent article de WithoutBoats
    Honnêtement, c’est l’un des aspects de Rust pour lesquels je suis content qu’il soit enfoui derrière les abstractions internes des runtimes asynchrones
    Cela dit, en dehors des implémentations personnalisées de Future, je me demande où Pin est réellement utilisé

    • Moi aussi, ça m’intéresse
      À en juger par la formulation, on pourrait imaginer l’utiliser dans du FFI
      Par exemple, si une fonction extern renvoie un *mut T et reçoit un pointeur, on pourrait l’envelopper dans Pin<&mut T> pour lui donner une meilleure sémantique
      Mais l’article dit : « Un autre fait concernant l’état épinglé d’un type est qu’il est complètement sans rapport avec la plupart des types. Si la valeur d’un type ne peut jamais contenir d’auto-référence, l’épingler ne sert à rien »
      Je suis encore très débutant en FFI, donc j’aimerais comprendre quelle est la meilleure façon de l’envelopper dans du Rust sûr
    • En FFI, il arrive qu’une API C expose des éléments sous forme de pointeurs plutôt que de références, et qu’ils ne doivent donc pas être déplacés
      Cela vaut aussi lorsqu’on interagit avec des types système qui dépendent de leur adresse
      Par exemple, pour certains mutex/futex de systèmes d’exploitation, la documentation du noyau indique que l’objet de verrouillage en espace utilisateur ne doit pas changer d’adresse après son initialisation ; il me semble donc que std utilise quelque chose d’équivalent à Pin
      Ce qui est particulier, c’est que l’adresse ne doit pas changer même lorsque le verrou n’est pas pris
      En général, la contrainte ne vaut que pendant que le verrou est pris, et dans ce cas, comme on ne peut pas déplacer l’objet visé par une référence active, Pin n’est pas nécessaire
  • On a l’impression qu’un énorme travail est fait pour éviter de corriger le vrai problème : l’inefficacité des threads.
    Tout le code asynchrone, absolument tout, sans exception, est un hack qui implémente des threads légers avec beaucoup de sucre syntaxique pour la gestion d’état.
    Dans des langages comme Rust, cela ajoute énormément de complexité qui ne devrait pas exister à l’origine.
    Si l’on corrigeait les problèmes d’efficacité et de scalabilité des threads, tout cela disparaîtrait.
    Pouf, disparu.
    C’est comparable au null dans des langages comme Java, cette « erreur à mille milliards de dollars ».
    Une seule décision de conception — ou ici, une absence de conception — engendre une complexité énorme.

    • Les threads ne prennent pas en charge l’annulation de manière raisonnable.
      L’annulation est très utile dans les applications réseau et les GUI.
      Les threads rendent difficile le fait d’exploiter pleinement à la fois le CPU et le réseau, sans monopoliser excessivement l’un ou l’autre.
      Dès qu’on commence à faire passer du travail d’un pool de threads à un autre, on s’engage sur la voie qui consiste à réimplémenter les futures.
      Sinon, on finit par travailler avec des callbacks/événements, le code se fragmente, et async/await est précisément le sucre syntaxique qui devait remédier à cela.
      L’alternative à l’annulation et aux timeouts consiste, comme en Go, à faire circuler un objet Context dans tout le code, ce qui pose le problème du code de bas niveau qui appelle naïvement des fonctions ne respectant pas correctement le Context.
      Ce n’est qu’à peine mieux que le problème des fonctions non asynchrones au sein de code asynchrone.
    • Je doute qu’il soit possible de réécrire le noyau Linux pour « corriger les problèmes d’efficacité et de scalabilité des threads » ; et même si c’était possible, l’ensemble des experts Rust capables de faire fonctionner Pin et celui des experts du noyau capables d’un tel travail ne seraient probablement pas les mêmes.
      Je me demande donc concrètement ce qu’il aurait fallu faire.
      Fallait-il simplement lever les bras au ciel et dire : « un jour, quelqu’un corrigera peut-être Linux pour rendre les threads magiquement rapides, donc nous n’ajouterons pas l’asynchrone à notre langage » ?
    • Marquer explicitement la différence entre les fonctions qui se synchronisent avec des processus concurrents et celles qui ne le font pas est en réalité une bonne chose.
    • Malheureusement, franchir la frontière de l’espace utilisateur a un coût, même si les « threads » sont très légers.
      De plus, faire du système d’exploitation le scheduler de toutes les tâches asynchrones obligerait tous les runtimes à utiliser le scheduler de l’OS, rendant impossibles divers designs de scheduler.
    • Les décisions nuisibles prises par Rust montrent une culture profondément ancrée consistant à pousser coûte que coûte des erreurs passées.
      Même après qu’il est apparu que « l’approche privilégiée » était impraticable, il semble qu’aucune réévaluation coût/bénéfice ne soit faite.
      « Nous voulons la fonctionnalité X, peu importe les conséquences » est rarement une stratégie gagnante dans la conception d’un langage.