II) Compilation du code

II.1) La compilation

II.1.a) Rôle des classes de jhelp.compiler.compil

II.1.b) Étapes de la compilation

II.2) Utilisation de jhelp.compiler.instance.ClassManager

II.3) Détails sur les classes d'un point de vu du byte code

Le code complet du compilateur se trouve : Code du compilateur de byte code
Pour le compiler vous aurez besoin de notre batterie d'utilitaires : Divers utilitaires
Le compilateur est fait pour compiler des fichiers aux formats décrit par la première partie : I) Le byte code.
Le code est diviser en deux packages :

II.1) La compilation

Pour simplifier le travail, nous avons utilisé les classe du package com.sun.org.apache.bcel.internal.generic (Inclut à la JRE).
Il contient tout ce qu'il faut pour représenter programmatiquement le byte code et le compilé.
Bien entendu il ne fait pas tout, il faut "nourrir" chacune des parties de manière approprié pour obtenir un résultat valide.
C'est ce que fait ce code, il se "contente" d'analyser un flux, remplir les classes de com.sun.org.apache.bcel.internal.generic et d'appeler la méthode qui construit tout.
Nous avons ajouter une inspecteur de pile. Son rôle est de vérifier si la pile de la méthode est correctement utilisée pour éviter des problèmes à l'exécution, car la compilation pourrait se faire, mais les erreurs ne seront pas clair, mal placés, de quoi pas mal s'arracher les cheveux.
De plus de notre point de vu il vaut mieux savoir à l'avance que notre code ne fonctionnera pas correctement. Notre inspecteur de pile n'est pas encore parfait et laisse encore passé certains problèmes de pile, mais permet d'en éviter pas mal.
Le point d'entré de la compilation est jhelp.compiler.compil.Compiler.
La méthode public com.sun.org.apache.bcel.internal.generic.ClassGen compile(java.io.InputStream) throws jhelp.compiler.compil.CompilerException prend en paramètre un flux sur le fichier à compiler. (Voir I) Le byte code pour connaître le format attendu)
L'objet com.sun.org.apache.bcel.internal.generic.ClassGen est une description de la classe compilé.
Pour récupéré le byte code il faut faire : classGen.getJavaClass().dump(outputStream); Remarque : la méthode dump peut levé une java.io.IOException. Et le paramètre est un objet étendant java.io.OutputStream.
Pour revenir à la méthode compile l'exception qu'elle peut levé donne, quand c'est possible, la ligne dans le code qui à provoqué l'exception.
Il peut être intéressant de vérifier (Avec un instanceof) que cette exception est une jhelp.compiler.compil.StackInspectorException. Car si c'en est une, une fois "casté", elle donne des information sur l'état de la pile au moment du non respect de la pile. Ainsi que le chemin parcouru pour arriver à la ligne fautive avec l'état de la pile pour chaque étape.
En effet le chemin de parcouru n'est pas forcément linéaire avec les instructions de saut. Notre inspecteur de pile essaie plusieurs combinaisons et s'arrête si une ne respecte pas la pile et renvoie comment il a obtenu l'erreur.

II.1.a) Rôle des classes de jhelp.compiler.compil

Compiler : CompilerContext, son rôle principale : Partager des informations de compilation entre les classes. Il enregistre divers informations ainsi que l'état de la compilation. Il stocke notamment : FieldInformation : Contient les informations sur un champs de la classe ou extérieur à celle-ci.

MethodDescription : Décrit la méthode en cours d'analyse. Son rôle : Tout ce que la méthode à accumulées pendant la phase d'analyse, serra transféré à CompilerContext pendant la dernière partie qui donne toutes les informations nécessaires sur la méthode (nom, paramètres, code, ...) à com.sun.org.apache.bcel.internal.generic.ClassGen
Pendant ce transfert elle calcul la "table des lignes". C'est une table d'association, qui associe un endroit où se trouve une instruction byte code à un numéro de ligne du code original. Cela sert ensuite à l'exécution pour dire à quelle ligne à été levée une exception.

CodeLine : Décrit une instruction de code pour une méthode. Elle stocke l'instruction et ses paramètres. Au moment de la compilation, après quelques vérifications, elle se convertit en la classe de com.sun.org.apache.bcel.internal.generic correspondante.

CompilerConstants : Contient les mots clefs pour la partie déclaration du format.

OpcodeConstants : Contient le nom des instructions byte code, ainsi que leur description (En commentaire).

StackInspector : Une fois qu'une méthode est finie d'être compilé, les labels et instructions de saut résolu. Juste avant de l'enregistrée, cette classe va regarder si la pile d'exécution de la méthode sera respectée lors de son exécution.

II.1.b) Étapes de la compilation

Ces informations peuvent vous aider à comprendre la logique que suit le code lors de la compilation.
  1. Enlève les lignes vides et commentaires
  2. Formate les lignes intéressantes et les transforme en : instruction suivit de paramètres
  3. Si l'instruction est class ⇒ récupérer le nom de la classe
  4. Si l'instruction est import ⇒ ajouter à la liste des imports
  5. Si l'instruction est extends, implements, field ou filed_reference enrichir la Constant Pool et mémorisé les références
  6. Si l'instruction est method, parameter, return, throws, enrichir la description de la méthode courante
  7. Si l'instruction est { indique le début du code de la méthode courante
  8. Si l'instruction est aucune ci dessus, ni }, c'est une instruction de code que l'on ajoute au code de la méthode en cours
  9. Si l'instruction est } on compile le code de la méthode en cours :
    1. Initialise la liste des variables locales
    2. Puisque c'est une méthode non statique on ajoute this comme première variable locale. Note : le byte code impose que this soit TOUJOURS la première variable locale d'une méthode non statique
    3. On ajoute à la liste des variables locales les paramètres de la méthodes dans l'ordre des déclarations. Notez que l'ordre est important.
    4. Maintenant on sauvegarde la taille de la liste des variables locales pour plus tard. Cette information servira à savoir où commence les variables locales déclarées avec VAR
    5. Compilation du code (En même temps on créé la "table des lignes") :
      1. Si l'instruction est VAR, on ajoute la variable à la liste des variables locale, on ne génère pas d'instruction byte code
      2. Si l'instruction est LABEL on se souvient du nom du label pour pouvoir l'associé à la prochaine vraie instruction byte code (C'est à dire ni VAR, ni LABEL). On ne génère pas d'instruction byte code
      3. Si l'instruction est une instruction de saut on génère une instruction de saut avec une destination inconnu pour le moment, puis on la mémorise pour la résoudre plus tard
      4. Si l'instruction est un "switch" on match avec des destinations inconnus (Pour chaque cas et la destination par défaut). On la sauvegarde pour la résoudre plus tard
      5. Les autres instructions sont directement généré correctement et n'auront pas besoin d'être résolu plus tard
    6. Résolution des instructions de saut et des "switch"
    7. Création, maintenant que c'est possible, du com.sun.org.apache.bcel.internal.generic.MethodGen
    8. Ajout des exceptions levés
    9. Ajout des variables locales déclarés avec VAR. Note : c'est ici que sert d'avoir sauvegardé là où elles commencent
    10. Ajout de la "table des lignes" précédemment créée
    11. Vérification que la pile pour la méthode est respectée
    12. Ajout de la méthode désormais prête
  10. Si on est à la fin du fichier, on renvoie l'instance de com.sun.org.apache.bcel.internal.generic.ClassGen remplit prête à l'emploi

II.2) Utilisation de jhelp.compiler.instance.ClassManager

jhelp.compiler.instance.ClassManager travail par unité de compilation. Une unité de compilation est un ensemble de sources à compiler.
Chaque compilation d'une source est effectué en parallèle des autres.
Les méthodes compileASMs vous demande un écouteur pour vous informer si une compilation échoue, et quand les compilations du groupe sont terminé.
Cette méthode n'attend pas que les compilations lancées en parallèle soit terminées pour sortir. Elle fournit par conter un identifiant de compilation.
Cet identifiant vous servira à identifier le groupe de compilation dans les retours de ClassManagerListener.
Pour utiliser une code compilé vous avez deux façons à votre disposition.
La plus pratique est que votre code implémente une interface du projet. Il suffit alors de voir la classe comme l'interface et d'appeler les méthodes de l'interface.
Un petit exemple pour être plus clair. Admettons que l'on est l'interface : package jhelp.example.operations; public interface Operation { public int execute(int first, int second); } Et que l'on est le ficher "add.asm" : class jhelp.asm.ADD import jhelp.example.operations.Operation implements Operation method execute parameter int first parameter int second return int { ILOAD first ILOAD second IADD IRETURN } Un exemple d'utilisation : //... //... classManagerListener : instance of ClassManagerListener to signal compilation status //... inputStreamASM : Stream on "add.asm" //... // Create the class manager that will do compilation final ClassManager classManager = new ClassManager(); // Launch the compilation of several class in parallel classManager.compileASMs(classManagerListener, inputStreamASM); // ... Et dans la classe qui implémente ClassManagerListener : Au passage vous remarquerez que le code de "add.asm" est compilé et intégré à l'application à la volé.
Pour être précis la classe n'est pas connue du ClassLoader courant mais du ClassLoader embarqué par ClassManager.
C'est pour cela qu'il faut passer par ClassManager pour avoir une instance ou faire de la réflexion.
Une autre solution est de passer par la réflexion. Les méthodes de ClassManager suivantes vous le permettront : A savoir, ClassManager, en plus des classes que vous lui donnez à compiler, connaît toutes les classes que connaît le ClassLoader de votre projet.
C'est à dire toute la JRE, les classes de l'application courante ainsi que toutes celles du "class path". C'est pour cela que l'exemple de tout à l'heure fonctionnait.
Il est donc tout a fait possible d'utiliser ClassManager avec d'autres classes que celles que l'on compile à la volée, même si c'est pas son but premier.

On peut, en plus des classes citées au-dessus ajouter d'autres classes connues au ClassManager. L'intérêt est de pouvoir utiliser de nouvelle classes sans avoir a recompiler le projet ni a arrêté l'application.
La méthode addClassFile ajoute un fichier .class au ClassManager. Le fichier doit bien sûr être valide, mais en plus son chemin doit finir par son package et le nom du fichier doit être le nom de la classe .class (Même nom que le compilateur JAVAC donne)).
Par exemple si on a le fichier "Test.class" qui représente la classe jhelp.example.test.Test, le chemin où se trouve le fichier devra se e finir par :.../jhelp/example/test/Test.class
La méthode addJarFile permet quand à elle d'ajouter tout un jar. A part que le jar soit valide elle n'exige rien d'autre.

Les deux dernières méthodes répondent à une problématique particulière que nous avons rencontré pour notre éditeur de byte code.
La problématique est la suivante : une fois qu'un ClassLoader à résolu une classe on ne peut pas lui faire oublié.
Il faut connaître comment sont chargé les classes dans un ClassLoader pour comprendre ce que veut dire résolu. En résumé une classe est résolu quand on demande sa description en mémoire pour la première fois.
En détail, quand en Java on fait un new, la JVM va demandé au ClassLoader une description de la classe à instanciée. Si celle-ci est déjà résolu il va donner directement la description.
Sinon, le ClassLoader va résoudre la classe en regardant les classes dont elle à besoin pour s'exécute, il va alors se lancé dans la résolution de toutes les classes référencés qu'il ne connaît pas encore avant de finir la résolution de la classe de départ.
Donc une classe peut être résolu soit directement ou indirectement.
Pour revenir à notre problématique on a ajouté la possibilité aux utilisateurs de l'éditeur non seulement la possibilité de compiler, mais aussi de pouvoir tester le code compiler.
Pour tester le code compiler il nous fallait une instance de la classe et donc celle-ci sera résolu. Mais vola admettons que l'utilisateur veuille modifier son code, il n'aurait pas pu le compiler à nouveau. Ce qui est plutôt gênant.
Pour contourner ce problème et permettre de changer une classe même résolue nous avons ajouter les deux méthodes suivantes :

II.3) Détails sur les classes d'un point de vu du byte code

D'un point de vu byte code, une classe contient un Constant Pool.
Ce Constant Pool va contenir toutes les constantes utilisées par la classes, les autres parties qui utilisent ces constantes se contentent de stockées un référence qui pointe à une valeur dans le Constant Pool.
C'est la JVM lors de l'exécution qui va prendre la référence et demandé au Constant Pool a quoi elle correspond.
Il y a plusieurs sorte des constantes stockées : Bien sûr tout ce qui est stocké dans la Constant Pool ne l'est qu'une fois.
Vous remarquerez que certaines constantes font références à d'autres. Ce qui vous donne une idée du travail effectué par la JVM à chaque fois qu'elle doit les résoudre.
A noté, il se peut que nous en ayons oublié.

En plus du Constant Pool, la classe contient des sections que nous ne détailleront pas ici.

La classes contient également les méthodes, nous allons un peu détaillé la section d'une méthode.
La section d'une méthode contient divers blocs d'informations, dont :