Implémentation

Introduction

Cette partie traite plus spécifiquement de l'implémentation d'un traducteur de bytecode. Les technologies et outils existants sont passés en revue, ainsi que les choix effectués. L'implémentation est proche de la partie théorique précédente.

Parcours des technologies existantes, choix effectués

Introduction

Pour développer un traducteur de bytecode, il est favorable d'utiliser au maximum des librairies et outils existants pour ne pas dupliquer de travail inutilement. Ainsi, on évite de perdre trop de temps à se concentrer sur le format interne binaire d'un fichier contenant du bytecode, si on possède une bonne abstraction.

Librairies de lecture/écriture de bytecode Java

Java est un langage qui existe de depuis un bon nombre d'années, et est beaucoup utilisé dans des projets de recherche. Ainsi, il existe plusieurs librairies de lecture, production et instrumentation de bytecode. On peut noter par exemple Jikes Bytecode Toolkit [[JikesBT]], BIT: Bytecode Instrumenting Tool [[Bit]], Gnu Bytecode [[GnuBT]], BCEL: Byte Code Engineering [[Bcel]]. On peut noter que toutes ces librairies sont écrites en Java.

BCEL est la librairie la plus utilisée dans d'autres projets, et offre le plus de fonctionnalités avancées. C'est donc la librairie choisie pour l'implémentation.

Librairies de lecture/écriture de bytecode CLR

La plate-forme CLR incorpore dans les classes de la librairie de base (BCL) le support pour gérer les informations du bytecode (assemblages, types, champs, ...), avec la librairie System.Reflection. Microsoft a rajouté une librairie similaire, System.Reflection.Emitdans son implémentation .Net, permettant de générer du bytecode. L'existence de cette librairie nous offre toutes les fonctionnalités nécessaires pour générer le bytecode.

Une autre façon de produire du code CIL est de produire une version texte du code assembleur CIL et de l'assembler avec un assembleur adéquat. Un tel assembleur existe notamment sur la plate-forme .Net.

Un portage de la libraire System.Reflection.Emit a été écrit par Laurent Rolaz [[Rolaz02]] aussi dans le cadre d'un traducteur de bytecode Java vers CIL. Ce portage utilise le principe mentionné précédemment, de produire une version textuelle du code assembleur qui devra ensuite être compilé avec un assembleur. On peut cependant noter que cette librairie n'implémente qu'un petit sous-ensemble de System.Reflection.Emit, est qu'elle n'a pas été abondamment testée. Cela implique qu'il sera nécessaire d'investir du temps pour l'amélioration de cette librairie en cas d'utilisation pour ce projet.

Il existe cependant un problème qui se pose avec les deux outils mentionnés ci-dessus: La libraires BCEL est écrite en Java, alors que la libraire .Net pour écrire le bytecode est en C#, donc en CIL. On a alors deux possibilités pour faire face à ce problème:

  1. Porter la librairie BCEL sur .Net, en la compilant par exemple avec J#. Ceci implique des modifications conséquentes sur la libraire BCEL, car J# ne possède qu'une version préliminaire de la libraire Java. Il faudrait aussi porter toutes les classes qui ne sont pas présentes dans la libraire Java de J#, mais qui sont utilisées par BCEL.

  2. Utiliser le portage de la libraire System.Reflection.Emit en Java, de façon à pouvoir utiliser BCEL directement.

C'est la deuxième solution qui a été retenu pour le projet, pour éviter le premier problème mentionné.

Cependant, cette solution pose un problème supplémentaire: l'accès aux assemblages .Net n'est pas disponible à partir de la plate-forme Java. Il y a plusieurs solutions possibles pour résoudre ce problème:

  1. Créer un fichier qui contient la description de tous les assemblages, type, et membres, par exemple sous format xml.

  2. Partir du principe que tous les champs, types, et membres seront accessibles après la traduction. Un fichier de configuration est utilisé pour faire la correspondance entre les classes et les assemblages, comme mentionné dans la la section intitulée “Partie théorique”.

Structure du traducteur, description de l'implémentation

Introduction

L'implémentation du traducteur de bytecode se nomme java2il. Le code source, binaire, ainsi que la documentation est disponible sur "http://java2il.sourceforge.net/".

Comment utiliser le logiciel

La description de l'installation est détaillée dans le fichier README.txt qui est disponible dans l'archive du logiciel.

Utilisation. Pour obtenir un aide sur les paramètres disponibles, il suffit de passer l'option --help lors de l'exécution.

Exemple d'utilisation. Pour traduire un fichier Hello.class, par exemple.

 ./java2il --assembly Hello Hello.class

Etant donné que la librairie Msil produit un fichier sous format texte, il faut ensuite l'assembler avec un assembleur, comme par exemple celui fourni avec "Microsoft .Net Framework". Exemple:

ilasm /out:Hello.exe Hello.exe.il
          

Note

java2il offre la possibilité d'assembler directement les fichiers produits. Pour ce faire, il faut qu'un assembleur soit accessible par java2il. Il faut aussi paramétrer quel type d'assembleur utiliser, étant donné que chaque assembleur à une syntaxe différente. Le type d'assembleur devra être défini dans le fichier de configuration java2il.properties (plus d'informations dans la la section intitulée “Fichier de configuration du traducteur”). L'option à utiliser se nomme --assemble.

Structure des fichiers Java

Description des fichiers

Java2Il.java

Fichier principal qui lit les paramètres de la ligne de commande, charge les fichiers classe JVM, etc...

AssemblyTranslator.java

Gère la traduction des paquetages en assemblages.

ClassTranslator.java

Gère la traduction des classes.

MethodTranslator.java

Gère la traduction des méthodes.

TypeTranslator.java

Classe utilisée pour traduire tous les types. Des méthodes statiques peuvent traduire des types simples, ou des types référence. Elle est aussi responsable de traduire les signatures des méthodes, champs, et modificateurs.

CodeVisitor.java

Classe abstraite pour définir des visiteurs du code des méthodes. Utilisée pour l'analyse et la production du code.

CodeAnalysisVisitor.java

Classe qui sert à la phase d'analyse du code. Elle va effectuer une vérification du code, pour la traduction des variables locales, comme décrit dans la section intitulée “Instructions d'accès aux variables / paramètres:”.

ExceptionAnalysisVisitor.java

Sous-classe de CodeAnalysisVisitor. Elle est chargée d'analyser toutes les constructions relatives aux exceptions. C'est elle qui va appliquer les algorithmes de reconstitution des portées des gestionnaires d'exception (voir la section intitulée “Instructions relatives aux exceptions” pour l'algorithme).

CodeEmitVisitor.java

Classe de génération de code. C'est ici que les instructions triviales sont directement traduites, par exemple.

ExceptionEmitVisitor.java

Sous-classe de CodeEmitVisitor, elle va définir les débuts et fins des blocs d'exception.

MemberMapper.java

Classe qui fait la traduction des noms des champs. C'est elle qui implémente l'algorithme de remplacement des méthodes sur les objets System.Object et System.String.

TranslationContext.java

Classe qui contient toutes les données globales de la traduction.

AssembliesFactory.java

Classe permettant de générer des références vers des assemblages. C'est dans ce fichier que l'algorithme de traduction des paquetages en assemblages est implémenté (cf. la section intitulée “Traduction des paquetages en assemblages”).

LocalVariableMapper.java

Cet objet contient toute l'information utilisée pour la traduction des accès aux variables locales et paramètres. Lors de l'analyse, des informations y sont enregistrées, et sont utilisées ensuite lors de la production du code (pour l'algorithme, voir la section intitulée “Instructions d'accès aux variables / paramètres:”).

Implémentation de la traduction des noms de paquetages

Comme expliqué dans la section intitulée “Traduction des paquetages en assemblages”, il est nécessaire d'avoir un fichier de configuration où stocker les paramètres relatifs à la correspondance entre les paquetages Java et assemblages .Net.

Dans le fichier AssembliesFactory.java, un fichier de configuration nommé assemblies.properties est lu lors de l'initialisation de la traduction. Voici ci-dessous un exemple d'un tel fichier:

Exemple 12. Fichier assemblies.properties

# assemblies.properties: Configuration file used to locate assemblies.
#

# assemblies signature
assembly.vjslib.ver     = 1:0:3300:0
assembly.vjslib.pubkey = B0:3F:5F:7F:11:D5:0A:3A

# package name remapping
packagemap.system         = System

# package to assemblies mapping
mapping.sun             = vjslib
mapping.org             = vjslib
mapping.java            = vjslib
mapping.javax           = vjslib
mapping.com.ms          = vjslib
mapping.System          = mscorlib

mapping.NativeCode      = classpath

          

Dans ce fichier, on trouve plusieurs informations:

  • Informations relatives à un assemblage: Il est possible de spécifier des informations propres à un assemblage, comme la version ainsi que la clef publique. Pour ce faire, il suffit de déclarer une propriété avec un nom comme assembly.XXX.ver pour la version et assembly.XXX.pubkey pour la clef publique. Le XXX représente le nom de l'assemblage à paramétrer.

  • Information de correspondance paquetage => assemblage: C'est ici que l'on peut spécifier les correspondances entres paquetages et assemblages. Pour ajouter une correspondance, il faut ajouter une propriété nommée mapping.XXXXXX représente le nom du paquetage Java. La valeur de la propriété est l'assemblage CLR.

Le fichier de configuration assemblages.properties est localisé par le chargeur de classes Java. Dans la distribution standard de java2il, on peut par exemple modifier le fichier à partir du dossier src/assemblies.properties, et relancer une compilation. La compilation va copier le fichier vers le répertoire build, accédé lors de l'exécution de java2il.

Fichier de configuration du traducteur

Il existe un fichier de configuration java2il.properties qui permet de modifier certaines options de traduction, comme par exemple quels sont les objets responsables de rediriger les appels de méthode de System.Object.

Le fonctionnement de ce fichier de configuration est similaire à assemblies.properties, pour paramétrer les correspondances entre paquetages et assemblages.

Voici un exemple de ce fichier, avec un explication des paramètres.

Exemple 13. Fichier java2il.properties

skipnatives=false

# if skipnatives is not true, this is the name of the Assembly where
# to remap the Il methods
nativepackage=NativeCode

# The kind of assembler syntax supported. For now, only "microsoft"
# and "pnet" flavor are supported. For the "pnet" assembler, see
# http://www.southern-storm.com.au/portable_net.html
ilasmflavor=microsoft

# The object where to remap java.lang.Object methods
stringmapper=com.ms.vjsharp.lang.StringImpl

# The object where to remap java.lang.String methods
objectmapper=com.ms.vjsharp.lang.ObjectImpl

          

Des commentaires expliquent à quoi servent les différents paramètres. On peut voir les deux propriétés qui servent à spécifier les fichiers où rediriger les méthodes java.lang.Object et java.langString. On remarque aussi que c'est ici que l'on peut paramétrer quel assembleur utiliser, avec la propriété ilasmflavor.

Limitations d'implémentations

Une difficulté majeure de la traduction est sans doute la reconstitution des blocs d'exceptions. Certaines méthodes qui comportent des blocs d'exception imbriqués posent parfois problème lors de la détection des limites de ces blocs. Ainsi, le code traduit peut parfois être erroné et ne plus être vérifiable.

La gestion des exceptions de la machine virtuelle n'est pas implémentée pour le moment. Les programmes qui dépendent du fait de pouvoir rattraper ces exceptions auront un comportement indéfini lors de la traduction.

Le vérificateur de la libraire BCEL a été utilisé pour faire l'analyse des types. Un problème est que ce vérificateur déclare parfois que certaines méthodes ne sont pas correctes, alors qu'elles le sont (elles ont été produites par le compilateur Java de Sun). Si la vérification échoue, il n'est plus possible de continuer la traduction, car l'information sur les types n'est pas disponible. Ce problème a été rencontré lors de la traduction de la libraires Java.

Conclusions, tests

Tests

Des tests de traduction ont été effectués sur un petit compilateur nommé Misc développé au laboratoire des Méthodes de Programmation du Département d'informatique de l'EPFL, dans le cadre d'un cours de compilation.

Les tests sont concluants, car le compilateur généré est fonctionnel, à l'identique de la version Java.

Points à améliorer

Classes internes. Il serait possible d'implémenter le support des classes internes lors de la traduction, comme mentionné dans la section intitulée “Classe internes (Inner Classes)”.

Gestion des correspondances entre exception CLR et JVM. La libraires msil de génération de code CIL assembleur n'implémente actuellement pas la gestion des blocs filter. Par conséquent, l'algorithme de correspondance entre exception CLR et JVM n'est actuellement pas implémenté. Ainsi, attraper un exception comme java.lang.NullPointerException ne fonctionnera pas dans les programmes traduits.

Amélioration de la recherche des blocs d'exception. Pour certaines méthodes qui contiennent des blocs d'exceptions imbriqués complexes, la traduction est parfois erronée, et le code invérifiable. Une amélioration à faire serait de mieux gérer les cas difficiles en améliorant les algorithmes.

Meilleure gestion des variables locales de type référence.  Actuellement, si un slot Java peut contenir des variables locales de types différents, le type du slot est celui du plus grand super type commun de tous les types. Des instruction castclass sont alors utilisées. Une meilleure solution serait de faire une analyse de flux, pour connaître les régions où les variables sont utilisées. Cette analyse permettrait d'éviter des instructions castclass en allouant la bonne quantité de variables locales pour chaques types.

Diverses optimisations des instructions. La traduction de certaines instructions n'est actuellement pas faite de la manière la plus optimale possible. Par exemple l'instruction tableswitch utilise une suite de comparaisons, sans algorithme dichotomique. Un meilleur aglorithme pourrait être utilisé pour cette instruction. Un autre exemple est la traduction des instruction *cmp*, qui pourraient parfois être traduites plus efficacement en analysant comment sont utilisés les valeures résultat.

Extensions à d'autres implémentations de la CLR. Pour le moment, java2il dépend de la libraires J#, qui n'est disponible que sur Windows avec l'implémentation .Net de Microsoft. Il serait intéressant de développer l'infrastructures nécessaire pour pouvoir utiliser d'autres implémentations. Les projets d'intérêt à ce sujet sont IKVM et Mono, qui sont décrits dans la section suivante.

Travaux d'intérêt

Cette section décrit d'autres travaux qui sont en relation avec le traducteur de bytecode.

Travaux d'intérêt

Ikvm

IKVM.Net [[Ikvm]] est une machine virtuelle pour la CLR. Elle est capable de faire tourner des programmes Java sur la plate-forme CLR, elle peut aussi traduire statiquement le bytecode JVM en bytecode pour la CLR.

Ce projet est donc très similaire à java2il. Il est écrit en C#, et tourne sur la CLR. Il est relativement mature (environ 21k lignes de code contre 11k pour java2il), et est capable de traduire des programmes Java très conséquents.

Il tourne déjà sur la plate-forme .Net de Microsoft et bientôt sur l'implémentation Mono.

Mono

Mono [[Mono]] est une implémentation logiciel libre de la CLR. L'implémentation ne couvre pas toutes les libraires disponibles sur .Net, mais les progrès sont rapides. Un avantage de cette implémentation est qu'elle est portable sur les Unix récents, et permet ainsi d'avoir une plate-forme CLR sur GNU/Linux.

L'intérêt pour cette implémentation dans le cadre de java2il est qu'il serait possible, en développant le code nécessaire, d'utiliser java2il sur Mono. Ainsi, java2il ne serait plus dépendant de l'implémentation de Microsoft .Net qui le restreint à Windows.

Portable.net

Portable.Net [[Pnet]] est une autre implémentation de la CLR sur Unix. Elle est à la base du projet Dotgnu (www.dotgnu.org). Portable.Net comporte un compilateur C#, une machine virtuelle, un assembleur, un vérificateur de code et les classes de base de la CLR.

L'intérêt de ce projet pour java2il est qu'il est possible d'avoir un assembleur de code CIL sur Unix.

Pour conclure, perspectives

La traduction de bytecode entre les deux plates-formes peut sembler simple à priori, mais il existe cependant certains problèmes qui peuvent se montrer relativement complexes.

L'implémentation du traducteur à atteint un bon niveau, en permettant à des programmes de taille descente d'être traduits. L'implémentation reste ouverte aux améliorations futures.