Greg's Devblog Par un développeur, pour les développeurs

13oct/102

Javaz : Compiler du Java en bytecode Caml

Dans le cadre de la matière "Typage et Polymorphisme", à la fac, on avait un choix de sujet (projets ou articles), et avec mon binôme aucun ne nous plaisait. Du coup il a voulu qu'on propose au prof notre propre sujet : compiler des sources Java en bytecode Caml...

Bon, le prof n'était pas trop content, mais il a fini par accepter le projet. On a déjà un petit truc basique qui tourne, et c'est assez intéressant. Le tout est écrit en OCaml, comme ça c'est l'occasion pour moi de m'y remettre un peu (après 1 ou 2 ans sans y avoir touché), et il faut dire que ça se prête particulièrement bien à la tache.

Les différentes étapes du projet sont relativement classiques :

1. Conversion en Ast

On commence donc par convertir les sources en Ast (Arbre de syntaxe abstraite), grâce à Yacc et Lex. En gros, c'est un parcours du fichier source, et on transforme tous les mots-clés et autres éléments repérés sous forme d'arbre.

Ainsi, l'expression (2 + 3) devient un noeud "opération + ", qui a "2" et "3" comme noeuds. Et donc par extension, 2 + 3 * 4 devient "*" avec comme fils gauche "+" (ayant lui-même les fils "2" et "3") et à droite "4". C'est une façon de parser qui revient à mettre le parenthésage implicite qui va bien, et qui permet en fait de calculer le résultat très facilement et récursivement (on applique le méthode de calcul sur le fils gauche, puis sur le fils droit, et fait l'opération correspondante sur le résultat).

La transformation en Ast permet déjà de vérifier si la syntaxe est valide selon la norme...

2. Typage de l'Ast

Une fois qu'on a cet Ast, on va chercher à le typer, ce qui va permettre au passage de vérifier la validité de tout un tas de trucs, et d'appliquer les casts implicites. En Java, le typage est explicite, donc le typeur n'a pas un énorme travail à faire pour "déduire" des types, il se contente de regarder ce qu'il a un peu partout et de valider ou pas. On a ainsi plusieurs opérations d'effectuées :

  • Typage de tous les noeuds de l'Ast : à la fin on obtient un Ast typé, correspondant au fait à tous les noeuds de l'Ast original (plus quelques nouveaux) auquel on ajoute le type résultant. Si on a un noeud "+" qui a comme fils un entier et un floatant, le noeud aura donc comme type "float".
  • Insertion des casts implicites : à chaque fois qu'on a 2 types différents qui se "rencontrent", on regarde comment caster l'un en l'autre de façon implicite. Ainsi, si on a une opération "+" qui prend un entier et un string, on va rajouter le cast de l'entier en string pour faire une concaténation de chaines... De même, si on affecte un entier à une variable qui est de type floatant, on va rajouter le cast. Et en fait on se rend compte qu'on a pas mal de cas, notamment à cause de tous les types d'entiers (byte, char, short, int, long).
  • Vérification de la validité des variables : pendant le parcours, on va "noter" toutes les variables locales que l'on rencontre (ainsi que les champs et paramètres, en fait), et donc quand on utilise une variable dans une méthode, on va tout de suite pouvoir vérifier si elle existe. Le cas échéant, on s'arrête avec un message d'erreur de type "Undeclared Variable XXX". Si elle existe, on va en profiter pour transformer le nom de la variable en une information un peu plus pratique le générateur de code pourra manipuler plus facilement : dans notre cas, on a choisi de représenter une variable locale par un couple d'entiers, le premier représentant le bloc relatif au courant auquel elle appartient, et le second le numéro de la variable dans ce bloc (par ordre de déclaration). Il est donc très facile pour le générateur de code de retrouver la variable dans la pile : soit c'est dans le bloc courant, donc on récupère sa position par rapport au pointeur de pile, soit c'est dans un bloc parent, et dans ce cas il suffit de connaitre le pointeur de pile correspondant à ce parent pour retrouver la position de la variable dans la pile.
  • Vérification des méthodes : on va appliquer le même principe aux méthodes (pas tout à fait fini, mais c'est en cours), vérifier que la méthode existe bien (et est accessible) dans un objet de cette classe, etc...
  • Transformation des boucles for en boucles while, histoire de ne pas se trainer des cas particuliers tout le temps. En cours de route on est tombé sur une petite subtilité : on ne peut pas se contenter de fusioner les instructions du corps avec le code à exécuter à la fin de chaque tour de boucle, car si on fait un "continue" au milieu du bloc, il faut bien exécuter les dernières instructions... Du coup la boucle while possède en fait 2 blocs : un bloc d'instruction pour le corps de la boucle, et une expression à évaluer après l'évaluation du bloc... Pour transformer la boucle for en boucle while, on créé juste un premier bloc d'instructions comprenant l'initialisation de la boucle, ainsi que la boucle while résultante. Ce qui permet de gérer automatiquement les variables déclarées à la volée dans la boucle for :)

Il reste encore pas mal de boulot au niveau du typeur, et c'est notamment lui qui va implémenter une première version de la gestion de surcharges...

3. Interprétation de l'Ast typé (optionnel)

Cette étape est un peu superflue et sert essentiellement pour des raisons de tests, car elle permet de tester le résultat de l'arbre typé en l'exécutant... Comme on tourne dans du code maison, on peut rajouter des affichages de debug un peu partout pour voir ce qui se passe. Et comme l'Ast est typé, l'interpréteur peut en fait vérifier les types à tout moment, sachant que jusqu'à maintenant la seule fois ou un type a été explosé (à savoir, une variable de type boolean contenant la valeur entière "0"), c'est quand j'ai fait péter le pointeur de pile ^^. C'est bien pratique pour repérer les conneries au niveau du typeur, comme des index de variables locales mal mis.

Les performances de l'interpréteur sont bien entendu catastrophique, puisqu'on ne fait que se balader dans un arbre dans un peu tous les sens, en récupérant les fils, puis en remontant, etc...

4. Génération de code

Pour l'instant, seul mon binôme s'est intéressé à cette partie, donc je n'ai pas encore mis les mains dedans. Mais en gros le principe est de prendre chacune des instructions/expressions de l'Ast typé, et de la traduire en code machine correspondante. On écrit ici du bytecode Caml, mais dans le fond c'est un peu la même démarche que quand on génère de l'assembleur (sauf que là ça serait un assembleur de plus haut niveau avec des instructions sympas ^^). Donc voilà, on pousse des valeurs sur la pile, on met le bytecode correspondant à l'addition, etc... C'est un peu long et fastidieux pour l'instant, mais quand on va arriver à la génération du code pour les objets ça devrait être assez sympa :p

Mots de la fin

Le projet n'est pas encore terminé, mais en quelques jours on a déjà bien avancé. C'est vraiment très instructif, car on en apprend à la fois sur Java (avec les spécifications à la con... c'est comme ça que j'ai appris qu'on pouvait mettre un label aux blocs pour ensuite appliquer ce label sur les continue/break !), sur Caml (car on a déjà plusieurs milliers de lignes de code, donc ça fait une bonne base de travail ^^), sur le typage en général, etc... En fait c'est une manière assez rapide de synthétiser tout un ensemble de cours que j'ai eu à la fac depuis 1 an (et d'autres en cours), donc c'est top !

Après, quant à savoir si le projet pourrait avoir un jour un intérêt autre que le délire de l'avoir fait, on est en train de se poser la question, et a priori... non ! Mais techniquement, ça marche déjà : on a fait plusieurs tests, notamment un qui consiste à calculer les nombres premiers dans les 10 000 premiers entiers, et on obtient bien le résultat voulu :)

12oct/100

Android : le RelativeLayout, c’est bien !

Dans l'application qu'on développe au boulot, presque toute la mise en forme est faite à partie de LinearLayout...

Ca a un côté à la fois simple et pratique, car au final on retombe sur un système de boite relativement classique : on peut créer une boite qui est soit verticale, soit horizontale, et on peut mettre autant de widgets qu'on veut dedans... Du coup on n'a pas de mauvaise surprise quand on fait tourner l'application, c'est clair et facile à visualiser.

Seulement voilà, il a fallu que je fasse un "TreeView" (une vue arborescente, façon explorateur windows où on peut cliquer sur les croix pour ouvrir un groupe, le tout à l'infini...), et là, au-delà de 3-4 niveaux de profondeur, on explose la pile lors de l'affichage (lors des appels des méthodes "draw").

Et là en regardant de plus près le fonctionnement de notre TreeView, le problème ressort assez rapidement : chaque item est un LinearLayout horizontal, dans lequel on met à gauche une image (pour ouvrir/fermer), et à droite du texte et les éléments "fils". On a donc à droite un LinearLayout vertical avec en haut le texte, et en bas un autre LinearLayout vertical, qui contient tous les enfants... Et pour le fun, comme il a fallu mettre un numéro devant le texte, le texte s'est transformé en un LinearLayout horizontal avec des textes de chaque côté...

Je vous épargne le calcul, mais en gros dès qu'on descend un peu dans l'arbre, on se retrouve à avoir des dizaines de LinearLayouts imbriqués les uns dans les autres, et ça, c'est fortement déconseillé. Je ne sais pas trop en pratique comment le rendu est fait, mais il n'est pas impossible que pour chaque ViewGroup (et donc LinearLayout, qui en hérite) Android fasse un rendu, puis que ce rendu soit utilisé pour le rendu du ViewGroup suivant, etc... Sauf que si dans notre cas on a autant de micro-LinearLayouts de partout imbriqués, on fait effectivement rapidement une quantité énorme de rendus, largement de quoi exploser la limite de mémoire imposée par Android... Et puis indépendamment de ça, c'est mal d'imbriquer autant de layouts les uns dans les autres car ça met pas mal de stress sur la routine qui s'occupe de calculer récursivement la taille de tous les éléments à afficher...

Il existe en fait une solution simple, et finalement beaucoup plus lisible : le RelativeLayout. Dans ce layout, la règle est simple : on définit les positions des objets relativement à leurs voisins et au layout. Donc on va par exemple dire que l'image est collée en haut à gauche, tandis que le texte est collé en haut du layout, et se trouve à droite de l'image.

Ca marche super bien, c'est très efficace, et ça permet de remplacer tout un tas de LinearLayouts par un seul RelativeLayout. On aurait probablement pu utiliser un des autres layouts, comme le GridLayout, mais je ne voulais pas trop du côté super "tableau" de la chose... Avec le RelativeLayout, on fait globalement ce qu'on veut, du moment qu'on arrive à définir les relations entre les widgets.

Si ça vous intéresse, plus d'informations sont disponibles sur le site officiel d'Android, avec une très bonne explication de la différence entre LinearLayout et RelativeLayout.

11oct/101

J programming language

Non, ce n'est pas du Java, c'est du J ! En voilà un langage un peu obscure dont pas grand monde ne doit se servir ^^

Je ne vais pas m'étendre sur le sujet, car je n'y connais absolument rien, mais je pense que ça vaut le coup de voir quelques lignes de code dans ce langage :

avg=: +/ % #
avg 1 2 3 4
2.5

Simple calcul de la valeur moyenne d'une liste. Le + pour l'addition des termes de la liste, le % pour la division, et # représente le nombre d'éléments dans la liste (en gros, on additionne tous les éléments, puis on divise par la taille ; ce qui fait bien la moyenne)

Bon, jusque là c'est facile, alors je vais plutôt vous proposer le quicksort :

quicksort=: (($:@(<#[) , (=#[) , $:@(>#[)) ({~ ?@#)) ^: (1<#)

Que du bonheur, non ?

10oct/100

Créer, trier, et afficher une liste de 100 entiers aléatoires…

Le hasard faisant bien les choses, ce matin je suis tombé sur une questions sur stackoverflow.com qui est finalement assez sympa : écrire le code le plus court possible pour créer, trier, et afficher une liste de 100 entiers aléatoires, dans le langage de son choix...

On peut voir ça comme un concours de bite inversé, à savoir que là il faut avoir la plus petite ^^ Mais en tout cas c'est aussi l'occasion de découvrir des techniques secrètes et des langages foireux (dont le J, qui fera l'objet d'un billet dans peu de temps...).

Je dirai que dans l'ensemble les langages fonctionnels sont à l'honneur, car ils sont à la fois courts et lisibles. Petit exemple en lisp :

(sort (loop repeat 100 collect (random 10000)) #'<)

Et puis un autre exemple, en Ruby cette fois-ci :

p (0..?d).map{rand 1<<32}.sort

Et vous, quels sont vos préférés ?

9oct/100

Java : break label

Parmi les différents projets que j'ai en cours, j'en ai un encore top secret (^^) qui implique de parser du code Java. Pour la beauté de la chose, c'est bien entendu fait en OCaml (amen), mais ce n'est pas du tout le sujet de ce billet.

En fait, parfois je trouve des trucs qui me paraissent bizarres dans la BNF (Backus Normal Form, une notation qui permet de décrire la syntaxe d'un langage), et là je viens de découvrir qu'on peut ajouter un label à un break... Du coup j'ai fait une petite recherche Google pour voir à quoi ça ressemble, et je suis tombé sur un site qui donne un exemple sympa de la chose :

OuterLoop: for (int i = 2;; i++) {
	for (int j = 2; j < i; j++) {
		if (i % j == 0) {
			continue OuterLoop;
		}
	}

	System.out.println(i);

	if (i == 37) {
		break OuterLoop;
	}
}

Dans cet exemple, on a le label qui est utilisé à la fois sur un break et sur un continue. Quel en est le principe : le label permet tout simplement de désigner la boucle sur laquelle on veut que l'action s'effectue. Donc on peut avoir de multiples boucles for/while imbriquées, grâce à ça on peut sortir de tous les niveaux d'un coup, ou juste de quelques uns.

Pas grand chose de plus à ajouter... Ah si, qui sait ce que fait ce code ?



Remplis sous: Java Aucun commentaire
8oct/100

iOS – Error launching remote program

L'iOS SDK, c'est trop cool : à chaque fois qu'on lance XCode et qu'on compile, c'est comme jouer à la roulette russe, on peut avoir une erreur ou une merde qui va nous faire perdre la soirée alors que la veille tout marchait super et qu'on n'y a pas touché...

Ce soir, c'était la compilation en mode AdHoc / AppStore qui s'est mise à merder :

Error launching remote program - The program being debugged is not being run.

Sur le net, on trouve une variété de solutions à ce problème, dont en voici une petite sélection... A noter que les solutions ne sont pas mutuellement exclusives, on peut en tester plusieurs pour espérer obtenir un résultat :

  • Redémarrer XCode, le device, et/ou le Mac. Assez efficace en général.
  • Faire un "Clean All" sur le projet.
  • Effacer tous les provisionning profiles du device, tous les certificats du mac, les retirer du site d'Apple, et tous les réinstaller.
  • Dans Entitlements.plist, cocher get-task-allow. A noter que chez moi ça donne juste un message d'erreur différent, et qu'en théorie il faut que ce soit décoché ^^

Aucune de ces solutions n'a marché dans mon cas, et en fait c'est mon collègue Jesus (oui, c'est vraiment son prénom !) qui a trouvé la solution : en vrai, quand on lance après avoir compilé, ça ne marche pas... Par contre, si on clique sur l'icone sur l'iPad, l'application se lance et marche normalement. Donc en gros, c'est juste le lancement depuis XCode qui chie. Bon, je n'ai perdu qu'une ou deux heures sur ce problème...

Ca peut paraitre bizarre, mais en fait en cherchant encore un peu sur un site j'ai fini par trouver une explication : en pratique, en mode AdHoc, ce n'est juste pas possible de debugger (ça, on est d'accord), et donc en fait le lancement depuis XCode, qui cherche à se connecter au device pour le debug (même basique, à savoir l'affichage de la console), échoue. Donc on pourrait dire qu'il y a une vraie raison derrière ce problème, et en regardant bien le message d'erreur, c'est effectivement ce qu'il nous indique...

Ce qui n'empêche pas que chez certaines personnes ça marche, et surtout que parfois (pour ne pas dire souvent) quand on a cette erreur les corrections évoquées ci-dessous sont "valables"... Parfois je crois qu'il ne vaut mieux pas essayer de comprendre...

Taggé comme: , , , , Aucun commentaire
6oct/100

Mandelbrot en parallèle

Aujourd'hui à la fac, c'était programmation parallèle d'un Mandelbrot. C'est du fractal, et ça se prête très bien à de la programmation parallèle, puisqu'en fait on itère N fois sur chaque pixel, indépendamment des voisins, pour savoir si on doit l'afficher ou pas (et avec quelle couleur).

La conception du code s'est faite en plusieurs phases :

1. Version séquentielle

Exécution du code en version séquentielle, histoire de voir le temps que ça prend et d'avoir un résultat qui puisse servir d'étalon... Résultat : entre 7 et 8 secondes pour générer l'image avec les paramètres par défaut.

2. Version parallèle, statique.

Réalisation d'une version pouvant tourner en parallèle avec N processus. La méthode est relativement simple et naïve : au démarrage de chaque processus, on récupère le numéro du processus (de 0 à N-1), ainsi que le nombre total de processus lancés. A partir de là, on découpe l'image en N bandes (on garde toute la largeur, on découpe en hauteur), chaque processus calcule sa portion, puis tout le monde envoie la portion calculée au processus 0, qui enregistre l'image finale (et affiche le temps total).

Et là, surprise, même avec 8 ou 10 processus, le gain est assez faible : on tourne en 2.5 à 3.5 secondes, soit à peine 2 à 3 fois plus vite (alors qu'on a pourtant 4 à 5 fois plus de puissance, normalement !).

La raison est en fait rapidement trouvée : les lignes du milieu mettent beaucoup plus de temps à calculer que les lignes aux extrémités (en fait tout ce qui est noir sur l'image a mis longtemps à être calculé). Du coup, les processus qui se tapent les bords en haut et en bas finissent super vite (en 0.05 secondes !), alors que les processus qui ont le milieu à calculer mettent plus de 2 secondes... C'est du beau gachi de puissance, il convient donc de concevoir un truc un peu plus dynamique qui puisse répartir au mieux la charge...

3. Version parallèle, mais dynamique !

La dernière version à être réalisée va donc avoir une approche beaucoup moins statique : le processus 0 ne va rien calculer, mais va en fait servir de chef d'orchestre, et va dire aux autres quoi calculer. A chaque fois qu'un processus a fini de calculer une portion de l'image, il l'envoie au processus 0, qui va ensuite lui redonner un ensemble de ligne à calculer.

Cette fois-ci, on va donc envoyer aux processus des blocs de 1 à 8 lignes à calculer (limites arbitraires et qu'on peut changer pour optimiser les choses), et donc au pire si un processus met beaucoup de temps pour calculer les 8 lignes du milieu, pendant ce temps les autres processus calculeront peut-être 32 lignes ou plus, puisque la charge est répartie dynamiquement !

Et là, le résultat est sans appel, c'est beaucoup mieux : on arrive sous la barre de la seconde, dès qu'on a 8 processus ou plus ! Et pourtant, le processus 0 ne calcule rien, il ne fait que récupérer le résultat des autres :p  On a donc à présent un système qui tient bien la charge, qui peut supporter un grand nombre de processus, et qui s'adapte aux besoins. On peut même avoir un parc hétérogène de machines, ce n'est plus gênant.

4. Pour aller plus loin...

On peut à présent réfléchir à des optimisations pour ce système.

En fait, la faille est que cette fois-ci on passe pas mal de temps dans les communications à cause des transferts de taille relativement faibles. De plus, si tous les processus finissent de calculer à peu près en même temps, alors ils vont tous devoir attendre que le processus 0 ait reçu les messages pour continuer, donc on a tout de même une partie du temps qui est perdu en communication et surtout en attente de communication. Plusieurs stratégies peuvent permettre de limiter ce temps, en voici quelques une en vrac :

  • Faire des paquets plus gros : si on fait des paquets plus gros, on fera au total moins de communications, donc en théorie on devrait aller plus vite. Sauf qu'on s'expose au risque d'avoir à attendre à la fin un processus trop lent qui aurait eu un gros bloc à finir... On pourrait donc imaginer faire calculer des blocs de grande taille au début, et réduire la taille et fur et à mesure de l'avancement. Comme ça, à la fin, tout le monde aura de petites taches, et devrait finir sensiblement en même temps...
  • Permettre au processus 0 de travailler ! Pourquoi devrait-il perdre son temps à attendre les autres ? En fait, il pourrait s'allouer une ligne à calculer, mais regarder tous les X pixels s'il n'a pas une communication en attente. Comme ça, il participe aussi au calcul, et on devrait gagner un peu (mais plus on aura de processus, moins ça sera intéressant, car plus il devra communiquer de toute façon)
  • Dans la version actuelle, les communications sont synchrones : quand on demande à avoir des lignes à calculer, on est bloqué en attendant que le processus maître réponde... Une meilleure stratégie pourrait être d'envoyer la demande de façon asynchrone (ie, non bloquante) quand on arrive au milieu du calcul, de façon à avoir la réponse sous le coude à la fin du calcul. Comme ça, dès que c'est fini, on a de fortes chances d'avoir le message près en réception. Dans le même genre, on pourrait imaginer se voir confier 2 taches à la fois par le processus maître, et donc quand on fini la première on en redemande une, etc... Je pense qu'au final ça revient sensiblement au même, à voir.
  • Mieux répartir la charge dès le départ. Cette stratégie est celle préconisée par Mathias, tout le mérite lui en revient ^^ En gros, on peut imaginer calculer au début une version simplifiée (calculée plus rapidement, ou alors plus petite) afin de localiser plus rapidement les zones noires, et donc dès le départ faire des paquets de taille variable selon la charge de travail estimée (avec les options par défaut, le temps pour traiter un pixel varie d'un facteur 10 000 entre les pixels les plus rapides à calculer et les plus longs). Au lieu d'envoyer des paquets de taille relativement fixe (et donc représentant une charge de travail très variable), on peut alors envoyer des paquets dont les charges effectives sont proches.
  • Tenter de répartir un peu mieux les temps de communications entre les différents processus : là le facteur ralentissant est que tout le monde communique avec le processus 0. On pourrait imaginer sous-découper les processus et l'image en 2 groupes (voire plus), leur faire se débrouiller avec ça, et puis à la fin rassembler les lignes calculées. On limite alors les communications car 2 processus (voire plus) vont centraliser les appels. Dans cette optique, on pourrait avoir une partie des processus qui remplissent l'image par le haut, et l'autre partie par le bas : quand les 2 groupes se sont rejoints, on s'arrête !

Et alors ?

Bah rien ^^ J'ai trouvé ça sympa comme sujet, donc je me suis dit que j'en ferai un billet :p Bon, c'est sûr que c'est plus facile de faire ce genre de chose à la fac où on peut monopoliser 16 PCs en même temps (dual core, donc potentiellement 32 processus à pleine vitesse), alors que chez soi on a bien moins de matériel... Je devrais peut-être économiser pour installer une ferme de PCs chez moi en fait :p

Remplis sous: Algo, C/C++ Aucun commentaire
5oct/100

Java : normaliser un String

Dernièrement je fais un ensemble de classes "dictionnaire" en Java pour l'utilisation dans des livres électroniques, et histoire de pouvoir facilement comparer ce qu'on tape avec les mots de la base, il est important que tout soit normalisé... On doit donc retirer tout ce qui est accent, en particulier, et ce n'est pas forcément très intuitif à faire quand on ne l'a jamais fait...

For heureusement, en Java on dispose déjà de tous les outils nécessaires pour gérer cela. Le code que j'utilise effectue en gros 2 passes :

  • Une première pour tout mettre en majuscules (ça simplifie la recherche par la suite). Il suffit d'utiliser mla méthode toUppercase sur la chaine en question.
  • Une deuxième pour retirer les accents. J'utilise pour se faire la méthode suivante :
public static String removeDiacriticalMarks(String string) {
    return Normalizer.normalize(string, Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
}

Avec ça, on récupère tout simplement la chaîne privée de tous ses accents, et on est tranquille !

2oct/100

Non, il n’y a pas de comparaisons ternaires en C !

Un jour en TP à la fac un autre étudiant me demandait pourquoi je tapais le code suivant :

if ((0 < x) && (x < 2)){
    ...
}

Personnellement, ça ne me parait pas choquant... Mais lui me soutenait qu'en C on peut faire beaucoup plus "joli", avec le code suivant, qui parait effectivement plus naturel :

if (0 < x < 2){
    ...
}

Bon, je dirais, pourquoi pas... Après tout, ça compile, donc comme il semble le penser, si ça compile, c'est que ça marche ! Mais qu'en est-il en réalité ?
En pratique, l'opérateur de comparaison '<' (ainsi que tous ses acolytes) est un opérateur binaire, qui ne prend donc que 2 opérandes. Alors comment fonctionne cette forme avec 2 opérateurs et 3 opérandes ?

Petit quiz express : qui renvoie le code suivant ?

    printf("%i\n", (0 < 3 < 2));

Si vous avez répondu '0', vous avez perdu ! En effet, ça renvoie '1'. En fait, on pourrait changer le 3 par n'importe quelle valeur, on aurait toujours 1. Une fois qu'on a compris que cet opérateur est binaire, et qu'on applique donc le parenthésage par défaut défaut, on voit clairement à quoi correspond ce code :

(0 < 3) < 2

Si ce code compile, c'est uniquement parce qu'on peut effectivement l'évaluer "correctement". (0 < 3) donne un booléen, mais en C un booléen est juste un entier ayant pour valeurs 0 ou 1, donc on peut comparer ce booléen de la même manière avec 2. Or, (0 < 2) et (1 < 2) renvoient toujours "1", quoi qu'il arrive. Donc ce test est juste foireux.

Ceci fait ressortir à nouveau l'importance de mettre des parenthèses, mais aussi les limites du typage en C : comme tout est un peu du même type (tout est plus ou moins entier, sauf les pointeurs, mais si on veut on peut les caster de toute façon), on peut écrire des opérations qui sont parfaitement fausses. Le même code en Java ne compilerait a priori pas (je n'ai pas testé), puisque la comparaison entre un booléen et un entier n'est pas autorisée.

Une fois encore, il ne faut surtout pas partir du principe que ça fonctionne si on n'a pas vérifié que le langage le supporte ! Et une compilation réussie ne garanti en aucun cas que le code va faire ce qu'on pense qu'il va faire !

29sept/102

Importance des parenthèses

J'ai déjà eu le cas d'un programmeur qui ne mettait quasiment jamais de parenthèses, genre "ça sert à rien". Un if peut rapidement avoir une tête du style :

if (a == 2 && b == 3 && c == 4 && ... && z == -2)

Je trouve ça relativement peu lisible, au final, et je rajoute toujours les parenthèses qui vont bien. Voici donc un petit code intéressant pour illustrer les soucis potentiels :

printf("%i %i %i\n", (-2)&0xFF, 254, (-2)&0xFF == 254);

Alors... A votre avis, qu'est-ce qui va être affiché ?

  • (-2)&0xFF affiche 254
  • 254 affiche... 254 (ok, c'était facile)
  • (-2)&0xFF == 254 affiche... 0 !

Et oui, on pourrait s'attendre à ce que ça affiche 1 (puisque (-2)&0xFF vaut 254), mais en pratique ce n'est pas le cas. Alors pourquoi ?

Tout simplement parce que ce qu'on obtient comme résultat est en fait le résultat de (-2) & (0xFF == 254). Comme 0xFF vaut 255, (0xFF == 254) vaut 0, et donc (-2) & 0 vaut 0... Dommage !

En fait, il vaut mieux mettre des parenthèses inutiles que de ne pas en mettre du tout... Car même avec trop de parenthèses, on sait ce qu'on va obtenir, alors que si on les retire cela signifie qu'on se repose complètement sur la priorité décidée par le compilo (ou plutôt la norme). Pas forcémnet une bonne chose à faire si on ne maitrise pas parfaitement le sujet !

Pour rebondir un peu sur ce cas (qui parait un peu improbable à cause du -2 qui traine), voici un que j'ai construit et qui renvoie toujours TRUE :

1&0xFF == 255
27sept/104

Qu’affiche le code suivant ?

Sous ce titre un peu racoleur se cache pourtant un phénomène assez étrange, qui fait qu'en C il n'est pas toujours facile de déterminer le résultat d'une instruction...

Voici donc un petit programme rapide, de quelques lignes ; je vous invite à essayer de deviner ce qu'il affiche...

#include <stdio.h>

int main(int argc, char **argv){
	int w = 0, x = 0, y = 0, z = 0;

	printf("w : %i %i\n", w++, w);
	printf("x : %i %i\n", x++, x+1);
	printf("y : %i %i\n", y, y++);
	printf("z : %i %i\n", z+1, z++);
	return 0;
}

Ca va paraître bizarre, mais en compilant le code sous OSX avec GCC je n'ai pas obtenu le même résultat que sous Windows (pourtant toujours avec GCC, mais probablement avec des options de compilation différentes). En utilisant cygwin et en compilant en ligne de commande (sous Windows, donc), j'obtiens bien le même résultat que sous OSX et Linux... Voici donc les 2 résultats obtenus :

Avec Code::Blocks

w : 0 0
x : 0 1
y : 1 0
z : 2 0

Ici, le résultat parait dans l'ensemble cohérent, si en tient compte d'un élément bizarre : on dirait que les opérations sont effectuées de droite à gauche : si dans la partie de droite on incrémente la variable, alors dans la partie de gauche la valeur sera (valeur+1), comme en témoignent les lignes 3 et 4.

Avec GCC sans option

w : 0 1
x : 0 1
y : 1 0
z : 2 0

Autre résultat, autre constant : cette fois-ci le résultat parait moins cohérent. En effet, les lignes 1 et 2, affichent le même résultat, alors que dans un cas on affiche "w", et dans l'autre "x+1"... L'explication semblerait être qu'on effectue en premier les opérations, de droite à gauche, puis on met les valeurs. Donc pour "w++, w", on incrémente d'abord w, puis on met les valeurs dans les arugments, alors que pour "x++, x+1", on calcule en premier x+1, puis on incremente avec x++, et les 2 valeurs stockées sont utilisées pour les arguments.

Que peut-on déduire de cet exemple foireux ?

  • Savoir exactement ce que fait un code n'est pas forcément simple.
  • Les warnings ne sont pas là pour rien... En l’occurrence, en activant les warnings on obtient ici "warning: operation on `w' may be undefined"... Pourtant, pas mal de monde semble considérer que "si ça compile, c'est que c'est bon", et on se retrouve avec des programmes bourrés de warnings (et donc potentiellement de bugs dans ce genre).

D'ailleurs, j'utilise souvent Code::Blocks, qui a une manie assez bizarre : si on compile avec "build and run" et qu'il n'y a pas d'erreur, et même s'il y a des centaines de warnings, Clode::Blocks vide le log de compilation et n'affiche rien... Il faut alors recompiler (sans exécuter) pour voir apparaître les warnings (pour peu qu'on ait fait une modification rapide dans un des fichiers pour "forcer" la recompilation...).

Méfiance !

Taggé comme: , , 4 Commentaires
26sept/105

Unsigned Byte en Java ?

java

L'autre jour, j'ai eu besoin de lire un tableau d'octets qui étaient en non signés... On converti donc les données en bytearray Java qui va bien, et là, quand j'affiche les valeurs : -86, -126, etc... Et oui, en Java un byte est signé ! Et sauf erreur de ma part (après quelques courtes recherches), pas moyen d'avoir de bytes non signés...

La solution existe pourtant, mais je ne la trouve ni élégante, ni pratique : il suffit d'utiliser :

bytearray[i]&0xFF

En effet, pour les opérations les bytes sont castés en int, et avec le ET binaire, on récupère une valeur entre 0 et 255. Du coup ça marche super... Mais ça reste un poil lourd, car au lieu d'avoir un vrai beau type, partout où on veut utiliser des octets non signés, il faut faire cette manip...

18sept/100

Java : classe interne et syntaxe sympa

Un truc que j'aime bien en Java est la possibilité de faire des classes internes, mais surtout de les déclarer à la volée, comme un porc, en ajoutant à l'endroit où on les créé les méthodes à implémenter... En gros, ce genre de chose :

class MyClass
{
	...
	public void init(){
		truc.setListener(new Listener(){
			public void onListenerAction(int action){
				...
			}
		});
	}
	...
}

Ca évite quand même de créer des micro-classes un peu partout, et puis comme ça la classe interne a quand même accès aux champs de la classe d'origine, donc c'est pratique.

Et puis comme je suis un peu un barbare, je me suis demandé comment on faisait si depuis la méthode onListenerAction on voulait utiliser un champs de MyClass qui porte le même nom qu'un des arguments... C'est con, puisque de toute façon dans ce cas il suffit de renommer l'argument, mais pas grave, j'ai quand même tester.

Normalement, on ferait "this.myField", mais comme le this se réfère à la classe interne, ça ne marche pas... Au début, je me suis dit que ça devait être comme en C++, on refile le nom de la classe d'où vient le champs, et ça suffit pour le retrouver. Donc j'ai testé ça :

class MyClass
{
	private int action;
	...
	public void init(){
		truc.setListener(new Listener(){
			public void onListenerAction(int action){
				MyClass.action = action;
			}
		});
	}
	...
}

Sauf que... ca ne marche pas ! Et le message d'erreur est simple : ce champs n'est pas statique, donc on ne peut pas l'utiliser directement sur MyClass... Jusque là, c'est logique, mais alors comment on fait ?

En fait, ce qu'on veut c'est l'instance de la classe MyClass depuis laquelle on a été appelé. Et c'est là que ça devient fun, car du coup on fait "MyClass.this".
Donc au final, on se retrouve à faire

public void onListenerAction(int action){
	MyClass.this.action = action;
}

Et voilà le travail ! (pour l'histoire, en fait en vrai l'exemple du boulot c'était avec un appel de méthode car la classe MyClass implémentait aussi l'interface Listener pour d'obscures raisons, et qu'on avait besoin de centraliser plusieurs Listener dedans.)

Je pense qu'il doit y avoir d'autres façon de faire, comme faire une variable locale finale de type MyClass qui pointe sur "this", et l'utiliser en lieu de place de "MyClass.this", mais c'est tellement moins classe ;)

14sept/100

De l’intérêt des langages fonctionnels

Je ne sais plus trop comment je suis tombé sur l'article de defmacro.org sur la programmation fonctionnelle, mais en tout cas je le trouve très intéressant pour toute personne n'étant pas, a priori, intéressé par ce type de programmation. Ca présente de façon claire et compréhensible pourquoi ce paradigme existe, et pourquoi il gagnerait à être utilisé.

J'ai rajouté le ce site dans ma liste de flux RSS, car il a l'air de publier des articles très intéressants :)  Donc si vous voulez un peu de lecture, rendez-vous sur http://www.defmacro.org/

14sept/100

Moteur de lumières en 2d

Je suis tombé ce matin sur un article fort sympathique, qui explique comment faire pour gérer des lumières en 2d : http://gregouar.developpez.com/tutoriels/jeux/moteur-lumieres-dynamiques-2d/

Le code est en C++, et utilise SFML. Et voici le résultat :

C'est pas mal du tout, et l'ensemble a l'air bien expliqué et simple à suivre. Je vous invite aussi à aller voir sur le tuto, car il a mis en ligne une petite vidéo (un gif, mais pas grave) que je trouve assez impressionnant et beaucoup plus parlant que cette image fixe...

Le principe utilisé est qu'une source lumineuse (dans toutes les directions, ou dans une direction précise), peut au final être décomposée en plusieurs triangles. Il "suffit" alors pour chaque triangle de calculer judicieusement les intersections avec les murs du décor pour avoir une lumière qui s'arrête au niveau des murs. Le seul souci est qu'il faudra faire attention aux performances, car il peut rapidement devenir très coûteux de calculer toutes les intersections ^^ Mais le reste, c'est de l'optimisation :)

Si jamais j'ai l'utilité dans un futur jeu (ou application), je ne manquerai pas d'essayer !