III. Partie III : Bytecode avec Plume Java Bytecode Assembler (PJBA)▲
Nous allons maintenant voir comment utiliser Plume Java Bytecode Assembler (PJBA) qui nous permettra d'avoir l'impression d'écrire du bytecode sans passer par des 0 et des 1. Nous l'utiliserons de manière intensive au cours des prochaines semaines. Sans lui, il aurait été nécessaire :
- soit d'utiliser plusieurs outils qui peuvent s'avérer compliqués à prendre en main ;
- soit d'écrire des fichiers binaires à la main, ce qui est, vous me l'accorderez, aussi utile que BrainFuck.
De plus, en ayant notre propre outil, nous allons pouvoir l'étudier en profondeur, puisqu'il a été conçu tout aussi bien pour être simple à utiliser qu'à comprendre.
Par convention, nous utiliserons l'extension .pjb pour identifier les fichiers devant être assemblés par PJBA, mais toutes les extensions sont possibles.
Le code est disponible sur Github (tag et branche).
Tous les articles déjà publiés de la série portent le tag jvmhardcore.
III-A. Getting Started▲
Pour commencer, nous allons voir un exemple complet d'une classe avec la méthode statique nommée add() de la partie précédente qui additionne deux entiers.
2.
3.
4.
5.
6.
7.
8.
9.
10.
.class org/isk/jvmhardcore/bytecode/parttwo/Adder
.method add(II)I
iload_0
iload_
iadd
ireturn
.methodend
.classend
Toutes les lignes commençant par un point (« . ») sont des directives et ce sont les choses qui nous intéresseront dans cette partie.
Pour l'instant, nous nous limiterons au strict minimum nous permettant d'étudier nos premières instructions. Dans quelques semaines, nous recréerons cet assembleur extrêmement simple. Nous pourrons ensuite y ajouter des fonctionnalités au fur et à mesure.
La version équivalente en Java est la suivante :
2.
3.
4.
5.
6.
7.
package
org.isk.jvmhardcore.bytecode.parttwo;
public
class
Adder {
public
static
int
add (
int
i1, int
i2) {
return
i1 +
i2;
}
}
Pour tester la méthode add() de la version Plume, il nous faut tout d'abord assembler le fichier Adder.pjb en un fichier .class. À partir de là, en ajoutant le fichier .class au classpath, nous pouvons appeler la méthode depuis un test unitaire écrit en Java.
Pour assembler les fichiers .pjb en .class :
ant assemble
Le test unitaire correspondant est le suivant :
2.
3.
4.
5.
6.
@
Test
public
void
add0
(
) {
final
int
sum =
Adder.add
(
2
, 3
);
Assert.assertEquals
(
5
, sum);
}
Pour l'exécuter depuis Ant :
ant test -DtestClass=org.isk.jvmhardcore.bytecode.parttwo.AdderTest -DtestMethod=add0
III-B. Descripteurs▲
Un descripteur est un moyen d'exprimer un type à l'aide de lettres et de symboles :
- la plupart des types sont représentés par un seul caractère (I pour int, F pour float, ou de manière moins évidente Z pour boolean, J pour long, etc.) ;
- le symbole [ indique un tableau du type indiqué ensuite. Par exemple [I signifie un tableau d'entiers. Il est possible d'avoir plusieurs crochets à la suite pour indiquer des tableaux à plusieurs dimensions ([[Z pour un tableau de tableaux de booléens) ;
- la lettre L signifie un objet du type qui suit (Ljava/lang/String;) ;
- seuls les types d'objets se terminent par un point-virgule.
Tous les descripteurs possibles sont présentés dans le tableau suivant :
Descripteur |
Type |
---|---|
Z |
boolean |
B |
byte |
S |
short |
C |
char |
I |
int |
J |
long |
F |
float |
D |
double |
V |
void |
[<type> |
tableau de type <type> |
L<type>; |
Objet de type <type> |
Note : tout comme en Java, en bytecode (et de fait dans un fichier .pjb), V (void) ne peut être utilisé qu'en type de retour d'une méthode.
Exemples :
Bytecode |
Java |
add(II)I |
int add(int i1, int i2) |
concat(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; |
String concat(String s1, String s2) |
merge([Z[Z)[Z |
boolean[] merge(boolean[] a1, boolean[] a2) |
III-C. Commentaires▲
Les commentaires d'une seule ligne sont identifiés soit par :
- un signe croisillon (#) ;
- une arobase (@);
- un point-virgule (;).
Tout ce qui se trouve à droite de ces signes est un commentaire.
Il est aussi possible d'avoir des commentaires de plusieurs lignes (/* … */) :
2.
3.
/* Ceci est
un commentaire
de plusieurs lignes */
III-D. Classes▲
La première directive que nous allons voir est .class. Elle indique à l'assembleur le nom complètement qualifié de la classe. De plus, ce nom indique le nom des répertoires contenant le fichier et celui du fichier .class généré.
2.
3.
.class <nom complètement qualifié de la classe>
# contenu de la classe
.endclass # Indique la fin d'une classe
Exemple :
2.
3.
.class org/isk/jvmhardcore/bytecode/parttwo/Adder
# ...
.endclass
Équivalent Java :
2.
3.
4.
5.
package
org.isk.jvmhardcore.bytecode.parttwo;
public
class
Adder {
}
Le nom du package et de la classe sont concaténés et les points inclus dans le nom du package sont remplacés par des slashes (/).
Le nom du package suivi de celui de la classe constituent le nom complètement qualifié d'une classe.
Limitations actuelles que nous lèverons ultérieurement :
- une seule directive .class par fichier est autorisée ;
- une classe est obligatoirement publique (public).
III-E. Méthodes▲
Une méthode est définie de la manière suivante :
2.
3.
.method <nom de la méthode>(<descripteur des paramètres>)<descripteur du retour>
# Instructions
.endmethod
Notons que contrairement à Java, le type de retour est complètement à la fin, comme nous l'avons vu dans le tableau précédent.
Notes :
- pour indiquer plusieurs paramètres, il suffit de les écrire les uns à la suite des autres sans espace ;
- le descripteur de la méthode (paramètres et type de retour) doit être unique. En revanche, contrairement à Java, deux descripteurs peuvent différer uniquement par le type de retour.
Limitations actuelles :
- une méthode est obligatoirement publique et statique (public static).
III-F. Noms▲
Les noms des packages, classes et méthodes doivent commencer par une lettre ASCII, le caractère underscore « _ » ou dollar « $ », puis des lettres ASCII, des chiffres ou les caractères underscore « _ » ou dollar ‘$'.
Par exemple : helloworld, $hello_world1, _hello$world2, _my/$package/DoSomething1, etc.
III-G. Encodage▲
Le projet jvm_hardcore doit être en UTF-8.
III-H. Créer un fichier classe en Java▲
PJBA permet de créer un fichier .class à partir d'un fichier texte comme nous venons de le voir. Mais il est aussi possible d'utiliser des objets Java. Néanmoins en l'état, pour qui ne sait pas de quoi est constitué le format .class, son utilisation est plutôt compliquée.
En reprenant encore une fois notre exemple de méthode add() :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
@
Test
public
void
assembleJava
(
) throws
Exception {
//
ConstantPoolEntries
final
String className =
"
org/isk/jvmhardcore/bytecode/parttwo/AdderJava
"
;
final
String methodName =
"
add
"
;
final
String methodDescriptor =
"
(II)I
"
;
//
ClassFile
final
ClassFile classFile =
new
ClassFile
(
className);
final
int
classNameIndex =
classFile.addConstantUTF8
(
className);
final
int
thisClassIndex =
classFile.addConstantClass
(
classNameIndex);
classFile.setThisClassIndex
(
thisClassIndex);
//
Method
final
int
codeAttributeIndex =
classFile.addConstantUTF8
(
Code.ATTRIBUTE_NAME);
final
int
methodIndex =
classFile.addConstantUTF8
(
methodName);
final
int
descriptorIndex =
classFile.addConstantUTF8
(
methodDescriptor);
final
Method method =
new
Method
(
codeAttributeIndex);
method.setNameIndex
(
methodIndex);
final
int
parameterCount =
Method.getParameterCount
(
methodDescriptor);
method.setDescriptorIndex
(
descriptorIndex, parameterCount);
classFile.addMethod
(
method);
//
Instructions
method.addInstruction
(
Instructions.iload_0
(
));
method.addInstruction
(
Instructions.iload_1
(
));
method.addInstruction
(
Instructions.iadd
(
));
method.addInstruction
(
Instructions.ireturn
(
));
this
.createFile
(
classFile);
}
Je ne m'attarderai pas sur cet exemple, nous y reviendrons un peu plus tard. De plus, une de nos actions sera de simplifier l'utilisation de PJBA en mode Java puisque même si l'on sait à quoi tout ceci correspond, 30 lignes pour créer une classe et une méthode qui ne fait qu'une addition, cela fait beaucoup.
Pour tester cette classe :
ant assemble
ant test -DtestClass=org.isk.jvmhardcore.bytecode.parttwo.AdderTest -DtestMethod=add1
III-I. Utilisation du code source▲
Pour pouvoir assembler les fichiers .pjb en .class, la structure des projets a dû évoluer :
project
| +- 01_src (code source)
| | +- main
| | | +- assembler (Assembleur)
| | | +- java
| | | +- pjb (Plume Java Bytecode)
| | +- test
| | | +- java
| | | +- resources
| +- 02_build (généré)
| | | +- assembler
| | | | +- classes (Assembleur compilé)
| | | | +- reports (Rapport d'assemblage)
| | | +- classes
| | | +- junit-data
| | | +- junit-reports
| | | +- pjb-classes (.pjb assemblés en .class)
| | | +- test-classes
| +- 03_dist (généré)
III-I-1. Assembleur▲
Le répertoire 01_src/main/assembler/ contient les classes permettant de générer des fichiers .class à partir d'un fichier .pjb ou du code Java. Par simplicité, le choix a été fait d'utiliser JUnit au lieu d'une méthode main() pour exécuter l'assembleur.
Le répertoire 02_build/assembler/ contient les composants compilés de l'assembleur et les rapports des tests unitaires exécutés pour pouvoir générer des fichiers .class.
La méthode assemblePjb() prend tous les fichiers du répertoire pjb/ et de ses enfants quelle que soit leur extension et appelle le parser PJBA (disponible dans le répertoire 02_libs sous le nom pjba.jar).
Les fichiers générés sont créés dans le répertoire 02_build/pjb-classes. Il doit être par conséquent rajouté au classpath pour que les tests unitaires testant ces classes et méthodes puissent y accéder.
III-I-2. Scripts de construction Ant▲
Une target assemble - déjà présentée au cours de cet article - a été rajoutée.
ant assemble
De plus, pour que les scripts de construction restent simples et qu'il soit possible de compiler/assembler tous les projets à partir du script principal (celui à la racine du projet), toutes les commandes (targets) Ant sont accessibles depuis le répertoire root de n'importe quel projet. Par conséquent, il est possible d'utiliser la target compile sur le projet bytecode/ alors que le répertoire 01_src/main/java n'existe pas. Son exécution se terminera en succès, mais ne fera rien, puisque nous testons si le répertoire 01_src/main/java existe avant de poursuivre l'exécution.
III-J. What's next ?▲
Au cours de cet article, nous nous sommes contenté de représenter une classe sous une forme proche de sa représentation en bytecode. Les choses sérieuses commenceront dès l'article suivant. Nous étudierons nos premières instructions.