Accueil Nos publications Blog JVM Hardcore – Part 20 – Bytecode – Champs et Méthodes de classes

JVM Hardcore – Part 20 – Bytecode – Champs et Méthodes de classes

academic_duke
Après avoir étudié les instructions nous permettant d’effectuer des opérations, de manipuler les variables locales et la pile, et plus récemment celles de comparaison et de contrôle, nous allons nous intéresser aujourd’hui aux instructions liées aux champs et aux méthodes de classes (statiques) mais aussi aux différents éléments présents dans un fichier .class permettant de définir des champs, de récupérer ou de fixer sa valeur, ou d’appeler une méthode.

Au cours de cet article, nous considérerons que les éléments introduits dans l’article Part 11 – Bytecode – Format d’un fichier class sont connus et ne seront pas expliqués à nouveau.

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

Tous les articles déjà publiés de la série portent le tag jvmhardcore.

Représentation de la pile (Rappel)

La JVM étant basée sur le modèle de la pile, il est essentiel de connaître quel est l’impact des instructions. Pour représenter l’état avant/après l’exécution d’une instruction, nous allons reprendre le format utiliser par la JVMS et qui est le suivant :

..., valeur1, valeur2 → ..., résultat, où les valeurs les plus à droite sont au sommet de la pile. valeur1 et valeur2 étant les deux valeurs utilisées pour le calcul et résultat le résultat.

Il est important de noter que dans cette représentation les long et les double sont considérés comme une seule valeur. Par conséquent, lorsque nécessaire nous présenterons les différents cas d’utilisation d’une instruction en utilisant plusieurs formes.

Appeler une méthode statique

L’instruction invokestatic (0xb8) permet d’appeler une méthode de classe (méthode possédant le modificateur static). Elle prend en argument un nombre non signé de deux octets représentant un index dans le pool de constantes de la classe. L’élément à cet index devant être du type ConstantMethodRef pouvant être représenté en Java de la manière suivante :

class ConstantMethodRef {
  final byte tag = 10;
  short classIndex;
  short nameAndTypeIndex;
}

Pour rappel :

  • Le champ tag correspond au type de la constante.
  • Les types byte, short et int doivent être considérés comme des nombres non signés, respectivement de un, deux et quatre octets.

En plus du champ tag, la structure ConstantMethodRef a deux autres champs :

  • classIndex contient l’index d’un élément de type ConstantClass, lui-même contenant l’index d’un élément de type ConstantUTF8 représentant le nom complètement qualifié de la classe dans laquelle est définie la méthode.
  • nameAndTypeIndex contient l’index d’un élément de type ConstantNameAndType contenant deux index pointant vers deux éléments de type ConstantUTF8.

La structure ConstantNameAndType peut être représentée par la classe Java suivante :

class ConstantNameAndType {
  final byte tag = 12;
  short nameIndex;
  short descriptorIndex;
}

où :

  • nameIndex contient l’index d’un élément de type ConstantUTF8 représentant le nom de la méthode appelée et ;
  • descriptorIndex contient l’index d’un élément de type ConstantUTF8 représentant le descripteur de la méthode appelée (par exemple (Ljava/lang/Object;IZ)V).

En PJB, l’instruction invokestatic peut être utilisée de la manière suivante :

invokestatic <nom_de_la_classe>
             <nom_de_la_méthode>
             <descripteur_de_la_méthode>

Note : les retours à la ligne sont présents uniquement pour une meilleure lisibilité.

État de la pile avant → après exécution : ..., [arg1 [, arg2 [...]]] → [r], ..., où arg1 et arg2 sont les paramètres de la méthode. Le paramètre au sommet de la pile est le paramètre le plus à droite de la méthode. r est la valeur retournée par la méthode si le type de retour n’est pas void.

Partons d’un simple exemple en Java :

public static int invokestatic(int i1, int i2, int i3) {
  int tmp = Math.max(i1, i2);
  return tmp + i3;
}

pouvant être converti de la manière suivante en PJB :

.method public static invokestatic(III)I
  iload_0
  iload_1
  invokestatic java/lang/Math max (II)I
  iload_2
  iadd
  ireturn
.methodend

Source

ou en Java/PJBA :

.newMethod(Method.MODIFIER_PUBLIC
         | Method.MODIFIER_STATIC,
           "invokestatic", "(III)I")
  iload_0()
  iload_1()
  invokestatic("java/lang/Math", "max", "(II)I")
  iload_2()
  iadd()
  ireturn();

Voyons à présent le test permettant de vérifier le calcul :

@Test
public void invokestatic() {
  final int i1 = TestStatic.invokestatic(10, 5, 9);
  Assert.assertEquals(19, i1);

  final int i2 = TestStatic.invokestatic(5, 10, 9);
  Assert.assertEquals(19, i2);
}

Source

Récupérer la valeur d’un champ statique

L’instruction getstatic (0xb2) permet de récupérer la valeur d’une variable de classe (possédant le modificateur static). Tout comme invokestatic, elle prend en argument un nombre non signé de deux octets représentant un index dans le pool de constantes de la classe pointant, cette fois-ci, vers un élément de type ConstantFieldRef.

La structure ConstantFieldRef peut être représentée en Java de la manière suivante :

class ConstantFieldRef {
  final byte tag = 9;
  short classIndex;
  short nameAndTypeIndex;
}

La seule différence avec la structure ConstantMethodRef est que les ConstantUTF8 pointées par l’objet de type nameAndTypeIndex représentent le nom du champ et son type.

En PJB, l’instruction getstatic peut être utilisée de la manière suivante :

getstatic <nom_de_la_classe>
          <nom_du_champ>
          <descripteur_du_champ>

État de la pile avant → après exécution : ... → v, ..., où v est la valeur du champ retournée par getstatic.

Pour commencer prenons une constante présente dans le JDK, Integer.MAX_VALUE.

public static int getstatic() {
  return Integer.MAX_VALUE;
}

Ce qui est simplement converti en PJB :

.method public static getstatic()I
  getstatic java/lang/Integer MAX_VALUE I
  ireturn
.methodend

Source

Le test vérifie seulement que l’on a récupéré la bonne valeur.

@Test
public void getstatic() {
  final int i = TestStatic.getstatic();
  Assert.assertEquals(Integer.MAX_VALUE, i);
}

Source

Fixer la valeur d’un champ statique

Actuellement, PJBA ne permet pas de définir des champs de classe (ou d’instance). Il nous faut donc rajouter les différents éléments nécessaires à l’ensemble des classes que nous avons actuellement.

Tout d’abord, nous devons définir la structure Field, que nous avions préalablement créée mais laissée vide. Elle est présente dans la structure ClassFile sous la forme d’une liste de Field.

class Field {
  short accessFlags;
  short nameIndex;
  short descriptorIndex;
  short attributesCount;
  FieldAttribute[] attributes;
}

accessFlags

Tout comme le champ accessFlags des classes ClassFile et Method, il permet d’indiquer les modificateurs d’un champ à l’aide de masques.

Nom du flag Valeur Mot clé Java
ACC_PUBLIC 0x0001 public
ACC_PRIVATE 0x0002 private
ACC_PROTECTED 0x0004 protected
ACC_STATIC 0x0008 static
ACC_FINAL 0x0010 final
ACC_SYNCHRONIZED 0x0020 synchronized
ACC_VOLATILE 0x0040 volatile
ACC_TRANSIENT 0x0080 abstract

Quelques règles :

  • un champ ne peut avoir que l’un des flags suivants à 1 à la fois : ACC_PRIVATE, ACC_PROTECTED ou ACC_PUBLIC.
  • les flags ACC_FINAL et ACC_VOLATILE sont exclusifs.

nameIndex

Le champ nameIndex contient l’index d’un élément de type ConstantUtf8 dans le pool de constantes contenant le nom du champ. Par exemple MAX_VALUE ou value.

descriptorIndex

Le champ descriptorIndex contient l’index d’un élément de type ConstantUtf8 dans le pool de constantes contenant le descripteur du champ. Par exemple Lorg/isk/pjb/Test; ou I.

attributesCount et attributes

Le champ attributesCount indique la taille du tableau attributes et le champ attributes est un tableau d’Attribute. Pour rappel, la structure peut être représentée de la manière suivante :

public class Attribute {
  short nameIndex;
  int attributeLength;
  byte[] info;
}

Un champ (Field) peut avoir les type d’attributs suivants :

  • ConstantValue : permet de définir la valeur d’une constante.
  • Synthetic : permet d’indiquer que le champ, n’existant pas dans le fichier source, a été rajouté par le compilateur
  • Deprecated : permet d’indiquer que le champ est déprécié

Nous verrons dans la partie suivante l’attribut ConstantValue.

Pour définir un champ en PJB, nous allons utiliser la directive .field de la manière suivante :

.field <modificateurs> <nom_du_champ> <descripteur_du_champ>

Voyons un exemple qui va fixer la valeur d’un champ (TEST_PUTSTATIC) et la retourner.

public class TestStatic {
  public static int TEST_PUTSTATIC;

  public static int putstatic(int i) {
    TEST_PUTSTATIC = i;
    return TEST_PUTSTATIC;
  }
}

Pour fixer la valeur du champ nous allons utiliser l’instruction putstatic (0xb3), prenant en argument un nombre non signé de deux octets représentant un index dans le pool de constantes de la classe pointant vers un élément de type ConstantFieldRef.

En PJB, l’instruction putstatic peut être utilisée de la manière suivante (hormis le nom de l’instruction les arguments sont identiques à ceux de l’instruction getstatic) :

putstatic <nom_de_la_classe>
          <nom_du_champ>
          <descripteur_du_champ>

État de la pile avant ? après exécution : ..., v → ..., ou v est la valeur qui sera assignée au champ.

.class public org/isk/pjb/TestStatic
  .field public static TEST_PUTSTATIC I

  .method public static putstatic(I)I
    iload_0
    putstatic org/isk/pjb/TestStatic TEST_PUTSTATIC I
    getstatic org/isk/pjb/TestStatic TEST_PUTSTATIC I
    ireturn
  .methodend
.classend

Source

En PJBA/Java, le code précédent peut être écrit de la manière suivante :

builder.newField(Field.MODIFIER_STATIC, "PUTSTATIC_FIELD", "I");
builder.newMethod(Method.MODIFIER_PUBLIC
                | Method.MODIFIER_STATIC,
                  "putstatic", "(I)I")
  .iload_0()
  .putstatic(fullyQualifiedName, "PUTSTATIC_FIELD", "I")
  .getstatic(fullyQualifiedName, "PUTSTATIC_FIELD", "I")
  .ireturn();

Source

Nous n’avons plus qu’à tester ce bout de code.

@Test
public void putstatic() {
  final int i = TestStatic.putstatic(10);
  Assert.assertEquals(10, i);
}

Mais ce test est en parti incomplet, nous souhaitons être sûr qu’après exécution de la méthode putstatic() la variable TEST_PUTSTATIC contient la valeur 10.

Pour ce faire nous allons tout d’abord vérifier les valeurs de champs non initialisés.

.field public static TEST_NOT_INITIALIZED_INT I
.field public static TEST_NOT_INITIALIZED_LONG J
.field public static TEST_NOT_INITIALIZED_FLOAT F
.field public static TEST_NOT_INITIALIZED_DOUBLE D
.field public static TEST_NOT_INITIALIZED_OBJ Ljava/lang/Object;

Source

@Test
public void notInitialized() {
  Assert.assertEquals(0, TestStatic.TEST_NOT_INITIALIZED_INT);
  Assert.assertEquals(0, TestStatic.TEST_NOT_INITIALIZED_LONG);
  Assert.assertEquals(0.0f, TestStatic.TEST_NOT_INITIALIZED_FLOAT, 0.0001);
  Assert.assertEquals(0.0, TestStatic.TEST_NOT_INITIALIZED_DOUBLE, 0.0001);
  Assert.assertNull(TestStatic.TEST_NOT_INITIALIZED_OBJ);
}

Nous constatons que tous les primitifs sont initialisés à 0 et les objets à null. Ceci est exactement le même comportement que l’on a en Java.

Nous pouvons poursuivre le test en essayant d’attribuer des valeurs à ces cinq champs :

@Test
public void notInitialized() {
  // ...

  TestStatic.TEST_NOT_INITIALIZED_INT = 111;
  TestStatic.TEST_NOT_INITIALIZED_LONG = 222_222;
  TestStatic.TEST_NOT_INITIALIZED_FLOAT = 333.333f;
  TestStatic.TEST_NOT_INITIALIZED_DOUBLE = 444_444.444_444;
  TestStatic.TEST_NOT_INITIALIZED_OBJ = "Hello JVMHardcore";

  Assert.assertEquals(111, TestStatic.TEST_NOT_INITIALIZED_INT);
  Assert.assertEquals(222_222, TestStatic.TEST_NOT_INITIALIZED_LONG);
  Assert.assertEquals(333.333f,
                      TestStatic.TEST_NOT_INITIALIZED_FLOAT, 0.0001);
  Assert.assertEquals(444_444.444_444,
                      TestStatic.TEST_NOT_INITIALIZED_DOUBLE, 0.0001);
  Assert.assertTrue("Hello JVMHardcore" == TestStatic.TEST_NOT_INITIALIZED_OBJ);
}

Source

Il nous suffit de compléter le test de la méthode putstatic() en ajoutant deux tests sur le champ TEST_PUTSTATIC (avant et après exécution de la méthode getstatic())

@Test
public void putstatic() {
  Assert.assertEquals(0, TestStatic.TEST_PUTSTATIC);
  final int i = TestStatic.putstatic(10);
  Assert.assertEquals(10, i);
  Assert.assertEquals(10, TestStatic.TEST_PUTSTATIC);
}

Source

Définir une constante

En Java, une constante est une variable définie avec le modificateur final. Cette variable pouvant être une variable locale d’une méthode, un champ de classe ou d’instance.

Pour la JVM, seuls les champs de classe de type primitif ou String possédant le modificateur final et dont la valeur est assignée lors de leur déclaration (par exemple final static int x = 10) sont considérés comme des constantes.

En PJB, il nous suffit de définir un champ comme nous l’avons fait précédemment et de lui assigner une valeur.

.field final public static TEST_INT I = 2147483647
.field final public static TEST_LONG J = 9223372036854775807l
.field final public static TEST_FLOAT F = 3.4028235E38f
.field final public static TEST_DOUBLE D = 1.7976931348623157E308d
.field final public static TEST_STRING Ljava/lang/String; = "Hello world"

Source

Notons que l’analyseur syntaxique de PJBA lèvera une exception si une valeur est assignée à un champ alors qu’il ne possède pas le modificateur final. Pour fixer la valeur d’un champ non final, il est nécessaire de procéder à une initialisation dans un bloc statique pour une champ statique et dans un constructeur pour un champ d’instance.

Le test nous permet de vérifier que les valeurs ont été correctement assignées aux champs.

@Test
public void constantsTester() {
  Assert.assertEquals(Integer.MAX_VALUE, TestStatic.TEST_INT);
  Assert.assertEquals(Long.MAX_VALUE, TestStatic.TEST_LONG);
  Assert.assertEquals(Float.MAX_VALUE, TestStatic.TEST_FLOAT, 0.0001);
  Assert.assertEquals(Double.MAX_VALUE, TestStatic.TEST_DOUBLE, 0.0001);
  Assert.assertEquals("Hello world", TestStatic.TEST_STRING);
}

Source

En revanche, nous ne pouvons pas tester qu’un champ possède une valeur constante.

Imaginons le champ suivant :

.field final public static TEST_RESET_FINAL I = 2147483647

En Java, il nous est impossible de changer la valeur du champ TEST_RESET_FINAL. TestStatic.TEST_RESET_FINAL = 0; donne une erreur lors de la compilation. Néanmoins, pour la JVM, ceci ne pose aucun problème (ou presque).

.method public static resetFinal(I)I
  iload_0
  putstatic org/isk/pjb/TestStatic TEST_RESET_FINAL I
  getstatic org/isk/pjb/TestStatic TEST_RESET_FINAL I
  ireturn
.methodend

Source

Avec un test unitaire nous allons vérifier que la méthode resetFinal() change la valeur du champ TEST_RESET_FINAL possédant le modificateur final.

@Test
public void resetFinal() {
  Assert.assertEquals(Integer.MAX_VALUE, TestStatic.TEST_RESET_FINAL);
  final int i = TestStatic.resetFinal(10);

  Assert.assertEquals(10, i);
  Assert.assertEquals(Integer.MAX_VALUE, TestStatic.TEST_RESET_FINAL);

  // Erreur lors de la compilation
  // TestStatic.TEST_PUTSTATIC = 125;
}

Source

Nous constatons que la méthode resetFinal() retourne la valeur modifiée, mais lorsque l’on accède au champ TEST_RESET_FINAL de l’extérieur de la classe (ligne 7) c’est la valeur originelle qui est retournée, c’est-à-dire Integer.MAX_VALUE et non 10. De plus, l’erreur de compilation du code à la ligne 10 nous permet de valider qu’il n’y a pas eu d’erreur lors de la génération du fichier .class, le champ TEST_RESET_FINAL possède bien le modificateur final.

Si l’on souhaite modifier ce champ à partir d’une méthode définie dans une classe externe (en dupliquant la méthode resetFinal() dans une classe nommée TestStaticUpdater) :

.class public org/isk/pjb/TestStaticUpdater
  .super java/lang/Object

  .method public static resetFinal(I)I
    iload_0
    putstatic org/isk/pjb/TestStatic TEST_RESET_FINAL I
    getstatic org/isk/pjb/TestStatic TEST_RESET_FINAL I
    ireturn
  .methodend
.classend

Source

une exception est levée lors de l’exécution :

@Test
public void resetFinal() {
  try {
    TestStaticUpdater.resetFinal(100);
    Assert.fail();
  } catch (IllegalAccessError e) {
    Assert.assertEquals("java.lang.IllegalAccessError", e.toString());
  }
}

Source

Dans un fichier .class, la valeur d’une constante est définie à l’aide de l’attribut ConstantValue, qui comme nous l’avons vu un peu plus haut est un attribut de la structure Field, pouvant être représenté par la classe Java suivante :

ConstantValue {
  short nameIndex;
  int attributeLength = 2;
  short constantValueIndex;
}

où :

  • nameIndex contient l’index d’un élément de type ConstantUtf8 dans le tableau ConstantPoolEntry contenant le nom du type de l’attribut (“ConstantValue”).
  • constantValueIndex contient l’index d’un élément de type ConstantInteger, ConstantLong, ConstantFloat, ConstantDouble ou ConstantString.

Notes :

  • En Java un champ possédant le modificateur final mais initialisé dans un constructeur ou un bloc statique ne possédera pas l’attribut ConstantValue.
  • Si un champ n’est pas final mais possède l’attribut ConstantValue, une nouvelle assignation ne posera de problème ni à la compilation ni à l’exécution. En revanche, la valeur retournée sera toujours celle définie dans cet attribut, lorsque l’on accède à ce champ depuis l’extérieur de la classe le définissant.
  • Si un champ est final mais ne possède pas l’attribut ConstantValue il est possible de lui assigner une nouvelle valeur uniquement lorsque la modification est faite à l’intérieur de la classe définissant le champ et en passant par des instructions bytecode.

Blocs statiques

En bytecode, les blocs statiques (bloc utilisant le mot clé static) sont des méthodes statiques ne prenant aucun paramètre, retournant void et dont le nom est <clinit>.

De fait, le code Java suivant :

public static int TEST_STATIC_BLOCK:

static {
    TEST_STATIC_BLOCK = 98765;
}

peut être écrit en PJB comme ceci :

.field public static TEST_STATIC_BLOCK I

.method static <clinit>()V
  ldc 98765
  putstatic org/isk/pjb/TestStatic TEST_STATIC_BLOCK I
  return
.methodend

Source

Le test permet de vérifier que le champ TestStatic.TEST_STATIC_BLOCK a été initialisé correctement.

@Test
public void staticBlock() {
  Assert.assertEquals(98_765, TestStatic.TEST_STATIC_BLOCK);
}

Source

Les champs de type primitif, les littérales et javac

Lors de l’assignation d’un champ à l’aide une constante (VALUE = Integer.MAX_VALUE), que le champ ait les modificateurs final, static ou aucun (comme nous le verrons dans l’article suivant) javac remplace l’appel à la constante par la valeur qu’elle contient.

En reprenant un exemple précédent :

public static int getstatic() {
  return Integer.MAX_VALUE;
}

Le bytecode généré par javac sera le suivant :

ldc 2147483647
ireturn

De même, la déclaration d’une constante assignant une variable (et non une littérale) verra la variable remplacée par la valeur.

final static int VALUE = Integer.MAX_VALUE;

Pour faire simple, ce code sera décompilé en PJB (image proche d’un fichier .class) de la manière suivante :

.field static VALUE I = 2147483647

Bien que nous ne l’ayons pas mentionné auparavant, il en va de même pour les constantes utilisées dans les case des structures switch/case.

Implémentation

L’ajout des structures Field, ConstantValue, etc., mais aussi des instructions invokestatic, getstatic et putstatic à PJBA étant similaires à tout ce que nous avons vu jusqu’à présent, nous balayerons rapidement et sans entrer dans les détails l’ensemble des modifications.

Constant.Xxx

Commençons par les structures ajoutées à PJBA. Les structures ConstantFieldRef, ConstantMethodRef et ConstantInterfaceMethodRef (que nous n’avons pas vue) ne diffèrent que par la valeur de leur tag. Nous avons créé une classe Ref héritée par les classes FieldRef, MethodRef et InterfaceMethodRef :

public static abstract class Ref extends ConstantPoolEntry {
  final public int classIndex;
  final public int nameAndTypeIndex;

  public Ref(ConstantPoolTag tag, int classIndex, int nameAndTypeIndex) {
    super(tag);
    this.classIndex = classIndex;
    this.nameAndTypeIndex = nameAndTypeIndex;
  }

  // ...
}

Source

Field, Visitor et Visitable

De même, pour les structures Method et Field nous pouvons créer une classe parente, nommée FieldAndMethod.

public abstract class FieldAndMethod<A extends Visitable>
                implements Visitable {
  protected int accessFlags;
  protected int nameIndex;
  protected int descriptorIndex;

  protected PjbaLinkedList<A> attributes;

  public FieldAndMethod() {
    super();

    this.attributes = new PjbaLinkedList<A>();
  }

  // ...
}

Source

De fait, les classes Method et Field ne font que :

  • définir les modificateurs possibles, pour chacun de ces deux types, sous la forme de constantes, mais aussi
  • implémenter la méthode accept(Visitor visitor) imposée par l’acceptation du contrat de l’interface Visitable.

Puisque nous souhaitons différencier les champs des classes Method et Field, nous avons ajouté de nouvelles méthodes à l’interface Visitor :

void visitFieldAccessFlags(int accessFlags);
void visitFieldNameIndex(int nameIndex);
void visitFieldDescriptorIndex(int descriptorIndex);
void visitFieldAttributesSize(int size);

Source

Instructions

Les instructions invokestatic, getstatic et putstatic prenant en argument un nombre de deux octets nous pouvons utiliser la classe ShortArgInstruction. En revanche, les méthodes invokestatic(), getstatic() et putstatic() doivent avoir deux paramètres. L’un représentant l’argument de l’instruction et l’autre l’impact de l’exécution de ces instructions dans la pile du cadre de la méthode les contenant. Jusqu’à présent, la spécification des instructions que nous avons vues définissait leur impact dans les variables locales et la pile. Mais avec les trois instructions décrites aujourd’hui, ce n’est plus le cas :

  • invokestatic dépile autant d’éléments qu’il y a de paramètres à la méthode appelée et empile le résultat si la méthode ne retourne pas void. Et ceci toujours en gardant à l’esprit que les types long et double prennent deux cases dans la pile
  • getstatic empile la valeur du champ appelé (la valeur prenant une ou deux cases dans la pile en fonction de son type)
  • putstatic dépile la valeur qui sera attribuée au champ appelé (la valeur prenant, encore et toujours, une ou deux cases dans la pile)

Voyons l’implémentation :

public static Instruction getstatic(short indexInCP, int sizeInStack) {
  return new ShortArgInstruction(0xb2, sizeInStack, 0, indexInCP);
}

public static Instruction putstatic(short indexInCP, int sizeInStack) {
  return new ShortArgInstruction(0xb3, -sizeInStack, 0, indexInCP);
}

public static Instruction invokestatic(short indexInCP, int stackDelta) {
  return new ShortArgInstruction(0xb8, stackDelta, 0, indexInCP);
}

Source

Pour calculer l’ensemble de ces valeurs, nous avons créé la classe DescriptorCounter et trois méthodes :

ClassFileBuilder et MethodBuilder

Pour pouvoir ajouter des champs à une classe nous avons ajouté plusieurs méthodes :

newField(final int fieldModifiers,
         final String fieldName,
         final String fieldDescriptor)

newConstantField(final int fieldModifiers,
                 final String fieldName,
                 final String fieldDescriptor,
                 final int value)

newConstantField(final int fieldModifiers,
                 final String fieldName,
                 final String fieldDescriptor,
                 final long value)

newConstantField(final int fieldModifiers,
                 final String fieldName,
                 final String fieldDescriptor,
                 final float value)

newConstantField(final int fieldModifiers,
                 final String fieldName,
                 final String fieldDescriptor,
                 final double value)

newConstantField(final int fieldModifiers,
                 final String fieldName,
                 final String fieldDescriptor,
                 final String value)

Source

Toutes utilisant la méthode createNewField() qui a la charge d’ajouter le champ à la classe.

private Field createNewField(final int fieldModifiers,
                             final String fieldName,
                             final String fieldDescriptor) {
  final int nameIndex = this.classFile.addConstantUTF8(fieldName);
  final int descriptorIndex = this.classFile.addConstantUTF8(fieldDescriptor);
  final Field field = new Field();
  field.setNameIndex(nameIndex);
  field.setDescriptorIndex(descriptorIndex);
  field.setAccessFlags(fieldModifiers);
  this.classFile.addField(field);

  return field;
}

Source

Notons que les méthodes newConstantField() forcent le modificateur final en appelant la méthode createNewField() :

this.createNewField(fieldModifiers | Field.MODIFIER_FINAL,
                    fieldName, fieldDescriptor);

Dans la classe MethodBuilder, nous avons ajouté les trois méthodes correspondant aux trois instructions. Elles ont la délicate tâche d’ajouter de nombreuses constantes au pool de constantes :

getstatic(String fullyQualifiedName,
          String fieldName,
          String fieldDescriptor)

putstatic(String fullyQualifiedName,
          String fieldName,
          String fieldDescriptor)

invokestatic(String fullyQualifiedName,
             String methodName,
             String methodDescriptor)

Source

MetaInstructions

Pour pouvoir prendre en compte ces nouvelles instructions nous avons ajouté les classes FieldAndMethodFactory et FieldAndMethodMetaInstruction, ainsi que les types ArgsType.FIELD et ArgsType.METHOD. Ceci nous permet d’ajouter trois MetaInstruction :

list.add(new FieldAndMethodMetaInstruction("getstatic",
         ArgsType.FIELD,
         new FieldAndMethodFactory() {

  @Override
  public Instruction buildInstruction(short indexInCP, int sizeInStack) {
    return Instructions.getstatic(indexInCP, sizeInStack);
  }
}));

list.add(new FieldAndMethodMetaInstruction("putstatic",
         ArgsType.FIELD,
         new FieldAndMethodFactory() {

  @Override
  public Instruction buildInstruction(short indexInCP, int sizeInStack) {
    return Instructions.putstatic(indexInCP, sizeInStack);
  }
}));

list.add(new FieldAndMethodMetaInstruction("invokestatic",
         ArgsType.METHOD,
         new FieldAndMethodFactory() {

  @Override
  public Instruction buildInstruction(short indexInCP, int sizeInStack) {
    return Instructions.invokestatic(indexInCP, sizeInStack);
  }
}));

Source

Disassembler et Dumpers

La structure Field étant similaire à la structure Method (à l’exception des attributs), sa prise en compte dans les classes Disassembler, PjbDumper et HexDumper ne mérite guère que l’on s’y attarde.

PjbParser

L’ajout de la directive .field est l’occasion d’effectuer un léger refactoring de la partie Tokenizer.

La classe PjbTokenizer est devenue une classe passe plats, déléguant les différents traitements aux classes DirectiveTokenizer, InstructionTokenizer, LiteralTokenizer, ModifierTokenizer, NameTokenizer et WhitespaceTokenizer.

De plus, la modification de la grammaire nous permet de prendre en compte de nouvelles littérales (des booléens et des caractères qui sont convertis en int), qui pourront être utilisées lors de l’assignation d’une valeur à un champ, mais aussi associées aux instructions bipush, sipush, ldc et ldc2_w. Le nouveau mécanisme est implémenté dans la classe LiteralTokenizer.

De fait, nous pouvons écrire :

.field final static MY_BOOLEAN Z = true
.field final static MY_CHAR = 'z'

sipush '!'
bipush false

Les nouveaux éléments de la grammaire sont présentés ci-dessous. Seuls les symboles qui nous intéressent ont été repris pour nous focaliser sur les champs.

class = ws classStartIdentifier ws classModifiers
        className classContent classEndIdentifier ws (* modifié *)
classContent = fields methods (* nouveau *)
fields = {field} (* nouveau *)
field = ws fieldStartIdentifier ws fieldModifiers ws fieldName ws
        fieldDescriptor ws oFieldAssignement (* nouveau *)
oFieldAssignement = [assignementOperator ws constantValue] ws (* nouveau *)
fieldName = identifier (* nouveau *)
fieldDescriptor = descriptor (* nouveau *)
identifier = asciiJavaLetter [{asciiJavaLetter | digit}]
fieldStartIdentifier = '.field' (* nouveau *)
fieldModifiers = {fieldModifier ws} (* nouveau *)
fieldModifier = 'public' | 'protected' | 'private' | 'static' | 'final' |
                'transient' | 'volatile' (* nouveau *)
descriptor = 'B' | 'C' | 'D' | 'F' | 'I' | 'J' | 'S' | 'Z' |
             '[' ['descriptor | 'L' className ';'
constantValue = character | string | boolean | integer | float (* nouveau *)
assignementOperator = '=' (* nouveau *)
asciiJavaLetter = ? a-z | A-Z | '_' | '$' ?
character = "'" ? One character between all characters excepted NULL ? "'" (* nouveau *)
string = '"' ? All characters but NULL ? '"'
boolean = 'true' | 'false' (* nouveau *)
integer = ? integer ? (* nouveau *)
float = ? float ? (* nouveau *)
digit = ? 0-9 ?

Pour intégrer ces nouveaux symboles, en plus des tokenizers, il est nécessaire de modifier les classes Symbols, EventType, Productions et PjbParser.

La classe PjbParser doit aussi prendre en compte les types ArgsType.FIELD et ArgsType.METHOD.

What’s next ?

Dans l’article suivant, nous nous intéresserons aux champs et méthodes d’instance.