JudoScript.COM Design principles of Judo the sport and the language
HomeJudo LanguageJuSP PlatformJamaica Language 
Judo ReferenceJuSP ReferenceWiki/WeblogTutorials/PresentationsDownloadsGoodiesFeedback  
Article: Dynamically Create Java Classes With JavaClassCreator
 

Search

 

 By FreeFind.com












Table Of Content

  1. JavaClassCreator and Jamaica
  2. Create a Class or Interface
    » API Method Partial Listing
  3. Bytecode Instructions and Macros
    » Bytecode Instructions
    » Macros
    » Indirect Instructions
    » Helper Methods
  4. Service Provider Methods
    » Opcode Constants
  5. Examples
  6. Code Listings

Dynamically Create Java Classes With JavaClassCreator

By James Jianbo Huang    March 2004       printer-friendly version

Abstract  


 

1. JavaClassCreator and Jamaica

The JavaClassCreator API is used by the Jamaica language to create JVM classes and interfaces. Jamaica is a JVM Macro Assembler language, which uses Java syntax to define a JVM class structure and symbolic names for bytecode instructions. This API is modeled after the Jamaica language, making it very easy to dynamically create Java classes. It supports the Jamaica-style symbolic assembling, and supports all the bytecode instructions and Jamaica macros, along with declarative class construction. It is an abstract class that delegates a handful of methods to its implementation while implementing the rest such as macros, etc. One difference between this API and Jamaica is that the API does not support package and import, so all class names must be complete with their package prefixes.

The class name for the API is com.judoscript.jamaica.JavaClassCreator. Currently two implementations are provided: class com.judoscript.jamaica.ASMJavaClassCreartor that utilizes the ASM package, and class com.judoscript.jamaica.BCELJavaClassCreator that utilizes the Jakarter-BCEL package.

 

»»» Top «««

 

2. Create a Class or Interface

Class JavaClassCreator has methods that starts a class or interface, creates data fields, constants and methods, and finishes the declaration. Therefore, many methods are context sensitive, i.e., they can only be called before or after certain events have happened. For instance, all the bytecode instruction and macro creation methods can only be called after a method is started and before that method is closed. During the process, all type names are Java names, e.g., "int" for primitive integer, "java.lang.String" for class names and "double[][]" for array types.

The following is the flow to create a JVM class using JavaClassCreator's key methods:

  1. Call startClass() or startInterface() to start. After this, the following methods are allowed: setSourceFileName(), getSourceFileName(), getClassName(), getRootClassName(), getSuperclassName() and getInterfaceNames().
  2. Create data fields and/or constants via calls to addField() and addConstant(). For the defined variables, can call getFieldType() and isStaticField().
  3. If a default constructor with no extra logic is intended, call addDefaultConstructor().
  4. For abstract methods, call addAbstractMethod().
  5. If there are static fields to initialize, create a static method named "<clinit>" that takes no parameters and returns void to initialize them.
  6. Create concrete methods. See below.
  7. Call endClass() or endClassToFile() to finish.
  8. To create more classes and/or interfaces, go to 1.
This is the flow to create a concrete method:
  1. Call startMethod() to start. After this, can call getMethodAccessFlags() and getMethodName() on the defined methods.
  2. Call addLocalVariable() to add local variables. This can be mixed with other code, but variables must be created before can be used by the instructions or macros. Note that method parameters are also variables so avoid name clashes. For non-static methods, the JVM specification mandates the this instance is the first on the operand stack. You can access it via this as well. Once defined, can call getLocalVariableIndex() and getVariableType() on the defined variables in this method.
  3. Add instructions and/or macros. They are presented in a dedicated section. Instructions can be preceeded by labels created by calls to setLabel().
  4. All methods must create a return instruction at the end, including those with a void return type.
  5. Before finishing the method, zero or more catch clauses can be created via the call to addCatchClause(). All the labels must be valid within this method. There is no explicit "finally clause" in JVM; you can use jump instructions to emulate a Java finally clause.
  6. Call endMethod() to finish.

API Method Partial Listing

Here is a list of the methods mentioned so far. Most of them are merely listed along with their parameter names; some are briefly annotated with Java comments.

void startClass(int accessFlags, String className, String superName, String[] implementList);
void startInterface(String itfName, String[] extendList);
void setSourceFileName(String fileName);
String getSourceFileName();
String getClassName();
String getClassRootName();
String getSuperclassName();
String[] getInterfaceNames();
void addField(int accessFlags, String name, String type);
void addConstant(int accessFlags, String fieldName, String type, Object value);
String getFieldType(String fieldName);
boolean isStaticField(String fieldName);
void addDefaultConstructor(int accessFlags);
void addAbstractMethod(int accessFlags, String name, String[] paramTypes, String[] paramNames,
       String returnType, String[] exceptions);
void startMethod(int accessFlags, String name, String[] paramTypes, String[] paramNames,
       String returnType, String[] exceptions);
int getMethodAccessFlags();
String getMethodName();
void addLocalVariable(String name, String type);
int getLocalVariableIndex(String varName);
String getVariableType(String varName);
void setLabel(String label);
void addCatchClause(String exceptionType, String startLabel, String endLabel, String actionLabel);
void endMethod();
byte[] endClass();
void endClassToFile(String fileName);

 

»»» Top «««

 

3. Bytecode Instructions and Macros

Bytecode Instructions

JavaClassCreator has methods to create each and every JVM bytecode instruction directly. The naming convention for these methods are inst_xxxx(), where xxxx is the mnemonic. Based on their parameters, they are grouped as below. For a functional grouping, refer to the Jamaica language and/or other sources.

Group 1 includes simple instructions with no parameters:

aconst_null iconst_m1 iconst_0 iconst_1 iconst_2 iconst_3 iconst_4 iconst_5
lconst_0 lconst_1 fconst_0 fconst_1 fconst_2 dconst_0 dconst_1
iload_0 iload_1 iload_2 iload_3 lload_0 lload_1 lload_2 lload_3
fload_0 fload_1 fload_2 fload_3 dload_0 dload_1 dload_2 dload_3
aload_0 aload_1 aload_2 aload_3
iaload laload faload daload aaload baload caload saload
istore_0 istore_1 istore_2 istore_3 lstore_0 lstore_1 lstore_2 lstore_3
fstore_0 fstore_1 fstore_2 fstore_3 dstore_0 dstore_1 dstore_2 dstore_3
astore_0 astore_1 astore_2 astore_3
iastore lastore fastore dastore aastore bastore castore sastore
pop pop2 dup dup_x1 dup_x2 dup2 dup2_x1 dup2_x2 swap
iadd ladd fadd dadd isub lsub fsub dsub
imul lmul fmul dmul idiv ldiv fdiv ddiv irem lrem frem drem
ineg lneg fneg dneg ishl lshl ishr lshr iushr lushr iand land ior lor ixor lxor
i2l i2f i2d l2i l2f l2d f2i f2l f2d d2i d2l d2f i2b i2c i2s
lcmp fcmpl fcmpg dcmpl dcmpg ireturn lreturn freturn dreturn areturn return
nop arraylength athrow monitorenter monitorexit

Group 2 includes load, store and ret instructions:

iload lload fload dload aload
istore lstore fstore dstore astore
ret
These instructions all take a variable name as parameter, e.g.,
void inst_iload(String varName);

Group 3 are instructions dealing with object types; they all take a type name as parameter:

new newarray anewarray checkcast instanceof

Group 4 are instructions to get and put data fields:

getstatic putstatic getfield putfield
The method signature is like:
void inst_getstatic(String className, String fieldName, String fieldType);

Group 5 is ldc and its variants:

ldc ldc_w ldc2_w
Each of them has two overloaded methods:
void inst_ldc(Object value);
void inst_ldc(Object value, String type);
where value is a Java primitive type wrapper or a String, and type is the intended type. The constant will be added to the constant pool. For certain values, implementation may optimize to such as bipush or iconst_0, etc.

Group 6 are for jump instructions; they all take a target label as parameter:

ifeq ifne iflt ifge ifgt ifle
if_icmpeq if_icmpne if_icmplt if_icmpge if_icmpgt if_icmple if_acmpeq if_acmpne
goto jsr ifnull ifnonnull goto_w jsr_w

Group 7 is invocation methods:

invokevirtual invokespecial invokestatic invokeinterface
Their signature is:
void invokevirtual(String className, String methodName, String[] paramTypes, String returnType);

Group 8 is for instructions otherwise not grouped. Their signatures are listed below:

void inst_bipush(byte value);
void inst_sipush(short value);
void inst_iinc(String varName, int increment);
void inst_multianewarray(String type, short dim);
void inst_tableswitch(int[] consts, String[] labels, String defaultLabel);
void inst_lookupswitch(int startConst, String[] labels, String defaultLabel);

Macros

Beyond individual instructions, JavaClassCreator has defined a number of macros for commonly used patterns. They can interpret a name to be a variable or field based on the context, and use the strong-type system to figure out what to do. They take an array element syntax, so array access is much simpler. In general, these macros will greatly shorten your programs and make them more readable. It is not uncommon that a method is implemented completely by variables and macros.

Access to variables, fields and arrays is represented in JavaClassCreator by one of its inner class, VarAccess.

public class JavaClassCreator {
  public static class VarAccess {
    public VarAccess(String name, int lineNum);
    public VarAccess(String name);
    public boolean isArray();
    public void setIndex(Object index);
    public String toString();
  }
}
If it is a single name, the API will try to find a variable in the current method or a field in the class. When its index is set, this is an array element accessing operation. The index can be a number or a VarAccess, or can be an array of those as a multi-dimensional array access.

Macros can be classified into two categories: Jamaica Macros and non-Jamaica ones. Jamaica macros are direct counterparts of those defined in the Jamaica language. They are all quite sophisticated. Other macros are simpler, usually are expanded into just one instruction, which is almost always context-sensitive. Most of them are used to implement Jamaica macros.

void macroSet(Object dest, Object value);
     // dest can be a String, a VarAccess or null;
     // if null, the value is left on the stack;
     // otherwise, the value is set to the dest.
void macroPrint(String cmd, String target, Object[] params);
     // Prints to java.lang.System.out or err.
     // cmd is "print", "println" or "flush".
     // target is null, "out" or "err".
     // params can be values or VarAccess's.
void macroObject(String type, String[] paramTypes, Object[] paramValues);
     // creates an object on the stack.
void macroArray(String type, int dims, Object[] sizes);
     // creates an array on the stack.
void macroStringConcat(Object[] vals);
     // vals can be values or VarAccess's. The concatenated string is left on stack.
void macroIterate(VarAccess coll, String var, String id);
     // coll must reference either a java.util.Iterator or Enumeration.
     // var must reference an Object, holding the object during each iteration.
     // var can be null, where the object is left on the stack.
     // This macro must be ended by a call to macroEndIterator() with a matchin id.
void macroEndIterate(String id);
void macroArrayIterate(VarAccess array, String var, String id);
     // array must reference an array.
     // var must reference an int to serve as the index used by the iteration.
     // The stack is not used.
     // This macro must be ended by a call to macroEndArrayIterator() with a matching id.
void macroEndArrayIterate(String, String id);
void macroIf(String op, Object left, Object right, String id, boolean hasElse);
     // op is one of these: <, <=, >, >=, == or !=.
     // left and right can be constants or references.
     // This macro must be ended by a macroElse() or macroEndIf() with a matching id.
void macroElse(String id);
     // This macro must be proceded by a macroIf() and
     // ended by a macroEndIf() with a matching id.
void macroEndIf(String id);
     // This macro must be proceeded by a macroIf() or macroElse() and
     // ended by a macroEndIf() with a matching id.

Non-Jamaica Macros are simple ones that do "intelligent" instruction generation.

void macroTypeCast(String fromType, String toType);
String macroGetField(String name);
     // Returns the type of the value loaded on the stack.
String macroLoadConstantOrVarOrField(Object value, String intendedType);
     // value can be a constant (including String) or a VarAccess.
     // Returns the type of the value loaded on the stack.
String macroLoadVarOrField(String name, String intendedType);
     // Returns the type of the value loaded on the stack.
String macroLoadVarOrField(VarAccess var, String intendedType);
     // Returns the type of the value loaded on the stack.

Indirect Instructions

In addition to the direct bytecode instructions and powerful macros, JavaClassCreator provides a number of flexible methods to create bytecode instructions. In fact, most direct instructions are delegated to these, as noted below:

void inst(int opcode);
     // For all group 1 instructions.
void instLoadStoreRet(int opcode, String varName);
     // For all group 2 instructions.
void instType(int opcode, String type);
     // For all group 3 instructions.
void instGetPut(int opcode, String className, String fieldName, String type);
     // For all group 4 instructions.
void instLdc(int opcode, Object value, String type);
     // For all group 5 instructions.
void instJump(int opcode, String label);
     // For all group 6 instructions.
void instInvoke(int opcode, String className, String methodName,
       String[] paramTypes, String returnType);
     // For all group 7 instructions.

Helper Methods

These are helper methods that can be handy at times.

String getMnemonic(int opcode);
String getType(Object value);
static String getConstTypeName(Object value);
static boolean isPrimitiveType(String type);
static boolean isInt(String type);
static int getLoadInstruction(String type);
static int getStoreInstruction(String type);
static int getArrayLoadStoreInstruction(String type, boolean isStore);
static VarAccess createVarAccess(String name);
static VarAccess createArrayAccess(String name, Object index);
static VarAccess createArrayAccess(String name, Object index1, Object index2);

 

»»» Top «««

 

4. Service Provider Methods

Implementation classes of JavaClassCreator are obliged to implement these abstract methods:

void setSourceFileName(String fileName);
String getSourceFileName();
String getClassName();
String getSuperclassName();
String[] getInterfaceNames();
String getClassRootName();
void startClass(int accessFlags, String className, String superName, String[] implementList);
void startInterface(String itfName, String[] extendList);
byte[] endClass();
void endClassToFile(String fileName);
void addField(int accessFlags, String name, String type);
void addConstant(int accessFlags, String fieldName, String type, Object value);
String getFieldType(String fieldName);
boolean isStaticField(String fieldName);
void addDefaultConstructor(int accessFlags);
void addAbstractMethod(int accessFlags, String name, String[] paramTypes, String[] paramNames,
       String returnType, String[] exceptions);
void startMethod(int accessFlags, String name, String[] paramTypes, String[] paramNames,
       String returnType, String[] exceptions);
void endMethod();
int getMethodAccessFlags();
String getMethodName();
void addLocalVariable(String name, String type);
int getLocalVariableIndex(String varName);
String getVariableType(String varName);
void addCatchClause(String exceptionType, String startLabel, String endLabel, String actionLabel);
void setLabel(String label);
void inst(int opcode);
void instLoadStoreRet(int opcode, String varName);
void instType(int opcode, String typeName);
void instGetPut(int opcode, String className, String fieldName, String type);
void instLdc(int opcode, Object value, String type);
void instJump(int opcode, String label);
void instInvoke(int opcode, String className, String methodName,
       String[] paramTypes, String returnType);
void inst_tableswitch(int[] consts, String[] labels, String defaultLabel);
void inst_lookupswitch(int startConst, String[] labels, String defaultLabel);
void inst_bipush(byte value);
void inst_sipush(short value);
void inst_iinc(String varName, int increment);
void inst_multianewarray(String arrayType, short dims);
String getMnemonic(int opcode);

Opcode Constants

JavaClassCreator does not define constants for the JVM bytecode opcodes, because all implementation packages have a collection of those. For instance, in the Jakarta-BCEL package, interface org.apache.bcel.Constants defines constants for all the bytecode opcodes.

 

»»» Top «««

 

5. Examples

Let's write some Java code to generate a Java class and use it instantly. The generated class will be like the following class written in Jamaica:

Listing 1. DynaHandler Genereated Within JCCTest.java
 
// We will write a Java class called JCCTest to generate this one.
// JCCTest has an inner interface, EventHandler, that is implemented here.
public class DynaHandler implements JCCTest$EventHandler
{
  .default_constructor 

  public void event(String e) { 
    %println "String event: ", e
  }

  public void event(int e) {
    %println "int event: ", e
  }

  public void event(int[] e) {
    int i;
    %set i = 0

    %if e != null
      aload e
      arraylength
      istore i
    %end_if

    %println "Array event: length=", i

    %if i > 0
      %array_iterate e i
        %println "  e[", i, "]=", e[i]
      %end_array_iterate
    %end_if
  }
}

This is the Java code that generates and uses a created class:

Listing 2. JCCTest.java
 
import java.lang.reflect.Modifier;
import com.judoscript.jamaica.*;

public class JCCTest extends ClassLoader
{
  public static final JCCTest classLoader = new JCCTest();

  private JCCTest() {}

  /**
   * This is the interface that any dynamica class will implement.
   */
  public static interface EventHandler {
    public void event(String e);
    public void event(int    e);
    public void event(int[]  e);
  }

  public static void main(String[] args) {
    try {
      // Create the dyncamic class
      Class cls = generateHandlerClass();

      // Create an instance of the dyncamic class and use it.
      EventHandler eh = (EventHandler)cls.newInstance();
      eh.event("Cool!");
      eh.event(5);
      eh.event(new int[] { 3, 6, 9 });
      eh.event((int[])null);
    } catch(Exception e) {
      e.printStackTrace();
    }
  }

  static Class generateHandlerClass() throws JavaClassCreatorException {
    String[] paramNames = new String[] { "e" }; // because they are all the same.

    JavaClassCreator jcc = new BCELJavaClassCreator();
    jcc.startClass(Modifier.PUBLIC, "DynaHandler", null, new String[]{ "JCCTest$EventHandler" });

    jcc.addDefaultConstructor(Modifier.PUBLIC);

    jcc.startMethod(Modifier.PUBLIC, "event", new String[]{ "java.lang.String" },
                    paramNames, "void", null);
    jcc. macroPrint("println", null, new Object[]{ "String event: ", jcc.createVarAccess("e") });
    jcc. inst_return();
    jcc.endMethod();

    jcc.startMethod(Modifier.PUBLIC, "event", new String[]{ "int" }, paramNames, "void", null);
    jcc. macroPrint("println", null, new Object[]{ "int event: ", jcc.createVarAccess("e") });
    jcc. inst_return();
    jcc.endMethod();

    jcc.startMethod(Modifier.PUBLIC, "event", new String[]{ "int[]" }, paramNames, "void", null);
    jcc. addLocalVariable("i", "int", new Integer(0));
    jcc. macroIf("!=", jcc.createVarAccess("e"), null, "?if_1", false);
    jcc.   inst_aload("e");
    jcc.   inst_arraylength();
    jcc.   inst_istore("i");
    jcc. macroEndIf("?if_1");
    jcc. macroPrint("println", null, new Object[]{
           "Array event: length=", jcc.createVarAccess("i")
         });
    jcc. macroIf(">", new JavaClassCreator.VarAccess("i"), new Integer(0), "?if_2", false);
    jcc.  macroArrayIterate(jcc.createVarAccess("e"), "i", "?iter_1");
    jcc.   macroPrint("println", null, new Object[]{
             " e[", jcc.createVarAccess("i"), "]=",
             jcc.createArrayAccess("e", jcc.createVarAccess("i"))
           });
    jcc.  macroEndArrayIterate("i", "?iter_1");
    jcc. macroEndIf("?if_2");
    jcc. inst_return();
    jcc.endMethod();

    byte[] bytes = jcc.endClass(); 

    return classLoader.defineClass("DynaHandler", bytes, 0, bytes.length);
  }
}

The output is:

% javac JCCTest.java
% Java JCCTest
String event: Cool!
int event: 5
Array event: length=3
 e[0]=3
 e[1]=6
 e[2]=9
Array event: length=0

 

»»» Top «««

 

6. Code Listings

  1. DynaHandler Genereated Within JCCTest.java
  2. JCCTest.java




Copyright © 2001-2005 JudoScript.COM. All Rights Reserved.