Tutoriel sur la comprhension de la machine virtuelle Java


prcdentsommairesuivant

XIII. Assembleur de bytecode

Voil quelques chapitres que notre priple dans les trfonds de la JVM a commenc. Nous avons abord de nombreux sujets, plus ou moins lis la JVM. Dans cet article nous allons recrer la bibliothque PJBA - que nous utilisions jusqu' prsent - nous permettant gnrer 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'aprs nous enrichirons la bibliothque PJBA. Nous pourrons ensuite reprendre notre tude - que nous avions mise de ct - des instructions et des lments 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 prcdent nous avons cr un graphe d'objets, tel qu'il est dcrit dans la JVMS. Tous les lments devant tre prsents dans un fichier .class ont t indiqus. Nanmoins, l'utilisation de tableaux est loin d'tre souple; en les gardant nous risquons d'avoir plus de problmes que de solutions. Par consquent, nous allons supprimer tous les attributs count et length, et remplacer les tableaux par des java.util.LinkedList, ou plus prcisment par une classe hritant 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 prcdent, nous avons vu que le type Attribute tait prsent dans la classe ClassFile, Method et Code. Or, tous les attributs ne peuvent pas tre utiliss partout. De fait, nous allons crer 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 prsent que dans une mthode. Modifions la classe en consquence:

 
Sélectionnez
public class Code extends Attribute implements MethodAttribute {

}

Source

XIII-C. Enrichissement des classes

Actuellement toutes nos classes ont des champs privs et utilisent toutes le constructeur par dfaut (un constructeur sans paramtre). prsent, il nous faut pouvoir fixer la valeur de ces champs simplement, sans pour autant transformer nos classes en builder. Cette tche fera partie du prochain article. Nous allons donc passer en revue tous les champs un par un. Cela peut paratre rbarbatif et rptitif, mais c'est une tche indispensable pour bien comprendre comment former un fichier .class (Pour une explication dtail: Format d'un fichier class.

XIII-C-1. ClassFile

Commenons 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 lverons les limitations de la version et des modificateurs d'une classe trs prochainement.

XIII-C-1-a. contantPool

Pour ajouter des constantes dans le pool de constantes, nous allons ajouter une mthode par constante qui prendra les mmes paramtres 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();
}

Nanmoins, arrtons-nous quelques secondes. Si l'on crit et excute 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 manire 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-mme pointant vers une constante de type ConstantUTF8. Dans le cas prsent, les deux instructions pointeront vers la mme constante.

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

 
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 consquent elles sont gales. Jusque-l, rien de trs surprenant. En revanche pour les objets les choses sont bien diffrentes.

Commenons par les objets de type String. Toutes les littrales de type String - dont la chane de caractres est identique - ont toujours la mme rfrence quel que soit l'endroit de leur dclaration.

Les littrales numriques sont quant elles autoboxes. C'est--dire que l'assignation Integer i = 5 est compile en Integer i = Integer.valueOf(5). Or, comme indiqu dans la javadoc la mthode 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 autoboxs en Integer sont gaux et dans le second (pour la valeur 5000) ne le sont pas.

Pour le type Long, bien que le systme de mise en cache ne soit pas requis, il est bien prsent.

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

Nanmoins, il est dconseill de se reposer sur l'autoboxing/unboxing pour effectuer une comparaison, il est prfrable de faire des comparaisons uniquement sur des types primitifs.

Note: Les tests prsents ci-dessus peuvent tre retrouvs 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 problmes. Il nous faut donc adapter les mthodes addConstantXYZ() en consquence.

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 prcdent les constantes ConstantLong et ConstantDouble prennent deux index dans le pool de constantes (n et n+1). Dans le fichier .class gnr, l'index n+1 n'est en ralit 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 mthode 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 paramtre de type String au constructeur prenant le nom compltement qualifi de la classe ou ajouter un setter, ce qui ncessitera en amont de crer une constante ConstantUTF8, puis une constante ConstantClass et de passer au setter l'index de cette dernire. Bien videmment nous pouvons mettre en place les deux solutions, nanmoins la premire 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 gnration de nos fichiers .class, le rpertoire dans lequel devra tre gnr le fichier (le package en terme Java) et le nom de la classe. Nous avons ces deux informations dans le nom compltement qualifi d'une classe (le paramtre du constructeur). Il nous faut donc le dcouper 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 mthode parseName() sera appele au dbut du constructeur.

noter que nous n'effectuons absolument aucune vrification quant la validit des donnes. De fait, la gnration se fera sans aucune erreur, en revanche si des donnes sont incorrectes, quelle qu'en soit la raison, les erreurs seront remontes par la JVM.

XIII-C-1-c. Tableau et liste

Le tableau et les listes sont aussi initialiss 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 intresserons uniquement la liste d'objets de type Method, qui seront ajouts trs simplement l'aide d'une mthode 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 dfinit les modificateurs d'une mthode qui sont, pour l'instant, dfinis 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 prcdente (Format d'un fichier class), les instructions sont dfinies dans un tableau de byte de la classe Code. Or dfinir des instructions ayant des arguments en utilisant des bytes n'est pas simple et surtout, ce n'est pas une conception oriente objets. Nous allons donc crer 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, dfinissons 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 prcdent, la classe Code a trois champs. Nous allons prsent dtailler comment leur valeur est fixe.

 
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 dduire l'impact de l'excution d'une instruction sur les variables locales et la pile d'oprandes.

Par exemple:

Instruction

Variables locales

Pile d'oprandes

Description

iload 6

7

+1

Prend la valeur (de catgorie 1) l'index 6 des variables locales et l'empile (+1)

lload_2

3

+2

Prend la valeur (de catgorie 2) l'index 2 des variables locales et l'empile (+2)

fstore_2

3

-1

Dpile une valeur de catgorie 1 (-1) et l'ajoute dans les variables locales l'index 2

dstore 3

5

-2

Dpile une valeur de catgorie 2 (-2) et l'ajoute dans les variables locales l'index 3

ldc

0

+1

Empile une rfrence (catgorie 1, +1)

iadd

0

-1

Dpile 2 valeurs de catgorie 1 (-2), les additionne et empile le rsultat (+1)

ladd

0

-2

Dpile 2 valeurs de catgorie 2 (-4), les additionne et empile le rsultat (+2)

La colonne Variables locales correspond la taille minimum que doit avoir les variables suite l'excution 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'lment prend aussi la case l'index suivant. D'o 4 + 1 = 5. La colonne Pile d'oprandes correspond la taille des valeurs empiles et dpiles suite l'excution de l'instruction.

Les instructions load et store sont un peu particulires. 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 manire 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 mthode d'instance;
  • les deux paramtres de la mthode;
  • et quatre variables temporaires

et nous utilisons seulement les quatre premiers index des variables locales. Nanmoins, une autre solution - bien plus simple - qui est utilise par javac est de considrer 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 mthodes dans la classe Code.

Voyons le dtail:

 
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 considre comme une mthode utilitaire, nous permettant de rajouter simplement des instructions. De plus, le fait d'utiliser des mthodes en plus des constantes a pour but d'homogniser l'instanciation d'une instruction lorsque nous utiliserons des instructions ayant des arguments. Les ILOAD_0, IADD, etc. tant publiques, rien ne nous empche 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 extrmement 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 connatre la taille d'une instruction.

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

Pour calculer la taille maximum de la pile d'oprandes, nous avons rajout un champ (currentStack) nous permettant de garder la taille courante de la pile suite l'ajout d'instructions. Nous pouvons trs 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 suprieure 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 intressons uniquement la valeur la plus leve, qui correspond soit l'index + 1 puisque le tableau est index partir de 0 pour les types de catgorie 1, soit l'index + 2 pour les types de catgorie 2.

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

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

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

Source

XIII-C-4. Crer une classe

Nous avons ajout des mthodes pour valoriser tous les champs de nos classes, voyons prsent comment les utiliser, en reprenant notre exemple habituel d'une mthode statique additionnant deux entiers et retournant un entier.

Puisque nous connaissons maintenant parfaitement comment est constitu un fichier .class, le code suivant est trs 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 mme technique que pour le projet bytecode, en utilisant un test unitaire pour gnrer nos fichiers .class.

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

XIII-C-4-a. Compter le nombre de paramtres d'un descripteur

Dans le code ci-dessus, une mthode demande une explication, puisque nous ne l'avions pas vue jusqu' prsent. La mthode countParameters(), compte le nombre de paramtres d'une mthode en analysant son descripteur. Pour un tre humain, cette mthode n'est pas forcment utile, puisque nous pouvons indiquer directement le nombre de paramtres de la mthode que nous sommes en train de crer. Nanmoins, il n'en va pas de mme pour l'analyseur syntaxique que nous crerons 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 zro.

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

En revanche, les tableaux sont reprsents 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 paramtre. Le cas extrme est celui illustr ci-dessus, un tableau plusieurs dimensions d'objets.

La tche tant lgrement complexe, le code de la mthode l'est aussi. Sachant que pour l'occasion l'optimisation du code a t mise en avant.

L'ide est de parcourir la chane en analysant chaque caractre, d'incrmenter le nombre de paramtres si le caractre est une lettre reprsentant un type ou s'il s'agit du premier crochet rencontr et d'ignorer tous les autres caractres.

En ignorant le cas des tableaux le code de la mthode est le suivant (Le dtail 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 chane de caractres en tableau de bytes
    final byte[] descriptor = methodDescriptor.getBytes(ENCODING);
 
    // Le premier caractre est une parenthse ouvrante
    int index = 1;
    // Reprsente le nombre de paramtres du descripteur
    int locals = 0;
    // Permet de savoir si le compte a changer entre les diffrentes itrations
    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 caractres 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 reprsent par un ou plusieurs caractres. Pour ce faire, nous devons utiliser une mthode rcursive qui contiendra le switch. Le problme est que nous mettons jour deux variables, index et locals. Utiliser un objet est gnralement la solution la plus simple. Nanmoins, dans un tel cas, utiliser un objet mutable rend le code difficile lire et maintenir. Nous allons donc choisir une autre solution, la mthode de packing/unpacking (rassembler plusieurs valeurs dans une mme variable) que nous avons dj vue dans la partie Manipulation de la pile. Nous savons que les variables locales ne peuvent pas excder 65 535 lments (soit un short non sign) et qu'un tableau en Java ne peut avoir plus de 2 147 483 647 lments (soit un int), or moins d'tre compltement drang, et dans ce cas il est conseill d'aller consulter, le descripteur d'une mthode ne fera pas plus de deux milliards de caractres. De fait, un long conviendra parfaitement. Les 32 premiers bits seront utiliss pour l'index et les 32 derniers pour le nombre de paramtres.

La mthode prcdente est prsent dcoupe en deux mthodes:

 
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 mthode countOneParameter() rcursivement en ignorant le paramtre locals. Nous avons les cas suivants:

  • Si l'lment suivant est un type primitif, l'index est incrment de 1 ou 2;
  • s'il s'agit d'un objet l'index est incrment d'autant de caractres le constituant et;
  • s'il s'agit nouveau d'un ou plusieurs crochets ouvrant, nous aurons des appels rcursifs successifs.

Tests unitaires

XIII-C-5. Gnration d'un fichier .class

Pour gnrer un fichier .class nous avons notre disposition la classe java.io.DataOutputStream que nous pouvons utiliser de la manire suivante:

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

En dfinitive, la seule chose qu'il nous manque est la mthode toBytecode() de la classe ClassFile. Pour viter d'avoir une mthode extrmement longue, nous allons dlguer chaque lment sa construction en bytecode. Pour ce faire, nous ajouterons dans toutes les classes une mthode toBytecode(). Et pour tre sr que toutes les classes ncessitant la mthode toBytecode() l'implmente, la meilleure solution est de crer 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 prsentes dans le package org.isk.jvmhardcore.jpba.struture sont bases sur le mme modle):

 
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 dtail. Nous sommes prsent capables de faire les modifications permettant de lever toutes les limitations que nous avons fixes. 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 mthode toBytecode(), mais en offrant de la souplesse et c'est bien l tout l'enjeu d'un code rutilisable.


prcdentsommairesuivant

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 ni 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.