JVM Hardcore – Part 20 – Bytecode – Champs et Méthodes de classes
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
etint
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 typeConstantClass
, lui-même contenant l’index d’un élément de typeConstantUTF8
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 typeConstantNameAndType
contenant deux index pointant vers deux éléments de typeConstantUTF8
.
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 typeConstantUTF8
représentant le nom de la méthode appelée et ;descriptorIndex
contient l’index d’un élément de typeConstantUTF8
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
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);
}
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
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);
}
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
ouACC_PUBLIC
. - les flags
ACC_FINAL
etACC_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 compilateurDeprecated
: 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
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();
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;
@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);
}
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);
}
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"
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);
}
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
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;
}
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
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());
}
}
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 typeConstantUtf8
dans le tableauConstantPoolEntry
contenant le nom du type de l’attribut (“ConstantValue”).constantValueIndex
contient l’index d’un élément de typeConstantInteger
,ConstantLong
,ConstantFloat
,ConstantDouble
ouConstantString
.
Notes :
- En Java un champ possédant le modificateur
final
mais initialisé dans un constructeur ou un bloc statique ne possédera pas l’attributConstantValue
. - Si un champ n’est pas
final
mais possède l’attributConstantValue
, 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’attributConstantValue
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
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);
}
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;
}
// ...
}
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>();
}
// ...
}
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’interfaceVisitable
.
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);
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 pasvoid
. Et ceci toujours en gardant à l’esprit que les typeslong
etdouble
prennent deux cases dans la pilegetstatic
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);
}
Pour calculer l’ensemble de ces valeurs, nous avons créé la classe DescriptorCounter et trois méthodes :
- fieldDescriptorUnits(final String fieldDescriptor) : compte le nombre de cases que prend le descripteur en paramètre dans la pile ou les variables locales.
- methodsDescriptorParamsUnits(final String methodDescriptor) : compte le nombre de cases que prennent les paramètres du descripteur de la méthode (
methodDescriptor
) dans la pile ou les variables locales. Cette méthode remplace la méthode countParameters() préalablement définie dans la classeMethod
(lien pointant vers la branchepart19
). - methodsDescriptorSignatureUnits(final String methodDescriptor) compte le nombre de cases que prend la valeur de retour que l’on soustrait au nombre de cases que prennent les paramètres du descripteur de la méthode (
methodDescriptor
) dans la pile ou les variables locales.
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)
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;
}
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)
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);
}
}));
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.