The simplest way to use ClassMaker is to create a class that extends ClassMakerBase
. ClassMakerBase
is an abstract class that uses some sensible defaults for all of the parameters that would otherwise need to be configured on the ClassMaker instance.
The only method that must be implemented in a class derived from ClassMakerBase
is void code()
. This method is used to generate the byte codes for the class.
public interface Unary { int square(int a); } public class SquareMaker extends ClassMakerBase { public void code() { Implements(Unary.class); Method("square", int.class, ACC_PUBLIC); Declare("a", int.class, 0); Begin(); Return(Mult(Get("a"), Get("a"))); End(); } } public void testSquare() throws Exception { ClassMaker maker = new SquareMaker(); Class squareClass = maker.defineClass(); Unary exec = (Unary)squareClass.newInstance(); assertEquals("Square test", 4, exec.square(2)); }
In order to call the generated class there has to be an interface in common between the generated class and the code which calls it. In the example above this is the Unary
interface.
The Unary
interface is a compiled java class that is known in the compiled java code and in the generated class; it is a common contract of interaction. The first thing the code()
method does is state that the generated class will implement the Unary
interface.
We will skip the byte-code generation for the moment to explain how SquareMaker
can be used to generate a Class. The test case called testSquare
at the bottom of the source code generates the class and uses it.
The defineClass()
method of ClassMakerBase
is called to generate the Class. The Class is loaded and resolved by the system ClassLoader
in a similar way to loading it from the classpath using the System.classFor
method. The difference is that the Class is generated as a stream of bytes by the SquareMaker
instance rather than being read from a class file.
Once the Class has been generated and loaded an instance can be created from it using newInstance
. The instance must be cast to the Unary
interface and after that it can be used like any other object. In this case we call the square
method and assert that the square of 2 gives a result of 4.
We can now look at the code that generates the stream of bytes.
public void code() { Implements(Unary.class); Method("square", int.class, ACC_PUBLIC); Declare("a", int.class, 0); Begin(); Return(Mult(Get("a"), Get("a"))); End(); }
The code()
method is called indirectly by defineClass()
to generate the byte codes for the Class.
As we said before, the first statement specifies that the generated Class will implement the Unary
interface. That interface declares one method called square
which accepts a single int
parameter and returns an int
result.
The Method
call begins a method and is given the name of the method, the return type and a bitset of access modifiers. The name of the method is "square". The return type, int
, is specified using the equivalent reflection Class. Any Class can be provided here as a return type; to return a string, for example, we would use String.class
.
The final parameter to the Method
call is a bitset of access modifiers. The ACC_PUBLIC
modifier states that the method will be publicly accessable. Valid options are: ACC_PUBLIC
, ACC_PROTECTED
, ACC_PRIVATE
or zero (0). These access modifiers, and a few others, are also used when declaring fields.
Now lets look at the body of the method.
Method("square", int.class, ACC_PUBLIC); Declare("a", int.class, 0); Begin(); Return(Mult(Get("a"), Get("a"))); End();
The Declare
call specifies that the method accepts an int
parameter called a
. Valid access modifiers for a formal parameter are ACC_FINAL
or zero (0).
The Declare
method is also used to declare local variables and member fields of the class. The only difference between the three forms is where they are used:
Method
and Begin
is a formal parameterThe body of the method pushes the value in variable a
onto the stack twice, then multiplies the values and returns the result. This has the effect of returning the square of the parameter provided to the method.
Generating expressions is very easy in ClassMaker. The byte-code is generated as a side-effect of calls to methods like Get
, Mult
and Return
. Methods that represent operators typically take one or two Type
parameters and return a Type
result. You might guess from this that the Get
method is a source of Type
arguments and the Return
method is a sink for them.
Return(Mult(Get("a"), Get("a")));
The syntax of the method calls creates a form of prefix notation, with the operator coming first followed by the two parameters. Prefix notations do not require operator precedence or parentheses to be unambiguous, which is an issue with infix notation. The semantics of the nested method calls is that they evaluate in postfix order, which means that they evaluate their parameters before the method is called. Since the byte-code is generated as a side effect of the method calls this means that the byte-code to load the parameters for an operator are generated prior to byte-code for the operator itself, which is exactly the behaviour we want for a stack based execution model.