Skip to content

Ease of use

Brian S. O'Neill edited this page Aug 28, 2024 · 16 revisions

The primary goal of Cojen/Maker is to make code generation easy, but without compromising any JVM functionality.

The Apache Commons BCEL project contains a HelloWorld example which generates the following Java code:

public class HelloWorld {
    public static void main(String[] argv) {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        String name = null;

        try {
            System.out.print("Please enter your name> ");
            name = in.readLine();
        } catch (IOException e) {
            System.out.println(e);
            return;
        }

        System.out.println("Hello, " + name);
    }
}

Here's how to use Cojen/Maker to generate and directly run the example:

    public static void main(String[] argv) throws Exception {
        // Begin making the class with an automatic name, since it will run directly.
        ClassMaker cm = ClassMaker.begin().public_();

        // Begin making the 'main' method.
        MethodMaker mm = cm.addMethod(null, "main", String[].class).public_().static_();

        // Quick reference to the System class, for input and output.
        var sys = mm.var(System.class);

        // Access System.in, wrap it in a BufferedReader, and assign to the 'in' variable.
        var in = mm.new_(BufferedReader.class, mm.new_(InputStreamReader.class, sys.field("in")));

        // Create local variable 'name', initially set to null.
        var name = mm.var(String.class).set(null);

        // Label the start of the exception handler.
        Label tryStart = mm.label().here();

        // Print the initial message and read the response.
        sys.field("out").invoke("print", "Please enter your name> ");
        name.set(in.invoke("readLine"));

        // Make the exception handler.
        mm.catch_(tryStart, IOException.class, ex -> {
            sys.field("out").invoke("println", ex);
            mm.return_();
        });

        // Print the hello message.
        sys.field("out").invoke("println", mm.concat("Hello, ", name));

        // Add the default public constructor, which will be auto-finished as empty.
        cm.addConstructor().public_();

        // Finish making the class and invoke the 'main' method.
        cm.finish().getMethod("main", String[].class).invoke(null, (Object) argv);
    }

For comparison, what follows is the BCEL code which generates the example and writes it to a file. It should be apparent that the code is much larger because it requires explicit JVM instructions and operand stack manipulation. Cojen/Maker is easier because this complexity is completely hidden. In fact, no knowledge of the JVM specification is required at all.

With BCEL:

    public static void main(final String[] argv) {
        final ClassGen cg = new ClassGen("HelloWorld", "java.lang.Object",
                "<generated>", Const.ACC_PUBLIC |
                Const.ACC_SUPER,
                null);
        final ConstantPoolGen cp = cg.getConstantPool(); // cg creates constant pool
        final InstructionList il = new InstructionList();
        final MethodGen mg = new MethodGen(Const.ACC_STATIC |
                Const.ACC_PUBLIC,// access flags
                Type.VOID,              // return type
                new Type[]{            // argument types
                        new ArrayType(Type.STRING, 1)
                },
                new String[]{"argv"}, // arg names
                "main", "HelloWorld",    // method, class
                il, cp);
        final InstructionFactory factory = new InstructionFactory(cg);

        final ObjectType i_stream = new ObjectType("java.io.InputStream");
        final ObjectType p_stream = new ObjectType("java.io.PrintStream");

        // Create BufferedReader object and store it in local variable `in'.
        il.append(factory.createNew("java.io.BufferedReader"));
        il.append(InstructionConst.DUP); // Use predefined constant, i.e. flyweight
        il.append(factory.createNew("java.io.InputStreamReader"));
        il.append(InstructionConst.DUP);
        il.append(factory.createFieldAccess("java.lang.System", "in", i_stream, Const.GETSTATIC));

        // Call constructors, i.e. BufferedReader(InputStreamReader())
        il.append(factory.createInvoke("java.io.InputStreamReader", "<init>",
                Type.VOID, new Type[]{i_stream},
                Const.INVOKESPECIAL));
        il.append(factory.createInvoke("java.io.BufferedReader", "<init>", Type.VOID,
                new Type[]{new ObjectType("java.io.Reader")},
                Const.INVOKESPECIAL));

        // Create local variable `in'
        LocalVariableGen lg = mg.addLocalVariable("in", new ObjectType("java.io.BufferedReader"), null, null);
        final int in = lg.getIndex();
        lg.setStart(il.append(new ASTORE(in))); // `i' valid from here

        // Create local variable `name'
        lg = mg.addLocalVariable("name", Type.STRING, null, null);
        final int name = lg.getIndex();
        il.append(InstructionConst.ACONST_NULL);
        lg.setStart(il.append(new ASTORE(name))); // `name' valid from here

        // try { ...
        final InstructionHandle try_start =
                il.append(factory.createFieldAccess("java.lang.System", "out", p_stream, Const.GETSTATIC));

        il.append(new PUSH(cp, "Please enter your name> "));
        il.append(factory.createInvoke("java.io.PrintStream", "print", Type.VOID,
                new Type[]{Type.STRING}, Const.INVOKEVIRTUAL));
        il.append(new ALOAD(in));
        il.append(factory.createInvoke("java.io.BufferedReader", "readLine",
                Type.STRING, Type.NO_ARGS, Const.INVOKEVIRTUAL));
        il.append(new ASTORE(name));

        // Upon normal execution we jump behind exception handler, the target address is not known yet.
        final GOTO g = new GOTO(null);
        final InstructionHandle try_end = il.append(g);

        /* } catch() { ... }
         * Add exception handler: print exception and return from method
         */
        final InstructionHandle handler =
                il.append(factory.createFieldAccess("java.lang.System", "out", p_stream, Const.GETSTATIC));
        // Little trick in order not to save exception object temporarily
        il.append(InstructionConst.SWAP);

        il.append(factory.createInvoke("java.io.PrintStream", "println", Type.VOID, new Type[]{Type.OBJECT}, Const.INVOKEVIRTUAL));
        il.append(InstructionConst.RETURN);
        mg.addExceptionHandler(try_start, try_end, handler, new ObjectType("java.io.IOException"));

        // Normal code continues, now we can set the branch target of the GOTO that jumps over the handler code.
        final InstructionHandle ih =
                il.append(factory.createFieldAccess("java.lang.System", "out", p_stream, Const.GETSTATIC));
        g.setTarget(ih);

        // String concatenation compiles to StringBuffer operations.
        il.append(factory.createNew(Type.STRINGBUFFER));
        il.append(InstructionConst.DUP);
        il.append(new PUSH(cp, "Hello, "));
        il.append(factory.createInvoke("java.lang.StringBuffer", "<init>",
                Type.VOID, new Type[]{Type.STRING},
                Const.INVOKESPECIAL));
        il.append(new ALOAD(name));

        // Concatenate strings using a StringBuffer and print them.
        il.append(factory.createInvoke("java.lang.StringBuffer", "append",
                Type.STRINGBUFFER, new Type[]{Type.STRING},
                Const.INVOKEVIRTUAL));
        il.append(factory.createInvoke("java.lang.StringBuffer", "toString",
                Type.STRING, Type.NO_ARGS,
                Const.INVOKEVIRTUAL));

        il.append(factory.createInvoke("java.io.PrintStream", "println",
                Type.VOID, new Type[]{Type.STRING},
                Const.INVOKEVIRTUAL));

        il.append(InstructionConst.RETURN);

        mg.setMaxStack(5); // Needed stack size
        cg.addMethod(mg.getMethod());

        il.dispose(); // Reuse instruction handles

        // Add public <init> method, i.e. empty constructor
        cg.addEmptyConstructor(Const.ACC_PUBLIC);

        // Get JavaClass object and dump it to file.
        try {
            cg.getJavaClass().dump("HelloWorld.class");
        } catch (final IOException e) {
            System.err.println(e);
        }
    }

With ASM:

    public static void main(String[] argv) throws Exception {
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);

        cw.visit(V12, ACC_PUBLIC | ACC_SUPER, "maker/HelloWorld", null, "java/lang/Object", null);

        // Add the default constructor.
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
        mv.visitInsn(RETURN);
        mv.visitMaxs​(0, 0);

        // Add the main method.
        mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
        Label tryStart = new Label();
        Label tryEnd = new Label();
        Label tryHandler = new Label();
        mv.visitTryCatchBlock(tryStart, tryEnd, tryHandler, "java/io/IOException");
        mv.visitTypeInsn(NEW, "java/io/BufferedReader");
        mv.visitInsn(DUP);
        mv.visitTypeInsn(NEW, "java/io/InputStreamReader");
        mv.visitInsn(DUP);
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "in", "Ljava/io/InputStream;");
        mv.visitMethodInsn(INVOKESPECIAL, "java/io/InputStreamReader", "<init>", "(Ljava/io/InputStream;)V", false);
        mv.visitMethodInsn(INVOKESPECIAL, "java/io/BufferedReader", "<init>", "(Ljava/io/Reader;)V", false);
        mv.visitVarInsn(ASTORE, 1);
        mv.visitInsn(ACONST_NULL);
        mv.visitVarInsn(ASTORE, 2);
        mv.visitLabel(tryStart);
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("Please enter your name> ");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "print", "(Ljava/lang/String;)V", false);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/BufferedReader", "readLine", "()Ljava/lang/String;", false);
        mv.visitVarInsn(ASTORE, 2);
        mv.visitLabel(tryEnd);
        Label label3 = new Label();
        mv.visitJumpInsn(GOTO, label3);
        mv.visitLabel(tryHandler);
        mv.visitVarInsn(ASTORE, 3);
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitVarInsn(ALOAD, 3);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V", false);
        mv.visitInsn(RETURN);
        mv.visitLabel(label3);
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitVarInsn(ALOAD, 2);
        mv.visitInvokeDynamicInsn("makeConcatWithConstants", "(Ljava/lang/String;)Ljava/lang/String;",
                                  new Handle(H_INVOKESTATIC, "java/lang/invoke/StringConcatFactory",
                                             "makeConcatWithConstants",
                                             "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;",
                                             false),
                                  new Object[]{"Hello, \u0001"});
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        mv.visitInsn(RETURN);
        mv.visitMaxs​(0, 0);

        byte[] bytes = cw.toByteArray();

        // Load the class and invoke the main method.
        Class<?> clazz = MethodHandles.lookup().defineClass(bytes);
        clazz.getMethod("main", String[].class).invoke(null, (Object) argv);
    }

Also see JEP-484.

Clone this wiki locally