martes, 26 de octubre de 2010

Compilando para la JVM

Este es el inicio de una serie de entradas sobre la aventura de compilar para la Java Virtual Machine (JVM) como parte de un curso de compiladores en la universidad. Por tanto, se asume que el lector conocerá elementos básicos del lenguaje de programación Java (primer, más no único, usuario de la JVM): Data types, primitivos, referencias, objetos, clases, métodos.

Para entrar en contexto, la idea es convertir programas escritos en una gramática libre de contexto similar a esta y unas reglas semánticas muy básicas, a bytecode con la estructura de un .class de la JVM.

De entrada les recomiendo echar un vistaso a tres links fundamentales (todos en inglés): esteeste y la especificación de la JVM. Nada que google no pueda revelar, pero resulta bastante útil tenerlos a la mano. 

¿Qué es la JVM?
La respuesta a esta pregunta se encuentra, al menos oficialmente, en The Java Virtual Machine Specification, que define tres cosas:
  • Un conjunto de instrucciones (bytecodes) con su respectivo significado.
  • Un formato de archivos llamado class, que se usa para transmitir bytecodes y demás estructura de una forma independiente de plataforma. 
  • Un algoritmo para verificar que los programas no comprometan la integridad de la JVM: verificación
Hablaremos a detalle sobre los primeros dos elementos de esta especificación más adelante.

javap - The Java Class File Diassembler
Una herramienta que será de mucha utilidad para analizar los .class generados por el compilador de java es javap. Su descripción en man javap. Los flags a utilizar:
  •  -l: 'Prints out line and local variable tables'
  • -c: 'Prints out diassembled code (...) for each of the methods in the class'
  • -s: 'Prints internal type signatures'
  • -verbose: 'Prints stack size, number of locals and args for methods'
Es de interés esta herramienta pues lo que nos urge es la generación de JVM bytecode similar a la que lleva a cabo javac.

Los flags anteriores nos dan una idea de los elementos a investigar: local variable tables, stack size, number of args, etc.

Comenzando con algo sencillo:
//StructA.java
public class StructA{}
Compilando y luego utilizando javap:
$ javac StructA.class
$ javap -c -l -s -verbose StructA
Compiled from "StructA.java"
public class StructA extends java.lang.Object
  SourceFile: "StructA.java"
  minor version: 0
  major version: 50
  Constant pool:
const #1 = Method #3.#10; //  java/lang/Object."<init>":()V
const #2 = class #11; //  StructA
const #3 = class #12; //  java/lang/Object
const #4 = Asciz <init>;
const #5 = Asciz ()V;
const #6 = Asciz Code;
const #7 = Asciz LineNumberTable;
const #8 = Asciz SourceFile;
const #9 = Asciz StructA.java;
const #10 = NameAndType #4:#5;//  "<init>":()V
const #11 = Asciz StructA;
const #12 = Asciz java/lang/Object;
{
public StructA();
  Signature: ()V
  LineNumberTable: 
   line 1: 0
  Code:
   Stack=1, Locals=1, Args_size=1
   0: aload_0
   1: invokespecial #1; //Method java/lang/Object."<init>":()V
   4: return
  LineNumberTable: 
   line 1: 0
}
Primeras cosas a observar:
  • La existencia de una constant pool de aspecto similar a una tabla de símbolos.
  • La definición de un método StructA con signature ()V. Además, la definición de un segmento code dentro de éste método, con variables Stack, Locals y Args_size.
  • Las instrucciones aload_0, invokespecial y return numeradas dentro del segmento de code. Descripciones para estas instrucciones aquí.
Vale la pena también notar que esta representación podría servir como código intermedio en una de las fases del compilador.