PROJET AUTOBLOG


Planet-Libre

source: Planet-Libre

⇐ retour index

Marthym : Hello OSGi World, Part 5, Fragment Bundles

samedi 23 septembre 2017 à 02:00

Disons que cette partie est un bonus si vous n’avez pas encore saturé ! Depuis le vous devez bien avoir remarqué que l’on utilise log4j 2 et que les logs ne s’affichent pas. À la place on a un message d’erreur qui dit qu’il manque un fichier de configuration. Et malheureusement, l’ajouter dans le répertoire de conf ne suffit pas, ni dans le répertoire courant. Il doit se trouver dans le classpath de log4j2-core.

Fragment Bundle

Le problème est là, l’isolation des classpaths induite par OSGi interdit donc de rajouter un fichier dans le classpath d’un bundle. À moins de modifier le jar original de log4j, ce qui n’est pas vraiment génial.

C’est là qu’interviennent les Fragments Bundles. En effet, ce sont des genres d’ajout fait à des bundles existant. Les fichiers qu’ils contiennent vont être ajouté au bundle qui les abrite et vont donc partager le même classpath.

Création du module how-log4j2

Pour créer un Fragment Bundle, il faut ajouter dans le MANIFEST.MF la ligne :

Fragment-Host: org.apache.logging.log4j.core

Pour dire que le fragment est hébergé par log4j2-core.

Dans la configuration du maven-bundle-plugin on a précisé


     combine.children="append">
        <_include>-src/main/osgi/osgi.bnd
    

Cette configuration permet, via un fichier src/main/osgi/osgi.bnd, d’ajouter des instructions à BND. On ajoute le module how-log4j2 avec le fichier osgi.bnd contenant la ligne nécessaire. Et on ajoute au module le ficher de configuration log4j2.

On build et on re-teste :

START LEVEL 1
   ID|State      |Level|Name
   16|Resolved   |    1|how-log4j2 (1.0.0.SNAPSHOT)|1.0.0.SNAPSHOT

Dans la liste des bundle on voit le nouveau Fragment. Les Fragments s’activent par l’intermédiaire de leur hôte. C’est pour cela qu’il est noté Resolved au lieu de Active.

Les logs s’affichent maintenant correctement !

Conclusion

Si vous êtes arrivé là, merci de votre patience et indulgence. N’hésitez pas à me faire un retour dans les commentaires !

J’utilise OSGi depuis 4 ans sur un projet conséquent en production. Quand on a commencé, on a essuyé les mêmes plâtres que décrit dans ces articles et même plus ! Au final le projet fonctionne bien mais la maintenance en est souvent complexe. Nous n’avons pas abordé la question des tests mais c’est tout aussi compliqué. En outre, les ressources que ce soit documentation ou humaines sont difficiles à trouver. La multitude d’implémentations de chaque partie du framework crée en plus une fragmentation qui amplifie encore le phénomène. De plus, vous avez du le voir, malgré l’ajout de la dépendance Xnio dans le composant http-server il arrive que l’erreur XNIO se produise. C’est une question de timing. Pour la résoudre définitivement, en plus de demander à Felix de vider son cache sur chaque démarrage, il faut ajouter un niveau de chargement des bundles. Un répertoire core qui serait chargé avant application et qui contiendrait tout sauf les jars de notre application. Mais on constate que la gestion de l’ordre de chargement est aussi un élément compliqué d’OSGi qui vient s’ajouter aux autres.

Pour ces raisons, je ne choisirais pas d’utiliser OSGi en production sur les prochains projets. C’est un sujet intéressant à explorer mais le coût en est très élevé comparé au gain qu’on en retire.

Code source: Part 5, Fragment Bundles

Hello OSGi World, Part 5, Fragment Bundles écrit à l'origine par Marthym pour J'ai acheté un PC neuf cassé ... le September 23, 2017.

Gravatar de Marthym
Original post of Marthym.Votez pour ce billet sur Planet Libre.

®om : Gnirehtet réécrit en Rust

jeudi 21 septembre 2017 à 17:00

Il y a quelques mois, j’ai présenté Gnirehtet, un outil de reverse tethering pour Android que j’ai écrit en Java.

Depuis, je l’ai réécrit en Rust.

Et il est également open source ! Téléchargez-le, branchez un téléphone ou une tablette Android, et exécutez :

./gnirehtet run

(adb doit être installé)

Pourquoi Rust?

À Genymobile, nous voulions que Gnirehtet ne nécessite pas d’environnement d’exécution Java (JRE), donc le besoin principal était de compiler l’application vers un binaire exécutable natif.

Par conséquent, j’ai d’abord pensé la réécrire en C ou C++. Mais à ce moment-là (début mai), apprendre Rust m’intéressait, après avoir vaguement entendu parler de ses fonctionnalités:

Cependant, je n’avais jamais écrit une ligne de Rust ni entendu parler de son système de possession, d’emprunt ou de durées de vie.

Mais je suis convaincu que le meilleur moyen d’apprendre un langage de programmation est de travailler à plein temps sur un projet dans ce langage.

J’étais motivé, donc après avoir vérifié que ça pouvait convenir (en gros, j’ai écrit un exemple utilisant la bibliothèque d’I/O asynchrone mio, et je l’ai exécuté à la fois sur Linux et Windows), j’ai décidé de réécrire Gnirehtet en Rust.

Apprendre Rust

Pendant la réécriture, j’ai dévoré successivement le Rust book, Rust by example et le Rustonomicon. J’ai beaucoup appris, et j’aime énormément ce langage. Beaucoup de ses fonctionnalités me manquent maintenant quand je travaille sur un projet C++ :

À propos de l’apprentissage, Paul Graham a écrit:

Reading and experience train your model of the world. And even if you forget the experience or what you read, its effect on your model of the world persists. Your mind is like a compiled program you’ve lost the source of. It works, but you don’t know why.

Pour les non-anglophones, ma propre traduction :

La lecture et l’expérience entraînent votre modèle du monde. Et même si vous oubliez l’expérience ou ce que vous avez lu, son effet sur votre modèle du monde persiste. Votre esprit est comme un programme compilé dont vous auriez perdu le code source. Ça fonctionne, mais vous ne savez pas pourquoi.

Certains des concepts de Rust (comme les durées de vie ou la sémantique de mouvement par défaut) m’ont fourni un jeu de données significativement différent, qui a sans aucun doute affecté mon modèle du monde (de la programmation).

Je ne vais pas présenter toutes ces fonctionnaliés (cliquez sur les liens de la documentation si ça vous intéresse). À la place, je vais essayer d’expliquer où et pourquoi Rust a resisté au design que je voulais implémenter, et comment repenser les problèmes dans le périmètre des contraintes de Rust.

La partie suivant nécessite une certaine connaissance de Rust. Vous pourriez vouloir la passer pour aller directement aux stats.

Difficultés

Je trouvais la conception de l’application Java plutôt réussie, donc je voulais reproduire l’architecture globale dans la version Rust (avec d’éventuelles adaptations pour la rustifier).

Mais j’ai lutté sur les détails, en particulier pour satisfaire le borrow checker. Les règles sont simples:

First, any borrow must last for a scope no greater than that of the owner. Second, you may have one or the other of these two kinds of borrows, but not both at the same time:

  • one or more references (&T) to a resource,
  • exactly one mutable reference (&mut T).

En français :

Premièrement, aucun emprunt ne doit avoir une portée plus grande que celle de son propriétaire. Deuxièmement, vous pouvez avoir l’un ou l’autre de ces types d’emprunts, mais pas les deux à la fois:

  • une ou plusieurs références (&T) vers une ressource,
  • exactement une référence mutable (&mut T).

Cependant, il m’a fallu un peu de temps pour réaliser comment elles entrent en conflit avec certains principes ou modèles de conception.

Voici donc mes retours. J’ai sélectionné 4 sujets qui sont suffisamment généraux pour être indépendants de ce projet particulier :

Encapsulation

Les règles d’emprunt contraignent l’encapsulation. C’est la première conséquence que j’ai réalisée.

Voici un exemple canonique :

pub struct Data {
    header: [u8; 4],
    payload: [u8; 20],
}

impl Data {
    pub fn new() -> Self {
        Self {
            header: [0; 4],
            payload: [0; 20],
        }
    }

    pub fn header(&mut self) -> &mut [u8] {
        &mut self.header
    }

    pub fn payload(&mut self) -> &mut [u8] {
        &mut self.payload
    }
}

fn main() {
    let mut data = Data::new();
    let header = data.header();
    let payload = data.payload();
}

Nous créons juste une nouvelle instance de Data, puis associons à des variables locales des références mutables vers les tableaux header et payload, en passant par des accesseurs.

Cependant, cela ne compile pas :

$ rustc sample.rs
error[E0499]: cannot borrow `data` as mutable more than once at a time
  --> sample.rs:21:19
   |
25 |     let header = data.header();
   |                  ---- first mutable borrow occurs here
26 |     let payload = data.payload();
   |                   ^^^^ second mutable borrow occurs here
27 | }
   | - first borrow ends here

Le compilateur ne peut pas faire l’hypothèse que header() et payload() retournent des références vers des données disjointes dans la structure Data. Par conséquent, chacun emprunte la structure data entièrement. Vu que les règles d’emprunt interdisent d’obtenir deux références mutables vers la même ressource, il rejette le second appel.

Parfois, nous faisons face à des limitations temporaires parce que le compilateur n’est pas (encore) assez malin. Ce n’est pas le cas ici : l’implémentation de header() pourrait très bien retourner une référence vers payload, ou écrire dans le tableau payload, enfreignant ainsi les règles d’emprunt. Et la validité d’un appel d’une méthode ne peut pas dépendre de l’implementation de la méthode.

Pour corriger le problème, le compilateur doit être capable de savoir que les variables locales header et payload référencent des données disjointes, par exemple en accédant aux champs directement :

    let header = &mut data.header;
    let payload = &mut data.payload;

ou en exposant une méthode fournissant les deux références simultanément :

struct Data {
    fn header_and_payload(&mut self) -> (&mut [u8], &mut [u8]) {
        (&mut self.header, &mut self.payload)
    }
}

fn main() {
    let mut data = Data::new();
    let (header, payload) = data.header_and_payload();
}

De même, dans l’implémentation d’une structure, les règles d’emprunt empêchent de factoriser du code dans une méthode privée facilement. Prenons cet exemple (artificiel) :

pub struct Data {
    buf: [u8; 20],
    prefix_length: usize,
    sum: u32,
    port: u16,
}

impl Data {
    pub fn update_sum(&mut self) {
        let content = &self.buf[self.prefix_length..];
        self.sum = content.iter().cloned().map(u32::from).sum();
    }

    pub fn update_port(&mut self) {
        let content = &self.buf[self.prefix_length..];
        self.port = (content[2] as u16) << 8 | content[3] as u16;
    }
}

Ici, le champ buf est un tableau stockant un préfixe et un contenu de manière contiguë.

Nous voulons factoriser la manière dont nous récupérons la slice content, pour que les méthodes update_*() n’aient pas à se préoccuper des détails. Essayons :

 impl Data {
     pub fn update_sum(&mut self) {
-        let content = &self.buf[self.prefix_length..];
+        let content = self.content();
         self.sum = content.iter().cloned().map(u32::from).sum();
     }

     pub fn update_port(&mut self) {
-        let content = &self.buf[self.prefix_length..];
+        let content = self.content();
         self.port = (content[2] as u16) << 8 | content[3] as u16;
     }
+
+    fn content(&mut self) -> &[u8] {
+        &self.buf[self.prefix_length..]
+    }
 }

Malheureusement, cela ne compile pas :

error[E0506]: cannot assign to `self.sum` because it is borrowed
  --> facto2.rs:11:9
   |
10 |         let content = self.content();
   |                       ---- borrow of `self.sum` occurs here
11 |         self.sum = content.iter().cloned().map(u32::from).sum();
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ assignment to borrowed `self.sum` occurs here

error[E0506]: cannot assign to `self.port` because it is borrowed
  --> facto2.rs:16:9
   |
15 |         let content = self.content();
   |                       ---- borrow of `self.port` occurs here
16 |         self.port = (content[2] as u16) << 8 & content[3] as u16;
   |

Comme dans l’exemple précédent, récupérer une référence à travers une méthode emprunte la structure complète (ici, self).

Pour contourner le problème, nous pouvons expliquer au compilateur que les champs sont disjoints :

impl Data {
    pub fn update_sum(&mut self) {
        let content = Self::content(&self.buf, self.prefix_length);
        self.sum = content.iter().cloned().map(u32::from).sum();
    }

    pub fn update_port(&mut self) {
        let content = Self::content(&self.buf, self.prefix_length);
        self.port = (content[2] as u16) << 8 | content[3] as u16;
    }

    fn content(buf: &[u8], prefix_length: usize) -> &[u8] {
        &buf[prefix_length..]
    }
}

Ça compile, mais cela va totalement à l’encontre de la factorisation : l’appelant doit fournir les champs nécessaires.

Comme alternative, nous pouvons utiliser une macro pour inliner le code :

macro_rules! content {
    ($self:ident) => {
        &$self.buf[$self.prefix_length..]
    }
}

impl Data {
    pub fn update_sum(&mut self) {
        let content = content!(self);
        self.sum = content.iter().cloned().map(u32::from).sum();
    }

    pub fn update_port(&mut self) {
        let content = content!(self);
        self.port = (content[2] as u16) << 8 | content[3] as u16;
    }
}

Mais c’est loin d’être idéal.

Je pense que nous devons juste l’accepter : l’encapsulation entre parfois en conflit avec les règles d’emprunt. Après tout, ce n’est pas si surprenant : imposer les règles d’emprunt nécessite de suivre chaque accès concret aux ressources, alors que l’encapsulation vise à les abstraire.

Observateur

Le design pattern observateur est utile pour enregistrer des événements sur un objet.

Dans certains cas, ce pattern pose des difficultés d’implémentation en Rust.

Pour faire simple, considérons que les événements sont des valeurs u32. Voici une implémentation possible :

pub trait EventListener {
    fn on_event(&self, event: u32);
}

pub struct Notifier {
    listeners: Vec<Box<EventListener>>,
}

impl Notifier {
    pub fn new() -> Self {
        Self { listeners: Vec::new() }
    }

    pub fn register<T: EventListener + 'static>(&mut self, listener: T) {
        self.listeners.push(Box::new(listener));
    }

    pub fn notify(&self, event: u32) {
        for listener in &self.listeners {
            listener.on_event(event);
        }
    }
}

Par commodité, implémentons notre trait EventListener pour les closures :

impl<F: Fn(u32)> EventListener for F {
    fn on_event(&self, event: u32) {
        self(event);
    }
}

Ainsi, son utilisation est simple :

    let mut notifier = Notifier::new();
    notifier.register(|event| println!("received [{}]", event));
    println!("notifying...");
    notifier.notify(42);

Cela affiche :

notifying...
received [42]

Jusqu’ici, tout va bien.

Cependant, les choses se compliquent si nous voulons modifier un état sur la réception d’un événement. Par exemple, implémentons une structure pour stocker tous les événements reçus :

pub struct Storage {
    events: Vec<u32>,
}

impl Storage {
    pub fn new() -> Self {
        Self { events: Vec::new() }
    }

    pub fn store(&mut self, value: u32) {
        self.events.push(value);
    }

    pub fn events(&self) -> &Vec<u32> {
        &self.events
    }
}

Pour pouvoir remplir ce Storage sur chaque événement reçu, nous devons d’une manière ou d’une autre le passer avec l’event listener, qui sera stocké dans le Notifier. Par conséquent, nous avons besoin qu’une instance de Storage soit partagée entre le code appelant et le Notifier.

Avoir deux références mutables vers le même objet enfreint évidemment les règles d’emprunt, donc nous avons besoin d’un pointeur à compteur de références.

Cependant, un tel pointeur est en lecture seul, donc nous avons également besoin d’un RefCell pour la mutabilité intérieure.

Ainsi, nous allons utiliser une instance de Rc>. Cela peut sembler trop verbeux, mais utiliser Rc> (ou Arc> pour la thread-safety) est très courant en Rust. Et il y a pire.

Voici ce que donne le code client :

    use std::cell::RefCell;
    use std::rc::Rc;

    let mut notifier = Notifier::new();

    // first Rc to the Storage
    let rc = Rc::new(RefCell::new(Storage::new()));

    // second Rc to the Storage
    let rc2 = rc.clone();

    // register the listener saving all the received events to the Storage
    notifier.register(move |event| rc2.borrow_mut().store(event));

    notifier.notify(3);
    notifier.notify(141);
    notifier.notify(59);
    assert_eq!(&vec![3, 141, 59], rc.borrow().events());

De cette manière, le Storage est correctement modifié à partir de l’event listener.

Tout n’est pas résolu pour autant. Dans cet exemple, c’était facile, nous avions accès à l’instance Rc>. Comment faire si nous avons seulement accès au Storage, par exemple si nous voulons que le Storage s’enregistre lui-même à partir de l’une de ses méthodes, sans que l’appelant n’ait à fournir l’instance Rc> ?

impl Storage {
    pub fn register_to(&self, notifier: &mut Notifier) {
        notifier.register(move |event| {
            /* how to retrieve a &mut Storage from here? */
        });
    }
}

Nous devons trouver un moyen de récupérer le Rc> à partir du Storage.

Pour cela, l’idée consiste à rendre Storage conscient de son pointeur à compteur de références. Bien sûr, cela n’a du sens que si Storage est construit dans un Rc>.

C’est exactement ce que enable_shared_from_this fournit en C++, donc nous pouvons nous inspirer de son fonctionnement : juste stocker un Weak>, downgradé à partir du Rc>, dans la structure elle-même. De cette manière, nous pouvons l’utiliser pour récupérer une référence &mut Storage à partir de l’event listener :

use std::rc::{Rc, Weak};
use std::cell::RefCell;

pub struct Storage {
    self_weak: Weak<RefCell<Storage>>,
    events: Vec<u32>,
}

impl Storage {
    pub fn new() -> Rc<RefCell<Self>> {
        let rc = Rc::new(RefCell::new(Self {
            self_weak: Weak::new(), // initialize empty
            events: Vec::new(),
        }));
        // set self_weak once we get the Rc instance
        rc.borrow_mut().self_weak = Rc::downgrade(&rc);
        rc
    }

    pub fn register_to(&self, notifier: &mut Notifier) {
        let rc = self.self_weak.upgrade().unwrap();
        notifier.register(move |event| rc.borrow_mut().store(event))
    }
}

Voici comment l’utiliser :

    let mut notifier = Notifier::new();
    let rc = Storage::new();
    rc.borrow().register_to(&mut notifier);
    notifier.notify(3);
    notifier.notify(141);
    notifier.notify(59);
    assert_eq!(&vec![3, 141, 59], rc.borrow().events());

Il est donc possible d’implémenter le design pattern observateur en Rust, mais c’est un peu plus difficile qu’en Java ;-)

Lorsque c’est possible, il est probablement préférable de l’éviter.

Partage de données mutables

Mutable references cannot be aliased.

En français :

Les références mutables ne peuvent pas être aliasées.

Comment partager des données mutables, alors ?

Nous avons vu que nous pouvions utiliser Rc> (ou Arc>), qui impose les règles d’emprunt à l’exécution. Cependant, ce n’est pas toujours désirable :

Au lieu de cela, nous pourrions utiliser des pointeurs bruts manuellement dans du code non-sûr, mais alors ce serait non-sûr.

Et il y a une autre solution, qui consiste à exposer des vues temporaires d’emprunt d’un objet. Laissez-moi expliquer.

Dans Gnirehtet, un paquet contient une référence vers les données brutes (stockées dans un buffer quelque part) ainsi que les valeur des champs des en-têtes IP et TCP/UDP (parsées à partir du tableau d’octets brut). Nous aurions pu utiliser une structure à plat pour tout stocker :

pub struct Packet<'a> {
    raw: &'a mut [u8],
    ipv4_source: u32,
    ipv4_destination: u32,
    ipv4_protocol: u8,
    // + other ipv4 fields
    transport_source: u16,
    transport_destination: u16,
    // + other transport fields
}

Le Packet aurait fourni des setters pour tous les champs d’en-têtes (modifiant à la fois les champs du paquet et le tableau d’octets). Par exemple :

impl<'a> Packet<'a> {
    pub fn set_transport_source(&mut self, transport_source: u16) {
        self.transport_source = transport_source;
        let transport = &mut self.raw[20..];
        BigEndian::write_u16(&mut transport[0..2], port);
    }
}

Mais cette conception ne serait pas terrible (surtout que les champs d’en-têtes TCP et UDP sont différents).

À la place, nous voudrions extraire les en-têtes d’IP et de transport vers des structures séparées, gérant leur propre partie du tableau d’octets :

// violates the borrowing rules

pub struct Packet<'a> {
    raw: &'a mut [u8], // the whole packet (including headers)
    ipv4_header: Ipv4Header<'a>,
    transport_header: TransportHeader<'a>,
}

pub struct Ipv4Header<'a> {
    raw: &'a mut [u8], // slice related to ipv4 headers
    source: u32,
    destination: u32,
    protocol: u8,
    // + other ipv4 fields
}

pub struct TransportHeader<'a> {
    raw: &'a mut [u8], // slice related to transport headers
    source: u16,
    destination: u16,
    // + other transport fields
}

Vous avez immédiatement repéré le problème : il y a plusieurs références vers la même ressource, le tableau d’octets raw, en même temps.

Remarquez que diviser le tableau n’est pas une possibilité ici, vu que les slices de raw se chevauchent : nous avons besoin d’écrire le paquet complet en une seule fois vers la couche réseau, donc le tableau raw dans Packet doit inclure les headers.

Nous avons besoin d’une solution compatible avec les règles d’emprunt.

Voici celle à laquelle je suis parvenu :

Et voici une simplification de l’implémentation réelle :

pub struct Packet<'a> {
    raw: &'a mut [u8],
    ipv4_header: Ipv4HeaderData,
    transport_header: TransportHeaderData,
}

pub struct Ipv4HeaderData {
    source: u32,
    destination: u32,
    protocol: u8,
    // + other ipv4 fields
}

pub struct TransportHeaderData {
    source: u16,
    destination: u16,
    // + other transport fields
}

pub struct Ipv4Header<'a> {
    raw: &'a mut [u8],
    data: &'a mut Ipv4HeaderData,
}

pub struct TransportHeader<'a> {
    raw: &'a mut [u8],
    data: &'a mut TransportHeaderData,
}

impl<'a> Packet<'a> {
    pub fn ipv4_header(&mut self) -> Ipv4Header {
        Ipv4Header {
            raw: &mut self.raw[..20],
            data: &mut self.ipv4_header,
        }
    }

    pub fn transport_header(&mut self) -> TransportHeader {
        TransportHeader {
            raw: &mut self.raw[20..40],
            data: &mut self.transport_header,
        }
    }
}

Les setters sont implémentés sur les vues, où ils détiennent une référence mutable vers le tableau brut :

impl<'a> TransportHeader<'a> {
    pub fn set_source(&mut self, source: u16) {
        self.data.source = source;
        BigEndian::write_u16(&mut raw[0..2], source);
    }

    pub fn set_destination(&mut self, destination: u16) {
        self.data.destination = destination;
        BigEndian::write_u16(&mut raw[2..4], destination);
    }
}

De cette manière, les règles d’emprunt sont respectées, et l’API est élégante :

    let mut packet = ;
    // "transport_header" borrows "packet" during its scope
    let mut transport_header = packet.transport_header();
    transport_header.set_source(1234);
    transport_header.set_destination(1234);

Limitations du compilateur

Rust est un langage jeune, et le compilateur a quelques problèmes ennuyeux.

Le pire, d’après moi, est lié aux durées de vie non-lexicales, qui provoque des erreurs inattendues :

struct Container {
    vec: Vec<i32>,
}

impl Container {
    fn find(&mut self, v: i32) -> Option<&mut i32> {
        None // we don't care the implementation
    }

    fn get(&mut self, v: i32) -> &mut i32 {
        if let Some(x) = self.find(v) {
            return x;
        }
        self.vec.push(v);
        self.vec.last_mut().unwrap()
    }
}
error[E0499]: cannot borrow `self.vec` as mutable more than once at a time
  --> sample.rs:14:9
   |
11 |         if let Some(x) = self.find(v) {
   |                          ---- first mutable borrow occurs here
...
14 |         self.vec.push(v);
   |         ^^^^^^^^ second mutable borrow occurs here
15 |         self.vec.last_mut().unwrap()
16 |     }
   |     - first borrow ends here

Heureusement, cela devrait être corrigé prochainement.

La fonctionnalité d’Impl Trait, permettant aux fonctions de retourner des types abstraits non-boxés, devrait aussi améliorer l’expérience (il y a aussi une proposition étendue).

Le compilateur produit généralement des messages d’erreur très utiles. Mais quand ce n’est pas le cas, ils peuvent être très déroutants.

Sûreté et pièges

Le premier chapitre du Rustonomicon dit :

Safe Rust is For Reals Totally Safe.

[…]

Safe Rust is the true Rust programming language. If all you do is write Safe Rust, you will never have to worry about type-safety or memory-safety. You will never endure a null or dangling pointer, or any of that Undefined Behavior nonsense.

En français :

La partie Sûre de Rust est Réellement Totallement Sûre.

[…]

Le Rust Sûr est le vrai langage de programmation Rust. Si vous n’écrivez que du Rust Sûr, vous n’aurez jamais à vous inquiétez de la sûreté des types ou de la mémoire. Vous n’aurez jamais à supporter un pointeur null ou dangling, ou l’un de ces comportements indéfinis insensés.

C’est le but. Et c’est presque vrai.

Leakpocalypse

Dans le passé, il a été possible d’écrire du code Rust sûr accédant à de la mémoire libérée.

Cette “leakpocalypse” a conduit à la clarification des guaranties de sûreté : ne pas exécuter un destructeur est maintenant considéré sûr. En d’autres termes, la sûreté mémoire ne peut plus reposer sur RAII (en fait, elle n’a jamais pu, mais cela n’a été remarqué que tardivement).

En conséquence, std::mem::forget est maintenant sûr, et JoinGuard a été déprécié et supprimé de la bibliothèque standard (il a été déplacé vers un crate séparé).

Les autres outils s’appuyant sur RAII (comme Vec::drain()) doivent prendre des précautions particulières pour garantir que la mémoire ne sera pas corrompue.

Ouf, la sûreté mémoire est (maintenant) sauvée.

Infinité indéfinie

En C et C++, les boucles infinies sans effets de bords sont un cas d’undefined behavior. À cause de cela, il est possible d’écrire des programmes qui, de façon inattendue, réfutent le dernier théorème de Fermat.

En pratique, le compilateur Rust s’appuie sur LLVM, qui (actuellement) applique ses optimisations en faisant l’hypothèse que les boucles infinies sans effets de bords ont un comportement indéfini. En conséquence, de tels undefined behaviors se produisent également en Rust.

Voici un exemple minimal pour l’observer :

fn infinite() {
    loop {}
}

fn main() {
    infinite();
}

Quand on l’exécute sans optimisations, il se comporte comme “attendu” :

$ rustc ub.rs && ./ub
^C                    (infinite loop, interrupt it)

Mais activer les optimisations fait paniquer le programme :

$ rustc -O ub.rs && ./ub
thread 'main' panicked at 'assertion failed: c.borrow().is_none()', /checkout/src/libstd/sys_common/thread_info.rs:51
note: Run with `RUST_BACKTRACE=1` for a backtrace.

Nous pouvons aussi produire des résultats inattendus sans plantage :

fn infinite(mut value: u32) {
    // infinite loop unless value initially equals 0
    while value != 0 {
        if value != 1 {
            value -= 1;
        }
    }
}

fn main() {
    infinite(42);
    println!("end");
}
$ rustc ub.rs && ./ub
^C                    (infinite loop, interrupt it)

Mais avec optimisations :

$ rustc -O ub.rs && ./ub
end

C’est un cas particulier, qui sera probablement corrigé dans le futur. En pratique, les garanties de sûreté de Rust sont très fortes (au prix d’être contraignantes).

Stats

C’est tout pour mes retours sur le langage lui-même.

En supplément, comparons les versions Java et Rust du serveur relais.

Nombre de lignes

$ cloc relay-{java,rust}/src
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Rust                            29            687            655           4506
Java                            37            726            701           2931
-------------------------------------------------------------------------------

(tests included)

Le projet Rust est significativement plus gros, pour plusieurs raisons :

La version Java contient plus de fichiers car les tests unitaires sont séparés, alors qu’en Rust ils se trouvent dans le même fichier que les classes qu’ils testent.

Juste pour information, voici les résultats pour le client Android :

$ cloc app/src
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Java                            15            198            321            875
XML                              6              7              2             76
-------------------------------------------------------------------------------
SUM:                            21            205            323            951
-------------------------------------------------------------------------------

Taille des binaires

--------------------------------------------
Java     gnirehtet.jar                   61K
--------------------------------------------
Rust     gnirehtet                      3.0M
         after "strip -g gnirehtet"     747K
         after "strip gnirehtet"        588K
--------------------------------------------

Le binaire Java lui-même est bien plus petit. La comparaison n’est pas juste cependant, vu qu’il nécessite l’environnement d’exécution Java :

$ du -sh /usr/lib/jvm/java-1.8.0-openjdk-amd64/
156M	/usr/lib/jvm/java-1.8.0-openjdk-amd64/

Utilisation mémoire

Avec une seule connection TCP ouvert, voici la consommation mémoire pour le serveur relais en Java :

$ sudo pmap -x $RELAY_JAVA_PID
                  Kbytes     RSS   Dirty
total kB         4364052   86148   69316

(résultat filtré)

Et pour le serveur relais en Rust :

$ sudo pmap -x $RELAY_RUST_PID
                  Kbytes     RSS   Dirty
total kB           19272    2736     640

Regardez la valeur RSS, qui indique la mémoire réellement utilisée.

Comment on pouvait s’y attendre, la version Java consomme plus de mémoire (86Mo) que la version Rust (moins de 3Mo). De plus, sa valeur est instable à cause de l’allocation de petits objets et leur garbage collection, qui génère aussi davantage de dirty pages. La valeur pour Rust, quant à elle, est très stable : une fois la connection créée, il n’y a plus d’allocations mémoire du tout.

Utilisation CPU

Pour comparer l’utilisation CPU, voici mon scénario : un fichier de 500Mo est hébergé par Apache sur mon ordinateur, je démarre le serveur relais avec perf stat, puis je télécharge le fichier à partir de Firefox sur Android. Dès que le fichier est téléchargé, je stoppe le serveur relais (Ctrl+C).

Voici les résultats pour la version Java :

$ perf stat -B java -jar gnirehtet.jar relay
 Performance counter stats for 'java -jar gnirehtet.jar relay':

      11805,458302      task-clock:u (msec)       #    0,088 CPUs utilized
                 0      context-switches:u        #    0,000 K/sec
                 0      cpu-migrations:u          #    0,000 K/sec
            28 618      page-faults:u             #    0,002 M/sec
    17 908 360 446      cycles:u                  #    1,517 GHz
    13 944 172 792      stalled-cycles-frontend:u #   77,86% frontend cycles idle
    18 437 279 663      instructions:u            #    1,03  insn per cycle
                                                  #    0,76  stalled cycles per insn
     3 088 215 431      branches:u                #  261,592 M/sec
        70 647 760      branch-misses:u           #    2,29% of all branches

     133,975117164 seconds time elapsed

Et pour la version Rust :

$ perf stat -B ./gnirehtet relay
 Performance counter stats for 'target/release/gnirehtet relay':

       2707,479968      task-clock:u (msec)       #    0,020 CPUs utilized
                 0      context-switches:u        #    0,000 K/sec
                 0      cpu-migrations:u          #    0,000 K/sec
             1 001      page-faults:u             #    0,370 K/sec
     1 011 527 340      cycles:u                  #    0,374 GHz
     2 033 810 378      stalled-cycles-frontend:u #  201,06% frontend cycles idle
       981 103 003      instructions:u            #    0,97  insn per cycle
                                                  #    2,07  stalled cycles per insn
        98 929 222      branches:u                #   36,539 M/sec
         3 220 527      branch-misses:u           #    3,26% of all branches

     133,766035253 seconds time elapsed

Je ne suis pas un expert pour analyser les résultats, mais de ce que je comprends de la valeur task-clock:u, la version Rust consomme 4× moins de temps CPU.

Conclusion

Réécrire Gnirehtet en Rust a été une formidable expérience, où j’ai appris un super langage et de nouveaux concepts de programmation. Et maintenant, nous avons une application native avec de meilleures performances.

Bon reverse tethering !

Gravatar de ®om
Original post of ®om.Votez pour ce billet sur Planet Libre.

Thuban : Bilan libre de septembre 2017 (n°10)

jeudi 21 septembre 2017 à 08:08

Un peu en avance ce mois-ci, mais je sais que la fin du mois ne sera pas très riche car l'emploi du temps est chargé. Voici donc un résumé de mes quelques contributions à des projets libres au cours du mois de septembre, en espérant vous donner envie d'en faire autant.

Manuel (auto)? hébergement
Concernant le manuel pour s'(auto?) héberger, j'ai bien avancé et je suis en mesure de le publier lorsqu'OpenBSD 6.2 le sera (d'ici un mois?). Les gains de la précédente version couvreront une publication simple (pas en librairie) et c'est très bien puisque de toutes façon il n'y a pas de ventes chez les libraires. Cependant, selon mes moyens, je voudrais accompagner le manuel d'un petit plus. À ce propos, ce sont vraiment vos remarques qui m'ont permi de le corriger/simplifier/améliorer. C'est en discutant avec des collègues néophytes que j'ai reçu une bonne claque :

- C'est super intéressant ce truc! Tu peux vraiment être tranquille au moins comme ça. C'est difficile?
- Ce n'est pas plus difficile que se mettre à jardiner. Il faut être soigneux surtout, et y aller étape par étape.
- Et ton bouquin là, il est long?
- Euh, environ 200 pages...
- Ah ouais quand même ! C'est pas si accessible que ça, s'il faut se taper 200 pages avant d'y arriver.

*VLAN.
C'est tellement vrai. Mis à part quelques êtres à parts, il faut être carrément motivé.
J'ai donc décidé d'accompagner le manuel complet par un e-book plus court qui contient des fiches pratiques. Vous y trouverez la recette pour avoir son serveur web, une autre pour avoir son serveur mail... Chaque chapitre ne dépasse pas les 4 pages (c'est le max) et va à l'essentiel.

Autres projets

Et voilà, ça sera tout :)

i'm out

Gravatar de Thuban
Original post of Thuban.Votez pour ce billet sur Planet Libre.

Carl Chenet : Déjà 100 abonnés au Courrier du hacker une semaine après son lancement

mercredi 20 septembre 2017 à 00:00

Une semaine après son lancement, le Courrier du hacker, une newsletter hebdomadaire publiée le vendredi qui propose les meilleurs liens du Logiciel Libre et Open Source francophones, vient de passer les 100 abonnés !

La première édition du Courrier du hacker a été publiée vendredi dernier à 80 abonnés. Le nombre d’abonnés avant la première édition a démontré l’intérêt d’une newsletter hebdomadaire récapitulative.

La source des liens du Courrier du hacker

En effet pour ceux qui ne peuvent pas se connecter régulièrement au Journal du hacker, il était dommage de passer à côté de nombres de liens très intéressants. D’où l’idée de rassembler au sein d’une newsletter arrivant directement dans vos e-mails le vendredi afin de la parcourir tranquillement pendant le week-end.

 

Si vous n’avez pas le temps de suivre l’évolution de l’actualité sur le Journal du hacker, n’hésitez pas à vous abonner via le formulaire ci-dessus ou directement sur le Courrier du hacker.

Gravatar de Carl Chenet
Original post of Carl Chenet.Votez pour ce billet sur Planet Libre.

Miamondo : Le logiciel libre, késako?

mardi 19 septembre 2017 à 22:45

Chères lectrices, chers lecteurs,

Je suis ravi d’accueillir le collectif Emmabuntüs en tant que nouvel auteur de Miamondo. Voici donc le tout premier article signé « Emmabuntüs » accompagné d’un beau dessin de Péhä. Cet article a également été publié aujourd’hui même dans le journal  l’âge de faire. Je vous souhaite une bonne lecture.

Benoît.


peha_logiciel_libre

 

Pour bien débuter cette nouvelle saison, revenons sur la définition du logiciel libre, que certains d’entre vous confondent avec un logiciel gratuit dénommé « Freeware » ou gratuiciel. Or, on va voir que la gratuité n’est pas la principale qualité d’un logiciel libre, et que les logiciels gratuits (gratuiciels) ne sont pas nécessairement libres… Cet amalgame vient du fait qu’il y a une ambiguïté en langue anglaise autour du mot « free » qui peut signifier libre aussi bien que gratuit. Donc dans notre contexte, « Free software » se traduit par « logiciel libre » et non pas « gratuit ». D’ailleurs, un certain nombre d’anglophones utilisent désormais le vocable « Software libre » pour bien marquer la différence.

Alors pourquoi parle-t-on de liberté pour un logiciel ? Cela a plus de sens pour les êtres vivants que pour du virtuel ! En fait, il s’agit d’une liberté d’usage pour vous, pour nous, qui est constituée de 4 libertés numérotées de 0 à 3 (humour typique des informaticiens, Ndlr) par la Free Software Foundation, permettant de définir le logiciel comme libre :

0. la liberté d’exécuter le programme par vous, pour tous vos usages.
1. la liberté d’étudier le fonctionnement du programme et de l’adapter à vos besoins.
2. la liberté de redistribuer des copies du programme -ce qui implique la possibilité de donner, aussi bien que de vendre des copies. Par exemple en France, le chiffre d’affaires du logiciel libre représentait 4,5 Milliards d’euros en 2016.
3. la liberté d’améliorer le programme et de distribuer vos améliorations au public, pour en faire profiter tout le monde.

Quand le logiciel ne remplit pas ces 4 libertés, il est qualifié de propriétaire (ou de privateur) même s’il est distribué gratuitement. En l’utilisant, vous acceptez implicitement la licence, qui vous interdit de l’étudier, de le modifier, de le céder à des tiers. Il ne vous appartient pas, vous avez simplement un droit d’usage très limité.

Pour compléter la présentation du logiciel libre, voici une réflexion le concernant, proposée par Richard Stallman, qui fonda il y a 30 ans ce mouvement libertaire, ainsi que la Free Software Foundation :

« Quand les utilisateurs ne contrôlent pas le programme, c’est le programme qui contrôle les utilisateurs. Le développeur contrôle le programme, et par ce biais, contrôle les utilisateurs. Ce programme non libre, ou « privateur », devient donc l’instrument d’un pouvoir injuste. »

A partir du principe du logiciel libre, sont nés de grands logiciels que vous utilisez tous les jours, comme Wikipédia, Linux (la base, entre autres, d’Android), Firefox, LibreOffice, VLC, etc. Preuve que la liberté est gage de qualité et d’utilisation pour tous !

Le concept de la licence du logiciel libre (GPL : GNU General Public License) a aussi permis la création des formats ouverts. Avec les formats, on ne fait pas référence au logiciel global, mais à la structure même des données du logiciel, qui est donc ouvertement publiée, ceci permettant à des développeurs de créer des logiciels pour traiter ces données dans le présent mais aussi dans le futur, comme la Pierre de Rosette a permis à Jean-François Champollion de déchiffrer les hiéroglyphes. Dans un format propriétaire, si l’éditeur arrête de développer son logiciel pour lire vos données, celles-ci deviennent de facto illisibles, car personne ne sait, et n’a souvent même pas le droit de savoir, comment elles sont codées 😦

Dans cet article, nous avons uniquement employé le terme logiciel libre, et non pas celui d’Open Source, que vous pouvez entendre ou lire dans les médias. Cela n’est pas une erreur de notre part, car ces deux concepts, bien que poursuivant un but similaire (celui de donner une plus grande liberté d’usage aux utilisateurs de logiciels) ont cependant une différence fondamentale qui réside dans leurs philosophies respectives. Selon Richard Stallman dont nous partageons complètement l’analyse,

«  L’open source est une méthodologie de développement; le logiciel libre est un mouvement social ».

Voici deux articles, qui vous permettrons de former votre propre opinion sur ce sujet :
Pourquoi l’expression « logiciel libre » est meilleure qu’« open source »
En quoi l’open source perd de vue l’éthique du logiciel libre

Le mois prochain dans L’âge de faire : comment trouver des ressources libres : musique, images, etc.

Collectif EmmabuntüsGPL V3
Dessin : Péhä CC-BY-SA 2017

 


Gravatar de Miamondo
Original post of Miamondo.Votez pour ce billet sur Planet Libre.