Journal Du reverse tethering, en Rust

Posté par (page perso) . Licence CC by-sa
42
25
sept.
2017

Bonjour nal,

La dernière fois, je t’ai présenté Gnirehtet, un outil de reverse tethering pour Android.

Eh bien, je l’ai réécrit en Rust.

Rust est un langage de programmation développé par Mozilla Research, utilisé entre autres par Servo, le futur moteur Web de Firefox.

Gnirehtet 2

Pour l’utilisateur, le principal avantage de cette version rouillée est de ne pas avoir à installer l’environnement d’exécution Java. Par ailleurs, la gestion de la ligne de commande est maintenant implémentée dans le serveur relais directement plutôt que dans des scripts shell, afin d’avoir un comportement plus cohérent entre les différentes plates‐formes.

L’utilisation est (preque) la même que la version 1. Pour partager la connexion de l’ordinateur avec un téléphone ou une tablette Android, il suffit d’exécuter :

./gnirehtet run

(bien sûr, adb doit être installé)

Pour plus de détails : README.

Le principe de fonctionnement est exactement le même. J’ai mis à jour la page développeur pour prendre en compte la version Rust.

Rust

Cette réécriture a été pour moi l’occasion d’apprendre Rust, qui utilise plusieurs concepts intéressants :

  • abstractions sans coût ;
  • sémantique de mouvement ;
  • garantie de sûreté de la mémoire ;
  • fils d’exécution sans accès concurrent ;
  • généricité avec les traits ;
  • filtrage par motif ;
  • inférence de type ;
  • environnement d’exécution minimal ;
  • bibliothèques de liaisons (bindings) C efficaces.

J’ai écrit mes retours sur le langage, en particulier sur les difficultés que peuvent poser les contraintes imposées par les borrowing rules, dans un billet de blog :

La nimage.

Autres liens

RIIR

Il paraît que certaines personnes demandent aux auteurs de réécrire leur logiciel en Rust. Ça m’a fait sourire, donc je vous le partage : Have you considered Rewriting It In Rust?.

  • # Nimage

    Posté par . Évalué à 4 (+2/-0).

  • # Gnirehtet réécrit en Rust

    Posté par . Évalué à 1 (+4/-5).

    Quand je vois la liste des difficultés que tu dresses pour cette réécriture, je me dis vraiment que je suis bien avec mon C++ et que je ne veux absolument pas passer à Rust. Je crois que le truc qui m'a le plus choqué, c'est le paragraphe «Infinité indéfinie» qui explique que Rust fait de la merde mais à la fin, la conclusion, c'est que «les garanties de sûreté de Rust sont très fortes», j'ai failli m'étrangler. Je préfère encore un langage qui ne me promet pas la lune (au moins, je sais que je peux m'attendre au pire et donc, je fais attention), plutôt qu'un langage qui me la promet mais me refile une météorite.

    • [^] # Re: Gnirehtet réécrit en Rust

      Posté par (page perso) . Évalué à 7 (+5/-0). Dernière modification le 26/09/17 à 08:46.

      Quand je vois la liste des difficultés que tu dresses pour cette réécriture, […] je ne veux absolument pas passer à Rust

      C'est vrai que les borrowing rules sont assez contraignantes pour la conception d'une application. En contrepartie, elles offrent un certain nombre de garanties à l'exécution, ce qui élimine toute une classe de bugs potentiels (et de failles de sécurité).

      Je crois que le truc qui m'a le plus choqué, c'est le paragraphe «Infinité indéfinie» qui explique que Rust fait de la merde mais à la fin, la conclusion, c'est que «les garanties de sûreté de Rust sont très fortes»

      Oui, à part ce bug (de LLVM), en pratique c'est vraiment le cas. Quand j'ai retravaillé sur un projet C++ après, ça m'a fait tout drôle d'avoir un segfault à l'exécution.

      un langage qui […] me refile une météorite

      Pour nuancer, note que le comportement est également indéfini en C++:

      #include <iostream>
      
      void infinite(int value) {
          while (value != 0) {
              if (value != 1) {
                  value -= 1;
              }
          }
      }
      
      int main() {
          infinite(42);
          std::cout << "end" << std::endl;
          return 0;
      }
      

      Sans optimisations :

      $ clang++ ub.cpp -o ub && ./ub
      ^C                    (infinite loop, interrupt it)
      

      Avec optimisations :

      $ clang++ -O1 ub.cpp -o ub && ./ub
      end
      

      blog.rom1v.com

      • [^] # Re: Gnirehtet réécrit en Rust

        Posté par (page perso) . Évalué à 4 (+2/-0).

        C'est vrai que les borrowing rules sont assez contraignantes pour la conception d'une application.

        Ça s'apparente même à de la contorsion plus qu'à de la programmation par moments.
        Il faut avouer que ce genre d'écritures

        Rc<RefCell<Storage>>

        C'est quand même pas super raisonnable pour coder un patron de conception des années 80 :D

        Attention, je ne dis pas que je ne comprends le pourquoi de cette écriture mais cela montre bien que le système d'emprunts est une grosse usine à gaz et me fait plus penser à une rustine d'une façon de faire issue du C++.
        En même temps, Graydon Hoare est issu du monde C++ (cf. Monotone) donc ce n'est pas plus étonnant que ça.

      • [^] # Re: Gnirehtet réécrit en Rust

        Posté par . Évalué à 5 (+4/-1).

        C'est vrai que les borrowing rules sont assez contraignantes pour la conception d'une application. En contrepartie, elles offrent un certain nombre de garanties à l'exécution, ce qui élimine toute une classe de bugs potentiels (et de failles de sécurité).

        Est-ce que le rapport contraintes/gains est si favorable que ça, à la lecture de ton post de blog, je n'en suis pas certain. Si je lis ce que tu as écrit, implémenter un simple observateur est rendu complexe alors que ce n'est pas vraiment le plus dur des patrons de conception. Il y a des langages qui offrent des garanties sans pour autant se mettre en travers de la route du programmeur en permanence.

        Pour nuancer, note que le comportement est également indéfini en C++

        Oui, c'est vrai, mais C++ n'a jamais dit qu'il garantissait les programmes et en particulier celui là. C'est ce que je veux dire quand je dis qu'il ne promet pas la lune. En gros, C++ te dit : «si tu fais de la merde dans ton code, attends toi à avoir de la merde à l'exécution, éventuellement pas de la même couleur».

        • [^] # Re: Gnirehtet réécrit en Rust

          Posté par (page perso) . Évalué à 4 (+2/-0).

          Est-ce que le rapport contraintes/gains est si favorable que ça

          Ça va dépendre de la pondération que l'on donne à ces contraintes et à ces gains. À quel point veut-on éviter les undefined behaviors ou les problèmes de sûreté mémoire, sachant que les éliminer dès la compilation augmente drastiquement la sécurité globale des applications? Ça va dépendre des applications, des personnes, de l'époque (on est plus attentif à la sécurité si les attaques se font plus nombreuses et causent davantage de dégâts)…

          Mais je te rejoins, parfois les borrowing rules peuvent sembler trop contraignantes.

          Pourtant, le langage lui-même a plein d'avantages, à tel point qu'on pourrait parfois envie d'avoir un Rust-like qui serait Rust privé des borrowing rules (donc on pourrait avoir plusieurs références mutables simultanément, sans avoir recours à des blocs unsafe) et les durées de vie… Ce ne serait plus Rust, évidemment, mais peut-être que ça pourrait répondre à un besoin.

          blog.rom1v.com

        • [^] # Re: Gnirehtet réécrit en Rust

          Posté par . Évalué à 6 (+3/-0).

          Franchement, réutiliser des array de mémoire sans copie est un moyen de faire un code très rapide qui use peu de mémoire, mais c'est aussi le meilleur moyen de se tirer une balle dans le pied, car il peut devenir très compliquer de savoir quel traitement à déjà eu lieu et l'état réel du buffer.

          Donc, que rust propose de vérifier des propriétés qui te garantissent que tu ne fais pas de la merde, c'est pas mal. Regardes les perfs qu'il obtient à la fin, et surtout la consommation mémoire.

          "La première sécurité est la liberté"

          • [^] # Re: Gnirehtet réécrit en Rust

            Posté par (page perso) . Évalué à 2 (+0/-0).

            car il peut devenir très compliquer de savoir quel traitement à déjà eu lieu et l'état réel du buffer.

            Peux-tu donner un exemple plus clair?
            Hormis dans un contexte multithreadé où l'accès à une même zone mémoire peut poser des problèmes, j'ai du mal à comprendre comment on peut ne pas savoir ce qui a été effectué sur une variable, y compris sa libération, l'exécution étant linéaire et les durées de vie connues (quoi appartient à qui notamment).

            • [^] # Re: Gnirehtet réécrit en Rust

              Posté par . Évalué à 3 (+0/-0).

              l'exécution étant linéaire et les durées de vie connues (quoi appartient à qui notamment).

              Au début seulement, si le code ne devient pas gros, et si personne n'a corrigé de bug avec un emplâtre mal placé.

              "La première sécurité est la liberté"

              • [^] # Re: Gnirehtet réécrit en Rust

                Posté par (page perso) . Évalué à 2 (+0/-0).

                Au début seulement, si le code ne devient pas gros

                Heu, c'est quoi la limite d'après toi ?

                si personne n'a corrigé de bug avec un emplâtre mal placé

                Ça, de toutes façons, même Rust n'y pourra pas grand chose.

                • [^] # Re: Gnirehtet réécrit en Rust

                  Posté par . Évalué à 7 (+4/-0).

                  Heu, c'est quoi la limite d'après toi ?

                  Le moment où tu n'as plus en tête tout le pipeline de traitement.

                  Ça, de toutes façons, même Rust n'y pourra pas grand chose.

                  Si justement, Rust te garanti d'avoir un seul propriétaire de la mémoire, tu ne peux pas avoir 2 pointeurs qui se baladent, c'est plus facile à suivre. C'est le principe de ce genre de contrainte, avoir une sémantique clair et simple.

                  "La première sécurité est la liberté"

                  • [^] # Re: Gnirehtet réécrit en Rust

                    Posté par (page perso) . Évalué à 2 (+0/-0).

                    Si justement, Rust te garanti d'avoir un seul propriétaire de la mémoire, tu ne peux pas avoir 2 pointeurs qui se baladent, c'est plus facile à suivre

                    C'est pour ça qu'il vaut toujours mieux bosser avec des références const ou non en fonction des besoins et ne pas utiliser le passage de pointeur du tout.
                    Et là, finalement en C++, se trainer des const &var, ça devient presque aussi chiant que le Rust :)
                    Tout ça, c'est une histoire d'encapsulation et de gestion des responsabilités.
                    Quand on passe un pointeur, on abandonne la responsabilité du pointé à quelqu'un d'autre.
                    De toutes façons, les pointeurs, c'est sale :D

                    • [^] # Re: Gnirehtet réécrit en Rust

                      Posté par . Évalué à 4 (+1/-0).

                      Entièrement d'accord, mais si ton object fait 100ko, tu ne peux pas le copier.

                      Le plus simple, c'est d'avoir des objets immuables. Ainsi, tu n'as aucune différence sémantique entre balader un pointeur et l'objet lui-même puisque personne ne peut le modifier, par contre, sa durée de vie n'est plus sous contrôle et un GC est nécessaire. Ocaml fonctionne comme ça. Si il y a plein de string partout, cela va bien plus vite, par contre, si il y a plein de string à vie courte, la consommation mémoire s'envole, car il est impossible de réutiliser un buffer.

                      "La première sécurité est la liberté"

                      • [^] # Re: Gnirehtet réécrit en Rust

                        Posté par (page perso) . Évalué à 2 (+0/-0).

                        Entièrement d'accord, mais si ton object fait 100ko, tu ne peux pas le copier.

                        Ben, justement, un const &var, ça ne copie que l'adresse pas l'objet et cela empêche la modification mais c'est vrai que le programmeur C++, surtout s'il vient du C, il aime pas ses trucs-là :D

                        Le plus simple, c'est d'avoir des objets immuables.

                        Oui ou modifiables seulement au travers de méthodes et pas en fournissant un accès direct.

                        Ocaml fonctionne comme ça. Si il y a plein de string partout, cela va bien plus vite, par contre, si il y a plein de string à vie courte, la consommation mémoire s'envole, car il est impossible de réutiliser un buffer.

                        Java fait idem pour les String ce qui entraine des petites incompréhensions chez certains quand ils modifient une chaîne passée en paramètres d'une méthode en espérant la voir mise à jour dans l'appelant :)

                      • [^] # Re: Gnirehtet réécrit en Rust

                        Posté par . Évalué à 3 (+1/-0).

                        Si il y a plein de string partout, cela va bien plus vite, par contre, si il y a plein de string à vie courte, la consommation mémoire s'envole, car il est impossible de réutiliser un buffer.

                        Pourquoi la consommation mémoire s'envolerait avec des string à vie courte ? Le tas est en deux parties : le tas mineur de taille fixe pour les petits objets à vie courte et le tas majeur redimensionnable pour les objets à vie longue. Le comportement mémoire dépendra du ratio entre le taux de création des string, leur taille et leur durée de vie, mais il est probable qu'elles ne quitteront jamais le tas mineur et seront collectées sans avoir besoin d'être promues dans le tas majeur, seule situation qui pourra provoquer l'allocation de mémoire supplémentaire.

                        Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

                        • [^] # Re: Gnirehtet réécrit en Rust

                          Posté par . Évalué à 4 (+1/-0).

                          Disons que j'ai un cas en tête qui doit tomber pile poile dans le cas pathologique. Il s'agissait de parser des fichiers de petites tailles. En C, j'aurais simplement utiliser un char[] de taille "suffisante", en ocaml, buffer ne propose pas les même fonction de parse que string, la conversion est donc obligatoire.

                          Donc, chaque fichier utilise une string de qq ko. J'imagine que vu la taille, elle n'est pas alloué dans le tas mineur mais directement dans le tas majeur. Donc, si tu parses 10 000 fichiers, la limitation sera le GC. En jouant sur les options, j'ai gagné 15% de perf, en utilisant beaucoup plus de mémoire. J'imagine que l'idéal serait un allocateur qui recycle les vieux objets de grandes tailles, cela évite de réallouer la mémoire.

                          Ce cas était tellement énervant, car en C ou C++, il n'y aurait eu aucune allocation pendant la durée de la boucle. Et au final, le temps était passé dans la gestion mémoire.

                          "La première sécurité est la liberté"

                          • [^] # Re: Gnirehtet réécrit en Rust

                            Posté par . Évalué à 2 (+0/-0).

                            C'est étrange, normalement le tas mineur a une taille de 2M sur une plateforme 64-bits, un fichier de quelques ko y rentre sans problème. Si tu traitais tes fichiers séquentiellement dans un boucle, je ne vois pas ce qui empêchait le GC de les collecter sans les envoyer dans le tas majeur. En revanche si tu commençais par lire tes 10_000 fichiers, puis que tu les traiter ensuite un par un, là effectivement tu devais solliciter beaucoup plus le GC. Mais dans ce cas ce n'est plus du tout le même algorithme qu'avec un char[] en C ou C++.

                            Sinon, pourquoi ne pas utiliser un buffer tampon de taille fixe comme tu l'aurais fait en C ou C++ ?

                            Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

                            • [^] # Re: Gnirehtet réécrit en Rust

                              Posté par . Évalué à 1 (+0/-0).

                              Je sais pas si c'est le cas ici, mais il y a un cas pathologique qui apparait quand on re-alloue à chaque fois la mémoire pour un objet deux fois plus gros, ça ne rentre jamais dans l'espace libéré.

                              Un objet de taille N peut se retrouver dans un tas de taille 2*N.

                            • [^] # Re: Gnirehtet réécrit en Rust

                              Posté par . Évalué à 3 (+0/-0).

                              J'étais resté sur un tas mineur de 256 Ko, cela a du changé.

                              C'était ce code : https://github.com/nicolasboulay/index2share (joli flop, avec une interface incompréhensible pour le commun des mortels, alors qu'il n'y a aucune option)

                              Sinon, pourquoi ne pas utiliser un buffer tampon de taille fixe comme tu l'aurais fait en C ou C++ ?

                              buffer n'offre pas la même api que string pour les conversions.

                              "La première sécurité est la liberté"

            • [^] # Re: Gnirehtet réécrit en Rust

              Posté par (page perso) . Évalué à 6 (+4/-0).

              Peux-tu donner un exemple plus clair?
              Hormis dans un contexte multithreadé où l'accès à une même zone mémoire peut poser des problèmes,

              Justement, tout l'intérêt de Rust est de pouvoir faire du code MultiThreadé sans risque de problèmes de concurrences.

              C'était ce qui était expliqué dans la dépêche au sujet de Stylo de Firefox: en amenant un moteur CSS multi-threadé, ils avaient besoin d'avoir une assurance que les corrections faites par des centaines de développeurs ne rencontrent pas les problèmes de concurrence. Ils utilisent justement Rust pour avoir cette assurance, car ce langage est capable d'empêcher le développeur de créer du code buggé (d'après leurs discours).

              C'est clair que c'est contraignant, mais les bugs de concurrences sont aussi hyper compliqués à résoudre, car ils dépendent énormément du contexte d'exécution. Ici, Rust fournit un langage plus difficile à appréhender (car il force le développeur à utiliser de nouveaux paradigmes), mais il fournit des concepts qui empêchent l'apparition de ces bugs.

              • [^] # Re: Gnirehtet réécrit en Rust

                Posté par (page perso) . Évalué à 2 (+0/-0).

                Justement, tout l'intérêt de Rust est de pouvoir faire du code MultiThreadé sans risque de problèmes de concurrences.

                C'est bien pour ça que j'avais précis Hormis dans un contexte multithreadé.
                J'ai effectivement bien vu l'intérêt, surtout par rapport au C++, qui reste somme toute très bas niveau pour tout ça.

                C'est clair que c'est contraignant, mais les bugs de concurrences sont aussi hyper compliqués à résoudre, car ils dépendent énormément du contexte d'exécution. Ici, Rust fournit un langage plus difficile à appréhender (car il force le développeur à utiliser de nouveaux paradigmes), mais il fournit des concepts qui empêchent l'apparition de ces bugs

                C'est clair que ce sont les bugs les plus difficiles à dénicher et que Rust fournit un moyen de minimiser les risques mais je trouve quand même la mécanique super lourde.

                • [^] # Re: Gnirehtet réécrit en Rust

                  Posté par . Évalué à 4 (+4/-2).

                  C'est clair que ce sont les bugs les plus difficiles à dénicher et que Rust fournit un moyen de minimiser les risques mais je trouve quand même la mécanique super lourde.

                  Alors rejoins le mouvement des Insoumis et prône l'abolition de la concurrence, ainsi que de la défense de la propriété privée poussée à l'extrême en Rust. Mais je sens qu'un riche milliardaire profitant des innovations technologiques de la firme créée par son père n'est pas prêt à franchir le pas ! :-P

                  Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

      • [^] # Re: Gnirehtet réécrit en Rust

        Posté par . Évalué à 3 (+2/-0).

        Je suis pas sûr que ce soit assez clair dans ton message : Il s'agit d'un bug de LLVM !
        Qui ne touche pas Rust en lui même mais uniquement cette implémentation là.

    • [^] # Re: Gnirehtet réécrit en Rust

      Posté par . Évalué à 8 (+6/-0).

      D'accord tu veux pas passer à Rust.

      Mais au moins, passe à Ada ! :p

      "Quand certains râlent contre systemd, d'autres s'attaquent aux vrais problèmes." (merci Sinma !)

      • [^] # Re: Gnirehtet réécrit en Rust

        Posté par (page perso) . Évalué à 2 (+0/-0).

        Je suis obligé de plussoyer :D
        Mais je m'étais juré de ne pas en parler !!

        Le problème, c'est que ça va m'obliger à parler des types Access et là, je suis pas super bon, je n'en ai quasiment jamais utilisé, les modes de passage de paramètres suffisant dans la plupart des cas ;)

      • [^] # Re: Gnirehtet réécrit en Rust

        Posté par . Évalué à 2 (+1/-1).

        Pour avoir fait de l'Ada dans ma jeunesse, je dois dire que je n'ai jamais ressenti des gênes comme celles décrites dans ce post de blog. Après, je débutais et je ne suis sans doute pas allé très loin dans la programmation Ada. Mais Ada me donne plutôt l'impression de fournir des outils pour aider le programmeur à bien écrire son code, tandis que Rust fournit des contraintes où le programmeur se demande comment il va les contourner pour réussir à faire ce qu'il veut.

        • [^] # Re: Gnirehtet réécrit en Rust

          Posté par (page perso) . Évalué à 4 (+2/-0).

          Rust fournit des contraintes où le programmeur se demande comment il va les contourner pour réussir à faire ce qu'il veut.

          En Ada, si tu penses C/C++, tu en arrives à essayer de contourner le système de type, à tout faire via des access types.
          Je pense que là, c'est pareil, ®om a commencé en Java où tout est référence (grosso hein, me taper pas dessus pour ça, j'ai pas envie d'écrire un commentaire de 60 lignes) et du coup, il faut faire de la gymnastique intellectuelle pour essayer de reproduire des patterns Java à Rust.

    • [^] # Re: Gnirehtet réécrit en Rust

      Posté par (page perso) . Évalué à 3 (+5/-4).

      Parfois, j'aimerais bien avoir des avis tranchés comme le tien.
      Ça a au moins l'avantage de ne pas douter.

    • [^] # Re: Gnirehtet réécrit en Rust

      Posté par . Évalué à 1 (+1/-0).

      Il faut tester ce langage (surtout en venant du C++) pour se rendre compte à quel point il est génial.

      J'ai personnellement fait du C++ quelque années professionnellement après mes études (je pense relativement bien connaître le langage). On m'a dit beaucoup de bien de Rust, et du coup j'ai décidé de lui laisser une chance.

      Les débuts étaient effectivement horribles. Je passais mon temps à subir les foudres du borrow checker. Mais j'ai décidé de m'acharner, et rapidement, j'ai compris l'intérêt du truc. On finit par acquérir une façon de programmer plus saine, où on est conscient du cycle de vie de chaque "objet" (je mets entre guillemet, puisque il n'y a pas d'objet à strictement parler en Rust).

      Le Rust m'a tellement plu que je ne veux plus me coltiner le C++ (alors que c'était mon langage préféré avant). Concrètement, je fais du C# au travail, et j'écris mes projets persos en Rust. Je suis beaucoup plus productif en Rust qu'en C++.

      Certes, la courbe d'apprentissage a été brusque, mais a posteriori, ça valait le coup. Un côté un peu frustrant est qu'il faut abandonner certains patrons de conception et les remplacer par d'autres, par exemple. C'est sans regret pour certains, comme le Singleton.

      Les avantages du Rust sur le C++?

      • La simplicité et la sécurité quand on ajoute une dépendance (venant du C++, c'est pour moi l'avantage no. 1, ex aequo avec le suivant)
      • La sécurité (pas de segfault, pas de data race)
      • la sémantique move par défaut
      • Composition plutôt qu'héritage, le système de trait
      • La saveur fonctionnelle du langage: les iterateurs, la syntaxe orienté expression, le pattern matching, etc.
      • La bibliothèque standard qui ressemble à quelque chose (on a tout ce dont on a besoin, pas besoin de réécrire soi-même des choses de base).

      Bref, pour toutes ces raisons (et d'autres encore), je pense que c'est un langage amené à monter fortement face au couple C/C++. Il suffit de voir sa croissance y compris au sein des entreprises pour se rendre compte que ce n'est pas un feu de paille.

Envoyer un commentaire

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.