Skip to content
LudwikJaniuk edited this page Nov 27, 2019 · 18 revisions

Introduction

This page is an introduction to the byte code generation library "Cafebabe". Rather than exposing an API-like reference, we present the capabilities of the library through examples.

Other pages of interest are:

Building

git clone git://github.com/psuter/cafebabe.git
cd cafebabe
sbt package

This should produce a .jar(the output of the last command will tell you its location) that you can then copy into the lib/ directory of your sbt project.

Documentation

Run:

sbt doc
firefox target/scala-*/api/index.html

You can also generate a jar with the documentation, for use with Eclipse for example:

sbt package-doc

Types in class files

Type in Java code in class files
Void void V
Integer int I
Boolean boolean Z
Byte byte B
Single precision floating point float F
Double precision floating point double D
Long integer long J
Short integer short S
Character char C
Object reference package.ClassName Lpackage/ClassName;
Array type[] [type
Examples
Byte array byte[] [B
Array of arrays of string String[][] [[Ljava/lang/String;

String [][]

Method signatures are written as the concatenation of the parameters in parentheses (without any separator), followed by the return type. For instance:

String repString(int times, String original) { ... }

...has the signature (ILjava/lang/String;)Ljava/lang/String;, and:

int foo(double d, boolean test, byte b) { ... }

...has the signature (DZB)I.

Examples

Creating a class file

To create a handler to a class file representation, simply write:

val classFile = new ClassFile("TestClass", None)

...where the first parameter is the (fully qualified) class name, and the second one is the name of a potential parent class wrapped in an Option value. Use:

val classFile = new ClassFile("TestSubClass", Some("TestClass"))

...to specify a parent class different from the default Java java.lang.Object class.

If the class file was generated from compiling an actual source file, you can attach that information to it:

classFile.setSourceFile("MyProg.mylang")

Once all the fields, methods and code chunks for the methods have been set, you can write the class file itself to the disk by using:

classFile.writeToFile("./Test.class")

Note that to comply with the JVM specification, the file name and path should match the qualified class name in the standard way (subdirectories represent packages, etc.).

Adding fields

To add a field to a class, use the following method:

val fh: FieldHandler = classFile.addField("Ljava/lang/String;", "name")

...where the first argument is the field's type properly encoded, and the second one the field's name. Fields are protected and non-final by default. The returned FieldHandler can be use to change these flags using setFlags(flags : U2) which assigns the access_flags field.

Adding methods

To add a method, use:

val mh: MethodHandler = classFile.addMethod("I", "sayHello", "IZLjava/lang/String;")

This adds a method named ''sayHello'', which returns an integer and takes as arguments an integer, a Boolean and a string. An accepted alternative syntax is the following:

val mh: MethodHandler = classFile.addMethod("I", "sayHello", "I", "Z", "Ljava/lang/String;")

...where the parameter types are explicitely separated.

Methods are public by default. The main usage of the MethodHandler is to get a CodeHandler to attach code to the method (see below).

Attaching code to methods

To attach code to a method, one needs to recover a CodeHandler instance referring to that method. The way to do this is the following:

val ch: CodeHandler = methodHandler.codeHandler

Alternatively, the handler can be recovered directly when the method is added as following:

val ch: CodeHandler = classFile.addMethod("V", "foo", "I").codeHandler

Standard Java Byte Codes

A CodeHandler instance can be used to add byte codes to a method body using a "C++ stream"-like notation. All Java byte codes are supported. The following is the code for a non-static method that multiplies its first (integer) argument by two and returns the result:

ch << ILOAD_1 << DUP << IADD << IRETURN

Java byte codes are written using exactly the names used in the JVM specification, except that they are fully-capitalized as in the example above.

Abstract Byte Codes

Additionally to "standard" Java byte codes, Cafebabe defines some "abstract" byte codes which are really just here for convenience purposes (most often to avoid having to explicitly deal with constant pool entries or to handle labels and jumps automatically). The convention is that they are capitalized using CamelCase to differentiate them from standard byte codes.

You can find a list of the available abstract byte codes here.

Getting slots for local variables

In order to get slot indices which are known not to be used at a given program point, we use the following method:

val fresh: Int = ch.getFreshVar

Note that all slots should be acquire through this method, as this is the only way to guarantee that the computation of the maximum number of locals will be accurate.

Getting fresh label names

To get fresh label names for flow-control abstract bytecodes, the following helper is available:

ch.getFreshLabel("afterloop")

The label name is guaranteed to be fresh and will be prefix with the string.

Freezing: Finalizing the code

When the body of a method is completely specified, one needs to call the .freeze method on its CodeHandler:

ch.freeze

This:

  • translates all abstract byte codes into actual byte codes
  • computes the jump offsets and introduce them at the right places
  • computes the maximum stack height
  • computes the maximum number of local variables

The last two numbers are required by the classfile format.

Miscellaneous

Default constructors

A default constructor can be automatically generated for a class file by calling:

classFile.addDefaultConstructor

Main method

A MethodHandler for a public, static method returning void, called main and with a string array as a single argument can be obtained by using:

val mainHandler = classFile.addMainMethod

...the code can then be attached using its CodeHandler as usual.

Debug printing

As long as the code is not "frozen", it can be printed out by calling:

codeHandler.print

This is used mostly for debugging jumps and labels, as method and field accesses are already encoded with constant pool indices at this point (displayed as RawBytes(...)).

Additional comments can be added to the stream of bytecodes using:

codeHandler << Comment("The loop starts here")

There comments are displayed in the debug print-out, but do not translate into any bytecode.

Authors

Cafebabe was written at EPFL by Philippe Suter and Sebastian Gfeller.

Let us know on GitHub if you find a bug.