JVM Hardcore – Part 22 – Bytecode – Manipuler des Tableaux

academic_duke
Bien qu’il nous reste encore quelques instructions à étudier, nous arrivons presque à la fin de notre périple et nous sommes à même d’implémenter en bytecode des exemples complets et plus complexes que ceux que nous avons vu jusqu’à présent.

Néanmoins, certains éléments d’un fichier .class et instructions vont nous permettre d’aller encore plus loin comme nous le verrons aujourd’hui avec une vingtaine d’instructions dédiées à la manipulation de tableaux, et dans les deux articles suivants qui traiteront respectivement des exceptions, des classes anonymes et des classes internes.

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 utilisé 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.

Tableaux de primitifs

L’instruction newarray (0xbc) permet de créer un tableau dont les éléments sont de type primitif. Elle prend en argument un nombre d’un octet correspondant au type du tableau (cf. tableau ci-dessous).

Type Valeur
boolean 4
char 5
float 6
double 7
byte 8
short 9
int 10
long 11

État de la pile avant → après exécution : ..., length → ..., arrayref, où length est la taille du tableau à créer et arrayref la référence du tableau nouvellement créé. Si la valeur de length est inférieure à zéro, une exception de type NegativeArraySizeException est levée.

Voyons comment créer un tableau en PJB. La méthode getNewarrayOfInts() prend en paramètre la taille du tableau à créer et retourne le tableau de type int nouvellement créé.

Notons qu’en PJB l’instruction newarray a pour argument le type du tableau et non la valeur représentant ce type.

.method public static getNewarrayOfInts(I)[I
  iload_0
  newarray int
  areturn
.methodend

Source

Le test unitaire nous permet de vérifier que le tableau créé est du bon type et de la bonne taille.

@Test
public void getNewarrayOfInts() {
  final int[] i = Array.getNewarrayOfInts(10);
  Assert.assertEquals(10, i.length);

  i[9] = 3;
  Assert.assertEquals(3, i[9]);
}

Source

Comme nous l’avons vu dans le tableau précédent, il est possible de créer des tableaux de type boolean, byte, char et short, qui contrairement aux valeurs primitives ne sont pas convertis en int.

.method public static getNewarrayOfBooleans(I)[Z
  iload_0
  newarray boolean
  areturn
.methodend

Source

Le test unitaire est identique au précédent, à la différence que cette fois nous vérifions que le tableau est de type boolean.

@Test
public void getNewarrayOfBooleans() {
  final boolean[] b = Array.getNewarrayOfBooleans(10);
  Assert.assertEquals(10, b.length);

  b[9] = true;
  Assert.assertTrue(b[9]);
}

Source

Tableaux d’objets

L’instruction anewarray (0xbd) permet de créer un tableau dont les éléments sont des objets. Elle prend en argument un nombre 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 ConstantClass correspondant au type du tableau.

État de la pile avant → après exécution : ..., length → ..., arrayref, où length est la taille du tableau à créer et arrayref la référence du tableau nouvellement créé. Si la valeur de length est inférieure à zéro, une exception de type NegativeArraySizeException est levée.

En PJB, créer un tableau d’objets est similaire à la création d’un tableau de primitifs, à la différence que cette fois nous utilisons le nom complètement qualifié de la classe pour représenter le type du tableau.

.method public static getNewarrayOfStrings(I)[Ljava/lang/String;
  iload_0
  anewarray java/lang/String
  areturn
.methodend

Source

Le test unitaire est donc similaire à ceux que nous avons vus précédemment.

@Test
public void getNewarrayOfStrings() {
  final String[] s = Array.getNewarrayOfStrings(10);
  Assert.assertEquals(10, s.length);

  s[9] = "hello";
  Assert.assertEquals("hello", s[9]);
}

Source

Récupérer une valeur à un index d’un tableau

Tous comme les instructions xload qui permettent de récupérer les valeurs des variables locales du cadre d’une méthode, nous avons les instructions xaload (notons le a supplémentaire) qui permettent de récupérer des valeurs à un index d’un tableau. Néanmoins, la comparaison s’arrête là puisqu’elles ne s’utilisent pas de la même manière.

Ces instructions n’ont aucun argument, elles utilisent des éléments de la pile.

État de la pile avant → après exécution : ..., arrayref, index → ..., value, où arrayref est la référence du tableau auquel nous souhaitons accéder, index comme son nom l’indique la position dans le tableau à laquelle se trouve la valeur que nous souhaitons récupérer et value la valeur récupérée. En termes Java, value = arrayref[index].

Il existe huit instructions différentes permettant d’accéder aux valeurs de huit types de tableaux que nous pouvons avoir.

Hex Mnémonique Description
0x2e iaload Récupère une valeur de type int d’un tableau et l’empile
0x2f laload Récupère une valeur de type long d’un tableau et l’empile
0x30 faload Récupère une valeur de type float d’un tableau et l’empile
0x31 daload Récupère une valeur de type double d’un tableau et l’empile
0x32 aaload Récupère la référence d’un objet dans un tableau et l’empile
0x33 baload Récupère une valeur de type byte d’un tableau et l’empile
0x34 caload Récupère une valeur de type char d’un tableau et l’empile
0x35 saload Récupère une valeur de type short d’un tableau et l’empile

Voyons un exemple avec un tableau de double.

.method public static daload([DI)D
  aload_0   @ arrayref
  iload_1   @ index
  daload
  dreturn   @ value
.methodend

Source

Le test unitaire nous confirme que la méthode daload() retourne bien la valeur à l’index passé en paramètre (2 pour le test).

@Test
public void daload() {
  final double[] array = new double[]{1.2, 2.3, 3.4, 4.5};
  final double d = Array.daload(array, 2);
  Assert.assertEquals(3.4, d, 0.0001);
}

Source

Ajouter des valeurs dans un tableau

Comme les instructions xload qui ont pour miroir les instruction xstore, les instructions xaload ont pour miroir les instructions xastore présentées dans le tableau suivant.

Hex Mnémonique Description
0x4f iastore Stocke une valeur de type int dans un tableau
0x50 lastore Stocke une valeur de type long dans un tableau
0x51 fastore Stocke une valeur de type float dans un tableau
0x52 dastore Stocke une valeur de type double dans un tableau
0x53 aastore Stocke la référence d’un objet dans un tableau
0x54 bastore Stocke une valeur de type byte dans un tableau
0x55 castore Stocke une valeur de type char dans un tableau
0x56 sastore Stocke une valeur de type short dans un tableau

Notons que ces instructions ne prennent pas d’argument.

État de la pile avant → après exécution : ..., arrayref, index, value → ..., où arrayref est la référence du tableau auquel nous souhaitons ajouter la valeur, index la position dans le tableau à laquelle la valeur doit être ajoutée et value la valeur à ajouter.

Une fois encore le code PJB est très simple. La méthode aastore() prend en paramètre un tableau de String, l’index auquel nous souhaitons ajouter la valeur et la valeur.

@ array, index, value
.method public static aastore([Ljava/lang/String;ILjava/lang/String;)V
  aload_0   @ array
  iload_1   @ index
  aload_2   @ value
  aastore
  return
.methodend

Source

Un tableau étant un objet, lorsqu’il est passé en paramètre d’une méthode, seule sa référence est transférée à la méthode. De fait, pour savoir si ce tableau a subi des modifications, il n’est pas nécessaire que la méthode le retourne. Dans le cas du test, il nous suffit d’utiliser la référence contenue par la variable s.

@Test
public void aastore() {
  final String[] s = new String[10];
  Array.aastore(s, 2, "hello");

  Assert.assertEquals("hello", s[2]);
}

Source

Initialiser un tableau à une dimension

L’instruction arraylength (0xbe) permet de récupérer la taille d’un tableau. Elle ne prend aucun argument, mais s’attend à avoir la référence du tableau dont on souhaite connaître la taille au sommet de la pile.

État de la pile avant → après exécution : ..., arrayref → ..., length, où arrayref est la référence du tableau duquel nous souhaitons connaître la taille et length est la taille du tableau.

Avec la même valeur

Dans un premier temps nous allons créer une méthode prenant en paramètre un tableau et la valeur à ajouter à tous les index. Sachant que par défaut tous les éléments d’un tableau de primitifs sont initialisés avec la valeur 0 et null pour un tableau d’objets.

En Java, il nous suffirait de quelques lignes de codes :

public static void initArraySameValue(int[] array, int value) {
  for (int i = 0; i < array.length; i++) {
    array[i] = value;
  }
}

En revanche en PJB, cela en nécessite un peu pluss :

.method public static initArraySameValue([II)V
  aload_0       @ array
  arraylength
  istore_2      @ array.length
  iconst_0
  istore_3      @ int i = 0
  loop:
  iload_3
  iload_2
  if_icmpge stop    @ i < array.length
  aload_0       @ array
  iload_3       @ i
  iload_1       @ value
  iastore       @ array[i] = value
  iinc 3 1      @ i++
  goto loop
  stop:
  return
.methodend

Source

Le test unitaire se contente quant à lui de vérifier la valeur à tous les index.

@Test
public void initArraySameValue() {
  final int[] i = new int[5];
  Array.initArray(i, 10);

  Assert.assertEquals(10, i[0]);
  Assert.assertEquals(10, i[1]);
  Assert.assertEquals(10, i[2]);
  Assert.assertEquals(10, i[3]);
  Assert.assertEquals(10, i[4]);
}

Source

Avec des valeurs différentes

Pour l’exemple suivant, nous souhaitons voir comment est compilée l’expression new int[]{1, 2, 3, 4, 5}, en prenant pour modèle la méthode suivante :

public int[] initArrayDiffValue() {
  return new int[]{1, 2, 3, 4, 5};
}

En réalité, cette forme d’initialisation n’est qu’un sucre syntaxique, puisqu’en bytecode nous sommes obligé de créer un tableau et d’ajouter les valeurs une à une à chaque index.

int[] array = new int[5];
array[0] = 1;
array[1] = 2;
array[2] = 3;
array[3] = 4;
array[4] = 5;

Il nous suffit de convertir le code précédent en PJB, qui bien que fastidieux à écrire reste très simple :

.method public static initArrayDiffValue()[I
  iconst_5
  newarray int  @ création du tableau
  dup
  iconst_0
  iconst_1
  iastore       @ array[0] = 1
  dup
  iconst_1
  iconst_2
  iastore       @ array[1] = 2
  dup
  iconst_2
  iconst_3
  iastore       @ array[2] = 3
  dup
  iconst_3
  iconst_4
  iastore       @ array[3] = 4
  dup
  iconst_4
  iconst_5
  iastore       @ array[4] = 5
  areturn
.methodend

Source

Notons que nous utilisons l’instruction dup pour dupliquer la référence du tableau de manière à éviter d’ajouter le tableau dans une variable locale pour la récupérer à chaque fois. De fait, nous avons toujours une référence du tableau en bas de la pile.

Le test unitaire vérifie la valeur stockée à chaque index.

@Test
public void initArrayDiffValue() {
  final int[] i =  Array.initArrayDiffValue();

  Assert.assertEquals(1, i[0]);
  Assert.assertEquals(2, i[1]);
  Assert.assertEquals(3, i[2]);
  Assert.assertEquals(4, i[3]);
  Assert.assertEquals(5, i[4]);
}

Source

Tableaux à plusieurs dimensions

L’instruction multianewarray (0xc5) permet de créer un tableau à plusieurs dimensions. Elle prend deux arguments :

  • le premier est 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 ConstantClass correspondant au type du tableau.
  • le second est un nombre non signé d’un octet représentant le nombre de dimensions du tableau. De fait, nous ne pouvons pas avoir des tableaux de plus de 255 dimensions.

La taille de chaque dimension est indiquée par une valeur dans la pile.

État de la pile avant → après exécution : ..., length1, [length2, ...] → ..., arrayref, où length1, length2, … sont les tailles de chaque dimension. La valeur au sommet de la pile est la taille de la dimension la plus à droite (int[length1][length2]).

.method public static getMultianewarray(II)[[Ljava/lang/Object;
  iload_0
  iload_1
  multianewarray [[Ljava/lang/Object; 2
  areturn
.methodend

Source

Le test unitaire nous permet de vérifier que le tableau créé a bien deux dimensions et que l’on peut insérer des objets.

@Test
public void multianewarray() {
  final Object[][] o = Array.getMultianewarray(5, 10);

  Assert.assertEquals(5, o.length);
  Assert.assertEquals(10, o[0].length);

  o[2][4] = "hello";

  Assert.assertEquals("hello", o[2][4]);
}

Source

Récupérer un sous-tableau d’un tableau à plusieurs dimensions

Un tableau à plusieurs dimensions est en réalité une série de sous-tableaux que nous pouvons récupérer à l’aide de l’instruction aaload.

@ array, index
.method public static getSubArray([[II)[I
  aload_0
  iload_1
  aaload  @ get sub array
  areturn
.methodend

Source

Pour tester la méthode getSubArray(), nous créons un tableau à deux dimensions nommé i, dans lequel nous ajoutons un sous-tableau (subArray) à l’index 2. Nous vérifions ensuite que le sous-tableau retourné est égal à subArray.

@Test
public void getSubArray() {
  final int[] subArray = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
  final int[][] i = new int[5][10];
  i[2] = subArray;

  final int[] actuals = Array.getSubArray(i, 2);

  Assert.assertArrayEquals(subArray, actuals);
}

Source

Ajouter une valeur à un tableau à plusieurs dimensions

Étant donnée qu’un tableau à plusieurs dimensions est une série de sous-tableaux, pour pouvoir ajouter une valeur il est tout d’abord nécessaire de récupérer chaque sous-tableau.

En Java, nous pouvons décomposer l’ajout de la valeur en deux étapes :

public static void setValue(int[][]array, int index1, int index2, value) {
  int[] subarray = array[index1];
  subarray[index2] = value;
}

Nous pouvons traduire ceci en PJB de la manière suivante :

@ array, index1, index2, value
.method public static setValue([[IIII)V
  aload_0
  iload_1
  aaload  @ récupère un sous-tableau
  iload_2
  iload_3
  iastore @ ajoute une valeur
  return
.methodend

Source

Le test unitaire vérifie que la valeur 1256 a bien été ajoutée aux index [2][3].

@Test
public void setValueMultiArray() {
  final int[][] i = new int[5][10];
  Array.setValue(i, 2, 3, 1256);

  Assert.assertEquals(1256, i[2][3]);
}

Source

Récupérer une valeur d’un tableau à plusieurs dimensions

Pour récupérer une valeur d’un tableau à plusieurs dimensions nous devons récupérer tout d’abord le sous-tableau dans lequel se trouve la valeur pour ensuite la retourner.

@ array, index1, index2
.method public static getValue([[III)I
  aload_0
  iload_1
  aaload  @ récupère le sous-tableau
  iload_2
  iaload  @ ajoute une valeur
  ireturn
.methodend

Source

Le test unitaire permet de vérifier que la valeur à l’index [2][3] est bien celle que nous avons ajoutée (10).

@Test
public void getValueMultiArray() {
  final int[][] array = new int[5][10];
  array[2][3] = 10;
  final int i = Array.getValue(array, 2, 3);

  Assert.assertEquals(10, i);
}

Source

Initialiser un tableau à deux dimensions

Initialiser un tableau à plusieurs dimensions en Java nécessite d’imbriquer autant de boucles qu’il y a de dimensions. Voyons un exemple avec deux dimensions :

public static void init2DArray(int[][] array, int value) {
  for (int i = 0; i < array.length; i++) {
    for (int j = 0; j < array[i].length; j++) {
      array[i][j] = value;
    }
  }
}

Ceci n’a absolument rien d’extraordinaire. En revanche en PJB, bien que nous ayons déjà vu toutes les instructions utilisées ci-dessous, le code est un peu plus complexe.

.method public static init2DArray([[II)V
  aload_0
  arraylength
  istore_2    @ Taille dimension 1
  iconst_0
  istore_3    @ index_1 = 0

  loop_1:
  iload_3
  iload_2
  if_icmpge end_loop_1
    aload_0
    iload_3
    aaload
    arraylength
    istore 4  @ Taille dimension 2
    iconst_0
    istore 5  @ index_2 = 0

    loop_2:
    iload 5
    iload 4
    if_icmpge end_loop_2
      aload_0
      iload_3
      aaload
      iload 5
      iload_1
      iastore   @ ajoute une valaur
      iinc 5 1  @ iinc index_2
    goto loop_2

  end_loop_2:
  iinc 3 1  @ iinc index_1
  goto loop_1

  end_loop_1:
  return
.methodend

Source

Pour tester que toutes les cases de notre tableau possèdent une valeur, nous allons tester avec un tableau de 3×2 :

@Test
public void init2DArray() {
  final int[][] i = new int[3][2];
  Array.init2DArray(i, 10);

  Assert.assertEquals(10, i[0][0]);
  Assert.assertEquals(10, i[0][1]);
  Assert.assertEquals(10, i[1][0]);
  Assert.assertEquals(10, i[1][1]);
  Assert.assertEquals(10, i[2][0]);
  Assert.assertEquals(10, i[2][1]);
}

Source

L’initialisation suivante new int[][]{{1, 1}, {2, 2}, {3, 3}} étant similaire à à celle d’un tableau à une dimension son implémentation en PJB peut faire office d’exercice pour le lecteur.

Implémentation

Bien que nous ayons vu de nombreuses instructions, seule l’instruction multianewarray nécessite une nouvelle classe associée :

Associées à la classe MultianewarrayInstruction, les classes MultianewarrayInstructionFactory et MultianewarrayMetaInstruction ont aussi étaient ajoutées. Tout comme les types ArgsType ARRAY_TYPE et ARRAY_MULTIDIM.

De plus, outre l’adaptation des classes Disassembler, HexDumper, PjbDumper et PjbParser, la méthode getArrayType(), permettant d’analyser le type du tableau à créer (argument de l’instruction newarray) a été ajoutée dans la classe NameTokenizer.

Et pour terminer, l’ensemble des instructions ont été ajoutées aux classes Instructions, MetaInstructions et MethodBuilder.

What’s next ?

Dans l’article suivant nous nous intéresserons aux exceptions.

Nombre de vue : 94

AJOUTER UN COMMENTAIRE