JVM Hello World with Groovy

Author: Paul King
Published: 2022-12-22 02:24PM


For those that haven’t seen it yet, the JVM Advent folks posted a great Groovy and Data Science blog post several days ago as part of the 2022 JVM Advent series. If you have an interest in Data Science, we recommend you check that out before continuing with this post.

Today’s post in the JVM Advent series is looking at the world of bytecode libraries on the JVM. Let’s look at creating the same hello world example from that post using Groovy with the ProGuardCORE, ASM, and Byte Buddy libraries.

First, we highly recommend you read the previously mentioned JVM Advent post first for more background. After all, it’s easy to create a simple hello-world class file example directly as a Java source file (as that post shows) or as a Groovy source file like this:

println 'Hello world'

The examples shown in this blog post illustrate how you could create the equivalent class file using libraries which let you manipulate the generated bytecode directly. It’s a bit of a deep dive if you want to know more about JVM internals and can also be handy for numerous use cases like building tools or modifying Java classes on the fly. I suggest you read the websites for those libraries if you want further details or additional motivation.

ProGuardCORE

The ProGuardCORE library lets you read, analyze, modify, and write Java class files. Here’s how we could use it to write a hello-world class file:

var name = 'HelloProGuardCORE'
var superclass = 'java/lang/Object'
var classBuilder = new ClassBuilder(CLASS_VERSION_1_8, PUBLIC, name, superclass).tap {
    addMethod(PUBLIC | STATIC, 'main', '([Ljava/lang/String;)V', 100, builder ->
        builder
            .getstatic('java/lang/System', 'out', 'Ljava/io/PrintStream;')
            .ldc("Hello from $name")
            .invokevirtual('java/io/PrintStream', 'println', '(Ljava/lang/String;)V')
            .return_()
    )
}

new File("${name}.class").withDataOutputStream { dos ->
    classBuilder.programClass.accept(new ProgramClassWriter(dos))
}

This is essentially the "Groovified" version of the example in the JVM Advent blog post. We are using the libraries ClassBuilder class, adding a method, then adding four bytecode statements as the body of the method. If you haven’t seen method and type descriptor syntax before, a few parts might seem a little strange, but you possibly won’t be surprised that it seems to be referencing a System.out.println call and passing it a constant String.

When we run this script, a HelloProGuardCORE class file is produced. We can invoke that class file in the normal way:

$ java HelloProGuardCORE
Hello from HelloProGuardCORE

We encourage you to read the JVM Advent post or the library documentation if you want more details.

ASM

ASM is an all-purpose Java bytecode manipulation and analysis framework. In fact, it’s the one that Groovy uses in its parser and some of its tools. Here is how to use it to generate more or less the same class as the previous example:

var name = 'HelloASM'
var superclass = 'java/lang/Object'
var cw = new ClassWriter(0)
cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, name, null, superclass, null)
cw.visitMethod(ACC_PUBLIC + ACC_STATIC, 'main', '([Ljava/lang/String;)V', null, null).with {
    visitCode()
    visitFieldInsn(GETSTATIC, 'java/lang/System', 'out', 'Ljava/io/PrintStream;')
    visitLdcInsn('Hello from ' + name)
    visitMethodInsn(INVOKEVIRTUAL, 'java/io/PrintStream', 'println', '(Ljava/lang/String;)V', false)
    visitInsn(RETURN)
    visitMaxs(3, 3)
    visitEnd()
}
cw.visitEnd()

new File("${name}.class").withDataOutputStream { dos ->
    dos.write(cw.toByteArray())
}

After running this script, a HelloASM class file is produced, and here is the output when running that class file:

$ java HelloASM
Hello from HelloASM

Parts of the code should look familiar to the previous example.

Byte Buddy

Byte Buddy is a code generation and manipulation library for creating and modifying Java classes. Its strengths lie in its ability to create and modify classes dynamically. So, its power is perhaps not needed for our simple example. A nice aspect of this library however, is that it hides some of the low-level details like type and method descriptors behind its fluent API. Here is our example:

var name = 'HelloByteBuddy'
new ByteBuddy()
    .subclass(Object)
    .name(name)
    .defineMethod('main', Void.TYPE, PUBLIC | STATIC)
    .withParameter(String[])
    .intercept(MethodCall.invoke(
        PrintStream.getMethod('println', String))
        .onField(System.getField('out'))
        .with('Hello from ' + name))
    .make()
    .saveIn('.' as File)

Like the other scripts, this also produces a class file which we can invoke as shown here:

$ java HelloByteBuddy
Hello from HelloByteBuddy

That wraps up our examples using the three libraries, but we have one more fun alternative to cover!

Using Groovy ASTs

Groovy is a very extensible language. It provides among other things, a compile-time metaprogramming mechanism called AST Transforms (Abstract Syntax Tree Transformations). This mechanism uses annotations to indicate to the compiler that special processing is required during compilation. A now somewhat outdated AST transform, @Bytecode, experimented with allowing you to write bytecode instructions directly in your Groovy code. Let’s look at using that AST transform here:

@CompileStatic @POJO
class HelloAST {
    @Bytecode
    static void main(args) {
        getstatic 'java/lang/System.out', 'Ljava/io/PrintStream;'
        ldc 'Hello from HelloAST'
        invokevirtual 'java/io/PrintStream.println', '(Ljava/lang/String;)V'
        return
    }
}

We are writing directly the instructions that the Java or Groovy compiler (with static compilation enabled) would produce. For this example, we don’t run the script to produce the class file, we just compile it using the Groovy compiler.

We definitely don’t recommend relying on the @Bytecode AST transform for any production code, but it can be fun to play with. We’ve also used the @CompileStatic and @POJO AST transforms to tell the compiler that we aren’t using any Groovy dynamic features, so that it should write Java-like code whenever possible and avoid calling the Groovy runtime.

We can examine the bytecode using javap and indeed it has bytecode similar to that produced by the other libraries:

public static void main(java.lang.String...);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0089) ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #21                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #23                 // String Hello from HelloAST
         5: invokevirtual #29                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   Ljava/lang/Object;

Because the code is not calling the Groovy runtime, we can invoke it directly without the Groovy jar:

$ java HelloAST
Hello from HelloAST

That wraps up our little tour of bytecode libraries. I hope you have learnt some additional JVM details!

Further information