Developpez.com

Club des développeurs et IT pro
Plus de 4 millions de visiteurs uniques par mois

Tutoriel sur la compréhension de la machine virtuelle Java


précédentsommairesuivant

XIII. Assembleur de bytecode

Voilà quelques chapitres que notre périple dans les tréfonds de la JVM a commencé. Nous avons abordé de nombreux sujets, plus ou moins liés à la JVM. Dans cet article nous allons recréer la bibliothèque PJBA - que nous utilisions jusqu'à présent - nous permettant générer des fichiers .class à partir d'un fichier texte. Dans l'article suivant il sera question d'étudier une solution permettant de traverser un arbre syntaxique simplement et dans celui d'après nous enrichirons la bibliothèque PJBA. Nous pourrons ensuite reprendre notre étude - que nous avions mise de côté - des instructions et des éléments constituant un fichier .class.

Dans la suite de cet article, et sauf mention contraire, le terme variables locales correspondra au tableau des variables locales d'un cadre (frame).

Le code est disponible sur Github (tag et branche).

XIII-A. Tableaux

Dans l'article précédent nous avons créé un graphe d'objets, tel qu'il est décrit dans la JVMS. Tous les éléments devant être présents dans un fichier .class ont été indiqués. Néanmoins, l'utilisation de tableaux est loin d'être souple ; en les gardant nous risquons d'avoir plus de problèmes que de solutions. Par conséquent, nous allons supprimer tous les attributs count et length, et remplacer les tableaux par des java.util.LinkedList, ou plus précisément par une classe héritant de LinkedList. Vous en comprendrez la raison avant la fin de l'article.

 
Sélectionnez
public class PjbaLinkedList<E> extends LinkedList<E> {

}

Source

À noter que nous ne toucherons pas au champ int[] interfaces de la classe ClassFile, et le champ byte[] code de la classe Code sera modifié un peu plus loin.

XIII-B. Attribute

Dans l'article précédent, nous avons vu que le type Attribute était présent dans la classe ClassFile, Method et Code. Or, tous les attributs ne peuvent pas être utilisés partout. De fait, nous allons créer trois interfaces qui auront pour seul objectif de contraindre le contenu des listes.

 
Sélectionnez
public interface ClassAttribute {

}

Source

 
Sélectionnez
public interface MethodAttribute {

}

Source

 
Sélectionnez
public interface CodeAttribute {

}

Source

Le seul attribut que nous avons vu pour l'instant est Code, qui ne peut être présent que dans une méthode. Modifions la classe en conséquence :

 
Sélectionnez
public class Code extends Attribute implements MethodAttribute {

}

Source

XIII-C. Enrichissement des classes

Actuellement toutes nos classes ont des champs privés et utilisent toutes le constructeur par défaut (un constructeur sans paramètre). À présent, il nous faut pouvoir fixer la valeur de ces champs simplement, sans pour autant transformer nos classes en builder. Cette tâche fera partie du prochain article. Nous allons donc passer en revue tous les champs un par un. Cela peut paraître rébarbatif et répétitif, mais c'est une tâche indispensable pour bien comprendre comment former un fichier .class (Pour une explication détail : Format d'un fichier class.

XIII-C-1. ClassFile

Commençons par tous les champs ayant des valeurs constantes.

 
Sélectionnez
final private int magicNumber = 0xcafebabe;
final private int version = 0x30; // 48.0
final private int accessFlags = 0x0001 | 0x0020; // public super

Note : Nous lèverons les limitations de la version et des modificateurs d'une classe très prochainement.

XIII-C-1-a. contantPool

Pour ajouter des constantes dans le pool de constantes, nous allons ajouter une méthode par constante qui prendra les mêmes paramètres que le constructeur de la constante et qui retournera l'index de la constante dans le pool de constantes.

Par exemple pour la constante ConstantUTF8 :

 
Sélectionnez
public int getCurrentCPIndex() {
  return this.constantPool.size() - 1;
}
 
public int addConstantUTF8(final String value) {
  final Constant.UTF8 constant = new Constant.UTF8(value);
  this.constantPool.add(constant);
  return this.getCurrentCPIndex();
}

Néanmoins, arrêtons-nous quelques secondes. Si l'on écrit et exécute le test unitaire ci-dessous nous constatons qu'il est correct :

 
Sélectionnez
@Test
public void stringEquality() {
  final String s1 = "Bonjour";
  final String s2 = "Bonjour";
 
  Assert.assertEquals(s1, s2);
  Assert.assertTrue(s1 == s2);
}

Les deux assignations peuvent être traduites en PJB de la manière suivante :

 
Sélectionnez
ldc "Bonjour"
astore_0
ldc "Bonjour"
astore_1

Dans un fichier .class, les instructions ldc pointent vers l'index d'une constante de type ConstantString, elle-même pointant vers une constante de type ConstantUTF8. Dans le cas présent, les deux instructions pointeront vers la même constante.

Mais new String("Bonjour") != new String("Bonjour"). Et ceci est aussi valable pour les types numériques :

 
Sélectionnez
5 == 5

et

 
Sélectionnez
Integer i1 = 5;
Integer i2 = 5;
i1 == i2;

mais

 
Sélectionnez
Integer i1 = 5_000;
Integer i2 = 5_000;
i1 != i2;

et

 
Sélectionnez
new Integer(5) != new Integer(5)

Nous le savions, Java aime faire les chose simplement et sans surprise…

Pour les types primitifs nous comparons des valeurs, par conséquent elles sont égales. Jusque-là, rien de très surprenant. En revanche pour les objets les choses sont bien différentes.

Commençons par les objets de type String. Toutes les littérales de type String - dont la chaîne de caractères est identique - ont toujours la même référence quel que soit l'endroit de leur déclaration.

Les littérales numériques sont quant à elles autoboxées. C'est-à-dire que l'assignation Integer i = 5 est compilée en Integer i = Integer.valueOf(5). Or, comme indiqué dans la javadoc la méthode valueOf() a un cache initialisé au chargement de la classe, contenant des objets de type Integer pour toutes les valeurs comprises entre -128 et 127 (oublions la fin de la phrase « et peut mettre en cache d'autres valeurs hors de cet intervalle »). Ceci explique pourquoi dans le premier cas (pour la valeur 5) deux primitifs autoboxés en Integer sont égaux et dans le second (pour la valeur 5000) ne le sont pas.

Pour le type Long, bien que le système de mise en cache ne soit pas requis, il est bien présent.

En revanche, il n'y a aucun mécanisme similaire pour les types Float et Double.

Néanmoins, il est déconseillé de se reposer sur l'autoboxing/unboxing pour effectuer une comparaison, il est préférable de faire des comparaisons uniquement sur des types primitifs.

Note : Les tests présentés ci-dessus peuvent être retrouvés dans la classe suivante : EqualityTest

Nous en venons à la conclusion que dans le pool de constantes d'un fichier .class, chaque constante peut être unique, sans que cela ne pose de problèmes. Il nous faut donc adapter les méthodes addConstantXYZ() en conséquence.

Prenons quelques exemples :

 
Sélectionnez
private int addConstant(final ConstantPoolEntry constant) {
  final int index = this.constantPool.indexOf(constant);
 
  if (index == -1) {
    this.constantPool.add(constant);
    return this.getCurrentCPIndex();
  } else {
    return index;
  }
}
 
public int addConstantUTF8(final String value) {
  final Constant.UTF8 constant = new Constant.UTF8(value);
  return this.addConstant(constant);
}
 
public int addConstantString(final int utf8Index) {
  final Constant.String constant = new Constant.String(utf8Index);
  return this.addConstant(constant);
}
 
public int addConstantLong(final long value) {
  final Constant.Long constant = new Constant.Long(value);
  final int index = this.addConstant(constant);
  this.constantPool.add(null);
  return index;
}
 
public int addConstantDouble(final double value) {
  final Constant.Double constant = new Constant.Double(value);
  final int index = this.addConstant(constant);
  this.constantPool.add(null);
  return index;
}

Comme mentionné dans l'article précédent les constantes ConstantLong et ConstantDouble prennent deux index dans le pool de constantes (n et n+1). Dans le fichier .class généré, l'index n+1 n'est en réalité pas utilisé. Imaginons que nous ajoutons une constante ConstantInt à l'index 11, puis une constante ConstantLong à l'index 12, la prochaine constante sera à l'index 14.

De plus, nous devons surcharger dans toutes les classes de constantes, les méthode hashCode() et equals().

Tests Unitaires

XIII-C-1-b. thisClass et superClass

Les champs thisClass et superClass ont pour valeur l'index d'une classe de type ConstantClass dans le pool de constantes. Dans cette version de PJBA, toute classe aura pour parent java.lang.Object. Nous allons donc pouvoir valoriser le champ dans le constructeur. Quant au champ thisClass plusieurs solutions s'offrent à nous. Ajouter un paramètre de type String au constructeur prenant le nom complètement qualifié de la classe ou ajouter un setter, ce qui nécessitera en amont de créer une constante ConstantUTF8, puis une constante ConstantClass et de passer au setter l'index de cette dernière. Bien évidemment nous pouvons mettre en place les deux solutions, néanmoins la première semble convenir à la majorité des situations.

 
Sélectionnez
public ClassFile(final String fullyQualifiedName) {
  //...
 
  // This
  final int classNameIndex = this.addConstantUTF8(fullyQualifiedName);
  this.thisClass  = this.addConstantClass(classNameIndex);
 
  // Parent - Codé en dur pour l'instant
  final String superName = "java/lang/Object";
  final int superNameIndex = this.addConstantUTF8(superName);
  this.superClass = this.addConstantClass(superNameIndex);
 
  //...
}

Avant de continuer, ajoutons deux champs qui nous seront utiles lors de la génération de nos fichiers .class, le répertoire dans lequel devra être généré le fichier (le package en terme Java) et le nom de la classe. Nous avons ces deux informations dans le nom complètement qualifié d'une classe (le paramètre du constructeur). Il nous faut donc le découper en 2 parties, sachant qu'une classe n'est pas obligatoirement dans un package.

 
Sélectionnez
private String className;
private String directories;
 
private void parseName(final String fullyQualifiedName) {
  int index = fullyQualifiedName.lastIndexOf(Ascii.SLASH);
 
  if (index >= 0) {
    this.directories = fullyQualifiedName.substring(0, ++index);
    this.className = fullyQualifiedName.substring(index);
  } else {
    this.className = fullyQualifiedName;
  }
}

La méthode parseName() sera appelée au début du constructeur.

À noter que nous n'effectuons absolument aucune vérification quant à la validité des données. De fait, la génération se fera sans aucune erreur, en revanche si des données sont incorrectes, quelle qu'en soit la raison, les erreurs seront remontées par la JVM.

XIII-C-1-c. Tableau et liste

Le tableau et les listes sont aussi initialisés dans le constructeur ;

 
Sélectionnez
public ClassFile(final String fullyQualifiedName) {
  //...
 
  this.interfaces = new int[0];
  this.fields = new PjbaLinkedList<>();
  this.methods = new PjbaLinkedList<>();
  this.attributes = new PjbaLinkedList<>();
 
  //...
}

Pour l'instant, nous nous intéresserons uniquement à la liste d'objets de type Method, qui seront ajoutés très simplement à l'aide d'une méthode addMethod() :

 
Sélectionnez
public void addMethod(final Method method) {
  this.methods.add(method);
}

Source

XIII-C-2. Method

Tout comme la classe ClassFile, la classe Method définit les modificateurs d'une méthode qui sont, pour l'instant, définis sous la forme d'une constante :

 
Sélectionnez
final private int accessFlags = 0x0001 | 0x0008; // public static

Les champs nameIndex et descriptorIndex ont pour valeur l'index de deux constantes ConstantUTF8 :

 
Sélectionnez
public void setNameIndex(int utf8NameIndex) {
  this.nameIndex = utf8NameIndex;
}
 
public void setDescriptorIndex(int utf8DescriptorIndex) {
  this.descriptorIndex = utf8DescriptorIndex;
}

Terminons par la liste attributes d'objets de type MethodAttribute :

 
Sélectionnez
public Method() {
  super();
 
  this.attributes = new PjbaLinkedList<>();
}
 
public void addAttibute(final MethodAttribute attribute) {
  this.attributes.add(attribute);
}

Source

XIII-C-3. Code

Comme mentionné dans la partie précédente (Format d'un fichier class), les instructions sont définies dans un tableau de byte de la classe Code. Or définir des instructions ayant des arguments en utilisant des bytes n'est pas simple et surtout, ce n'est pas une conception orientée objets. Nous allons donc créer une classe Instruction :

 
Sélectionnez
public class Instruction {
  final private int opcode;
 
  public Instruction(final int opcode) {
    super();
    this.opcode = opcode;
  }
}

puis remplacer le tableau de byte de la classe Code par une liste d'Instruction.

Pour pouvoir tester notre assembleur, définissons quelques instructions :

 
Sélectionnez
public class Instructions {
  final public static int ILOAD_0_OPCODE = 0x1a;
  final public static int ILOAD_1_OPCODE = 0x1b;
  final public static int IADD_OPCODE = 0x60;
  final public static int IRETURN_OPCODE = 0xac;
}

Comme nous l'avons vu dans l'article précédent, la classe Code a trois champs. Nous allons à présent détailler comment leur valeur est fixée.

 
Sélectionnez
private int maxStack;
private int maxLocals;
private int codeLength;

Chaque instruction a une taille, 1 octet auquel s'ajoute la taille de ses arguments si elle en a. De plus, nous pouvons déduire l'impact de l'exécution d'une instruction sur les variables locales et la pile d'opérandes.

Par exemple :

Instruction

Variables locales

Pile d'opérandes

Description

iload 6

7

+1

Prend la valeur (de catégorie 1) à l'index 6 des variables locales et l'empile (+1)

lload_2

3

+2

Prend la valeur (de catégorie 2) à l'index 2 des variables locales et l'empile (+2)

fstore_2

3

-1

Dépile une valeur de catégorie 1 (-1) et l'ajoute dans les variables locales à l'index 2

dstore 3

5

-2

Dépile une valeur de catégorie 2 (-2) et l'ajoute dans les variables locales à l'index 3

ldc

0

+1

Empile une référence (catégorie 1, +1)

iadd

0

-1

Dépile 2 valeurs de catégorie 1 (-2), les additionne et empile le résultat (+1)

ladd

0

-2

Dépile 2 valeurs de catégorie 2 (-4), les additionne et empile le résultat (+2)

La colonne Variables locales correspond à la taille minimum que doit avoir les variables suite à l'exécution de l'instruction. Par exemple dans l'instruction dstore 3, le 3 indique que le tableau doit avoir une taille au moins égale à 4 et le ‘d‘ que l'élément prend aussi la case à l'index suivant. D'où 4 + 1 = 5. La colonne Pile d'opérandes correspond à la taille des valeurs empilées et dépilées suite à l'exécution de l'instruction.

Les instructions load et store sont un peu particulières. Prenons un cas simple :

 
Sélectionnez
public int locals(int i1, int i2) {
  int t1 = i1 + 2;
  int t2 = i2 + 3;
  int t3 = t1 * t2;
  int t4 = t2 / t1;
  return t4 - t3;
}

Le code ci-dessus peut être écrit en bytecode, sans aucune transformation, de la manière suivante :

 
Sélectionnez
// int t1 = i1 + 2;
iload_1   // i1
iconst_2  // 2
iadd      // i1 + 2
istore_0  // t1
 
// int t2 = i2 + 3;
iload_2   // i2
iconst_3  // 3
iadd      // i2 + 3
istore_1  // t2
 
// int t3 = t1 * t2;
iload_0   // t1
iload_1   // t2
imul      // t1 * t2
istore_2  // t3
 
// int t4 = t2 / t1;
iload_1   // t1
iload_0   // t2
idiv      // t1 / t2
istore_3  // t4
 
// return t4 - t3;
iload_3   // t4
iload_4   // t3
isub      // t4 - t3
ireturn   // return

Nous avons donc sept variables :

  • this puisque nous sommes dans le cas d'une méthode d'instance ;
  • les deux paramètres de la méthode ;
  • et quatre variables temporaires

et nous utilisons seulement les quatre premiers index des variables locales. Néanmoins, une autre solution - bien plus simple - qui est utilisée par javac est de considérer qu'une variable a une place fixe dans les variables locales et de laisser le compilateur JIT effectuer les optimisations.

Ceci se traduit par :

  • la modification de la classe Instruction et de la gestion des instructions :.
  • l'ajout de méthodes dans la classe Code.

Voyons le détail :

 
Sélectionnez
public class Instruction {
  final private int opcode;
  final private int stack;
  final private int locals;
  final private int length;
 
  public Instruction(int opcode, int stack, int locals, int length) {
    this.opcode = opcode;
    this.stack = stack;
    this.locals = locals;
    this.length = length;
  }
}

Source

 
Sélectionnez
public class NoArgInstruction extends Instruction {
 
    public NoArgInstruction(int opcode, int stack, int locals) {
        super(opcode, stack, locals, 1);
    }
}

Source

 
Sélectionnez
public class Instructions {
  final public static int ILOAD_0_OPCODE = 0x1a;
  final public static int ILOAD_1_OPCODE = 0x1b;
  final public static int IADD_OPCODE = 0x60;
  final public static int IRETURN_OPCODE = 0xac;
 
  final public static Instruction ILOAD_0 = new NoArgInstruction(ILOAD_0_OPCODE, 1, 1);
  final public static Instruction ILOAD_1 = new NoArgInstruction(ILOAD_1_OPCODE, 1, 2);
  final public static Instruction IADD = new NoArgInstruction(IADD_OPCODE, -1, 0);
  final public static Instruction IRETURN = new NoArgInstruction(IRETURN_OPCODE, 0, 0);
 
  public static Instruction iload_0() {
    return ILOAD_0;
  }
 
  public static Instruction iload_1() {
    return ILOAD_1;
  }
 
  public static Instruction iadd() {
    return IADD;
  }
 
  public static Instruction ireturn() {
    return IRETURN;
  }
}

La classe Instructions doit être considérée comme une méthode utilitaire, nous permettant de rajouter simplement des instructions. De plus, le fait d'utiliser des méthodes en plus des constantes a pour but d'homogénéiser l'instanciation d'une instruction lorsque nous utiliserons des instructions ayant des arguments. Les ILOAD_0, IADD, etc. étant publiques, rien ne nous empêche de les utiliser.

Source

La base de la classe Code est la suivante :

 
Sélectionnez
public class Code extends Attribute implements MethodAttribute {
  public final static String ATTRIBUTE_NAME = "Code";
 
  private int maxStack;
  private int maxLocals;
  private int codeLength;
 
  private PjbaLinkedList<Instruction> instructions;
  private PjbaLinkedList<Exception> exceptions;
  private PjbaLinkedList<CodeAttribute> attributes;
 
  public Code(final int attributeNameIndex) {
    super(attributeNameIndex);
 
    this.instructions = new PjbaLinkedList<>();
    this.exceptions = new PjbaLinkedList<>();
    this.attributes = new PjbaLinkedList<>();
  }
}

À chaque fois que l'on ajoute une instruction, nous mettons à jour les champs maxStack, maxLocals et length et nous ajoutons l'instruction à la liste d'instructions.

 
Sélectionnez
public void addInstruction(final Instruction instruction) {
  this.addStack(instruction.getStack());
  this.addLocals(instruction.getLocals());
  this.addLength(instruction.getLength());
  this.instructions.add(instruction);
}

Le champ length est extrêmement simple à valoriser puisque nous prenons la taille de chaque instruction et nous les additionnons. En revanche, nous verrons que dans certains cas il est difficile - mais pas impossible - de connaître la taille d'une instruction.

 
Sélectionnez
private void addLength(int length) {
  this.codeLength += length;
}

Pour calculer la taille maximum de la pile d'opérandes, nous avons rajouté un champ (currentStack) nous permettant de garder la taille courante de la pile suite à l'ajout d'instructions. Nous pouvons très bien avoir une suite d'instructions qui vont augmenter la taille de la pile, suivie par des instructions la faisant diminuer. De fait, nous modifions la variable maxStack uniquement si la variable currentStack est supérieure à la valeur actuelle de maxStack.

 
Sélectionnez
private int currentStack;
 
private void addStack(int stack) {
  this.currentStack += stack;
 
  if (this.maxStack < this.currentStack) {
    this.maxStack = this.currentStack;
  }
}

Comme nous l'avons vu, pour fixer la valeur du champ maxLocals, nous nous intéressons uniquement à la valeur la plus élevée, qui correspond soit à l'index + 1 puisque le tableau est indexé à partir de 0 pour les types de catégorie 1, soit à l'index + 2 pour les types de catégorie 2.

 
Sélectionnez
private void addLocals(int locals) {
  if (this.maxLocals < locals) {
    this.maxLocals = locals;
  }
}

Pour terminer, nous devons ajouter une méthode nous permettant d'indiquer le nombre de cases que prennent les paramètres dans le tableau des variables locales. Hormis, les types long et double prenant deux cases, tous les autres en prennent une. La méthode setParameterCount() est indispensable puisqu'une méthode peut avoir plusieurs paramètres, mais aucune variable locale (au sens Java).

 
Sélectionnez
public void setParameterCount(int parameterCount) {
  this.addLocals(parameterCount);
}

Source

XIII-C-4. Créer une classe

Nous avons ajouté des méthodes pour valoriser tous les champs de nos classes, voyons à présent comment les utiliser, en reprenant notre exemple habituel d'une méthode statique additionnant deux entiers et retournant un entier.

Puisque nous connaissons maintenant parfaitement comment est constitué un fichier .class, le code suivant est très simple à comprendre.

 
Sélectionnez
// ConstantPoolEntries
final String className = "org/isk/jvmhardcore/pjba/MyFirstClass";
final String methodName = "add";
final String methodDescriptor = "(II)I";
 
// ClassFile
final ClassFile classFile = new ClassFile(className);
 
// Method
final int methodIndex = classFile.addConstantUTF8(methodName);
final int descriptorIndex = classFile.addConstantUTF8(methodDescriptor);
final Method method = new Method();
method.setNameIndex(methodIndex);
method.setDescriptorIndex(descriptorIndex);
classFile.addMethod(method);
 
// Code
final int codeAttributeIndex = classFile.addConstantUTF8(Code.ATTRIBUTE_NAME);
final Code code = new Code(codeAttributeIndex);
final int parameterCount = method.countParameters(methodDescriptor);
code.setParameterCount(parameterCount);
 
// Instructions
code.addInstruction(Instructions.iload_0());
code.addInstruction(Instructions.iload_1());
code.addInstruction(Instructions.iadd());
code.addInstruction(Instructions.ireturn());
method.addAttibute(code);

Pour tester ce code, nous allons utiliser la même technique que pour le projet bytecode, en utilisant un test unitaire pour générer nos fichiers .class.

La Classe à assembler, l'Assembleur et le test unitaire de la classe.

XIII-C-4-a. Compter le nombre de paramètres d'un descripteur

Dans le code ci-dessus, une méthode demande une explication, puisque nous ne l'avions pas vue jusqu'à présent. La méthode countParameters(), compte le nombre de paramètres d'une méthode en analysant son descripteur. Pour un être humain, cette méthode n'est pas forcément utile, puisque nous pouvons indiquer directement le nombre de paramètres de la méthode que nous sommes en train de créer. Néanmoins, il n'en va pas de même pour l'analyseur syntaxique que nous créerons prochainement.

Pour rappel, un descripteur peut avoir la forme suivante :

 
Sélectionnez
([[[Ljava.lang.Object;[[LObject;)V

ce qui donnerait en Java :

 
Sélectionnez
void nomDeLaMethode(java.lang.Object[][][], Object[][])

En repartant d'une situation plus simple, les types primitifs sont assez simples à analyser. Les types long et double prennent deux cases dans les variables locales, les autres une seule et void zéro.

Pour les classes, nous savons qu'elles commencent par la lettre « L » et se terminent par un point-virgule « ; ».

En revanche, les tableaux sont représentés par un crochet ouvrant « [ » suivi du type du tableau, et pour les tableaux à plusieurs dimensions les crochets se suivent mais comptent toujours pour un seul paramètre. Le cas extrême est celui illustré ci-dessus, un tableau à plusieurs dimensions d'objets.

La tâche étant légèrement complexe, le code de la méthode l'est aussi. Sachant que pour l'occasion l'optimisation du code a été mise en avant.

L'idée est de parcourir la chaîne en analysant chaque caractère, d'incrémenter le nombre de paramètres si le caractère est une lettre représentant un type ou s'il s'agit du premier crochet rencontré et d'ignorer tous les autres caractères.

En ignorant le cas des tableaux le code de la méthode est le suivant (Le détail de chaque étape est en commentaire) :

 
Sélectionnez
private static final String ENCODING = "UTF-8";
 
public int countParameters(final String methodDescriptor) {
  try {
    // Nous transformons la chaîne de caractères en tableau de bytes
    final byte[] descriptor = methodDescriptor.getBytes(ENCODING);
 
    // Le premier caractère est une parenthèse ouvrante
    int index = 1;
    // Représente le nombre de paramètres du descripteur
    int locals = 0;
    // Permet de savoir si le compte a changer entre les différentes itérations
    int previousLocals = locals;
 
    for (;;) {
      byte character = descriptor[index++];
 
      switch (character) {
        case Ascii.UPPERCASE_B: // byte
        case Ascii.UPPERCASE_C: // char
        case Ascii.UPPERCASE_F: // float
        case Ascii.UPPERCASE_I: // int
        case Ascii.UPPERCASE_S: // short
        case Ascii.UPPERCASE_Z: // boolean
          locals++;
          break;
        case Ascii.UPPERCASE_J: // long
        case Ascii.UPPERCASE_D: // double
          locals += 2;
          break;
        // Nous ignorons tous les caractères jusqu'au point-virgule
        case Ascii.UPPERCASE_L: // reference
          locals++;
          while (index < descriptor.length
              && (character = descriptor[index++]) != Ascii.SEMICOLON);
          break;
      }
 
      // Nous avons rencontré un crochet fermant ou un V
      // Il n'y a plus rien à compter
      if (locals == previousLocals) {
        break;
      }
 
      previousLocals = locals;
    }
 
    return locals;
  } catch (UnsupportedEncodingException e) {
    throw new RuntimeException("Should never happen", e);
  }
}

En introduisant les tableaux, le code ci-dessus a besoin d'être modifié. Si nous rencontrons un crochet, nous ajoutons 1 à la variable locals, puis nous devons ignorer les crochets suivants ainsi qu'un type qui peut être représenté par un ou plusieurs caractères. Pour ce faire, nous devons utiliser une méthode récursive qui contiendra le switch. Le problème est que nous mettons à jour deux variables, index et locals. Utiliser un objet est généralement la solution la plus simple. Néanmoins, dans un tel cas, utiliser un objet mutable rend le code difficile à lire et à maintenir. Nous allons donc choisir une autre solution, la méthode de packing/unpacking (rassembler plusieurs valeurs dans une même variable) que nous avons déjà vue dans la partie Manipulation de la pile. Nous savons que les variables locales ne peuvent pas excéder 65 535 éléments (soit un short non signé) et qu'un tableau en Java ne peut avoir plus de 2 147 483 647 éléments (soit un int), or à moins d'être complètement dérangé, et dans ce cas il est conseillé d'aller consulter, le descripteur d'une méthode ne fera pas plus de deux milliards de caractères. De fait, un long conviendra parfaitement. Les 32 premiers bits seront utilisés pour l'index et les 32 derniers pour le nombre de paramètres.

La méthode précédente est à présent découpée en deux méthodes :

 
Sélectionnez
public int countParameters(final String methodDescriptor) {
  try {
    final byte[] descriptor = methodDescriptor.getBytes(ENCODING);
 
    int index = 1; // Step over the first element: Left parenthesis
    int locals = 0;
    int previousLocals = locals;
 
    for (;;) {
      final long pack = countOneParameter(descriptor, index, locals);
      locals = (int) (pack & 0xffff);
      index = (int) (pack >>> 32);
 
      // We encountered a right bracket or a void
      if (locals == previousLocals) {
        break;
      }
 
      previousLocals = locals;
    }
 
    return locals;
  } catch (UnsupportedEncodingException e) {
    throw new RuntimeException("Should never happen", e);
  }
}
 
private long countOneParameter(byte[] descriptor, int index, int locals) {
  byte character = descriptor[index++];
 
  switch (character) {
    case Ascii.UPPERCASE_B: // byte
    case Ascii.UPPERCASE_C: // char
    case Ascii.UPPERCASE_F: // float
    case Ascii.UPPERCASE_I: // int
    case Ascii.UPPERCASE_S: // short
    case Ascii.UPPERCASE_Z: // boolean
      locals++;
      break;
    case Ascii.UPPERCASE_J: // long
    case Ascii.UPPERCASE_D: // double
      locals += 2;
      break;
    case Ascii.LEFT_BRACKET: // array
      locals++;
      final long pack = this.countOneParameter(descriptor, index, 0)
      index = (int) (pack >>> 32);
      break;
    case Ascii.UPPERCASE_L: // reference
      locals++;
      while (index < descriptor.length &&
             (character = descriptor[index++]) != Ascii.SEMICOLON)
        ;
      break;
  }
 
  // Locals maximum value = 65,535
  // Arrays maximum size in Java = 2,147,483,647
  return (((long) index) << 32) | locals;
}

Source

Si l'on fait l'impasse sur le packing/unpacking qui n'a rien de particulier en soit, le seul changement est l'ajout d'un case dans le switch :

 
Sélectionnez
case Ascii.LEFT_BRACKET: // Array
  locals++;
  final long pack = this.countOneParameter(descriptor, index, 0);
  index = (int) (pack >>> 32);
  break;

Nous prenons bien en compte le premier crochet, puis appelons la méthode countOneParameter() récursivement en ignorant le paramètre locals. Nous avons les cas suivants :

  • Si l'élément suivant est un type primitif, l'index est incrémenté de 1 ou 2 ;
  • s'il s'agit d'un objet l'index est incrémenté d'autant de caractères le constituant et ;
  • s'il s'agit à nouveau d'un ou plusieurs crochets ouvrant, nous aurons des appels récursifs successifs.

Tests unitaires

XIII-C-5. Génération d'un fichier .class

Pour générer un fichier .class nous avons à notre disposition la classe java.io.DataOutputStream que nous pouvons utiliser de la manière suivante :

 
Sélectionnez
final ClassFile classFile = new ClassFile();
// ...Création de la classe...
 
final String directoryStr = classFile.getDirectories();
 
// Création des répertoires contenant le fichier .class, s'ils n'existent pas.
final File directory = new File(directoryStr);
if (!directory.exists()) {
  directory.mkdirs();
}
 
// Création d'un fichier .class
final String filename = directoryStr + classFile.getClassName() + ".class";
final FileOutputStream file = new FileOutputStream(filename);
 
// Génération du contenu du fichier
final DataOutput bytecode = new DataOutputStream(file);
classFile.toBytecode(bytecode);
file.close();

En définitive, la seule chose qu'il nous manque est la méthode toBytecode() de la classe ClassFile. Pour éviter d'avoir une méthode extrêmement longue, nous allons déléguer à chaque élément sa construction en bytecode. Pour ce faire, nous ajouterons dans toutes les classes une méthode toBytecode(). Et pour être sûr que toutes les classes nécessitant la méthode toBytecode() l'implémente, la meilleure solution est de créer une interface BytecodeEnabled.

 
Sélectionnez
public interface BytecodeEnabled {
  void toBytecode(final DataOutput dataOutput) throws IOException;
}

Source

Prenons par exemple la classe ClassFile (toutes les autres classes présentes dans le package org.isk.jvmhardcore.jpba.struture sont basées sur le même modèle) :

 
Sélectionnez
public class ClassFile implements BytecodeEnabled {
  @Override
  public void toBytecode(DataOutput dataOutput) throws IOException {
    dataOutput.writeInt(this.magicNumber);
    dataOutput.writeInt(this.version);
    dataOutput.writeShort(this.constantPool.size());
    this.constantPool.toBytecode(dataOutput);
    dataOutput.writeShort(this.accessFlags);
    dataOutput.writeShort(this.thisClass);
    dataOutput.writeShort(this.superClass);
    dataOutput.writeShort(this.interfaces.length);
    // short interfaces[interfacesCount];
    dataOutput.writeShort(this.fields.size());
    this.fields.toBytecode(dataOutput);
    dataOutput.writeShort(this.methods.size());
    this.methods.toBytecode(dataOutput);
    dataOutput.writeShort(this.attributes.size());
    this.attributes.toBytecode(dataOutput);
  }
}

Source

XIII-D. What's next?

Ce fut long, mais nous avons pu voir notre assembleur en détail. Nous sommes à présent capables de faire les modifications permettant de lever toutes les limitations que nous avons fixées. Avec une telle conception, nous constaterons que l'ajout des instructions à notre assembleur est trivial.

Dans l'article suivant, nous allons voir comment traverser un graphe d'objets simplement, tout comme nous l'avons fait avec la méthode toBytecode(), mais en offrant de la souplesse et c'est bien là tout l'enjeu d'un code réutilisable.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2016 SOAT. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.