domingo, 16 de junio de 2013

Java - Como hacer una JVM | Parte 3

Continuamos con nuestro super tutorial para la creación de una JVM, si no habéis leído las 2 primeras partes hacedlo antes de poneros con esta!

Como hacer una JVM | Parte 1
Como hacer una JVM | Parte 2

Y seguimos donde lo dejamos, creando nuestro area de clases.

ClassArea

El ClassArea contendrá las clases estáticas de nuestra JVM, esa estructura estará implementada como un HashTable, el índice será el nombre de la clase y el dato una clase extra que crearemos llamada JClass, está clase actuará como recubridora de las de BCEL.

Además, como ya hemos mencionado, necesitamos poder leer un .class y añadirlo a nuestra área de clases.

Código ClassArea.java
public class ClassArea {
 private Hashtable<String, JClass> classes;

 
 public ClassArea() {
  classes = new Hashtable<String, JClass>();
 }

 public boolean addClass(JClass jClass) {
  if (jClass == null)
   return false;

  classes.put(jClass.jClassInfo.getClassName(), jClass);
  return true;
 }

 /**
  * Carga una clase del hashtable o desde directorio si se encuentra, en otro
  * caso error, ademas la anyade al classpath
  * 
  * @param name
  *            clase a cargar
  * @return
  */
 public JClass loadClass(String name) {
  JClass jClass = classes.get(name);
  if (jClass != null)
   return jClass;
  try {
   jClass = new JClass(Repository.lookupClass(name));
   addClass(jClass);
  } catch (ClassNotFoundException e) {
   e.printStackTrace();
   return jClass;
  }
  return jClass;
 }
}

Tenemos nuestro HashTable con nombre-clase, y además nuestro método para cargar clases, que usa la librería BCEL, abstrayéndonos completamente de tareas de parseo y demás, el método static lookupClass() de la clase Repository toma el nombre de una clase y devuelve el JavaClass que mencionamos en la primera parte, este lo usamos para crear nuestro propio JClass.

Código JClass.java
public class JClass {

 JavaClass jClassInfo;
 ClassGen jClassGen;
 private JField[] jFields;

 public JClass(JavaClass jClass) {
  jClassInfo = jClass;
  jClassGen = new ClassGen(jClass);
  jFields = new JField[jClass.getFields().length];
  for (int i = 0; i < jFields.length; i++) {
   jFields[i] = new JField(jClass.getFields()[i]);
  }
 }
 
 public JMethod getMainMethod() {
  return getMethod("main",null);
 }
 
 /**
  * Busca un metodo basandose en su nombre y/o signatura
  * 
  * @param methodName
  *            nombre del metodo
  * @param signature
  *            signatura del metodo o null si no se requiere
  * @return instancia del metodo encapsulado en JMethod
  */
 public JMethod getMethod(String methodName, String signature) {
  Method searchedMethod = null;
  for (Method m : jClassInfo.getMethods())
   if (m.getName().equals(methodName)) {
    if (signature != null) {
     if (m.getSignature().equals(signature)) {
      searchedMethod = m;
      break;
     }
    } else {
     searchedMethod = m;
     break;
    }
   }
  return searchedMethod != null ? new JMethod(searchedMethod, jClassGen)
    : null;
 }
  .
  .
  .
}
Podéis ver lo que sería esta clase, las referencias a las clases de BCEL y el constructor iniciando los fields. Además notar que para la creación del ClassGen es necesario usar el JavaClass.
Declaramos un método por defecto para obtener el método principal (main), que posteriormente usaremos en nuestro programa de ejecución.
Pero es importante resaltar la obtención de métodos de una clase, la búsqueda debe ser no solo por nombre, sino por signatura, recordad que java permite sobrecarga de métodos, es decir, métodos con el mismo nombre pero con distintos parámetros/valor de retorno, usando la signatura conseguimos diferenciarlos correctamente, (más sobre sobrecarga).

Código JField .java
public class JField {
 public Field fieldInfo;
 private JValue value;

 public JField(Field field) {
  fieldInfo = field;
  ConstantValue val = field.getConstantValue();
  ConstantPool cp;
  int index;
  Constant c = null;
  if (val != null) {
   cp = val.getConstantPool();
   index = val.getConstantValueIndex();
   c = cp.getConstant(index);
  }
  switch (field.getType().getType()) {
  case Constants.T_INT:
   if (val != null) {
    ConstantInteger cl = (ConstantInteger) c;
    value = new JInteger(cl.getBytes());
   } else
    value = new JInteger();
   break;
  }
 }
  .
  .
  .
}
También mostramos la clase JField, que representa una propiedad de una clase, recubriendo al Field de BCEL, y conteniendo además el valor propio del campo como un JValue, notar en su creación el uso de la ConstantPool (esta en toas partes la jodia), para permitir la creación del field con un valor por defecto, relativo a propiedades inicializadas previa creación de objeto. Se muestra solo para int pero su obtención para el resto de variables sería similar.

StackFrame

Como dijimos los frames están formados por el contador de programa, el método concreto que se esta ejecutando, y un puntero al frame padre, de nuevo creamos una clase recubridora para los métodos de BCEL, JMethod. Ademas necesitamos métodos para acceder a las instrucciones e información dentro de cada frame.

Código StackFrame.java
public class StackFrame {

 Stack<JValue> operands;
 JValue[] locals;
 private int pc;
 private JMethod method;
 private StackFrame cf;
 public byte error;

 // added
 JClass clase;

 public StackFrame(StackFrame cf, JMethod meth, JClass clase) {
  this(cf, meth, clase, null);
 }
 
 public StackFrame(StackFrame cf, JMethod meth, JClass clase, JValue thisRef) {
  int maxLocals=meth.getMaxLocals();
  if (thisRef != null) {
   locals = new JValue[maxLocals];
   if(maxLocals>0)locals[0] = thisRef;
  } else
   locals = new JValue[maxLocals];
  this.cf = cf;
  operands = new Stack<JValue>();
  pc = 0;
  error=0;
  method = meth;

  this.clase = clase;
 }

 public InstructionHandle nextInstruction() {
  return method.get(pc);
 }
  .
  .
  .
}
Arriba vemos un esquema de nuestro StackFrame, habría más métodos que podemos añadir para facilitar operaciones pero esos son los básicos. Añadimos la clase del método que se esta ejecutando para facilitar obtener información a la hora de ejecución, además notar los dos métodos constructores.
Como dije en la segunda parte de este tutorial, cuando se llama a un método de un objeto, el frame guarda en la primera posición de variables locales una referencia a ese objeto, esto es controlado en nuestra implementación comprobando si thisRef es null, y actuando en consecuencia.

Código JMethod.java
public class JMethod {

 Method methodInfo;
 MethodGen methodGen;

 public JMethod(Method method, ClassGen classGen) {
  methodInfo = method;
  methodGen = new MethodGen(method, classGen.getClassName(),
    classGen.getConstantPool());
 }
 
 public short[] getCode() {
  byte[] code = methodInfo.getCode().getCode();
  short[] codePlus = new short[code.length];
  // convert -opcodes to +opcodes
  for (int i = 0; i < code.length; i++) {
   codePlus[i] = (short) (code[i] < 0 ? code[i] + 256 : code[i]);
  }
  return codePlus;
 }
 
  .
  .
  .
}
De nuevo la clase recubridora usa la librería BCEL, destacar el método getCode() que obtiene el código del método a través de BCEL, y como mencioné lo transforma a valores positivos.

Heap

Nuestro Heap será bastante limitado y sencillo, simplemente será un array de JValue, JValue será el tipo de dato base que nuestra JVM usará, cada tipo de dato concreto deberá heredar de este JValue, por ejemplo JInteger. Además necesitamos un puntero a la siguiente posición libre de memoria.

Código JValue.java
public abstract class JValue{
 public Type type;
}
El Type es una clase de BCEL, que representa a un tipo de la JVM.

JObject será nuestra representación de objeto y como dije será un JValue.

Código JObject.java
public class JObject extends JValue {
 public int heapPtr;
 public JClass jClass;
 // campos de la clase representado para objetos
 public JField[] jFields;

 //creacion a null
 public JObject() {
  jClass=null;
  jFields=null;
 }

 /**
  * Crea un objeto con posicion heapPtr de tipo jClass. Se reserva espacio
  * para sus fields en base a los fields de jClass
  * 
  * @param heapPtr
  *            posicion en el heap
  * @param jClass
  *            clase instanciada
  */
 public JObject(int heapPtr, JClass jClass) {
  int count = jClass.GetObjectFieldCount();
  this.heapPtr = heapPtr;
  type = Type.OBJECT;
  this.jClass = jClass;
  if (jClass != null) {
   jFields = new JField[count];
   for (int i = 0; i < jFields.length; i++) {
    jFields[i] = new JField(jClass.getFields()[i]);
   }
  }
 }

 public void setFieldValue(int i, JValue val) {
  jFields[i].setValue(val);
 }

 public void setFieldValue(String name, JValue val) {
  for (int i = 0; i < jFields.length; i++) {
   if (jFields[i].getName().equals(name))
    jFields[i].setValue(val);
  }
 }

 public JField getField(int i) {
  return jFields[i];
 }

 public JField getField(String name) {
  for (int i = 0; i < jFields.length; i++) {
   if (jFields[i].getName().equals(name))
    return jFields[i];
  }
  return null;
 }
}
En la creación del objeto establecemos su tipo y creamos sus fields, en base a los que nos ofrece su clase, además añadimos una serie de métodos de utilidad para trabajar con sus propiedades.

Código Heap.java
public class Heap {

 private int nextObjectID;
 private JValue[] heap;

 public Heap(int size) {
  nextObjectID = 0;
  heap = new JValue[size];
 }
 
 /**
  * Crea un objeto de tipo mainClass y lo guarda en el heap
  * 
  * @param mainClass
  *            tipo de objeto a crear
  * @return la referencia al objeto
  */
 public JObject createObject(JClass mainClass) {
  JObject object = new JObject(nextObjectID++, mainClass);
  heap[object.heapPtr] = object;
  return object;
 }
 
 
 /**
  * Crea un objeto de tipo String y lo guarda en el heap
  * 
  * @param strValue
  *            valor del JString a crear
  * @param classes
  *            ClassArea a usar
  * @return
  */
 public JObject createStringObject(String strValue, ClassArea classes) {
  JClass stringClass = classes.loadClass("java.lang.String");

  JObject object = (JObject) createObject(stringClass);

  JArray arrayString = new JArray(nextObjectID, strValue.length(),
    Type.CHAR);
  arrayString.setStr(strValue);
  object.setFieldValue(0, arrayString);
  object.setFieldValue("count", new JInteger(arrayString.nElems));

  return object;
 }
  .
  .
  .
}

He dejado fuera la creación de arrays por simplificar algo e ir directo a los objetos que es más interesante. El constructor no tiene mucho misterio y la creación de objetos en Heap tampoco, gracias al uso del constructor de JObject, tras crearlo se añade al heap y se devuelve la referencia a la JVM para que lo añada en la pila.

Tenemos un método especial de creación de objetos String debido a la instrucción LDC, que permite la creación directa de un String y su carga en la pila, los String los hemos codificado como un JArray, (cada array se toma como otro objeto, internamente tiene un array de JValue para permitir arrays de cualquier tipo soportado por nuestra JVM), y tras la creación modificamos 2 de sus fields, el que guarda el propio String (en nuestro caso el JArray), y el que guarda su tamaño llamado count, existen otros, pero estos dos son los básicos para que nuestros objetos String puedan ser utilizados.

Ejecución

Todo esto está muy bonito pero... ¿Cómo cojones lo pongo a funcionar?, tiene algo de miga aunque no demasiado (gracias a nuestro desarrollo POO!), así que tenemos que crearnos otra clase, por ejemplo Exec, que será la que usará la JVM para ponerla a trabajar.

Código Exec.java
public class Exec {

 public static void main(String[] args) {
  execute(args[0], args[1]);
 }
 
 private static void execute(String strClass, String size) {

  // creamos heap
  Heap heap = new Heap(Integer.parseInt(size));
  // creamos area de clases
  ClassArea ca = new ClassArea();
  // leemos clase y buscamos metodo principal
  JClass mainClass = ca.loadClass(strClass);
  JMethod mainMethod = mainClass.getMainMethod();
  // creamos frame principal
  StackFrame mainFrame = new StackFrame(null, mainMethod, mainClass);

  // creamos jvm
  JVM jvm = new JVM(mainFrame, heap, ca);

  // ejecucion
  jvm.run();
 }
}

En este programa principal se toman dos argumentos, el nombre de la clase y el tamaño del heap. Cuando tenemos creadas nuestras 3 estructuras se las pasamos a la JVM y a ejecutar, (recordar que no veremos nada porque no hemos puesto ningún tipo de salida por pantalla), además si cogéis cualquier .class probablemente falle porque hay cantidad de instrucciones que no están implementadas.

El método que se ejecutará será el método main, que se obtiene mediante el método getMainMethod().

Notar que el nombre de una clase no es solo su nombre, sino que incluye el paquete en el que esta, por ejemplo si cogéis una clase llamada A.java que esta en com.prueba.tests, su nombre en realidad es com.prueba.tests.A.

Conclusión

Bueno, hasta aquí hemos llegado, hay algunas cosas que han quedado en el aire (excepciones, propiedades static, recolector de basura, threads) pero para un tema tan denso como es la máquina virtual de java creo que se ha presentado información bastante útil para cualquiera que quiera empezar a indagar en este mundillo, nada más.

Un saludo!

No hay comentarios:

Publicar un comentario

Ponte un nombre aunque sea falso, que Anó-nimo queda mu feo :(