32

I am writing an interpreter in Java for a domain-specific language with some scripting capabilities. I have already implemented a parser and now need to do a back end. To this end I am considering either to write my own interpreter (either working with abstract syntax trees or with some custom bytecodes) or target JVM (emit and execute Java bytecode at runtime).

Could someone with more experience in this area say how feasible is the approach of targeting JVM and what libraries would you recommend to use for emitting Java bytecode?

vitaut
  • 49,672
  • 25
  • 199
  • 336

3 Answers3

33

Here is a working "hello world" made with ObjectWeb ASM (a library which I recommend):

package hello;

import java.lang.reflect.Method;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class HelloWorldASM implements Opcodes {
    public static byte[] compile(String name) {
        ClassWriter cw = new ClassWriter(0);
        MethodVisitor mv;

        cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER, "hello/HelloWorld", null,
                "java/lang/Object", null);

        cw.visitSource("HelloWorld.java", null);

        {
            mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(4, l0);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>",
                    "()V");
            mv.visitInsn(RETURN);
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLocalVariable("this", "Lhello/HelloWorld;", null, l0, l1,
                    0);
            mv.visitMaxs(1, 1);
            mv.visitEnd();
        }
        {
            mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main",
                    "([Ljava/lang/String;)V", null, null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(7, l0);
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out",
                    "Ljava/io/PrintStream;");
            mv.visitLdcInsn(String.format("Hello, %s!", name));
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println",
                    "(Ljava/lang/String;)V");
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLineNumber(8, l1);
            mv.visitInsn(RETURN);
            Label l2 = new Label();
            mv.visitLabel(l2);
            mv.visitLocalVariable("args", "[Ljava/lang/String;", null, l0, l2,
                    0);
            mv.visitMaxs(2, 1);
            mv.visitEnd();
        }
        cw.visitEnd();

        return cw.toByteArray();
    }

    public static class DynamicClassLoader extends ClassLoader {
        public Class<?> define(String className, byte[] bytecode) {
            return super.defineClass(className, bytecode, 0, bytecode.length);
        }
    };

    public static void main(String[] args) throws Exception {
        DynamicClassLoader loader = new DynamicClassLoader();
        Class<?> helloWorldClass = loader.define("hello.HelloWorld",
                compile("Test"));
        Method method = helloWorldClass.getMethod("main", String[].class);
        method.invoke(null, (Object) new String[] {});
    }
}

To generate the code, I found very useful Bytecode Outline for Eclipse plug-in. Although you could use the ASMifier (included with ASM) like this:

ClassReader cr = new ClassReader(new FileInputStream("HelloWorld.class"));
cr.accept(new ASMifierClassVisitor(new PrintWriter(System.out)), 0);

At runtime, if you need to obtain the Class object for the created class, you can load your class by extending a class loader and publishing (through another method, for instance) the defineClass method and providing the class as a byte array, as listed in the example.

You can also handle the created class with an interface, like in this example:

package hello;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class HelloWorldPlugin implements Opcodes {
    public static interface Plugin {
        void sayHello(String name);
    }

    public static byte[] compile() {

        ClassWriter cw = new ClassWriter(0);
        MethodVisitor mv;

        cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER, "hello/MyClass", null,
                "java/lang/Object",
                new String[] { "hello/HelloWorldPlugin$Plugin" });

        cw.visitInnerClass("hello/HelloWorldPlugin$Plugin",
                "hello/HelloWorldPlugin", "Plugin", ACC_PUBLIC + ACC_STATIC
                        + ACC_ABSTRACT + ACC_INTERFACE);

        {
            mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(5, l0);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>",
                    "()V");
            mv.visitInsn(RETURN);
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLocalVariable("this", "Lhello/MyClass;", null, l0, l1, 0);
            mv.visitMaxs(1, 1);
            mv.visitEnd();
        }
        {
            mv = cw.visitMethod(ACC_PUBLIC, "sayHello",
                    "(Ljava/lang/String;)V", null, null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(9, l0);
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out",
                    "Ljava/io/PrintStream;");
            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
            mv.visitInsn(DUP);
            mv.visitLdcInsn("Hello, ");
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder",
                    "<init>", "(Ljava/lang/String;)V");
            mv.visitVarInsn(ALOAD, 1);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder",
                    "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder",
                    "toString", "()Ljava/lang/String;");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println",
                    "(Ljava/lang/String;)V");
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLineNumber(10, l1);
            mv.visitInsn(RETURN);
            Label l2 = new Label();
            mv.visitLabel(l2);
            mv.visitLocalVariable("this", "Lhello/MyClass;", null, l0, l2, 0);
            mv.visitLocalVariable("name", "Ljava/lang/String;", null, l0, l2,
                    1);
            mv.visitMaxs(4, 2);
            mv.visitEnd();
        }
        cw.visitEnd();

        return cw.toByteArray();
    }

    public static class DynamicClassLoader extends ClassLoader {
        public DynamicClassLoader(ClassLoader parent) {
            super(parent);
        }

        public Class<?> define(String className, byte[] bytecode) {
            return super.defineClass(className, bytecode, 0, bytecode.length);
        }
    };

    public static void main(String[] args) throws Exception {
        DynamicClassLoader loader = new DynamicClassLoader(Thread
                .currentThread().getContextClassLoader());
        Class<?> helloWorldClass = loader.define("hello.MyClass", compile());
        Plugin plugin = (Plugin) helloWorldClass.newInstance();
        plugin.sayHello("Test");
    }
}

Have fun.

PS: I can add comments to the code if not clear enough. I didn't because the answer is already too long. Nevertheless, my suggestion for you is to try debugging it.

Martín Schonaker
  • 7,273
  • 4
  • 32
  • 55
15

I can suggest that you take a look at these libraries:

Bozho
  • 588,226
  • 146
  • 1,060
  • 1,140
  • Thanks for the links. Have you used any of these and which one would you recommend? – vitaut Oct 25 '10 at 16:06
  • I have used Javassist and CGLIB, but for way simpler tasks than your needs. One thing to note is that BCEL uses CGLIB, so perhaps it has bigger capabilities. – Bozho Oct 25 '10 at 16:17
1

From a different perspective I ask if you considered to use XText. This is designed to enable you to create DSL, code editor with code completion, compiler, code generator and so on. I think it is real cool and have a nice documentation. Worth to take a look at it. You can create a compiler easily based on it for your DSL.

Gábor Lipták
  • 9,646
  • 2
  • 59
  • 113
  • 1
    Thanks for the link. It seems that with XText I will have to reimplement the frontend which I would like to avoid. – vitaut Nov 07 '10 at 10:09
  • Yeah, I understand. It depends on your team size and company size. I think time by time it is necessary to completely give up something propietary to use something what others also use. – Gábor Lipták Nov 07 '10 at 20:54