Skip to content
Groovy AST Transformation to allow writing Jasmin code (JVM bytecode) directly on groovy files
Groovy Java
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
gradle/wrapper
grasmin-core
grasmin-tests
libs
.gitignore
README.md
gradlew
gradlew.bat
settings.gradle

README.md

Grasmin

Write Jasmin (JVM assembly) code directly in your Groovy files

Grasmin allows you to write Jasmin code (which is basically JVM assembly bytecode instructions) directly on your Groovy files by annotating methods, or even entire classes, with the @JasminCode annotation.

This annotation is a Groovy AST Transformation, which allows manipulation of source code during the compilation process.

For example, you could write Hello World as follows:

class Hello {

  @JasminCode
  static void main(args) {
     """
     .limit stack 2
     getstatic java/lang/System/out Ljava/io/PrintStream;
     ldc "Hello World!"
     invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
     return
     """
  }

}

The @JasminCode annotation supports a single parameter, outputDebugFile. You may provide a value for it, with the path to a file, if you want Grasmin to write the contents of the Jasmin file used to generate the byte-code to it for debugging purposes.

For example:

@JasminCode( outputDebugFile = 'jasmin-files/hello.j' )

Most JVM instructions should work with Jasmin code. Let me know if you have any issues.

Using Grasmin

Currently, you need to clone this repo and build it with Gradle:

gradlew install

gradlew (Gradle wrapper) will download Gradle automatically if you don't already have it, then build Grasmin.

This will put the following artifact in your local Maven repo:

group = 'com.athaydes.grasmin'
name = 'grasmin-core'
version = 0.1

If you don't use Maven and Gradle, just do gradlew jar to create a jar in the build/libs folder.

The Jasmin Jar can be found on SourceForge.

Annotating classes

When you annotate a class with @JasminCode, everything in the class will be converted into a Jasmin file, which will then be used directly by Jasmin to generate the bytecode.

That means that all methods in the class must be written in Jasmin code!

For better performance, this is the recommended approach.

You can expect the generated bytecode to be effectively what you wrote, except for the following small convenient exceptions Grasmin provides:

  • class variables (including static variables) can be initialized directly as you would do in Java or Groovy:
String myString = "Hello Grasmin!"
static final int myInt = 10
  • you do not need to explicitly declare a default constructor. It will be automatically generated for you.

Notice that if you declare a default constructor, it is your responsibility to initialize non-static variables as Grasmin would have done it in the automatically generated default constructor otherwise.

Here's a simple example of a class annotated with @JasminCode:

@JasminCode( outputDebugFile = 'exampleJasminClass.j' )
class ExampleJasminClass {

    private String name
    private int i

    public static final String staticString = 'a-string'
    public static final int staticInt = 123

    // default constructor is declared, so must initialize the instance variables here!
    ExampleJasminClass() {
        """\
        .limit stack 2
        aload_0
        invokenonvirtual java/lang/Object/<init>()V
        aload_0
        ldc "John"
        putfield grasmin/test_target/ExampleJasminClass/name Ljava/lang/String;
        aload_0
        bipush 55
        putfield grasmin/test_target/ExampleJasminClass/i I
        return"""
    }

    // concatenate two Strings, returning the result
    String concat( String a, String b ) {
        """\
        .limit locals 3
        .limit stack 2
        aload_1
        aload_2
        invokevirtual java/lang/String/concat(Ljava/lang/String;)Ljava/lang/String;
        areturn"""
        // a.concat( b )
    }

    // simple getter
    public String getName() {
        """\
        aload_0
        getfield grasmin/test_target/ExampleJasminClass/name Ljava/lang/String;
        areturn"""
    }

    // int getter returns with ireturn
    public int getI() {
        """\
        aload_0
        getfield grasmin/test_target/ExampleJasminClass/i I
        ireturn"""
        111 // ignored
    }

}

And the following is the output produced by javap -c on the class file generated by the above class declaration:

Compiled from "grasmin.test_target.ExampleJasminClass.j"
public class grasmin.test_target.ExampleJasminClass {
  public static final java.lang.String staticString;

  public static final int staticInt;

  public static {};
    Code:
       0: ldc           #15                 // String a-string
       2: putstatic     #38                 // Field staticString:Ljava/lang/String;
       5: ldc           #22                 // int 123
       7: putstatic     #6                  // Field staticInt:I
      10: return

  public grasmin.test_target.ExampleJasminClass();
    Code:
       0: aload_0
       1: invokespecial #35                 // Method java/lang/Object."<init>":()V
       4: aload_0
       5: ldc           #14                 // String John
       7: putfield      #7                  // Field name:Ljava/lang/String;
      10: aload_0
      11: bipush        55
      13: putfield      #28                 // Field i:I
      16: return

  public java.lang.String concat(java.lang.String, java.lang.String);
    Code:
       0: aload_1
       1: aload_2
       2: invokevirtual #10                 // Method java/lang/String.concat:(Ljava/lang/String;)Ljava/lang/String;
       5: areturn

  public java.lang.String getName();
    Code:
       0: aload_0
       1: getfield      #7                  // Field name:Ljava/lang/String;
       4: areturn

  public int getI();
    Code:
       0: aload_0
       1: getfield      #28                 // Field i:I
       4: ireturn
}

Notice that because the class file is generated solely based on your declaration, no Groovy features can be used with annotated classes (in fact, you do not need to have Groovy in the classpath to run classes that do not directly use Groovy code in the JasminCode).

Annotating methods

Any method, in an otherwise normal Groovy class, can be annotated with @JasminCode. Annotated methods can have any signature.

If your method needs to return a particular type, just add a dummy value as the last statement of your method to make your IDE compiler happy... during Groovy compilation, Grasmin will only use the first statement of your method (which should be a String or a property that evaluates to a String), ignoring any subsequent statements (which will not even be present in the compiled class file).

The following method returns the sum of two integers (notice the use of both @JasminCode and @GroovyStatic - you almost always want to use them together on methods to avoid the cost of calling a method via Groovy's normal dynamic method dispatch):

class Hello {
    @CompileStatic
    @JasminCode
    int sum( int a, int b ) {
        """
        .limit stack 2
        .limit locals 2
        iload_0
        iload_1
        iadd
        ireturn
        """
        0 // ignored, but makes the IDE happy
    }
}

Keep reading below to see exactly what the generated byte-code is for this example.

How it works

Annotated classes

When you annotate a class with @JasminCode, during the semantics analysis phase, Grasmin will basically replace the normal Groovy class file which would have normally been generated by groovyc with one created by Jasmin based on a j file created by interpreting only the first statement of each method as being Jasmin code, ie. a String (or an expression which evaluates to a String) containing Jasmin code.

As mentioned earlier, Grasmin will also:

  • initialize any static variables declared in the normal Groovy syntax.
  • initialize instance variables which have an initial value in an automatically generated default constructor, if no default constructor was declared.

Notice that, because you actually declare the class in a normal Groovy file, you can use it in your Java or Groovy project with full support from the IDE, even though at run-time the actual implementation of the class will be turned into the byte-code instructions provided by the Strings in the first statement of each method.

Annotated methods

Grasmin turns any method annotated with @JasminCode into a call to a static method (called run) of a class created by Jasmin from the Jasmin code provided in the annotated method.

The example above (in the sum function) produces the following class, as output by javap:

Compiled from "Hello.groovy"
public class com.athaydes.grasmin.Hello implements groovy.lang.GroovyObject {

  // ... lots of Java/Groovy boilerplate

  public int sum(int, int);
    Code:
       0: iload_1       
       1: iload_2       
       2: invokestatic  #121                // Method com_athaydes_grasmin_Hello_sum.run:(II)I
       5: ireturn       
       6: ldc           #35                 // int 0
       8: ireturn       

  // ... more boilerplate
}

the two last lines in sum are dead-code, probably introduced by the Groovy compiler as a default return value

And the new class produced with Jasmin to hold the implementation of sum:

Compiled from "com_athaydes_grasmin_Hello_sum.j"
public class com_athaydes_grasmin_Hello_sum {
  public static int run(int, int);
    Code:
       0: iload_0       
       1: iload_1       
       2: iadd          
       3: ireturn       
}

The above is the whole output of javap (without the verbose key). The j file mentioned in the first line is the temporary file used by Grasmin as input for Jasmin.

Future work and performance

Unfortunately, delegating a method call to a static method of an external class does not seem to be very efficient for a short algorithm, at least, so gains in performance cannot be guaranteed! However, I am sure there would be cases where handcrafted Assembly cannot be beaten either by javac or JIT optimizations. I would love to hear of any examples.

There are some performance tests in this directory which show that, for example, writing the simple GCD Euclidean algorithm in Jasmin is actually less efficient than just writing the non-recursive algorithm in either Java or Groovy (Groovy with @CompileStatic actually runs consistently faster than Java).

Here are some results:

each row represents 10_000 runs of the GCD algorithm with a pair of random integers, run 100 times with each implementation (values are average time taken in nano-seconds)

Java Groovy @CompileStatic Grovy @TailRecursive @JasminCode
2.794.043,06 2.497.728,88 2.870.736,07 3.249.961,17
2.915.521,62 2.684.497,9 2.813.709,1 3.455.433,63
2.926.648,15 2.634.187,88 2.935.640,63 3.904.040,23

Implementations:

  • Java - non-recursive algorithm written in Java
  • Groovy @CompileStatic - non-recursive algorithm written in Groovy, annotated with @CompileStatic
  • Groovy @TailRecursive - recursive algorithm written in Groovy, annotated with both @CompileStatic and @TailRecursive
  • @JasminCode - non-recursive algorithm written in Jasmin assembly via Grasmin

Note: in the last run, the order in which the algorithms were run was modified to: Java, @JasminCode, Groovy@CompileStatic, Groovy@TailRecursive. This did not seem to impact on the relative results.

You can’t perform that action at this time.